-
Notifications
You must be signed in to change notification settings - Fork 566
mcp: choose the interaction pattern from the negotiated protocol version #1266
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
Open
jmrplens
wants to merge
6
commits into
modelcontextprotocol:main
Choose a base branch
from
jmrplens:jmrp-mrtr-uses-the-negotiated-version
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+381
−10
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
e0a971f
mcp: choose the interaction pattern from the negotiated protocol version
jmrplens 6759c16
mcp: classify notification delivery by the negotiated protocol version
jmrplens b6f3c55
mcp: rename the predicate to negotiatedLegacyProtocol
jmrplens da8f87c
mcp: restate protocolVersion after #1274 and shorten comments
jmrplens 85f855f
Merge branch 'main' into jmrp-mrtr-uses-the-negotiated-version
guglielmo-san bb0e7c5
Merge branch 'main' into jmrp-mrtr-uses-the-negotiated-version
jmrplens File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
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.
what do you think about setting the
ss.state.NegotiatedProtocolVersionalso in case of new protocol version?Uh oh!
There was an error while loading. Please reload this page.
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.
(ignore this) Parts of this are factually wrong; see the correction below in this thread.
I think it is the right direction and that it should not be this PR, because doing it properly changes behaviour rather than tidying a field.
Four places record a version today, and only one of them negotiates:
initializerecords both, andNegotiatedProtocolVersionis the result ofnegotiatedVersion(params.ProtocolVersion, ...).handlerecordsInitializeParamsfromvalidatedMeta.initializeParamson the first new-protocol call. That is the version the client declared in_meta.server/discoverrecordsInitializeParamswith the version it was asked about.streamable.gosynthesizesInitializeParamsfrom theMCP-Protocol-Versionheader, for old-protocol requests with no handshake.So writing the declared version into
NegotiatedProtocolVersionat the other three would put a value in a field whose name says it was agreed by both sides, when nobody agreed anything. That is the same confusion this PR is about, one field further in.Running it through
negotiatedVersionfirst would make it true, and then the fallback inprotocolVersion()could go. What stops me proposing it here is that negotiation has a second half:initializetells the client what it got, inInitializeResult.ProtocolVersion. None of the other three has a handshake response to say it in. A client that declared a version we downgrade would be served the older behaviour and never be told, which is worse than the current state, where the declared version is at least recorded as declared.What I would suggest instead, as its own change: decide what those paths do when the declared version is one the server does not support at all.
initializeanswers with the closest supported version. The_metapath currently accepts whatever arrives. Refusing the call with an error is the honest equivalent of a downgrade the client can see, and once that is settled, recording the negotiated version at all four places is a mechanical follow-up and the fallback disappears.If you would rather have the field written at all four now and treat the "declared but unsupported" question later, say so and I will push it here. My preference is to leave
protocolVersion()preferring the negotiated value and falling back to the declared one, which reads as what it is: one place negotiates, the others record what they were told.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.
Two corrections to my answer above, both in your favour.
I wrote that "the
_metapath currently accepts whatever arrives". It does not.ServerSession.handlerefuses a_metaversion outsidess.server.protocolVersionswithCodeUnsupportedProtocolVersionbefore anything is recorded, and #1268 routes an unrecognised string into that same check. The streamable header path answers an unsupported legacy header with 400 as the transport spec requires, andserver/discoveris a new-protocol call behind the same check, whose refusal carriesSupported, which is how a SEP-2575 client learns the list. So the "decide what an unsupported declared version does" step I proposed as a prerequisite is already done everywhere it applies.initializeis the one path that downgrades instead of refusing, and the lifecycle spec requires exactly that.Which means your suggestion is smaller than I made it sound: recording the negotiated version at the other three places is available now, and I was wrong to put a prerequisite in front of it.
The second correction is to my own conclusion. I implied that once the field is set everywhere, the fallback in
protocolVersion()could go. It cannot.ServerSessionOptions.Stateis exported, so a caller may supplyInitializeParamswith no negotiated version, and state persisted before #1199 has none either. The fallback is what reads those correctly and should stay.One thing that turned up while checking, which is an argument for doing this rather than a detail of it:
ioConn.sessionUpdatedreads onlyNegotiatedProtocolVersion, so over stdio a SEP-2575 session is currently treated as 2025-03-26 and accepts JSON-RPC batches that 2025-06-18 and later forbid. Recording the version on the_metapath fixes that as a side effect; the streamable handler already refuses them from the header.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 is implemented and open as #1274, with #1272 as the issue behind it. It is based on main rather than on this branch, so it does not wait on this one.
Since the question was raised here, the offer stands the other way round too: if you would rather have it inside this pull request, say so and I will fold it in and close #1274. It is one commit.
Either way there is a small follow-up between the two, which I would rather name than leave for a rebase to surface. The doc comment
protocolVersion()carries on this branch says a SEP-2575 session and a synthesized streamable state hold their version inInitializeParamsalone. That stops being true once #1274 lands, on either path. The fallback itself stays, for a caller-suppliedServerSessionOptions.Stateand for state persisted before #1199; only the sentence needs a touch.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.
#1274 has merged, so I rebased this branch onto main and restated the doc comment of
protocolVersion()inda8f87c: SEP-2575 and synthesized sessions now recordNegotiatedProtocolVersiontoo, so the fallback to the declared version only covers a caller-suppliedServerSessionOptions.Stateand state persisted before #1199. I also cut every comment this PR adds to three lines, and updated the description to match.