Skip to content

mcp: add StreamableHTTPHandler.Close for graceful shutdown#1001

Open
piyushbag wants to merge 1 commit into
modelcontextprotocol:mainfrom
piyushbag:fix-440-streamable-handler-close
Open

mcp: add StreamableHTTPHandler.Close for graceful shutdown#1001
piyushbag wants to merge 1 commit into
modelcontextprotocol:mainfrom
piyushbag:fix-440-streamable-handler-close

Conversation

@piyushbag

@piyushbag piyushbag commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Problem

StreamableHTTPHandler holds stateful sessions in an internal map but had no public way to shut them down. Callers embedding the handler in an HTTP server (or running it in tests) could not reliably tear down all MCP sessions or block new ones during shutdown. The issue author proposed a Close() method; the codebase already had a private closeAll() helper with a TODO noting that a public API must also prevent new sessions from being added.

Approach

  • Add Close() error on StreamableHTTPHandler: set a closed flag, snapshot and clear the sessions map under lock, then call session.Close() on each entry outside the lock (avoids deadlocking with session onClose callbacks).
  • Reject new traffic in ServeHTTP with 503 Service Unavailable once closed.
  • Make Close idempotent (second call is a no-op).
  • Keep the existing test-only closeAll() as a thin delegate to Close().
  • Add TestStreamableHTTPHandlerClose covering session teardown, empty map, idempotency, and 503 after close.

Why this is better

  • Matches the API shape requested in the issue (Close() error, closes all sessions, prevents new sessions).
  • Enables idiomatic cleanup: defer handler.Close() in main or test teardown without reaching into unexported fields.
  • Minimal diff: extends existing session lifecycle patterns rather than introducing callbacks or new types.
  • Aligns with graceful-shutdown patterns in other MCP SDKs (explicit handler/session disposal) while staying Go-native.

Fixes #440

@piyushbag

Copy link
Copy Markdown
Contributor Author

Local verification on fix-440-streamable-handler-close (78d7176):

go test ./... -count=1
ok  github.com/modelcontextprotocol/go-sdk/mcp
ok  github.com/modelcontextprotocol/go-sdk/oauthex

go test -race ./mcp/... -run TestStreamableHTTPHandlerClose -count=1
ok  github.com/modelcontextprotocol/go-sdk/mcp

This matches the shutdown shape discussed in #440. Let me know if you want Close to return the first session error vs. swallowing after best-effort teardown.

@piyushbag piyushbag force-pushed the fix-440-streamable-handler-close branch from 78d7176 to 1da2ef5 Compare June 16, 2026 10:44
@piyushbag

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (includes #1000) and force-pushed 1da2ef5.

Local gate:

go test ./... -count=1
ok  github.com/modelcontextprotocol/go-sdk/mcp
ok  github.com/modelcontextprotocol/go-sdk/oauthex

GitHub CI on this push: lint, test 1.25/1.26, race, client/server conformance, CodeQL — all green.

Still happy to align with whatever came out of the proposal meeting if tying cleanup to http.Server shutdown is preferred over a public Close() on the handler.

@piyushbag

Copy link
Copy Markdown
Contributor Author

@findleyr When you have a moment: #1001 is rebased on current main and GitHub CI is green on 1da2ef5 (lint, test, race, conformance, CodeQL). Still the same Close() shape from the issue unless the proposal meeting pointed toward tying teardown to http.Server shutdown instead.

Expose public Close() to tear down all sessions and reject new requests
with 503, matching the API proposed in modelcontextprotocol#440.
@piyushbag piyushbag force-pushed the fix-440-streamable-handler-close branch from 1da2ef5 to 5b8c780 Compare June 17, 2026 14:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: add StreamableHTTPHandler.Close

1 participant