Skip to content

Server logs a fabricated 'initialize' (with synthesized rmcp/3.8.0 peer identity) for no-initialize SEP-2567 requests, ignoring clientInfo from _meta #1425

Open

Description

@pty819

Summary

On rmcp 3.8.0, a streamable-HTTP server emits a Service initialized as server peer_info=Some(InitializeRequestParams { ... }) INFO log line for every no-initialize (SEP-2567, protocol 2026-07-28) request, as though an initialize handshake had occurred. The logged peer identity is a synthesized Implementation { name: "rmcp", version: "3.8.0" }, and a clientInfo explicitly provided in the request's _meta (io.modelcontextprotocol/clientInfo) is ignored — never surfaced in the log.

This makes server-side logs actively misleading for client-behavior analysis: a sessionless 2.0 client appears in logs as a legacy initialize handshake from an rmcp client.

Environment

  • rmcp 3.8.0, server role, transport-streamable-http-server (StreamableHttpService + LocalSessionManager::default(), axum nest_service("/mcp", ...))
  • Client: any no-initialize 2.0 caller (curl below; also observed with a real third-party client identifying as zcode/0.17.1)

Reproduction

  1. Start any streamable-HTTP server built on rmcp 3.8 (e.g. the counter example) and note its stderr/journal.
  2. Send a no-initialize request (no initialize ever sent):
curl -sS -X POST http://127.0.0.1:8000/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' -H 'Mcp-Method: tools/list' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{
    "io.modelcontextprotocol/protocolVersion":"2026-07-28",
    "io.modelcontextprotocol/clientInfo":{"name":"distinctive-probe-XYZ","version":"9.9.9"},
    "io.modelcontextprotocol/clientCapabilities":{}}}}'
  1. The request succeeds (HTTP 200, tool list returned), and the server logs:
INFO serve_inner: rmcp::service: Service initialized as server peer_info=Some(InitializeRequestParams {
  meta: None,
  protocol_version: ProtocolVersion("2026-07-28"),
  capabilities: ClientCapabilities { ... all None ... },
  client_info: Implementation { name: "rmcp", title: None, version: "3.8.0", ... }
})

Two discrepancies vs. what actually happened on the wire:

  • No initialize request was ever sent; the log fabricates one.
  • The client identified itself as distinctive-probe-XYZ/9.9.9 in _meta; the log shows rmcp/3.8.0.

Real-world impact

We spent three rounds of diagnosis mis-attributing a third-party client's behavior because its sessionless 2.0 requests were logged as initialize handshakes from "rmcp 3.8.0". This included incorrectly concluding the client used a legacy handshake and had a clientInfo of rmcp/3.8.0 (wire capture later disproved both). Details and retraction: zai-org/feedback#1372.

Suggested direction

  • For no-initialize requests, log something that reflects the actual lifecycle (e.g. stateless request served), not Service initialized ... InitializeRequestParams.
  • Surface io.modelcontextprotocol/clientInfo from _meta when present, so per-request identities are observable; if absent, make the synthesized default obviously synthetic (or omit identity from the log).

Happy to provide full tcpdump captures or a minimal repro project if useful.

