Skip to content

mcp: every initialize returns 500 — the handshake telemetry clones the request body after the handler consumed it #10190

Description

@JSONbored

Part of epic #8286 (Phase 3 — AI observability). Regression from #10177.

Problem

Every MCP initialize request returns 500. The handshake-telemetry read added in #10177 clones the request after the MCP handler has already consumed its body:

const response = await createMcpHandler(server, )(c.req.raw, c.env, executionCtx);   // consumes the body
if (response.status < 400) {
  if (usageMetadata.rpcMethod === "initialize") {
    recordMcpInitialize(c.env, defer, await readInitializeHandshake(c.req.raw), analyticsContext);
  }
}

readInitializeHandshake calls request.clone(). The Fetch spec forbids cloning a request whose body is already disturbed, so this throws:

TypeError: unusable
    at _Request.clone (node:internal/deps/undici/undici:10293:17)
    at readInitializeHandshake (src/mcp/server.ts:769:30)
    at handleMcpRequest (src/mcp/server.ts:696:49)

handleMcpRequest's own catch block rethrows, so the request ends as unhandled_route_error → 500.

Why the telemetry gate does not save it

recordMcpInitialize no-ops when POSTHOG_API_KEY is unset — but the await readInitializeHandshake(...) is an argument, evaluated before the call. The throw happens whether or not telemetry is configured.

Impact

The handler had already produced a correct 2xx MCP response; it is discarded and replaced with a 500 purely by the instrumentation that runs after it. Only initialize is affected — but initialize is the first call of every MCP session, so no client can complete a handshake.

tools/list is unaffected: recordMcpToolsList reads the server's own registered tool names and never touches the request body.

Reproduction

test/integration/api.test.ts > api routes > serves private MCP tool listing and tool calls fails on main at the expect(telemetryDownInitialize.status).toBe(200) assertion, receiving 500.

Deliverables

  • Read the JSON-RPC body once, before the handler consumes it, and derive both the usage metadata and the clientInfo handshake from that single parse.
  • No request.clone() anywhere after createMcpHandler has run.
  • Regression test asserting an initialize request returns 2xx and that the handshake fields still reach the telemetry sink.

Metadata

Metadata

Assignees

Labels

maintainer-onlyOwner-only work — yields no Gittensor points.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions