Skip to content

fix: tolerate duplicate initialize requests on the same session#2938

Open
christensenjairus wants to merge 1 commit into
github:mainfrom
christensenjairus:fix-duplicate-initialize
Open

fix: tolerate duplicate initialize requests on the same session#2938
christensenjairus wants to merge 1 commit into
github:mainfrom
christensenjairus:fix-duplicate-initialize

Conversation

@christensenjairus

Copy link
Copy Markdown

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

  • Adds 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

MCP impact

  • No tool or API changes
  • Tool schema or behavior changed
  • New tool added

Prompts tested (tool changes only)

Security / limits

  • No security or limits impact
  • Auth / permissions considered
  • Data exposure, filtering, or token/size limits considered

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

  • I am renaming tools as part of this PR (e.g. a part of a consolidation effort)
    • I have added the new tool aliases in deprecated_tool_aliases.go
  • I am not renaming tools as part of this PR

Note: 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

  • Linted locally with ./script/lint
  • Tested locally with ./script/test

Docs

  • Not needed
  • Updated (README / docs / examples)

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>
@christensenjairus
christensenjairus requested a review from a team as a code owner July 23, 2026 13:32
@christensenjairus

Copy link
Copy Markdown
Author

@SamMorrowDrums @IrynaKulakova Could I get a review on this? Asking you two because you did #2787

@SamMorrowDrums

Copy link
Copy Markdown
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.

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.

3 participants