Activity

  1. added
    bugSomething is not working
    P2Medium: important but non-blocking improvement
    T-serviceService layer changes
    T-transportTransport layer changes
    ready for workIssue is well-defined and ready to be picked up
    on Nov 3, 2026
  2. github-actions commented on Nov 3, 2026

    @github-actions
    Contributor

    @jamadeo is on triage rotation this week and is the reviewer for this one.

    Codex labeled this bug / P2 (T-service, T-transport). Please confirm or correct those labels.

    Automated triage. Labels are a first pass and the assigned maintainer has the final say.

  3. jamadeo commented on Nov 3, 2026

    @jamadeo
    Contributor

    Thanks for the report - yes this is misleading. Logging the identity from _meta makes sense. As for the lifecycle accuracy, I think we could simply make it neutral, e.g. "Server service started"

  4. jokemanfire commented on Nov 8, 2026

    @jokemanfire
    Member

    Reproduced on 3.8.0, and the cause is a synthesized peer identity plus a log message that is written for a handshake that never happened.

    Repro (no server code beyond the default config)

    StreamableHttpService<Calculator, LocalSessionManager> with with_legacy_session_mode(false), one POST with exactly your headers/body (Mcp-Method: tools/list, _meta clientInfo distinctive-probe-XYZ/9.9.9), log level INFO:

    INFO serve_inner: rmcp::service: Service initialized as server peer_info=Some(InitializeRequestParams {
      meta: None, protocol_version: ProtocolVersion("2026-07-28"),
      capabilities: ClientCapabilities { experimental: None, extensions: None, roots: None, sampling: None, elicitation: None },
      client_info: Implementation { name: "rmcp", title: None, version: "3.8.0", description: None, icons: None, website_url: None } })
    INFO serve_inner: rmcp::service: input stream terminated
    status=200 OK
    

    Both of your observations reproduce: no initialize was sent, and the _meta clientInfo never appears.

    Where it comes from

    1. peer_info_for_stateless_request (crates/rmcp/src/transport/streamable_http_server/tower.rs:2432-2451) builds an InitializeRequestParams for any request that is not an initialize: it takes the protocol version from the MCP-Protocol-Version header and hardcodes

      meta: None,
      capabilities: ClientCapabilities::default(),
      client_info: Implementation::default(),

      Implementation::default() is from_build_env() (crates/rmcp/src/model.rs:1700-1703), i.e. rmcp + CARGO_PKG_VERSION — hence rmcp/3.8.0.

    2. That synthesized value is passed to serve_directly_with_ct(..., peer_info, ...) (tower.rs:1537-1545, same for the negotiated path at 2308-2316) and becomes the peer's identity (service.rs:1291).

    3. serve_inner logs it unconditionally when a peer identity exists (service.rs:1351-1353):

      if R::IS_CLIENT { info!(?peer_info, "Service initialized as client") }
      else { info!(?peer_info, "Service initialized as server") }

    Nothing reads _meta even though the accessors exist and are already used for validation nearby: RequestMetaObject::client_info() / client_capabilities() (crates/rmcp/src/model/meta.rs:459-477).

    Options

    A. Read the client context out of _meta (recommended). In peer_info_for_stateless_request:

    let meta = request.request.get_meta();
    Some(InitializeRequestParams {
        meta: Some(meta.clone()),
        protocol_version: version,
        capabilities: meta.client_capabilities().unwrap_or_default(),
        client_info: meta.client_info().unwrap_or_default(),  // or an explicitly synthetic fallback
    })

    Logs become truthful for the SEP-2575 path, and handlers that inspect peer_info() (capabilities, client name) see what the client actually declared. Caveat: clientInfo is optional in the draft schema, so the fallback still needs a decision — I'd use something obviously synthetic (Implementation::new("unknown", "unknown")) rather than rmcp/<version>, so it can never be mistaken for an rmcp client again.

    B. Stop claiming an initialize happened. The message, not just the payload, is the bug: a no-initialize request should log e.g. stateless request served method=tools/list, at INFO or DEBUG. This needs the stateless origin plumbed into serve_inner (a flag, or a small enum next to peer_info) so it can pick the right wording; it is a slightly wider change than A but it is what makes the log self-describing.

    C. Minimal band-aid. Keep the synthesis, swap Implementation::default() for a synthetic identity and demote the line to DEBUG. Smallest diff, but the INFO line still says "Service initialized" if anyone turns DEBUG off... rather: the misleading wording survives for every other log consumer; I'd only take this as a stopgap.

    A + B together are what I'd merge: A removes the wrong identity, B removes the wrong verb. They also make the fix testable — a request with _meta.clientInfo should yield that client_info in peer_info, and a stateless request should not emit "Service initialized".

    Happy to send the PR for A (plus B if you want it in the same change); say the word and I'll put it against main with tests. I have not opened one, since this is your report and the fallback-identity choice is a maintainer call.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P2Medium: important but non-blocking improvementT-serviceService layer changesT-transportTransport layer changesbugSomething is not workingready for workIssue is well-defined and ready to be picked up

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions