Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ The following environment variables are **required** for configuring the Sysdig
You can also set the following variables to override the default configuration:

- `SYSDIG_MCP_TRANSPORT`: The transport protocol for the MCP Server (`stdio`, `streamable-http`, `sse`). Defaults to: `stdio`.
- `SYSDIG_MCP_API_SKIP_TLS_VERIFICATION`: Whether to skip TLS verification for the Sysdig API connection (useful for self-signed certificates). Defaults to: `false`.
- `SYSDIG_MCP_MOUNT_PATH`: The URL prefix for the streamable-http/sse deployment. Defaults to: `/sysdig-mcp-server`
- `SYSDIG_MCP_LOGLEVEL`: Log Level of the application (`DEBUG`, `INFO`, `WARNING`, `ERROR`). Defaults to: `INFO`
- `SYSDIG_MCP_LISTENING_PORT`: The port for the server when it is deployed using remote protocols (`streamable-http`, `sse`). Defaults to: `8080`
Expand Down
18 changes: 16 additions & 2 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
"crypto/tls"
"fmt"
"log/slog"
"net/http"
Expand Down Expand Up @@ -82,13 +83,26 @@ func setupLogger(logLevel string) {
}

func setupSysdigClient(cfg *config.Config) (sysdig.ExtendedClientWithResponsesInterface, error) {
sysdigClient, err := sysdig.NewSysdigClient(
sysdigClientOptions := []sysdig.IntoClientOption{
sysdig.WithVersion(Version),
sysdig.WithFallbackAuthentication(
sysdig.WithHostAndTokenFromContext(),
sysdig.WithFixedHostAndToken(cfg.APIHost, cfg.APIToken),
),
)
}

if cfg.SkipTLSVerification {
transport := http.DefaultTransport.(*http.Transport).Clone()
if transport.TLSClientConfig == nil {
transport.TLSClientConfig = &tls.Config{}
}
transport.TLSClientConfig.InsecureSkipVerify = true
httpClient := &http.Client{Transport: transport}

sysdigClientOptions = append(sysdigClientOptions, sysdig.WithHTTPClient(httpClient))
}

sysdigClient, err := sysdig.NewSysdigClient(sysdigClientOptions...)
if err != nil {
return nil, fmt.Errorf("error creating sysdig client: %w", err)
}
Expand Down
3 changes: 3 additions & 0 deletions docs/TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
**Problem**: "unable to authenticate with any method"
- **Solution**: For `stdio`, verify `SYSDIG_MCP_API_HOST` and `SYSDIG_MCP_API_TOKEN` env vars are set correctly. For remote transports, check `Authorization: Bearer <token>` header format.

**Problem**: Connection failing with "certificate signed by unknown authority"
- **Solution**: If using a self-signed certificate (e.g. on-prem), set `SYSDIG_MCP_API_SKIP_TLS_VERIFICATION=true`.

**Problem**: Tests failing with "command not found"
- **Solution**: Enter Nix shell with `nix develop` or `direnv allow`. All dev tools are provided by the flake.

Expand Down
61 changes: 44 additions & 17 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@ package config
import (
"fmt"
"os"
"strconv"
"strings"
)

type Config struct {
APIHost string
APIToken string
Transport string
ListeningHost string
ListeningPort string
MountPath string
LogLevel string
APIHost string
APIToken string
SkipTLSVerification bool
Transport string
ListeningHost string
ListeningPort string
MountPath string
LogLevel string
}

func (c *Config) Validate() error {
Expand All @@ -27,13 +30,14 @@ func (c *Config) Validate() error {

func Load() (*Config, error) {
cfg := &Config{
APIHost: getEnv("SYSDIG_MCP_API_HOST", ""),
APIToken: getEnv("SYSDIG_MCP_API_TOKEN", ""),
Transport: getEnv("SYSDIG_MCP_TRANSPORT", "stdio"),
ListeningHost: getEnv("SYSDIG_MCP_LISTENING_HOST", "localhost"),
ListeningPort: getEnv("SYSDIG_MCP_LISTENING_PORT", "8080"),
MountPath: getEnv("SYSDIG_MCP_MOUNT_PATH", "/sysdig-mcp-server"),
LogLevel: getEnv("SYSDIG_MCP_LOGLEVEL", "INFO"),
APIHost: getEnv("SYSDIG_MCP_API_HOST", ""),
APIToken: getEnv("SYSDIG_MCP_API_TOKEN", ""),
SkipTLSVerification: getEnv("SYSDIG_MCP_API_SKIP_TLS_VERIFICATION", false),
Transport: getEnv("SYSDIG_MCP_TRANSPORT", "stdio"),
ListeningHost: getEnv("SYSDIG_MCP_LISTENING_HOST", "localhost"),
ListeningPort: getEnv("SYSDIG_MCP_LISTENING_PORT", "8080"),
MountPath: getEnv("SYSDIG_MCP_MOUNT_PATH", "/sysdig-mcp-server"),
LogLevel: getEnv("SYSDIG_MCP_LOGLEVEL", "INFO"),
}

if err := cfg.Validate(); err != nil {
Expand All @@ -43,9 +47,32 @@ func Load() (*Config, error) {
return cfg, nil
}

func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
type envType interface {
~string | ~bool
}

func getEnv[T envType](key string, fallback T) T {
value, ok := os.LookupEnv(key)
if !ok {
return fallback
}

switch any(fallback).(type) {
case string:
return any(value).(T)

case bool:
value = strings.TrimSpace(value)
if value == "" {
return fallback
}

b, err := strconv.ParseBool(value)
if err != nil {
return fallback
}
return any(b).(T)
}

return fallback
}
17 changes: 17 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ var _ = Describe("Config", func() {
Expect(cfg.ListeningPort).To(Equal("8080"))
Expect(cfg.MountPath).To(Equal("/sysdig-mcp-server"))
Expect(cfg.LogLevel).To(Equal("INFO"))
Expect(cfg.SkipTLSVerification).To(BeFalse())
})
})

Expand All @@ -106,6 +107,7 @@ var _ = Describe("Config", func() {
BeforeEach(func() {
_ = os.Setenv("SYSDIG_MCP_API_HOST", "env-host")
_ = os.Setenv("SYSDIG_MCP_API_TOKEN", "env-token")
_ = os.Setenv("SYSDIG_MCP_API_SKIP_TLS_VERIFICATION", "true")
_ = os.Setenv("SYSDIG_MCP_TRANSPORT", "http")
_ = os.Setenv("SYSDIG_MCP_LISTENING_HOST", "0.0.0.0")
_ = os.Setenv("SYSDIG_MCP_LISTENING_PORT", "9090")
Expand All @@ -118,6 +120,7 @@ var _ = Describe("Config", func() {
Expect(err).NotTo(HaveOccurred())
Expect(cfg.APIHost).To(Equal("env-host"))
Expect(cfg.APIToken).To(Equal("env-token"))
Expect(cfg.SkipTLSVerification).To(BeTrue())
Expect(cfg.Transport).To(Equal("http"))
Expect(cfg.ListeningHost).To(Equal("0.0.0.0"))
Expect(cfg.ListeningPort).To(Equal("9090"))
Expand All @@ -126,6 +129,20 @@ var _ = Describe("Config", func() {
})
})

Context("with invalid boolean env var", func() {
BeforeEach(func() {
_ = os.Setenv("SYSDIG_MCP_API_HOST", "host")
_ = os.Setenv("SYSDIG_MCP_API_TOKEN", "token")
_ = os.Setenv("SYSDIG_MCP_API_SKIP_TLS_VERIFICATION", "invalid-bool")
})

It("should fall back to default value", func() {
cfg, err := config.Load()
Expect(err).NotTo(HaveOccurred())
Expect(cfg.SkipTLSVerification).To(BeFalse())
})
})

Context("without required env vars", func() {
It("should return an error", func() {
_, err := config.Load()
Expand Down
16 changes: 14 additions & 2 deletions internal/infra/sysdig/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,22 @@ func WithVersion(version string) RequestEditorFn {
}
}

func NewSysdigClient(requestEditors ...RequestEditorFn) (ExtendedClientWithResponsesInterface, error) {
type IntoClientOption interface {
AsClientOption() ClientOption
}

func (r RequestEditorFn) AsClientOption() ClientOption {
return WithRequestEditorFn(r)
}

func (c ClientOption) AsClientOption() ClientOption {
return c
}

func NewSysdigClient(requestEditors ...IntoClientOption) (ExtendedClientWithResponsesInterface, error) {
editors := make([]ClientOption, len(requestEditors))
for i, e := range requestEditors {
editors[i] = WithRequestEditorFn(e)
editors[i] = e.AsClientOption()
}

return NewClientWithResponses("", editors...)
Expand Down
69 changes: 69 additions & 0 deletions internal/infra/sysdig/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package sysdig_test

import (
"context"
"crypto/tls"
"log"
"net/http"
"net/http/httptest"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig"
)

var _ = Describe("Client TLS", func() {
var ts *httptest.Server

BeforeEach(func() {
// Start a TLS server with a self-signed certificate
ts = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/users/me/permissions" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"permissions":[]}`))
return
}
w.WriteHeader(http.StatusNotFound)
}))
// Redirect server logs to GinkgoWriter to avoid noise in test output
ts.Config.ErrorLog = log.New(GinkgoWriter, "", 0)
})

AfterEach(func() {
ts.Close()
})

It("should fail request with self-signed cert by default", func() {
// Create client pointing to the TLS server without custom transport
client, err := sysdig.NewSysdigClient(
sysdig.WithFixedHostAndToken(ts.URL, "dummy-token"),
)
Expect(err).NotTo(HaveOccurred())

// Attempt request
_, err = client.GetMyPermissionsWithResponse(context.Background())
Expect(err).To(HaveOccurred())
// Verification that it failed due to certificate issues
Expect(err.Error()).To(Or(ContainSubstring("certificate"), ContainSubstring("unknown authority")))
})

It("should succeed request when using custom HTTP client with InsecureSkipVerify", func() {
// Create custom HTTP client that skips verification
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
httpClient := &http.Client{Transport: transport}

// Create client using the custom HTTP client
client, err := sysdig.NewSysdigClient(
sysdig.WithFixedHostAndToken(ts.URL, "dummy-token"),
sysdig.WithHTTPClient(httpClient),
)
Expect(err).NotTo(HaveOccurred())

// Attempt request
resp, err := client.GetMyPermissionsWithResponse(context.Background())
Expect(err).NotTo(HaveOccurred())
Expect(resp.HTTPResponse.StatusCode).To(Equal(http.StatusOK))
})
})
2 changes: 1 addition & 1 deletion package.nix
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{ buildGoModule, versionCheckHook }:
buildGoModule (finalAttrs: {
pname = "sysdig-mcp-server";
version = "0.5.4";
version = "0.6.0";
src = ./.;
# This hash is automatically re-calculated with `just rehash-package-nix`. This is automatically called as well by `just bump`.
vendorHash = "sha256-jf/px0p88XbfuSPMry/qZcfR0QPTF9IrPegg2CwAd6M=";
Expand Down
Loading