@@ -665,7 +665,12 @@ export async function handleMcpRequest(c: AppContext): Promise<Response> {
665665 if ( ! identity ) return c . json ( { error : "unauthorized" } , 401 ) ;
666666
667667 const telemetry = buildMcpClientTelemetry ( c . req . raw . headers , { defaultClientName : "mcp" } ) ! ;
668- const usageMetadata = await describeMcpUsageRequest ( c . req . raw , telemetry . metadata ) ;
668+ // ONE clone-and-parse of the JSON-RPC body, here, BEFORE createMcpHandler below consumes it (#10190).
669+ // A second `request.clone()` after that point throws `TypeError: unusable` -- the Fetch spec forbids
670+ // cloning a request whose body is already disturbed -- which is why the post-response handshake read this
671+ // replaces turned every `initialize` into an unhandled 500.
672+ const envelope = await readMcpRequestEnvelope ( c . req . raw ) ;
673+ const usageMetadata = describeMcpUsageRequest ( envelope , c . req . raw . method , telemetry . metadata ) ;
669674 const startedAt = Date . now ( ) ;
670675 const executionCtx = getExecutionContext ( c ) ;
671676 // #9525: the dispatch chokepoint's sink is built per request so its deferred work rides this
@@ -693,7 +698,7 @@ export async function handleMcpRequest(c: AppContext): Promise<Response> {
693698 // a rejected handshake never inflates the session/client counts.
694699 if ( response . status < 400 ) {
695700 if ( usageMetadata . rpcMethod === "initialize" ) {
696- recordMcpInitialize ( c . env , defer , await readInitializeHandshake ( c . req . raw ) , analyticsContext ) ;
701+ recordMcpInitialize ( c . env , defer , readInitializeHandshake ( envelope ) , analyticsContext ) ;
697702 } else if ( usageMetadata . rpcMethod === "tools/list" ) {
698703 // Names come from this server's own registration chokepoint, not the cross-server contract
699704 // registry, so the event reports what was actually advertised to THIS client.
@@ -765,20 +770,35 @@ function trimmedHeader(value: string | null): string | undefined {
765770 * client. Returns an empty handshake for a malformed or absent body: the fields are optional by
766771 * contract, and a session that connected is still worth counting.
767772 */
768- async function readInitializeHandshake ( request : Request ) : Promise < McpInitializeTelemetry > {
769- const body = await request . clone ( ) . json ( ) . catch ( ( ) => null ) ;
770- if ( ! body || typeof body !== "object" ) return { } ;
771- const clientInfo = ( body as { params ?: { clientInfo ?: { name ?: unknown ; version ?: unknown } } } ) . params ?. clientInfo ;
773+ function readInitializeHandshake ( envelope : McpRequestEnvelope | null ) : McpInitializeTelemetry {
774+ const clientInfo = envelope ?. params ?. clientInfo ;
772775 return {
773776 clientName : typeof clientInfo ?. name === "string" ? clientInfo . name : undefined ,
774777 clientVersion : typeof clientInfo ?. version === "string" ? clientInfo . version : undefined ,
775778 } ;
776779}
777780
778- async function describeMcpUsageRequest ( request : Request , telemetryMetadata : Record < string , unknown > | undefined ) : Promise < Record < string , unknown > > {
781+ /** The JSON-RPC envelope fields the telemetry paths read. Deliberately structural and permissive: this is an
782+ * unvalidated client body, and every consumer below re-checks the type of the field it uses. */
783+ type McpRequestEnvelope = {
784+ method ?: unknown ;
785+ params ?: { name ?: unknown ; clientInfo ?: { name ?: unknown ; version ?: unknown } } ;
786+ } ;
787+
788+ /** Clone-and-parse the request body exactly once, at the top of {@link handleMcpRequest} (#10190). Returns
789+ * null for an absent or malformed body -- the telemetry fields are all optional by contract, and a request
790+ * that is still worth counting must never be failed over its own instrumentation. */
791+ async function readMcpRequestEnvelope ( request : Request ) : Promise < McpRequestEnvelope | null > {
779792 const body = await request . clone ( ) . json ( ) . catch ( ( ) => null ) ;
780- if ( ! body || typeof body !== "object" ) return { transport : "http" , method : request . method , ...telemetryMetadata } ;
781- const envelope = body as { method ?: unknown ; params ?: { name ?: unknown } } ;
793+ return body && typeof body === "object" ? ( body as McpRequestEnvelope ) : null ;
794+ }
795+
796+ function describeMcpUsageRequest (
797+ envelope : McpRequestEnvelope | null ,
798+ method : string ,
799+ telemetryMetadata : Record < string , unknown > | undefined ,
800+ ) : Record < string , unknown > {
801+ if ( ! envelope ) return { transport : "http" , method, ...telemetryMetadata } ;
782802 const rpcMethod = typeof envelope . method === "string" ? envelope . method : undefined ;
783803 const toolName = envelope . params && typeof envelope . params . name === "string" ? envelope . params . name : undefined ;
784804 return {
0 commit comments