Skip to content

Commit 07a0594

Browse files
authored
feat: add opt-in stateless mode for streamable-http transport (#73)
## Summary - Add `SYSDIG_MCP_STATELESS` env var (default `false`) to enable stateless mode for the `streamable-http` transport - When enabled, each HTTP request is self-contained — no `initialize` handshake or `Mcp-Session-Id` tracking required - Required for AWS Bedrock AgentCore compatibility, where `invoke-agent-runtime` is a single-shot HTTP POST with no session continuity ## AgentCore deployment config ```bash SYSDIG_MCP_TRANSPORT=streamable-http SYSDIG_MCP_STATELESS=true SYSDIG_MCP_LISTENING_PORT=8000 SYSDIG_MCP_MOUNT_PATH=/mcp ``` ## Test plan - [x] `just check` passes (fmt, lint, 142/142 specs) - [x] Manual test: `tools/list` succeeds without prior `initialize` when stateless - [x] Manual test: no `Mcp-Session-Id` header returned in stateless mode - [x] Manual test: `mcp-remote` connects and proxies through stateless server - [x] Default behavior (`SYSDIG_MCP_STATELESS=false`) unchanged — not a breaking change
1 parent 0ee81ca commit 07a0594

12 files changed

Lines changed: 66 additions & 35 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ You can also set the following variables to override the default configuration:
213213
- `SYSDIG_MCP_LOGLEVEL`: Log Level of the application (`DEBUG`, `INFO`, `WARNING`, `ERROR`). Defaults to: `INFO`
214214
- `SYSDIG_MCP_LISTENING_PORT`: The port for the server when it is deployed using remote protocols (`streamable-http`, `sse`). Defaults to: `8080`
215215
- `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.
216+
- `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`.
216217

217218
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.
218219

cmd/server/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,8 @@ func startServer(cfg *config.Config, handler *mcp.Handler) error {
147147
}
148148
case "streamable-http":
149149
addr := fmt.Sprintf("%s:%s", cfg.ListeningHost, cfg.ListeningPort)
150-
slog.Info("MCP Server listening", "addr", addr, "mountPath", cfg.MountPath)
151-
if err := http.ListenAndServe(addr, handler.AsStreamableHTTP(cfg.MountPath)); err != nil {
150+
slog.Info("MCP Server listening", "addr", addr, "mountPath", cfg.MountPath, "stateless", cfg.Stateless)
151+
if err := http.ListenAndServe(addr, handler.AsStreamableHTTP(cfg.MountPath, cfg.Stateless)); err != nil {
152152
return fmt.Errorf("error serving streamable http: %w", err)
153153
}
154154
case "sse":

docker-base-aarch64.nix

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
imageName = "quay.io/sysdig/sysdig-mini-ubi9";
3-
imageDigest = "sha256:39d40b40c28b784c9f50e74759eef17524b02c395a4fba6f5d625e0de2df3dd9";
4-
hash = "sha256-tKMD8maxhYpXddPLvCtdgJMYGOY4tugQ40na33//Zl4=";
3+
imageDigest = "sha256:339b8759c1b68fe92242f62fe400e9f3beabf5563d30b6a34fc9602a09c97d5f";
4+
hash = "sha256-C/Xhw7g13zei4sIYtqn2yK5NGpVgawdO8OHS4DeAKWI=";
55
finalImageName = "quay.io/sysdig/sysdig-mini-ubi9";
66
finalImageTag = "1";
77
}

docker-base-amd64.nix

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
imageName = "quay.io/sysdig/sysdig-mini-ubi9";
3-
imageDigest = "sha256:39d40b40c28b784c9f50e74759eef17524b02c395a4fba6f5d625e0de2df3dd9";
4-
hash = "sha256-2OwX3zig0K85FGETxBy3AapbwycUYGIC1fKXlcHAcO4=";
3+
imageDigest = "sha256:339b8759c1b68fe92242f62fe400e9f3beabf5563d30b6a34fc9602a09c97d5f";
4+
hash = "sha256-iglxgvoHj3lzaBO/GxLWwXPQqnZGfwkgmEf2qiO3kbw=";
55
finalImageName = "quay.io/sysdig/sysdig-mini-ubi9";
66
finalImageTag = "1";
77
}

flake.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go.mod

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.26
44

55
require (
66
github.com/mark3labs/mcp-go v0.45.0
7-
github.com/oapi-codegen/runtime v1.2.0
7+
github.com/oapi-codegen/runtime v1.3.0
88
github.com/onsi/ginkgo/v2 v2.28.1
99
github.com/onsi/gomega v1.39.1
1010
github.com/spf13/cobra v1.10.2
@@ -16,27 +16,27 @@ require (
1616
github.com/Masterminds/semver/v3 v3.4.0 // indirect
1717
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
1818
github.com/bahlo/generic-list-go v0.2.0 // indirect
19-
github.com/buger/jsonparser v1.1.1 // indirect
19+
github.com/buger/jsonparser v1.1.2 // indirect
2020
github.com/go-logr/logr v1.4.3 // indirect
2121
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
2222
github.com/google/go-cmp v0.7.0 // indirect
2323
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc // indirect
2424
github.com/google/uuid v1.6.0 // indirect
2525
github.com/inconshreveable/mousetrap v1.1.0 // indirect
2626
github.com/invopop/jsonschema v0.13.0 // indirect
27-
github.com/mailru/easyjson v0.9.1 // indirect
27+
github.com/mailru/easyjson v0.9.2 // indirect
2828
github.com/spf13/cast v1.10.0 // indirect
2929
github.com/spf13/pflag v1.0.10 // indirect
3030
github.com/stretchr/testify v1.11.1 // indirect
3131
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
3232
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
3333
go.yaml.in/yaml/v3 v3.0.4 // indirect
34-
golang.org/x/mod v0.33.0 // indirect
35-
golang.org/x/net v0.51.0 // indirect
34+
golang.org/x/mod v0.34.0 // indirect
35+
golang.org/x/net v0.52.0 // indirect
3636
golang.org/x/sync v0.20.0 // indirect
3737
golang.org/x/sys v0.42.0 // indirect
38-
golang.org/x/text v0.34.0 // indirect
39-
golang.org/x/tools v0.42.0 // indirect
38+
golang.org/x/text v0.35.0 // indirect
39+
golang.org/x/tools v0.43.0 // indirect
4040
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
4141
gopkg.in/yaml.v3 v3.0.1 // indirect
4242
)

go.sum

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP
66
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
77
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
88
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
9-
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
10-
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
9+
github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
10+
github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
1111
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
1212
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1313
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=
4343
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
4444
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
4545
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
46-
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
47-
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
46+
github.com/mailru/easyjson v0.9.2 h1:dX8U45hQsZpxd80nLvDGihsQ/OxlvTkVUXH2r/8cb2M=
47+
github.com/mailru/easyjson v0.9.2/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
4848
github.com/mark3labs/mcp-go v0.45.0 h1:s0S8qR/9fWaQ3pHxz7pm1uQ0DrswoSnRIxKIjbiQtkc=
4949
github.com/mark3labs/mcp-go v0.45.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
5050
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
5151
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
5252
github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE=
5353
github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A=
54-
github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4=
55-
github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0=
54+
github.com/oapi-codegen/runtime v1.3.0 h1:vyK1zc0gDWWXgk2xoQa4+X4RNNc5SL2RbTpJS/4vMYA=
55+
github.com/oapi-codegen/runtime v1.3.0/go.mod h1:kOdeacKy7t40Rclb1je37ZLFboFxh+YLy0zaPCMibPY=
5656
github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
5757
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
5858
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
@@ -90,18 +90,18 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
9090
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
9191
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
9292
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
93-
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
94-
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
95-
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
96-
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
93+
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
94+
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
95+
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
96+
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
9797
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
9898
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
9999
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
100100
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
101-
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
102-
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
103-
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
104-
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
101+
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
102+
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
103+
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
104+
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
105105
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
106106
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
107107
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

internal/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type Config struct {
1616
ListeningPort string
1717
MountPath string
1818
LogLevel string
19+
Stateless bool
1920
}
2021

2122
func (c *Config) Validate() error {
@@ -38,6 +39,7 @@ func Load() (*Config, error) {
3839
ListeningPort: getEnv("SYSDIG_MCP_LISTENING_PORT", "8080"),
3940
MountPath: getEnv("SYSDIG_MCP_MOUNT_PATH", "/sysdig-mcp-server"),
4041
LogLevel: getEnv("SYSDIG_MCP_LOGLEVEL", "INFO"),
42+
Stateless: getEnv("SYSDIG_MCP_STATELESS", false),
4143
}
4244

4345
if err := cfg.Validate(); err != nil {

internal/config/config_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ var _ = Describe("Config", func() {
8383
Expect(cfg.MountPath).To(Equal("/sysdig-mcp-server"))
8484
Expect(cfg.LogLevel).To(Equal("INFO"))
8585
Expect(cfg.SkipTLSVerification).To(BeFalse())
86+
Expect(cfg.Stateless).To(BeFalse())
8687
})
8788
})
8889

@@ -113,6 +114,7 @@ var _ = Describe("Config", func() {
113114
_ = os.Setenv("SYSDIG_MCP_LISTENING_PORT", "9090")
114115
_ = os.Setenv("SYSDIG_MCP_MOUNT_PATH", "/custom")
115116
_ = os.Setenv("SYSDIG_MCP_LOGLEVEL", "DEBUG")
117+
_ = os.Setenv("SYSDIG_MCP_STATELESS", "true")
116118
})
117119

118120
It("should load all values from the environment", func() {
@@ -126,6 +128,7 @@ var _ = Describe("Config", func() {
126128
Expect(cfg.ListeningPort).To(Equal("9090"))
127129
Expect(cfg.MountPath).To(Equal("/custom"))
128130
Expect(cfg.LogLevel).To(Equal("DEBUG"))
131+
Expect(cfg.Stateless).To(BeTrue())
129132
})
130133
})
131134

internal/infra/mcp/mcp_handler.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,15 @@ func (h *Handler) ServeStdio(ctx context.Context, stdin io.Reader, stdout io.Wri
8787
return server.NewStdioServer(h.server).Listen(ctx, stdin, stdout)
8888
}
8989

90-
func (h *Handler) AsStreamableHTTP(mountPath string) http.Handler {
90+
func (h *Handler) AsStreamableHTTP(mountPath string, stateless bool) http.Handler {
9191
mux := http.NewServeMux()
92-
httpServer := server.NewStreamableHTTPServer(h.server)
92+
93+
var opts []server.StreamableHTTPOption
94+
if stateless {
95+
opts = append(opts, server.WithStateLess(true))
96+
}
97+
98+
httpServer := server.NewStreamableHTTPServer(h.server, opts...)
9399
mux.Handle(mountPath, authMiddleware(httpServer))
94100
return mux
95101
}

0 commit comments

Comments
 (0)