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
Part of epic #8286 (Phase 3 — AI observability). Regression from #10177.
Problem
Every MCP
initializerequest returns 500. The handshake-telemetry read added in #10177 clones the request after the MCP handler has already consumed its body:readInitializeHandshakecallsrequest.clone(). The Fetch spec forbids cloning a request whose body is already disturbed, so this throws:handleMcpRequest's own catch block rethrows, so the request ends asunhandled_route_error→ 500.Why the telemetry gate does not save it
recordMcpInitializeno-ops whenPOSTHOG_API_KEYis unset — but theawait 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
initializeis affected — butinitializeis the first call of every MCP session, so no client can complete a handshake.tools/listis unaffected:recordMcpToolsListreads 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 callsfails onmainat theexpect(telemetryDownInitialize.status).toBe(200)assertion, receiving 500.Deliverables
clientInfohandshake from that single parse.request.clone()anywhere aftercreateMcpHandlerhas run.initializerequest returns 2xx and that the handshake fields still reach the telemetry sink.