From cbfa70fb2aba239dcd4ddfde80aab65f8dd8d7d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Magall=C3=B3n?= Date: Thu, 19 Mar 2026 16:56:01 +0100 Subject: [PATCH 1/3] feat: add opt-in stateless mode for streamable-http transport --- README.md | 1 + cmd/server/main.go | 4 ++-- internal/config/config.go | 2 ++ internal/config/config_test.go | 3 +++ internal/infra/mcp/mcp_handler.go | 10 ++++++++-- internal/infra/mcp/mcp_handler_test.go | 20 +++++++++++++++++++- 6 files changed, 35 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b692816..3f6c11b 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,7 @@ You can also set the following variables to override the default configuration: - `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` - `SYSDIG_MCP_LISTENING_HOST`: The host for the server when it is deployed using remote protocols (`streamable-http`, `sse`). Defaults to all interfaces (`:port`). Set to `127.0.0.1` for local-only access. +- `SYSDIG_MCP_STATELESS`: Enable stateless mode for `streamable-http` transport, where each request is self-contained with no session tracking (useful for AWS Bedrock AgentCore). Defaults to: `false`. You can find your API token in the Sysdig Secure UI under **Settings > Sysdig Secure API**. Make sure to copy the token as it will not be shown again. diff --git a/cmd/server/main.go b/cmd/server/main.go index 1ba8c12..708455c 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -147,8 +147,8 @@ func startServer(cfg *config.Config, handler *mcp.Handler) error { } case "streamable-http": addr := fmt.Sprintf("%s:%s", cfg.ListeningHost, cfg.ListeningPort) - slog.Info("MCP Server listening", "addr", addr, "mountPath", cfg.MountPath) - if err := http.ListenAndServe(addr, handler.AsStreamableHTTP(cfg.MountPath)); err != nil { + slog.Info("MCP Server listening", "addr", addr, "mountPath", cfg.MountPath, "stateless", cfg.Stateless) + if err := http.ListenAndServe(addr, handler.AsStreamableHTTP(cfg.MountPath, cfg.Stateless)); err != nil { return fmt.Errorf("error serving streamable http: %w", err) } case "sse": diff --git a/internal/config/config.go b/internal/config/config.go index 5e47345..9faed91 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -16,6 +16,7 @@ type Config struct { ListeningPort string MountPath string LogLevel string + Stateless bool } func (c *Config) Validate() error { @@ -38,6 +39,7 @@ func Load() (*Config, error) { ListeningPort: getEnv("SYSDIG_MCP_LISTENING_PORT", "8080"), MountPath: getEnv("SYSDIG_MCP_MOUNT_PATH", "/sysdig-mcp-server"), LogLevel: getEnv("SYSDIG_MCP_LOGLEVEL", "INFO"), + Stateless: getEnv("SYSDIG_MCP_STATELESS", false), } if err := cfg.Validate(); err != nil { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 8c09099..3800bbb 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -83,6 +83,7 @@ var _ = Describe("Config", func() { Expect(cfg.MountPath).To(Equal("/sysdig-mcp-server")) Expect(cfg.LogLevel).To(Equal("INFO")) Expect(cfg.SkipTLSVerification).To(BeFalse()) + Expect(cfg.Stateless).To(BeFalse()) }) }) @@ -113,6 +114,7 @@ var _ = Describe("Config", func() { _ = os.Setenv("SYSDIG_MCP_LISTENING_PORT", "9090") _ = os.Setenv("SYSDIG_MCP_MOUNT_PATH", "/custom") _ = os.Setenv("SYSDIG_MCP_LOGLEVEL", "DEBUG") + _ = os.Setenv("SYSDIG_MCP_STATELESS", "true") }) It("should load all values from the environment", func() { @@ -126,6 +128,7 @@ var _ = Describe("Config", func() { Expect(cfg.ListeningPort).To(Equal("9090")) Expect(cfg.MountPath).To(Equal("/custom")) Expect(cfg.LogLevel).To(Equal("DEBUG")) + Expect(cfg.Stateless).To(BeTrue()) }) }) diff --git a/internal/infra/mcp/mcp_handler.go b/internal/infra/mcp/mcp_handler.go index b91096a..f9eea98 100644 --- a/internal/infra/mcp/mcp_handler.go +++ b/internal/infra/mcp/mcp_handler.go @@ -87,9 +87,15 @@ func (h *Handler) ServeStdio(ctx context.Context, stdin io.Reader, stdout io.Wri return server.NewStdioServer(h.server).Listen(ctx, stdin, stdout) } -func (h *Handler) AsStreamableHTTP(mountPath string) http.Handler { +func (h *Handler) AsStreamableHTTP(mountPath string, stateless bool) http.Handler { mux := http.NewServeMux() - httpServer := server.NewStreamableHTTPServer(h.server) + + var opts []server.StreamableHTTPOption + if stateless { + opts = append(opts, server.WithStateLess(true)) + } + + httpServer := server.NewStreamableHTTPServer(h.server, opts...) mux.Handle(mountPath, authMiddleware(httpServer)) return mux } diff --git a/internal/infra/mcp/mcp_handler_test.go b/internal/infra/mcp/mcp_handler_test.go index e559b17..2be937b 100644 --- a/internal/infra/mcp/mcp_handler_test.go +++ b/internal/infra/mcp/mcp_handler_test.go @@ -110,7 +110,7 @@ var _ = Describe("McpHandler", func() { BeforeEach(func() { // Default middleware setup for HTTP tests - h := handler.AsStreamableHTTP("/") + h := handler.AsStreamableHTTP("/", false) testClient = NewHTTPTestClient(h) }) @@ -182,6 +182,24 @@ var _ = Describe("McpHandler", func() { h := handler.AsSSE("/sse") Expect(h).NotTo(BeNil()) }) + + It("AsStreamableHTTP with stateless should serve tools/list without initialize", func(ctx SpecContext) { + h := handler.AsStreamableHTTP("/", true) + statelessClient := NewHTTPTestClient(h) + + handler.RegisterTools(&dummyTool{name: "tool1"}) + + mockClient.EXPECT(). + GetMyPermissionsWithResponse(gomock.Any(), gomock.Any()). + Return(&sysdig.GetMyPermissionsResponse{ + HTTPResponse: &http.Response{StatusCode: 200}, + JSON200: &sysdig.UserPermissions{Permissions: []string{}}, + }, nil) + + // Call tools/list directly without initialize — should work in stateless mode + resp := statelessClient.ListTools(ctx, nil) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + }, NodeTimeout(time.Second*5)) }) Context("Stdio", func() { From 6f688f9c4f78c959643f2cfebe8b086dc55fcd3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Magall=C3=B3n?= Date: Thu, 19 Mar 2026 17:36:35 +0100 Subject: [PATCH 2/3] test: assert no Mcp-Session-Id header in stateless mode --- internal/infra/mcp/mcp_handler_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/infra/mcp/mcp_handler_test.go b/internal/infra/mcp/mcp_handler_test.go index 2be937b..c134924 100644 --- a/internal/infra/mcp/mcp_handler_test.go +++ b/internal/infra/mcp/mcp_handler_test.go @@ -199,6 +199,7 @@ var _ = Describe("McpHandler", func() { // Call tools/list directly without initialize — should work in stateless mode resp := statelessClient.ListTools(ctx, nil) Expect(resp.StatusCode).To(Equal(http.StatusOK)) + Expect(resp.Header.Get("Mcp-Session-Id")).To(BeEmpty()) }, NodeTimeout(time.Second*5)) }) From c0e16a1e57fb85cea81df043a22001cc59f77f3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Magall=C3=B3n?= Date: Fri, 20 Mar 2026 10:44:02 +0100 Subject: [PATCH 3/3] build: release v1.0.6 --- docker-base-aarch64.nix | 4 ++-- docker-base-amd64.nix | 4 ++-- flake.lock | 6 +++--- go.mod | 14 +++++++------- go.sum | 28 ++++++++++++++-------------- package.nix | 4 ++-- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/docker-base-aarch64.nix b/docker-base-aarch64.nix index 59aaa75..339e1ba 100644 --- a/docker-base-aarch64.nix +++ b/docker-base-aarch64.nix @@ -1,7 +1,7 @@ { imageName = "quay.io/sysdig/sysdig-mini-ubi9"; - imageDigest = "sha256:39d40b40c28b784c9f50e74759eef17524b02c395a4fba6f5d625e0de2df3dd9"; - hash = "sha256-tKMD8maxhYpXddPLvCtdgJMYGOY4tugQ40na33//Zl4="; + imageDigest = "sha256:339b8759c1b68fe92242f62fe400e9f3beabf5563d30b6a34fc9602a09c97d5f"; + hash = "sha256-C/Xhw7g13zei4sIYtqn2yK5NGpVgawdO8OHS4DeAKWI="; finalImageName = "quay.io/sysdig/sysdig-mini-ubi9"; finalImageTag = "1"; } diff --git a/docker-base-amd64.nix b/docker-base-amd64.nix index e2b1faf..0af8dee 100644 --- a/docker-base-amd64.nix +++ b/docker-base-amd64.nix @@ -1,7 +1,7 @@ { imageName = "quay.io/sysdig/sysdig-mini-ubi9"; - imageDigest = "sha256:39d40b40c28b784c9f50e74759eef17524b02c395a4fba6f5d625e0de2df3dd9"; - hash = "sha256-2OwX3zig0K85FGETxBy3AapbwycUYGIC1fKXlcHAcO4="; + imageDigest = "sha256:339b8759c1b68fe92242f62fe400e9f3beabf5563d30b6a34fc9602a09c97d5f"; + hash = "sha256-iglxgvoHj3lzaBO/GxLWwXPQqnZGfwkgmEf2qiO3kbw="; finalImageName = "quay.io/sysdig/sysdig-mini-ubi9"; finalImageTag = "1"; } diff --git a/flake.lock b/flake.lock index 861b7f0..752172b 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1772927210, - "narHash": "sha256-FdRDRoV0jRTiPK5ID22BaUX5P0wdsclpxtIOjaEy9Lo=", + "lastModified": 1773628058, + "narHash": "sha256-hpXH0z3K9xv0fHaje136KY872VT2T5uwxtezlAskQgY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "0e6cdd5be64608ef630c2e41f8d51d484468492f", + "rev": "f8573b9c935cfaa162dd62cc9e75ae2db86f85df", "type": "github" }, "original": { diff --git a/go.mod b/go.mod index b03187e..f7bc497 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26 require ( github.com/mark3labs/mcp-go v0.45.0 - github.com/oapi-codegen/runtime v1.2.0 + github.com/oapi-codegen/runtime v1.3.0 github.com/onsi/ginkgo/v2 v2.28.1 github.com/onsi/gomega v1.39.1 github.com/spf13/cobra v1.10.2 @@ -16,7 +16,7 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect - github.com/buger/jsonparser v1.1.1 // indirect + github.com/buger/jsonparser v1.1.2 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/go-cmp v0.7.0 // indirect @@ -24,19 +24,19 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/jsonschema v0.13.0 // indirect - github.com/mailru/easyjson v0.9.1 // indirect + github.com/mailru/easyjson v0.9.2 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/mod v0.33.0 // indirect - golang.org/x/net v0.51.0 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/net v0.52.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect - golang.org/x/tools v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect + golang.org/x/tools v0.43.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6833588..05eab67 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= +github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -43,16 +43,16 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= -github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mailru/easyjson v0.9.2 h1:dX8U45hQsZpxd80nLvDGihsQ/OxlvTkVUXH2r/8cb2M= +github.com/mailru/easyjson v0.9.2/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mark3labs/mcp-go v0.45.0 h1:s0S8qR/9fWaQ3pHxz7pm1uQ0DrswoSnRIxKIjbiQtkc= github.com/mark3labs/mcp-go v0.45.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= -github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= -github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/oapi-codegen/runtime v1.3.0 h1:vyK1zc0gDWWXgk2xoQa4+X4RNNc5SL2RbTpJS/4vMYA= +github.com/oapi-codegen/runtime v1.3.0/go.mod h1:kOdeacKy7t40Rclb1je37ZLFboFxh+YLy0zaPCMibPY= github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= @@ -90,18 +90,18 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/package.nix b/package.nix index 9faca9d..d479231 100644 --- a/package.nix +++ b/package.nix @@ -1,10 +1,10 @@ { buildGo126Module, versionCheckHook }: buildGo126Module (finalAttrs: { pname = "sysdig-mcp-server"; - version = "1.0.5"; + version = "1.0.6"; src = ./.; # This hash is automatically re-calculated with `just rehash-package-nix`. This is automatically called as well by `just update`. - vendorHash = "sha256-Snb0kLN7ItduIXG1XVc2XOlXUaAqQILR4c2jvVXAVHk="; + vendorHash = "sha256-kYKEkSXdohQuIUwDIZMywhnarG7t4U1R8K1PaFkL1Vg="; subPackages = [ "cmd/server"