Repository navigation
fix: server/discover probe must not commit the negotiated protocol version - #1956
Open
PrathmeshPawar2110 wants to merge 1 commit into
Open
PrathmeshPawar2110 wants to merge 1 commit into
PrathmeshPawar2110 wants to merge 1 commit into
Conversation
…rsion A dual-path stdio client sends a server/discover probe and, on timeout, falls back to initialize on the same connection. If the server processes the (still in-flight) probe first, SetNegotiatedProtocolVersion latched the session to 2026-07-28, and the subsequent initialize was then rejected with "Method 'initialize' is not available on protocol version '2026-07-28'" even though the client never saw the probe succeed. If the race went the other way, the session silently downgraded instead with no error at all. A discovery probe is just that — a probe, not a commitment. Only an ordinary modern request (one actually relying on the negotiated version) should latch the session: - McpServerImpl.ReadRequestMetadata no longer calls SetNegotiatedProtocolVersion for a server/discover request when no version is established yet; ValidateInitializeRequestBoundary's comment is updated to match (a discovery probe, successful or not, never establishes a version). - SendListChangedNotificationAsync's legacy-broadcast gate now checks McpProtocolVersions.SupportsInitializeHandshake(_negotiatedProtocolVersion) instead of IsJuly2026OrLaterProtocol(), so an uncommitted probe isn't treated as an initialize-handshake (legacy) client and doesn't get a blind session-wide broadcast it was never entitled to. Tests: - NegotiatedProtocolVersionTests: a server/discover request leaves NegotiatedProtocolVersion null; a subsequent request still establishes it. - RawStreamConformanceTests: a successful (not just failed) discover probe no longer blocks a same-connection initialize fallback; Initialize_AfterEstablishedModernRequest_IsRejected now covers both tools/list and a prior discover probe as the establishing event; a new test confirms a tool list-changed notification isn't broadcast to a session that has only sent an uncommitted discover probe. Documented the race and the DiscoverProbeTimeout workaround in docs/concepts/stateless/stateless.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This branch has not been deployed
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.
Problem
A dual-path stdio client sends a
server/discoverprobe and, on timeout, falls back toinitializeon the same connection. If the server processes the (still in-flight) probe first,SetNegotiatedProtocolVersionlatched the session to2026-07-28, and the subsequentinitializewas then rejected:even though the client never actually saw the probe succeed (it had already timed out). If the race went the other way, the session silently downgraded instead, with no error at all.
Repro: a 2.3.0 stdio server that does ~6s of work before
RunAsync, connected with defaultMcpClientOptions. The outcome depends on thread-pool ordering.Fix
A discovery probe is just that — a probe, not a commitment. Only an ordinary modern request (one actually relying on the negotiated version) should latch the session:
McpServerImpl.ReadRequestMetadatano longer callsSetNegotiatedProtocolVersionfor aserver/discoverrequest when no version is established yet.ValidateInitializeRequestBoundary's comment is updated to match: a discovery probe, successful or not, never establishes a version.SendListChangedNotificationAsync's legacy-broadcast gate now checksMcpProtocolVersions.SupportsInitializeHandshake(_negotiatedProtocolVersion)instead ofIsJuly2026OrLaterProtocol(), so an uncommitted probe isn't treated as an initialize-handshake (legacy) client and doesn't get a blind session-wide broadcast it was never entitled to.Documented the race and the
DiscoverProbeTimeoutworkaround indocs/concepts/stateless/stateless.md.Testing
NegotiatedProtocolVersionTests: aserver/discoverrequest leavesNegotiatedProtocolVersionnull; a subsequent request still establishes it.RawStreamConformanceTests: a successful (not just failed) discover probe no longer blocks a same-connectioninitializefallback;Initialize_AfterEstablishedModernRequest_IsRejectednow covers bothtools/listand a prior discover probe as the establishing event; a new test confirms a tool list-changed notification isn't broadcast to a session that has only sent an uncommitted discover probe.Fixes #1954
Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com