feat(node): export toWebRequest(), the IncomingMessage→Request conversion inside toNodeHandler (#2390)

This commit is contained in:
Felix Weinberger 2026-06-30 11:47:44 +01:00 • committed by GitHub
commit 6cc7b1cb66
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 245 additions and 41 deletions

View file

@ -23,7 +23,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
import { createServer } from 'node:http';
import { parseExampleArgs } from '@mcp-examples/shared';
import { NodeStreamableHTTPServerTransport, toNodeHandler } from '@modelcontextprotocol/node';
import { NodeStreamableHTTPServerTransport, toNodeHandler, toWebRequest } from '@modelcontextprotocol/node';
import type {
CallToolResult,
ElicitRequestFormParams,
@ -276,23 +276,15 @@ if (transport === 'stdio') {
createServer((req, res) => {
void (async () => {
// Read the body once for the predicate and pass it forward.
let body: unknown;
if (req.method === 'POST') {
const chunks: Buffer[] = [];
for await (const chunk of req) chunks.push(chunk as Buffer);
const raw = Buffer.concat(chunks).toString('utf8');
try {
body = raw ? JSON.parse(raw) : undefined;
} catch {
body = undefined;
}
}
const probe = new globalThis.Request(`http://localhost${req.url ?? '/'}`, {
method: req.method,
headers: req.headers as Record<string, string>
});
await ((await isLegacyRequest(probe, body)) ? handleLegacy(req, res, body) : modern(req, res, body));
// `toWebRequest` reads the Node body into a web-standard `Request`,
// so the body now lives in `request`, not `req`. Ask the predicate
// first — it classifies an internal clone, leaving `request`
// readable for the `.json()` both arms need (reading `.json()`
// first would make the predicate's internal clone throw).
const request = await toWebRequest(req);
const legacy = await isLegacyRequest(request);
const body: unknown = req.method === 'POST' ? await request.json().catch(() => {}) : undefined;
await (legacy ? handleLegacy(req, res, body) : modern(req, res, body));
})().catch(error => {
console.error('[server] request error:', error instanceof Error ? error.message : error);
if (!res.headersSent) res.writeHead(500).end();

View file

@ -14,7 +14,7 @@ import { randomUUID } from 'node:crypto';
import { parseExampleArgs } from '@mcp-examples/shared';
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { NodeStreamableHTTPServerTransport, toNodeHandler } from '@modelcontextprotocol/node';
import { NodeStreamableHTTPServerTransport, toNodeHandler, toWebRequest } from '@modelcontextprotocol/node';
import type { McpRequestContext } from '@modelcontextprotocol/server';
import { createMcpHandler, isInitializeRequest, isLegacyRequest, McpServer } from '@modelcontextprotocol/server';
import cors from 'cors';
@ -72,14 +72,10 @@ app.use(
);
app.post('/mcp', async (req: Request, res: Response) => {
// The predicate inspects the same headers + body the entry does. Express
// has parsed the JSON body; pass it as `parsedBody` so the predicate need
// not re-read the stream.
const probe = new globalThis.Request(`http://localhost${req.url}`, {
method: req.method,
headers: req.headers as Record<string, string>
});
await ((await isLegacyRequest(probe, req.body)) ? handleLegacy(req, res) : modernNode(req, res, req.body));
// `toWebRequest` builds the web-standard `Request` the predicate takes.
// Express has already parsed (and consumed) the JSON body — pass it along.
const probe = await toWebRequest(req, req.body);
await ((await isLegacyRequest(probe)) ? handleLegacy(req, res) : modernNode(req, res, req.body));
});
// GET (standalone SSE stream / reconnect with Last-Event-ID) and DELETE
// (explicit session termination per the MCP spec) are sessionful-2025-only —