Appearance
MCP request body consumed before transport
Problem
Production Claude.ai Remote MCP integration returns a generic "Error occurred during tool execution" for every tool call. wrangler tail shows each POST /mcp initialize reply with HTTP 400 (~1.6 s), which means the JSON-RPC handshake never completes — so any subsequent tools/call is impossible. The Claude.ai host reflects the failure as the generic error string above; the real cause is invisible to the end user.
What didn't work
- Assuming it was a tool-handler bug.
ask.ts/categories.tsnever run — the transport layer fails beforetools/calldispatch. Looking at the tool code misleads the investigation. - Assuming it was OAuth / bearer token expiry. Auth middleware logs show 200 for token lookup; the 400 comes after auth succeeds.
- Removing
extractToolNamesfrom middleware. Even without our own read,@nuxtjs/mcp-toolkit'screateMcpHandlerrunstagEvlogContext(event)before the middleware — it also callsreadBody(event)to summarise the JSON-RPC request for evlog. So the stream is consumed regardless of what we do inside our middleware.
Solution
On Cloudflare Workers, event.web.request is the native Request object. When readBody(event) drains its body stream, the downstream MCP transport (providers/node.js → toWebRequest(event) → transport.handleRequest(request)) gets the same disturbed Request and fails await request.json() inside the MCP SDK, returning HTTP 400 / -32700 parse_error.
Rehydrate the request body at the end of our middleware:
ts
// server/utils/mcp-rehydrate-request-body.ts
import type { H3Event } from 'h3'
import { readBody } from 'h3'
export async function rehydrateMcpRequestBody(event: H3Event): Promise<void> {
const web = (event as unknown as { web?: { request?: Request } }).web
const original = web?.request
if (!original) return
if (original.method === 'GET' || original.method === 'HEAD') return
const parsed = await readBody(event) // hits H3 cache, does NOT re-drain
const bodyText =
parsed === undefined || parsed === null
? ''
: typeof parsed === 'string'
? parsed
: JSON.stringify(parsed)
;(event as unknown as { web: { request: Request } }).web.request = new Request(original.url, {
method: original.method,
headers: original.headers,
body: bodyText,
duplex: 'half',
} as RequestInit)
}Call it at the end of defineMcpHandler({ middleware }) in server/mcp/index.ts:
ts
middleware: async (event) => {
await runMcpMiddleware(event, {
/* ... */
})
await rehydrateMcpRequestBody(event as unknown as H3Event)
}Key points:
readBody(event)hits the H3 body cache populated by the earliertagEvlogContext/extractToolNamesreads. It does not try to re-read the already-drained native stream.- The new
Requestbuilt frombodyTexthas a pristine body stream the MCP SDK can consume. duplex: 'half'is required by Workers / Undici when constructing aRequestwith a body.- Live
event.web.requestswap is safe:toWebRequest(event)in@nuxtjs/mcp-toolkitreturnsevent.web.requestdirectly when it's already aRequestinstance.
Prevention
- Upgrade
@nuxtjs/mcp-toolkitcautiously. IftagEvlogContextorextractToolNameschange their body-read behaviour, re-runpnpm test:contracts+ a livewrangler tailafter any bump. - Unit test:
test/unit/mcp-rehydrate-request-body.test.tsasserts that the rehydratedRequestexposes a readable body identical to the cached parse. - Monitor signals:
wrangler tail --format prettyafter any MCP-layer change should showPOST /mcp status 200forinitialize. A 400 here reopens this regression.
References
openspec/changes/fix-mcp-transport-body-consumed/— the fix change.openspec/specs/mcp-knowledge-tools/spec.md— ADDED requirement "MCP handler middleware preserves request body for transport".node_modules/@nuxtjs/mcp-toolkit/dist/runtime/server/mcp/utils.js—tagEvlogContextimplementation (upstream source of firstreadBodycall).