fix: tolerate duplicate initialize requests on the same session#2938
Open
christensenjairus wants to merge 1 commit into
Open
fix: tolerate duplicate initialize requests on the same session#2938christensenjairus wants to merge 1 commit into
christensenjairus wants to merge 1 commit into
Conversation
go-sdk v1.7.0-pre.1 (bumped in github#2787) started rejecting a second "initialize" call on an already-initialized session with `duplicate "initialize" received`, fixing modelcontextprotocol/go-sdk#961 (a duplicate initialize with changed params could silently overwrite the session's stored InitializeParams). go-sdk v1.6.1 and earlier (github-mcp-server v1.5.0) accepted repeat calls and returned the same result every time. Some MCP clients resend "initialize" on the same transport/session when a handshake is retried (e.g. a slow first response triggers a client-side retry without tearing down the connection), which relied on that old idempotent behavior and now hard-fails on v1.6.0+. Add a receiving middleware that caches the first successful InitializeResult and replays it for later "initialize" calls on the same server, instead of forwarding to the SDK and hitting its error. The response is otherwise constant for the process's lifetime, so a single cached result (not a per-session cache) is enough and avoids any per-session memory growth. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Author
|
@SamMorrowDrums @IrynaKulakova Could I get a review on this? Asking you two because you did #2787 |
albashaalbasha4454-sudo
approved these changes
Jul 24, 2026
Collaborator
|
@christensenjairus what is your target? Self-hosted http? I ask because this will not fix our remote server in production because we have to do session middleware outside the SDK with persistence in Redis, and I'd have to look at what the impact there is. Let me know please. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
go-sdk v1.7.0-pre.1 (bumped in #2787) started rejecting a second "initialize" call on an already-initialized session with
duplicate "initialize" received, fixing modelcontextprotocol/go-sdk#961 (a duplicate initialize with changed params could silently overwrite the session's stored InitializeParams). go-sdk v1.6.1 and earlier (github-mcp-server v1.5.0) accepted repeat calls and returned the same result every time.Some MCP clients resend "initialize" on the same transport/session when a handshake is retried (e.g. a slow first response triggers a client-side retry without tearing down the connection), which relied on that old idempotent behavior and now hard-fails on v1.6.0+. I found this change in behavior while using Stacklok ToolHive w/ the vMCP feature.
Why
Fixes #(no issues for this yet)
What changed
MCP impact
Prompts tested (tool changes only)
Security / limits
Upstream's issue (go-sdk#961) was that a duplicate initialize with changed parameters could silently overwrite ServerSession.InitializeParams() mid-session — letting a later request quietly change the negotiated protocol version/capabilities that other code relies on. My middleware short-circuits before calling next() whenever the session is already initialized, so the SDK's own ss.initialize() (the code that does the overwrite) never runs for a duplicate call. InitializeParams() stays pinned to whatever the first call sent, for the life of the session — same protection as upstream's fix. The difference is just what the client sees in response: instead of an error, it gets the original negotiated result. A second call with different params is silently ignored, never honored.
Auth interaction: I checked whether OAuth logic hooks into initialize and could be skipped by my short-circuit. On current main it doesn't — OAuth was refactored (PR #2870) into a ToolHandlerMiddleware that runs at tool-call time, not a receiving-middleware on initialize. (On v1.6.0 there was an OAuth receiving-middleware gated on initialize — worth re-checking if this is ever backported to that older base.)
Data exposure: the cached InitializeResult only holds ProtocolVersion, Capabilities, Instructions, ServerInfo — static server config, nothing per-user or secret.
Blast radius: dedupeInitializeMiddleware() is only wired into NewStdioMCPServer. The HTTP/remote path (pkg/http/handler.go) builds its server via a separate factory and never gets this middleware — so there's no scenario where one HTTP client's negotiated capabilities could leak into another's session through this cache. For stdio, there's exactly one session per process, so the single shared (not per-session) cache never crosses a trust boundary.
Tool renaming
deprecated_tool_aliases.goNote: if you're renaming tools, you must add the tool aliases. For more information on how to do so, please refer to the official docs.
Lint & tests
./script/lint./script/testDocs