Skip to content

fix(transport): propagate real clientInfo to peer_info in stateless mode - #1173

Open
github-gregory-bougeard wants to merge 1 commit into
modelcontextprotocol:mainfrom
github-gregory-bougeard:fix/stateless-client-info-placeholder
Open

fix(transport): propagate real clientInfo to peer_info in stateless mode#1173
github-gregory-bougeard wants to merge 1 commit into
modelcontextprotocol:mainfrom
github-gregory-bougeard:fix/stateless-client-info-placeholder

Conversation

@github-gregory-bougeard

Copy link
Copy Markdown

Summary

Fixes #1172.

In stateless streamable-HTTP mode (StreamableHttpServerConfig::legacy_session_mode = false), peer_info_for_stateless_request reconstructs a synthetic InitializeRequestParams for every request so context.protocol_version() works inside handlers. It correctly reconstructs the protocol version, but always set client_info/capabilities to Implementation::default()/ClientCapabilities::default() — even for the literal initialize request, whose body does contain the real clientInfo.

That placeholder is exactly what Peer::peer_info() returns, and exactly what serve_inner logs via tracing::info!(?peer_info, "Service initialized as server") — so every real client (Claude Code, Gemini CLI, custom clients, ...) shows up identically as client_info: { name: "rmcp", version: "<sdk version>" } in server-side logs in stateless mode, regardless of what they actually sent. Any handler reading context.peer.peer_info() sees the same placeholder, even though the initialize handler's own typed request argument correctly carries the real value.

Fix

For the initialize request specifically, the full InitializeRequestParams is already available in the request body — this PR uses it verbatim instead of only extracting protocol_version out of it. Non-initialize requests are unchanged: there's no session in stateless mode to recover the original handshake from, so they still fall back to the placeholder for client_info/capabilities (only protocol_version is reconstructed, from the MCP-Protocol-Version header).

fn peer_info_for_stateless_request(
    request: &crate::model::JsonRpcRequest<ClientRequest>,
    headers: &HeaderMap,
) -> Option<InitializeRequestParams> {
    if let ClientRequest::InitializeRequest(ref init) = request.request {
        return Some(init.params.clone());
    }
    let version = headers
        .get(HEADER_MCP_PROTOCOL_VERSION)
        .and_then(|v| v.to_str().ok())
        .and_then(|s| serde_json::from_value(serde_json::Value::String(s.to_owned())).ok())
        .unwrap_or(ProtocolVersion::V_2025_03_26);
    Some(InitializeRequestParams {
        meta: None,
        protocol_version: version,
        capabilities: ClientCapabilities::default(),
        client_info: Implementation::default(),
    })
}

Testing

Adds crates/rmcp/tests/test_stateless_client_info.rs: spins up a real StreamableHttpService in stateless JSON-response mode with a handler that echoes both request.client_info (the handler's typed argument) and context.peer.peer_info().map(|p| p.client_info) (what serve_inner logs) back via InitializeResult::instructions, sends a real initialize request with a distinctive clientInfo, and asserts both match the client's real identity.

  • Fails against the pre-fix code (peer_client_info.name == "rmcp" instead of the real client name) — verified locally by reverting just the tower.rs change and re-running.
  • Passes with the fix.
  • Existing test_stateless_protocol_version.rs and test_stateless_server_requests.rs (same module) still pass unchanged — this change doesn't touch the protocol-version reconstruction, only threads through capabilities/client_info alongside it for the initialize case.
  • cargo clippy -p rmcp --features server,client,transport-streamable-http-server,reqwest --lib produces the same pre-existing warning set as main (verified via git stash diff), no new warnings from this change.
  • cargo +nightly fmt -p rmcp -- --check clean.

Scope

Deliberately narrow: only the initialize branch changes. Non-initialize requests in stateless mode still can't recover the original client identity (no session to persist it in), so their client_info/capabilities placeholder 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.

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.
@github-gregory-bougeard
github-gregory-bougeard requested a review from a team as a code owner August 13, 2026 17:16
@github-actions github-actions Bot added T-dependencies Dependencies related changes T-test Testing related changes T-config Configuration file changes T-core Core library changes T-transport Transport layer changes labels Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-config Configuration file changes T-core Core library changes T-dependencies Dependencies related changes T-test Testing related changes T-transport Transport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stateless streamable-HTTP logs a placeholder client_info ("rmcp") instead of the real client identity

1 participant