fix(transport): propagate real clientInfo to peer_info in stateless mode - #1173
Open
github-gregory-bougeard wants to merge 1 commit into
Conversation
peer_info_for_stateless_request always attached Implementation::default() (the rmcp crate's own name/version) to the synthesized Peer for every stateless streamable-HTTP request, discarding the real client_info the client sent in its initialize body. This is what serve_inner logs as "Service initialized as server"/"as client", so every real client showed up identically as "rmcp"/"<sdk version>" in server logs, and any handler reading context.peer.peer_info() saw the same placeholder instead of the real identity — even though the initialize handler's own typed argument correctly carried the real value all along. For the initialize request specifically, the full InitializeRequestParams (protocol version, capabilities, and client_info) is already available in the request body, so use it verbatim instead of only extracting the protocol version. Other requests still fall back to the placeholder, since stateless mode has no session to recover the original handshake from. Adds test_stateless_client_info.rs, which fails against the previous behavior and passes with this fix.
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
Fixes #1172.
In stateless streamable-HTTP mode (
StreamableHttpServerConfig::legacy_session_mode = false),peer_info_for_stateless_requestreconstructs a syntheticInitializeRequestParamsfor every request socontext.protocol_version()works inside handlers. It correctly reconstructs the protocol version, but always setclient_info/capabilitiestoImplementation::default()/ClientCapabilities::default()— even for the literalinitializerequest, whose body does contain the realclientInfo.That placeholder is exactly what
Peer::peer_info()returns, and exactly whatserve_innerlogs viatracing::info!(?peer_info, "Service initialized as server")— so every real client (Claude Code, Gemini CLI, custom clients, ...) shows up identically asclient_info: { name: "rmcp", version: "<sdk version>" }in server-side logs in stateless mode, regardless of what they actually sent. Any handler readingcontext.peer.peer_info()sees the same placeholder, even though theinitializehandler's own typedrequestargument correctly carries the real value.Fix
For the
initializerequest specifically, the fullInitializeRequestParamsis already available in the request body — this PR uses it verbatim instead of only extractingprotocol_versionout of it. Non-initializerequests are unchanged: there's no session in stateless mode to recover the original handshake from, so they still fall back to the placeholder forclient_info/capabilities(onlyprotocol_versionis reconstructed, from theMCP-Protocol-Versionheader).Testing
Adds
crates/rmcp/tests/test_stateless_client_info.rs: spins up a realStreamableHttpServicein stateless JSON-response mode with a handler that echoes bothrequest.client_info(the handler's typed argument) andcontext.peer.peer_info().map(|p| p.client_info)(whatserve_innerlogs) back viaInitializeResult::instructions, sends a realinitializerequest with a distinctiveclientInfo, and asserts both match the client's real identity.peer_client_info.name == "rmcp"instead of the real client name) — verified locally by reverting just thetower.rschange and re-running.test_stateless_protocol_version.rsandtest_stateless_server_requests.rs(same module) still pass unchanged — this change doesn't touch the protocol-version reconstruction, only threads throughcapabilities/client_infoalongside it for theinitializecase.cargo clippy -p rmcp --features server,client,transport-streamable-http-server,reqwest --libproduces the same pre-existing warning set asmain(verified viagit stashdiff), no new warnings from this change.cargo +nightly fmt -p rmcp -- --checkclean.Scope
Deliberately narrow: only the
initializebranch changes. Non-initializerequests in stateless mode still can't recover the original client identity (no session to persist it in), so theirclient_info/capabilitiesplaceholder is unchanged — fixing that would need a broader design discussion (e.g. an explicit "this peer_info is synthetic" marker, or per-request capability propagation per SEP-2575) that felt out of scope for this fix.