-
Notifications
You must be signed in to change notification settings - Fork 20
feat: migrate HTTP transport to stateless mode, upgrade go-sdk to v1.7.0 #162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -130,18 +130,6 @@ var ( | |
| " Docs: https://prometheus.io/docs/prometheus/latest/querying/api/#tsdb-admin-apis", | ||
| ).Default("false").Bool() | ||
|
|
||
| flagMcpKeepaliveInterval = kingpin.Flag( | ||
| "mcp.keepalive-interval", | ||
| "Interval for sending keepalive pings to connected MCP sessions."+ | ||
| " If the peer fails to respond, the session is closed."+ | ||
| " Most useful for HTTP transports to prevent idle connections from dropping.", | ||
| ).Default("30s").Duration() | ||
|
|
||
| flagMcpSessionTimeout = kingpin.Flag( | ||
| "mcp.session-timeout", | ||
| "Idle session timeout for HTTP transport MCP sessions.", | ||
| ).Default("10m").Duration() | ||
|
|
||
|
Comment on lines
-133
to
-144
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removing flags is user-facing and can cause unexpected breakages in other ways (ie, the binary bombing at starting when provided with unexpected flags that suddenly don't work). We should keep the flags for now, turn them into no-ops, and deprecate them. Let's leave the flag declarations here and update the help descriptions/README with the deprecation warning. we can still continue to unwire the timeout/keepalive configs |
||
| flagDocsAutoUpdate = kingpin.Flag( | ||
| "docs.auto-update", | ||
| "Enable automatic documentation updates from the official prometheus/docs repository."+ | ||
|
|
@@ -219,7 +207,6 @@ func main() { | |
| DocsFS: docsFs, | ||
| ToonOutputEnabled: *flagMcpToonOutputEnabled, | ||
| ClientLoggingEnabled: *flagMcpClientLogging, | ||
| KeepAlive: *flagMcpKeepaliveInterval, | ||
| }) | ||
| if err != nil { | ||
| logger.Error("Failed to create MCP server", "err", err) | ||
|
|
@@ -296,7 +283,7 @@ func main() { | |
| case "http": | ||
| logger.Debug("starting MCP server", "transport", "http") | ||
|
|
||
| httpMcpHandler := mcp.NewStreamableHTTPHandler(mcpServer, logger, *flagMcpSessionTimeout) | ||
| httpMcpHandler := mcp.NewStreamableHTTPHandler(mcpServer, logger) | ||
| http.Handle("/mcp", httpMcpHandler) | ||
| <-cancel | ||
| default: | ||
|
|
@@ -355,15 +342,11 @@ func main() { | |
| func initHTTPServer(logger *slog.Logger) *http.Server { | ||
| server := &http.Server{ | ||
| // These are TCP-level timeouts for individual HTTP | ||
| // request/response cycles, not MCP session timeouts. MCP | ||
| // sessions are long lived, tracked by session ID, and managed | ||
| // through the go-sdk separately from these HTTP server values. | ||
| // | ||
| // Important: Because SSE/HTTP transports are streams, the | ||
| // WriteTimeout must be disabled because the response "never | ||
| // finishes". | ||
| // request/response cycles. Stateless HTTP transport uses | ||
| // application/json responses that complete in a single round | ||
| // trip, so standard timeouts apply. | ||
|
Comment on lines
+345
to
+347
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this is accurate to the spec, it does not require JSON and responses do not need to be a single round trip. References:
there's other bits in the spec relevant to this, but I think this makes the point |
||
| ReadTimeout: 30 * time.Second, | ||
| WriteTimeout: 0, | ||
| WriteTimeout: *flagPrometheusTimeout, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's keep the WriteTimeout: 0, we need it disabled to continue to support stream responses. We should also probably bump IdleTimeout: 120 * time.Second as well -- this should reduce HTTP connection churn for normal clients that are potentially above that idle timeout, such as longer prometheus scrape intervals/LB health checks. The latter of which is arguably more consequential, as I'm realizing now this probably also means potentially sporadic 502s. |
||
| IdleTimeout: 30 * time.Second, | ||
|
iavael marked this conversation as resolved.
|
||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -156,7 +156,6 @@ type ServerConfig struct { | |
| DocsFS fs.FS | ||
| ToonOutputEnabled bool | ||
| ClientLoggingEnabled bool | ||
| KeepAlive time.Duration | ||
| } | ||
|
|
||
| // NewServer creates a new MCP server using the official Go SDK. | ||
|
|
@@ -204,7 +203,6 @@ func NewServer(ctx context.Context, cfg ServerConfig) (*mcp.Server, *ServerConta | |
| &mcp.ServerOptions{ | ||
| Instructions: instrx, | ||
| Logger: logger.WithGroup("go_sdk_logger"), | ||
| KeepAlive: cfg.KeepAlive, | ||
| Capabilities: caps, | ||
| }, | ||
| ) | ||
|
|
@@ -229,22 +227,20 @@ func NewServer(ctx context.Context, cfg ServerConfig) (*mcp.Server, *ServerConta | |
| return server, container, nil | ||
| } | ||
|
|
||
| // NewStreamableHTTPHandler creates an HTTP handler for the MCP server. | ||
| // NewStreamableHTTPHandler creates an HTTP handler for the MCP server using | ||
| // stateless HTTP transport. In stateless mode, each request is handled | ||
| // independently without session tracking, and responses are returned as | ||
| // application/json rather than text/event-stream. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this comment is also inaccurate |
||
| // It wraps the handler with auth context middleware to forward Authorization headers. | ||
| func NewStreamableHTTPHandler(server *mcp.Server, logger *slog.Logger, sessionTimeout time.Duration) http.Handler { | ||
| if sessionTimeout == 0 { | ||
| // 0 value for session timeout means that sessions never close. | ||
| // Set a default if unset. | ||
| sessionTimeout = 1 * time.Hour | ||
| } | ||
|
|
||
| func NewStreamableHTTPHandler(server *mcp.Server, logger *slog.Logger) http.Handler { | ||
| handler := mcp.NewStreamableHTTPHandler( | ||
| func(r *http.Request) *mcp.Server { | ||
| return server | ||
| }, | ||
| &mcp.StreamableHTTPOptions{ | ||
| SessionTimeout: sessionTimeout, | ||
| Logger: logger, | ||
| Stateless: true, | ||
| JSONResponse: true, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In fact, enabling it actually silently breaks client notification logging -- this switches response away from |
||
| Logger: logger, | ||
| }, | ||
| ) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be reduced to a s/Streamable/Stateless/