@modelcontextprotocol/node
Advanced tools
+156
-3
@@ -1,7 +0,66 @@ | ||
| import { AuthInfo, JSONRPCMessage, MessageExtraInfo, RequestId, Transport, WebStandardStreamableHTTPServerTransportOptions } from "@modelcontextprotocol/server"; | ||
| import { AuthInfo, JSONRPCMessage, McpHandlerRequestOptions, MessageExtraInfo, RequestId, Transport, WebStandardStreamableHTTPServerTransportOptions } from "@modelcontextprotocol/server"; | ||
| import { IncomingMessage, ServerResponse } from "node:http"; | ||
| //#region src/streamableHttp.d.ts | ||
| //#region src/middleware/hostHeaderValidation.d.ts | ||
| /** | ||
| * Node.js request guard for DNS rebinding protection. | ||
| * Validates the `Host` header hostname (port-agnostic) against an allowed list. | ||
| * | ||
| * Unlike the framework adapters, plain `node:http` has no middleware chain, so | ||
| * the guard returns whether the request may proceed: when it returns `false` | ||
| * it has already answered the request with a `403` JSON-RPC error and the | ||
| * caller must not handle it further. | ||
| * | ||
| * @param allowedHostnames - List of allowed hostnames (without ports). | ||
| * For IPv6, provide the address with brackets (e.g., `[::1]`). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const validateHost = hostHeaderValidation(['localhost', '127.0.0.1', '[::1]']); | ||
| * http.createServer((req, res) => { | ||
| * if (!validateHost(req, res)) return; | ||
| * void transport.handleRequest(req, res); | ||
| * }); | ||
| * ``` | ||
| */ | ||
| declare function hostHeaderValidation(allowedHostnames: string[]): (req: IncomingMessage, res: ServerResponse) => boolean; | ||
| /** | ||
| * Convenience guard for localhost DNS rebinding protection. | ||
| * Allows only `localhost`, `127.0.0.1`, and `[::1]` (IPv6 localhost) hostnames. | ||
| */ | ||
| declare function localhostHostValidation(): (req: IncomingMessage, res: ServerResponse) => boolean; | ||
| //#endregion | ||
| //#region src/middleware/originValidation.d.ts | ||
| /** | ||
| * Node.js request guard for Origin header validation. | ||
| * Validates the `Origin` header hostname (port-agnostic) against an allowed list. | ||
| * | ||
| * Requests without an `Origin` header pass (non-browser MCP clients do not send | ||
| * one); a present value that is not allowed, or that cannot be parsed, is | ||
| * rejected with `403`. The guard returns whether the request may proceed: when | ||
| * it returns `false` it has already answered the request and the caller must | ||
| * not handle it further. | ||
| * | ||
| * @param allowedOriginHostnames - List of allowed origin hostnames (without scheme or port). | ||
| * For IPv6, provide the address with brackets (e.g., `[::1]`). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const validateOrigin = originValidation(['localhost', '127.0.0.1', '[::1]']); | ||
| * http.createServer((req, res) => { | ||
| * if (!validateOrigin(req, res)) return; | ||
| * void transport.handleRequest(req, res); | ||
| * }); | ||
| * ``` | ||
| */ | ||
| declare function originValidation(allowedOriginHostnames: string[]): (req: IncomingMessage, res: ServerResponse) => boolean; | ||
| /** | ||
| * Convenience guard for localhost Origin validation. | ||
| * Allows only origins whose hostname is `localhost`, `127.0.0.1`, or `[::1]` (IPv6 localhost). | ||
| */ | ||
| declare function localhostOriginValidation(): (req: IncomingMessage, res: ServerResponse) => boolean; | ||
| //#endregion | ||
| //#region src/streamableHttp.d.ts | ||
| /** | ||
| * Configuration options for {@linkcode NodeStreamableHTTPServerTransport} | ||
@@ -95,2 +154,10 @@ * | ||
| /** | ||
| * Forwards the supported protocol versions to the wrapped Web Standard | ||
| * transport for `MCP-Protocol-Version` header validation. Called by the | ||
| * protocol layer during connect; without this delegation a server's | ||
| * `supportedProtocolVersions` option never reached the Node adapter's | ||
| * header validation. | ||
| */ | ||
| setSupportedProtocolVersions(versions: string[]): void; | ||
| /** | ||
| * Handles an incoming HTTP request, whether `GET` or `POST`. | ||
@@ -121,3 +188,89 @@ * | ||
| //#endregion | ||
| export { NodeStreamableHTTPServerTransport, StreamableHTTPServerTransportOptions }; | ||
| //#region src/toNodeHandler.d.ts | ||
| /** | ||
| * Minimal duck-typed shape of a Node.js `IncomingMessage` accepted by | ||
| * {@linkcode toNodeHandler}. Kept structural so the adapter stays free of | ||
| * `node:` imports. | ||
| */ | ||
| interface NodeIncomingMessageLike extends AsyncIterable<unknown> { | ||
| method?: string; | ||
| url?: string; | ||
| headers: Record<string, string | string[] | undefined>; | ||
| /** Validated authentication info attached by upstream middleware (pass-through). */ | ||
| auth?: AuthInfo; | ||
| } | ||
| /** Minimal duck-typed shape of a Node.js `ServerResponse` accepted by {@linkcode toNodeHandler}. */ | ||
| interface NodeServerResponseLike { | ||
| writeHead(statusCode: number, headers?: Record<string, string>): unknown; | ||
| write(chunk: string | Uint8Array): unknown; | ||
| end(chunk?: string | Uint8Array): unknown; | ||
| on(event: string, listener: (...args: unknown[]) => void): unknown; | ||
| destroyed?: boolean; | ||
| } | ||
| /** | ||
| * The web-standard fetch face of an `McpHttpHandler` (or any | ||
| * fetch-shaped MCP handler) — the only surface {@linkcode toNodeHandler} | ||
| * touches. Accepting the face structurally keeps the adapter usable with | ||
| * hand-wired compositions that route over `isLegacyRequest` and produce a | ||
| * `Response` directly. | ||
| */ | ||
| interface FetchLikeMcpHandler { | ||
| fetch: (request: Request, options?: McpHandlerRequestOptions) => Promise<Response>; | ||
| } | ||
| /** | ||
| * A Node.js `(req, res, parsedBody?)` request handler produced by | ||
| * {@linkcode toNodeHandler}. The third argument is an optional pre-parsed body | ||
| * (`req.body` from `express.json()`); a function third argument (Express's | ||
| * `next` when the handler is mounted as middleware) is ignored. | ||
| */ | ||
| type NodeMcpRequestHandler = (req: NodeIncomingMessageLike, res: NodeServerResponseLike, parsedBody?: unknown) => Promise<void>; | ||
| /** Options for {@linkcode toNodeHandler}. */ | ||
| interface ToNodeHandlerOptions { | ||
| /** | ||
| * Called when the adapter answers `500` because request conversion or | ||
| * `handler.fetch` itself threw (e.g. a closed handler). Restores the | ||
| * observability the removed `.node` face had via the entry's own | ||
| * `onerror` — entry-internal failures are still reported through | ||
| * `handler.fetch` and surface via the entry's `onerror` option as before. | ||
| */ | ||
| onerror?: (error: Error) => void; | ||
| } | ||
| /** | ||
| * Adapts a web-standard MCP handler (`handler.fetch`) to a Node.js | ||
| * `(req, res, parsedBody?)` request handler. The returned function converts the | ||
| * Node request to a web-standard `Request`, calls `handler.fetch`, then writes | ||
| * the `Response` back to `res` (honoring write backpressure for streamed SSE | ||
| * responses). | ||
| * | ||
| * `req.auth` is forwarded as the handler's pass-through `authInfo`. A function | ||
| * third argument (Express's `next`) is ignored, never treated as a body. | ||
| * | ||
| * Pass `{ onerror }` to observe the adapter-level error fallback (request | ||
| * conversion / `handler.fetch` throw) before the `500` response is written. | ||
| */ | ||
| declare function toNodeHandler(handler: FetchLikeMcpHandler, opts?: ToNodeHandlerOptions): NodeMcpRequestHandler; | ||
| /** Options for {@linkcode toWebRequest}. */ | ||
| interface ToWebRequestOptions { | ||
| /** An `AbortSignal` to attach to the constructed `Request` (`request.signal`). */ | ||
| signal?: AbortSignal; | ||
| } | ||
| /** | ||
| * Convert a Node.js `IncomingMessage` (duck-typed — an Express `req` works) to | ||
| * the web-standard `Request` that `handler.fetch()` and `isLegacyRequest()` | ||
| * take. This is the conversion {@linkcode toNodeHandler} performs internally, | ||
| * exported for hand-wired compositions: | ||
| * | ||
| * ```ts | ||
| * const probe = await toWebRequest(req, req.body); | ||
| * await ((await isLegacyRequest(probe)) ? legacy(req, res) : modern(req, res, req.body)); | ||
| * ``` | ||
| * | ||
| * With no `parsedBody` the Node stream is read to completion — read the body | ||
| * from the returned `Request` afterwards, not from `req`. When a body parser | ||
| * already consumed the stream (`express.json()`), pass the parsed value as | ||
| * `parsedBody` and nothing is read from `req`. | ||
| */ | ||
| declare function toWebRequest(req: NodeIncomingMessageLike, parsedBody?: unknown, options?: ToWebRequestOptions): Promise<Request>; | ||
| //#endregion | ||
| export { type FetchLikeMcpHandler, type NodeIncomingMessageLike, type NodeMcpRequestHandler, type NodeServerResponseLike, NodeStreamableHTTPServerTransport, StreamableHTTPServerTransportOptions, type ToNodeHandlerOptions, type ToWebRequestOptions, hostHeaderValidation, localhostHostValidation, localhostOriginValidation, originValidation, toNodeHandler, toWebRequest }; | ||
| //# sourceMappingURL=index.d.mts.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.d.mts","names":[],"sources":["../src/streamableHttp.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;AA4JwB,KAjIZ,oCAAA,GAAuC,+CAiI3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cApFX,iCAAA,YAA6C;;;;wBAMjC;;;;;;;;;;;;;gCAyCS;0BAIN;;;;oCAOU,wBAAwB;8BAI9B,wBAAwB;;;;;WAQrC;;;;WAOA;;;;gBAOK;uBAA+C;MAAc;;;;;;;;;;;qBAcxD;WAA2B;UAAiB,uCAAuC;;;;;;4BA4BlF"} | ||
| {"version":3,"file":"index.d.mts","names":[],"sources":["../src/middleware/hostHeaderValidation.ts","../src/middleware/originValidation.ts","../src/streamableHttp.ts","../src/toNodeHandler.ts"],"sourcesContent":[],"mappings":"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"} |
+247
-2
@@ -0,4 +1,99 @@ | ||
| import { WebStandardStreamableHTTPServerTransport, localhostAllowedHostnames, localhostAllowedOrigins, validateHostHeader, validateOriginHeader } from "@modelcontextprotocol/server"; | ||
| import { getRequestListener } from "@hono/node-server"; | ||
| import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/server"; | ||
| //#region src/middleware/hostHeaderValidation.ts | ||
| /** | ||
| * Node.js request guard for DNS rebinding protection. | ||
| * Validates the `Host` header hostname (port-agnostic) against an allowed list. | ||
| * | ||
| * Unlike the framework adapters, plain `node:http` has no middleware chain, so | ||
| * the guard returns whether the request may proceed: when it returns `false` | ||
| * it has already answered the request with a `403` JSON-RPC error and the | ||
| * caller must not handle it further. | ||
| * | ||
| * @param allowedHostnames - List of allowed hostnames (without ports). | ||
| * For IPv6, provide the address with brackets (e.g., `[::1]`). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const validateHost = hostHeaderValidation(['localhost', '127.0.0.1', '[::1]']); | ||
| * http.createServer((req, res) => { | ||
| * if (!validateHost(req, res)) return; | ||
| * void transport.handleRequest(req, res); | ||
| * }); | ||
| * ``` | ||
| */ | ||
| function hostHeaderValidation(allowedHostnames) { | ||
| return (req, res) => { | ||
| const result = validateHostHeader(req.headers.host, allowedHostnames); | ||
| if (result.ok) return true; | ||
| res.writeHead(403, { "Content-Type": "application/json" }); | ||
| res.end(JSON.stringify({ | ||
| jsonrpc: "2.0", | ||
| error: { | ||
| code: -32e3, | ||
| message: result.message | ||
| }, | ||
| id: null | ||
| })); | ||
| return false; | ||
| }; | ||
| } | ||
| /** | ||
| * Convenience guard for localhost DNS rebinding protection. | ||
| * Allows only `localhost`, `127.0.0.1`, and `[::1]` (IPv6 localhost) hostnames. | ||
| */ | ||
| function localhostHostValidation() { | ||
| return hostHeaderValidation(localhostAllowedHostnames()); | ||
| } | ||
| //#endregion | ||
| //#region src/middleware/originValidation.ts | ||
| /** | ||
| * Node.js request guard for Origin header validation. | ||
| * Validates the `Origin` header hostname (port-agnostic) against an allowed list. | ||
| * | ||
| * Requests without an `Origin` header pass (non-browser MCP clients do not send | ||
| * one); a present value that is not allowed, or that cannot be parsed, is | ||
| * rejected with `403`. The guard returns whether the request may proceed: when | ||
| * it returns `false` it has already answered the request and the caller must | ||
| * not handle it further. | ||
| * | ||
| * @param allowedOriginHostnames - List of allowed origin hostnames (without scheme or port). | ||
| * For IPv6, provide the address with brackets (e.g., `[::1]`). | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const validateOrigin = originValidation(['localhost', '127.0.0.1', '[::1]']); | ||
| * http.createServer((req, res) => { | ||
| * if (!validateOrigin(req, res)) return; | ||
| * void transport.handleRequest(req, res); | ||
| * }); | ||
| * ``` | ||
| */ | ||
| function originValidation(allowedOriginHostnames) { | ||
| return (req, res) => { | ||
| const result = validateOriginHeader(req.headers.origin, allowedOriginHostnames); | ||
| if (result.ok) return true; | ||
| res.writeHead(403, { "Content-Type": "application/json" }); | ||
| res.end(JSON.stringify({ | ||
| jsonrpc: "2.0", | ||
| error: { | ||
| code: -32e3, | ||
| message: result.message | ||
| }, | ||
| id: null | ||
| })); | ||
| return false; | ||
| }; | ||
| } | ||
| /** | ||
| * Convenience guard for localhost Origin validation. | ||
| * Allows only origins whose hostname is `localhost`, `127.0.0.1`, or `[::1]` (IPv6 localhost). | ||
| */ | ||
| function localhostOriginValidation() { | ||
| return originValidation(localhostAllowedOrigins()); | ||
| } | ||
| //#endregion | ||
| //#region src/streamableHttp.ts | ||
@@ -115,2 +210,12 @@ /** | ||
| /** | ||
| * Forwards the supported protocol versions to the wrapped Web Standard | ||
| * transport for `MCP-Protocol-Version` header validation. Called by the | ||
| * protocol layer during connect; without this delegation a server's | ||
| * `supportedProtocolVersions` option never reached the Node adapter's | ||
| * header validation. | ||
| */ | ||
| setSupportedProtocolVersions(versions) { | ||
| this._webStandardTransport.setSupportedProtocolVersions(versions); | ||
| } | ||
| /** | ||
| * Handles an incoming HTTP request, whether `GET` or `POST`. | ||
@@ -152,3 +257,143 @@ * | ||
| //#endregion | ||
| export { NodeStreamableHTTPServerTransport }; | ||
| //#region src/toNodeHandler.ts | ||
| /** | ||
| * Adapts a web-standard MCP handler (`handler.fetch`) to a Node.js | ||
| * `(req, res, parsedBody?)` request handler. The returned function converts the | ||
| * Node request to a web-standard `Request`, calls `handler.fetch`, then writes | ||
| * the `Response` back to `res` (honoring write backpressure for streamed SSE | ||
| * responses). | ||
| * | ||
| * `req.auth` is forwarded as the handler's pass-through `authInfo`. A function | ||
| * third argument (Express's `next`) is ignored, never treated as a body. | ||
| * | ||
| * Pass `{ onerror }` to observe the adapter-level error fallback (request | ||
| * conversion / `handler.fetch` throw) before the `500` response is written. | ||
| */ | ||
| function toNodeHandler(handler, opts) { | ||
| return async (req, res, parsedBody) => { | ||
| if (typeof parsedBody === "function") parsedBody = void 0; | ||
| let finished = false; | ||
| const abort = new AbortController(); | ||
| res.on("close", () => { | ||
| if (!finished) abort.abort(); | ||
| }); | ||
| if (res.destroyed === true) abort.abort(); | ||
| let response; | ||
| try { | ||
| const request = await toWebRequest(req, parsedBody, { signal: abort.signal }); | ||
| response = await handler.fetch(request, { | ||
| ...req.auth !== void 0 && { authInfo: req.auth }, | ||
| ...parsedBody !== void 0 && { parsedBody } | ||
| }); | ||
| } catch (error) { | ||
| try { | ||
| opts?.onerror?.(error instanceof Error ? error : new Error(String(error))); | ||
| } catch {} | ||
| response = internalServerErrorResponse(echoableRequestId(parsedBody)); | ||
| } | ||
| const headers = {}; | ||
| for (const [name, value] of response.headers) headers[name] = value; | ||
| res.writeHead(response.status, headers); | ||
| if (response.body === null) { | ||
| finished = true; | ||
| res.end(); | ||
| return; | ||
| } | ||
| let drainResolve; | ||
| const releaseDrainWait = () => { | ||
| drainResolve?.(); | ||
| drainResolve = void 0; | ||
| }; | ||
| res.on("drain", releaseDrainWait); | ||
| const closed = new Promise((resolve) => { | ||
| abort.signal.addEventListener("abort", () => resolve(), { once: true }); | ||
| }); | ||
| try { | ||
| for await (const chunk of response.body) { | ||
| if (abort.signal.aborted) break; | ||
| if (res.write(chunk) === false) await Promise.race([new Promise((resolve) => { | ||
| drainResolve = resolve; | ||
| }), closed]); | ||
| } | ||
| } catch {} | ||
| finished = true; | ||
| res.end(); | ||
| }; | ||
| } | ||
| function singleHeaderValue(value) { | ||
| return Array.isArray(value) ? value[0] : value; | ||
| } | ||
| /** | ||
| * Convert a Node.js `IncomingMessage` (duck-typed — an Express `req` works) to | ||
| * the web-standard `Request` that `handler.fetch()` and `isLegacyRequest()` | ||
| * take. This is the conversion {@linkcode toNodeHandler} performs internally, | ||
| * exported for hand-wired compositions: | ||
| * | ||
| * ```ts | ||
| * const probe = await toWebRequest(req, req.body); | ||
| * await ((await isLegacyRequest(probe)) ? legacy(req, res) : modern(req, res, req.body)); | ||
| * ``` | ||
| * | ||
| * With no `parsedBody` the Node stream is read to completion — read the body | ||
| * from the returned `Request` afterwards, not from `req`. When a body parser | ||
| * already consumed the stream (`express.json()`), pass the parsed value as | ||
| * `parsedBody` and nothing is read from `req`. | ||
| */ | ||
| async function toWebRequest(req, parsedBody, options) { | ||
| const method = (req.method ?? "GET").toUpperCase(); | ||
| const url = `http://${singleHeaderValue(req.headers["host"]) ?? singleHeaderValue(req.headers[":authority"]) ?? "localhost"}${req.url ?? "/"}`; | ||
| const headers = new Headers(); | ||
| for (const [name, value] of Object.entries(req.headers)) { | ||
| if (value === void 0 || name.startsWith(":")) continue; | ||
| if (Array.isArray(value)) for (const item of value) headers.append(name, item); | ||
| else headers.set(name, value); | ||
| } | ||
| let body; | ||
| if (method !== "GET" && method !== "HEAD") if (parsedBody === void 0) { | ||
| const decoder = new TextDecoder(); | ||
| let collected = ""; | ||
| for await (const chunk of req) collected += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true }); | ||
| collected += decoder.decode(); | ||
| if (collected.length > 0) body = collected; | ||
| } else { | ||
| const serialized = JSON.stringify(parsedBody); | ||
| headers.delete("content-encoding"); | ||
| headers.delete("transfer-encoding"); | ||
| if (serialized === void 0) headers.delete("content-length"); | ||
| else { | ||
| body = serialized; | ||
| headers.set("content-length", String(new TextEncoder().encode(serialized).byteLength)); | ||
| } | ||
| } | ||
| return new Request(url, { | ||
| method, | ||
| headers, | ||
| ...options?.signal !== void 0 && { signal: options.signal }, | ||
| ...body !== void 0 && { body } | ||
| }); | ||
| } | ||
| /** | ||
| * The JSON-RPC id to echo on an adapter-built error response: the body's `id` | ||
| * when the body is a single JSON-RPC request whose id is a string or number, | ||
| * `null` otherwise. | ||
| */ | ||
| function echoableRequestId(body) { | ||
| if (body === null || typeof body !== "object" || Array.isArray(body)) return null; | ||
| const { method, id } = body; | ||
| if (typeof method !== "string") return null; | ||
| return typeof id === "string" || typeof id === "number" ? id : null; | ||
| } | ||
| function internalServerErrorResponse(id) { | ||
| return Response.json({ | ||
| jsonrpc: "2.0", | ||
| error: { | ||
| code: -32603, | ||
| message: "Internal server error" | ||
| }, | ||
| id | ||
| }, { status: 500 }); | ||
| } | ||
| //#endregion | ||
| export { NodeStreamableHTTPServerTransport, hostHeaderValidation, localhostHostValidation, localhostOriginValidation, originValidation, toNodeHandler, toWebRequest }; | ||
| //# sourceMappingURL=index.mjs.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.mjs","names":[],"sources":["../src/streamableHttp.ts"],"sourcesContent":["/**\n * Node.js Streamable HTTP Server Transport\n *\n * This is a thin wrapper around {@linkcode WebStandardStreamableHTTPServerTransport} that provides\n * compatibility with Node.js HTTP server (`IncomingMessage`/`ServerResponse`).\n *\n * For web-standard environments (Cloudflare Workers, Deno, Bun), use {@linkcode WebStandardStreamableHTTPServerTransport} directly.\n */\n\nimport type { IncomingMessage, ServerResponse } from 'node:http';\n\nimport { getRequestListener } from '@hono/node-server';\nimport type {\n AuthInfo,\n JSONRPCMessage,\n MessageExtraInfo,\n RequestId,\n Transport,\n WebStandardStreamableHTTPServerTransportOptions\n} from '@modelcontextprotocol/server';\nimport { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server';\n\n/**\n * Configuration options for {@linkcode NodeStreamableHTTPServerTransport}\n *\n * This is an alias for {@linkcode WebStandardStreamableHTTPServerTransportOptions} for backward compatibility.\n */\nexport type StreamableHTTPServerTransportOptions = WebStandardStreamableHTTPServerTransportOptions;\n\n/**\n * Server transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification.\n * It supports both SSE streaming and direct HTTP responses.\n *\n * This is a wrapper around {@linkcode WebStandardStreamableHTTPServerTransport} that provides Node.js HTTP compatibility.\n * It uses the `@hono/node-server` library to convert between Node.js HTTP and Web Standard APIs.\n *\n * In stateful mode:\n * - Session ID is generated and included in response headers\n * - Session ID is always included in initialization responses\n * - Requests with invalid session IDs are rejected with `404 Not Found`\n * - Non-initialization requests without a session ID are rejected with `400 Bad Request`\n * - State is maintained in-memory (connections, message history)\n *\n * In stateless mode:\n * - No Session ID is included in any responses\n * - No session validation is performed\n *\n * @example Stateful setup\n * ```ts source=\"./streamableHttp.examples.ts#NodeStreamableHTTPServerTransport_stateful\"\n * const server = new McpServer({ name: 'my-server', version: '1.0.0' });\n *\n * const transport = new NodeStreamableHTTPServerTransport({\n * sessionIdGenerator: () => randomUUID()\n * });\n *\n * await server.connect(transport);\n * ```\n *\n * @example Stateless setup\n * ```ts source=\"./streamableHttp.examples.ts#NodeStreamableHTTPServerTransport_stateless\"\n * const transport = new NodeStreamableHTTPServerTransport({\n * sessionIdGenerator: undefined\n * });\n * ```\n *\n * @example Using with a pre-parsed request body (e.g. Express)\n * ```ts source=\"./streamableHttp.examples.ts#NodeStreamableHTTPServerTransport_express\"\n * app.post('/mcp', (req, res) => {\n * transport.handleRequest(req, res, req.body);\n * });\n * ```\n */\nexport class NodeStreamableHTTPServerTransport implements Transport {\n private _webStandardTransport: WebStandardStreamableHTTPServerTransport;\n private _requestListener: ReturnType<typeof getRequestListener>;\n // Store auth and parsedBody per request for passing through to handleRequest\n private _requestContext: WeakMap<Request, { authInfo?: AuthInfo; parsedBody?: unknown }> = new WeakMap();\n\n constructor(options: StreamableHTTPServerTransportOptions = {}) {\n this._webStandardTransport = new WebStandardStreamableHTTPServerTransport(options);\n\n // Create a request listener that wraps the web standard transport\n // getRequestListener converts Node.js HTTP to Web Standard and properly handles SSE streaming\n // overrideGlobalObjects: false prevents Hono from overwriting global Response, which would\n // break frameworks like Next.js whose response classes extend the native Response\n this._requestListener = getRequestListener(\n async (webRequest: Request) => {\n // Get context if available (set during handleRequest)\n const context = this._requestContext.get(webRequest);\n return this._webStandardTransport.handleRequest(webRequest, {\n authInfo: context?.authInfo,\n parsedBody: context?.parsedBody\n });\n },\n { overrideGlobalObjects: false }\n );\n }\n\n /**\n * Gets the session ID for this transport instance.\n */\n get sessionId(): string | undefined {\n return this._webStandardTransport.sessionId;\n }\n\n /**\n * Sets callback for when the transport is closed.\n */\n set onclose(handler: (() => void) | undefined) {\n this._webStandardTransport.onclose = handler;\n }\n\n get onclose(): (() => void) | undefined {\n return this._webStandardTransport.onclose;\n }\n\n /**\n * Sets callback for transport errors.\n */\n set onerror(handler: ((error: Error) => void) | undefined) {\n this._webStandardTransport.onerror = handler;\n }\n\n get onerror(): ((error: Error) => void) | undefined {\n return this._webStandardTransport.onerror;\n }\n\n /**\n * Sets callback for incoming messages.\n */\n set onmessage(handler: ((message: JSONRPCMessage, extra?: MessageExtraInfo) => void) | undefined) {\n this._webStandardTransport.onmessage = handler;\n }\n\n get onmessage(): ((message: JSONRPCMessage, extra?: MessageExtraInfo) => void) | undefined {\n return this._webStandardTransport.onmessage;\n }\n\n /**\n * Starts the transport. This is required by the {@linkcode Transport} interface but is a no-op\n * for the Streamable HTTP transport as connections are managed per-request.\n */\n async start(): Promise<void> {\n return this._webStandardTransport.start();\n }\n\n /**\n * Closes the transport and all active connections.\n */\n async close(): Promise<void> {\n return this._webStandardTransport.close();\n }\n\n /**\n * Sends a JSON-RPC message through the transport.\n */\n async send(message: JSONRPCMessage, options?: { relatedRequestId?: RequestId }): Promise<void> {\n return this._webStandardTransport.send(message, options);\n }\n\n /**\n * Handles an incoming HTTP request, whether `GET` or `POST`.\n *\n * This method converts Node.js HTTP objects to Web Standard Request/Response\n * and delegates to the underlying {@linkcode WebStandardStreamableHTTPServerTransport}.\n *\n * @param req - Node.js `IncomingMessage`, optionally with `auth` property from middleware\n * @param res - Node.js `ServerResponse`\n * @param parsedBody - Optional pre-parsed body from body-parser middleware\n */\n async handleRequest(req: IncomingMessage & { auth?: AuthInfo }, res: ServerResponse, parsedBody?: unknown): Promise<void> {\n // Store context for this request to pass through auth and parsedBody\n // We need to intercept the request creation to attach this context\n const authInfo = req.auth;\n\n // Create a custom handler that includes our context\n // overrideGlobalObjects: false prevents Hono from overwriting global Response, which would\n // break frameworks like Next.js whose response classes extend the native Response\n const handler = getRequestListener(\n async (webRequest: Request) => {\n return this._webStandardTransport.handleRequest(webRequest, {\n authInfo,\n parsedBody\n });\n },\n { overrideGlobalObjects: false }\n );\n\n // Delegate to the request listener which handles all the Node.js <-> Web Standard conversion\n // including proper SSE streaming support\n await handler(req, res);\n }\n\n /**\n * Close an SSE stream for a specific request, triggering client reconnection.\n * Use this to implement polling behavior during long-running operations -\n * client will reconnect after the retry interval specified in the priming event.\n */\n closeSSEStream(requestId: RequestId): void {\n this._webStandardTransport.closeSSEStream(requestId);\n }\n\n /**\n * Close the standalone GET SSE stream, triggering client reconnection.\n * Use this to implement polling behavior for server-initiated notifications.\n */\n closeStandaloneSSEStream(): void {\n this._webStandardTransport.closeStandaloneSSEStream();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwEA,IAAa,oCAAb,MAAoE;CAChE,AAAQ;CACR,AAAQ;CAER,AAAQ,kCAAmF,IAAI,SAAS;CAExG,YAAY,UAAgD,EAAE,EAAE;AAC5D,OAAK,wBAAwB,IAAI,yCAAyC,QAAQ;AAMlF,OAAK,mBAAmB,mBACpB,OAAO,eAAwB;GAE3B,MAAM,UAAU,KAAK,gBAAgB,IAAI,WAAW;AACpD,UAAO,KAAK,sBAAsB,cAAc,YAAY;IACxD,UAAU,SAAS;IACnB,YAAY,SAAS;IACxB,CAAC;KAEN,EAAE,uBAAuB,OAAO,CACnC;;;;;CAML,IAAI,YAAgC;AAChC,SAAO,KAAK,sBAAsB;;;;;CAMtC,IAAI,QAAQ,SAAmC;AAC3C,OAAK,sBAAsB,UAAU;;CAGzC,IAAI,UAAoC;AACpC,SAAO,KAAK,sBAAsB;;;;;CAMtC,IAAI,QAAQ,SAA+C;AACvD,OAAK,sBAAsB,UAAU;;CAGzC,IAAI,UAAgD;AAChD,SAAO,KAAK,sBAAsB;;;;;CAMtC,IAAI,UAAU,SAAoF;AAC9F,OAAK,sBAAsB,YAAY;;CAG3C,IAAI,YAAuF;AACvF,SAAO,KAAK,sBAAsB;;;;;;CAOtC,MAAM,QAAuB;AACzB,SAAO,KAAK,sBAAsB,OAAO;;;;;CAM7C,MAAM,QAAuB;AACzB,SAAO,KAAK,sBAAsB,OAAO;;;;;CAM7C,MAAM,KAAK,SAAyB,SAA2D;AAC3F,SAAO,KAAK,sBAAsB,KAAK,SAAS,QAAQ;;;;;;;;;;;;CAa5D,MAAM,cAAc,KAA4C,KAAqB,YAAqC;EAGtH,MAAM,WAAW,IAAI;AAiBrB,QAZgB,mBACZ,OAAO,eAAwB;AAC3B,UAAO,KAAK,sBAAsB,cAAc,YAAY;IACxD;IACA;IACH,CAAC;KAEN,EAAE,uBAAuB,OAAO,CACnC,CAIa,KAAK,IAAI;;;;;;;CAQ3B,eAAe,WAA4B;AACvC,OAAK,sBAAsB,eAAe,UAAU;;;;;;CAOxD,2BAAiC;AAC7B,OAAK,sBAAsB,0BAA0B"} | ||
| {"version":3,"file":"index.mjs","names":["response: Response","headers: Record<string, string>","drainResolve: (() => void) | undefined","body: string | undefined","serialized: string | undefined"],"sources":["../src/middleware/hostHeaderValidation.ts","../src/middleware/originValidation.ts","../src/streamableHttp.ts","../src/toNodeHandler.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http';\n\nimport { localhostAllowedHostnames, validateHostHeader } from '@modelcontextprotocol/server';\n\n/**\n * Node.js request guard for DNS rebinding protection.\n * Validates the `Host` header hostname (port-agnostic) against an allowed list.\n *\n * Unlike the framework adapters, plain `node:http` has no middleware chain, so\n * the guard returns whether the request may proceed: when it returns `false`\n * it has already answered the request with a `403` JSON-RPC error and the\n * caller must not handle it further.\n *\n * @param allowedHostnames - List of allowed hostnames (without ports).\n * For IPv6, provide the address with brackets (e.g., `[::1]`).\n *\n * @example\n * ```ts\n * const validateHost = hostHeaderValidation(['localhost', '127.0.0.1', '[::1]']);\n * http.createServer((req, res) => {\n * if (!validateHost(req, res)) return;\n * void transport.handleRequest(req, res);\n * });\n * ```\n */\nexport function hostHeaderValidation(allowedHostnames: string[]): (req: IncomingMessage, res: ServerResponse) => boolean {\n return (req, res) => {\n const result = validateHostHeader(req.headers.host, allowedHostnames);\n if (result.ok) {\n return true;\n }\n res.writeHead(403, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n jsonrpc: '2.0',\n error: {\n code: -32_000,\n message: result.message\n },\n id: null\n })\n );\n return false;\n };\n}\n\n/**\n * Convenience guard for localhost DNS rebinding protection.\n * Allows only `localhost`, `127.0.0.1`, and `[::1]` (IPv6 localhost) hostnames.\n */\nexport function localhostHostValidation(): (req: IncomingMessage, res: ServerResponse) => boolean {\n return hostHeaderValidation(localhostAllowedHostnames());\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http';\n\nimport { localhostAllowedOrigins, validateOriginHeader } from '@modelcontextprotocol/server';\n\n/**\n * Node.js request guard for Origin header validation.\n * Validates the `Origin` header hostname (port-agnostic) against an allowed list.\n *\n * Requests without an `Origin` header pass (non-browser MCP clients do not send\n * one); a present value that is not allowed, or that cannot be parsed, is\n * rejected with `403`. The guard returns whether the request may proceed: when\n * it returns `false` it has already answered the request and the caller must\n * not handle it further.\n *\n * @param allowedOriginHostnames - List of allowed origin hostnames (without scheme or port).\n * For IPv6, provide the address with brackets (e.g., `[::1]`).\n *\n * @example\n * ```ts\n * const validateOrigin = originValidation(['localhost', '127.0.0.1', '[::1]']);\n * http.createServer((req, res) => {\n * if (!validateOrigin(req, res)) return;\n * void transport.handleRequest(req, res);\n * });\n * ```\n */\nexport function originValidation(allowedOriginHostnames: string[]): (req: IncomingMessage, res: ServerResponse) => boolean {\n return (req, res) => {\n const result = validateOriginHeader(req.headers.origin, allowedOriginHostnames);\n if (result.ok) {\n return true;\n }\n res.writeHead(403, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n jsonrpc: '2.0',\n error: {\n code: -32_000,\n message: result.message\n },\n id: null\n })\n );\n return false;\n };\n}\n\n/**\n * Convenience guard for localhost Origin validation.\n * Allows only origins whose hostname is `localhost`, `127.0.0.1`, or `[::1]` (IPv6 localhost).\n */\nexport function localhostOriginValidation(): (req: IncomingMessage, res: ServerResponse) => boolean {\n return originValidation(localhostAllowedOrigins());\n}\n","/**\n * Node.js Streamable HTTP Server Transport\n *\n * This is a thin wrapper around {@linkcode WebStandardStreamableHTTPServerTransport} that provides\n * compatibility with Node.js HTTP server (`IncomingMessage`/`ServerResponse`).\n *\n * For web-standard environments (Cloudflare Workers, Deno, Bun), use {@linkcode WebStandardStreamableHTTPServerTransport} directly.\n */\n\nimport type { IncomingMessage, ServerResponse } from 'node:http';\n\nimport { getRequestListener } from '@hono/node-server';\nimport type {\n AuthInfo,\n JSONRPCMessage,\n MessageExtraInfo,\n RequestId,\n Transport,\n WebStandardStreamableHTTPServerTransportOptions\n} from '@modelcontextprotocol/server';\nimport { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server';\n\n/**\n * Configuration options for {@linkcode NodeStreamableHTTPServerTransport}\n *\n * This is an alias for {@linkcode WebStandardStreamableHTTPServerTransportOptions} for backward compatibility.\n */\nexport type StreamableHTTPServerTransportOptions = WebStandardStreamableHTTPServerTransportOptions;\n\n/**\n * Server transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification.\n * It supports both SSE streaming and direct HTTP responses.\n *\n * This is a wrapper around {@linkcode WebStandardStreamableHTTPServerTransport} that provides Node.js HTTP compatibility.\n * It uses the `@hono/node-server` library to convert between Node.js HTTP and Web Standard APIs.\n *\n * In stateful mode:\n * - Session ID is generated and included in response headers\n * - Session ID is always included in initialization responses\n * - Requests with invalid session IDs are rejected with `404 Not Found`\n * - Non-initialization requests without a session ID are rejected with `400 Bad Request`\n * - State is maintained in-memory (connections, message history)\n *\n * In stateless mode:\n * - No Session ID is included in any responses\n * - No session validation is performed\n *\n * @example Stateful setup\n * ```ts source=\"./streamableHttp.examples.ts#NodeStreamableHTTPServerTransport_stateful\"\n * const server = new McpServer({ name: 'my-server', version: '1.0.0' });\n *\n * const transport = new NodeStreamableHTTPServerTransport({\n * sessionIdGenerator: () => randomUUID()\n * });\n *\n * await server.connect(transport);\n * ```\n *\n * @example Stateless setup\n * ```ts source=\"./streamableHttp.examples.ts#NodeStreamableHTTPServerTransport_stateless\"\n * const transport = new NodeStreamableHTTPServerTransport({\n * sessionIdGenerator: undefined\n * });\n * ```\n *\n * @example Using with a pre-parsed request body (e.g. Express)\n * ```ts source=\"./streamableHttp.examples.ts#NodeStreamableHTTPServerTransport_express\"\n * app.post('/mcp', (req, res) => {\n * transport.handleRequest(req, res, req.body);\n * });\n * ```\n */\nexport class NodeStreamableHTTPServerTransport implements Transport {\n private _webStandardTransport: WebStandardStreamableHTTPServerTransport;\n private _requestListener: ReturnType<typeof getRequestListener>;\n // Store auth and parsedBody per request for passing through to handleRequest\n private _requestContext: WeakMap<Request, { authInfo?: AuthInfo; parsedBody?: unknown }> = new WeakMap();\n\n constructor(options: StreamableHTTPServerTransportOptions = {}) {\n this._webStandardTransport = new WebStandardStreamableHTTPServerTransport(options);\n\n // Create a request listener that wraps the web standard transport\n // getRequestListener converts Node.js HTTP to Web Standard and properly handles SSE streaming\n // overrideGlobalObjects: false prevents Hono from overwriting global Response, which would\n // break frameworks like Next.js whose response classes extend the native Response\n this._requestListener = getRequestListener(\n async (webRequest: Request) => {\n // Get context if available (set during handleRequest)\n const context = this._requestContext.get(webRequest);\n return this._webStandardTransport.handleRequest(webRequest, {\n authInfo: context?.authInfo,\n parsedBody: context?.parsedBody\n });\n },\n { overrideGlobalObjects: false }\n );\n }\n\n /**\n * Gets the session ID for this transport instance.\n */\n get sessionId(): string | undefined {\n return this._webStandardTransport.sessionId;\n }\n\n /**\n * Sets callback for when the transport is closed.\n */\n set onclose(handler: (() => void) | undefined) {\n this._webStandardTransport.onclose = handler;\n }\n\n get onclose(): (() => void) | undefined {\n return this._webStandardTransport.onclose;\n }\n\n /**\n * Sets callback for transport errors.\n */\n set onerror(handler: ((error: Error) => void) | undefined) {\n this._webStandardTransport.onerror = handler;\n }\n\n get onerror(): ((error: Error) => void) | undefined {\n return this._webStandardTransport.onerror;\n }\n\n /**\n * Sets callback for incoming messages.\n */\n set onmessage(handler: ((message: JSONRPCMessage, extra?: MessageExtraInfo) => void) | undefined) {\n this._webStandardTransport.onmessage = handler;\n }\n\n get onmessage(): ((message: JSONRPCMessage, extra?: MessageExtraInfo) => void) | undefined {\n return this._webStandardTransport.onmessage;\n }\n\n /**\n * Starts the transport. This is required by the {@linkcode Transport} interface but is a no-op\n * for the Streamable HTTP transport as connections are managed per-request.\n */\n async start(): Promise<void> {\n return this._webStandardTransport.start();\n }\n\n /**\n * Closes the transport and all active connections.\n */\n async close(): Promise<void> {\n return this._webStandardTransport.close();\n }\n\n /**\n * Sends a JSON-RPC message through the transport.\n */\n async send(message: JSONRPCMessage, options?: { relatedRequestId?: RequestId }): Promise<void> {\n return this._webStandardTransport.send(message, options);\n }\n\n /**\n * Forwards the supported protocol versions to the wrapped Web Standard\n * transport for `MCP-Protocol-Version` header validation. Called by the\n * protocol layer during connect; without this delegation a server's\n * `supportedProtocolVersions` option never reached the Node adapter's\n * header validation.\n */\n setSupportedProtocolVersions(versions: string[]): void {\n this._webStandardTransport.setSupportedProtocolVersions(versions);\n }\n\n /**\n * Handles an incoming HTTP request, whether `GET` or `POST`.\n *\n * This method converts Node.js HTTP objects to Web Standard Request/Response\n * and delegates to the underlying {@linkcode WebStandardStreamableHTTPServerTransport}.\n *\n * @param req - Node.js `IncomingMessage`, optionally with `auth` property from middleware\n * @param res - Node.js `ServerResponse`\n * @param parsedBody - Optional pre-parsed body from body-parser middleware\n */\n async handleRequest(req: IncomingMessage & { auth?: AuthInfo }, res: ServerResponse, parsedBody?: unknown): Promise<void> {\n // Store context for this request to pass through auth and parsedBody\n // We need to intercept the request creation to attach this context\n const authInfo = req.auth;\n\n // Create a custom handler that includes our context\n // overrideGlobalObjects: false prevents Hono from overwriting global Response, which would\n // break frameworks like Next.js whose response classes extend the native Response\n const handler = getRequestListener(\n async (webRequest: Request) => {\n return this._webStandardTransport.handleRequest(webRequest, {\n authInfo,\n parsedBody\n });\n },\n { overrideGlobalObjects: false }\n );\n\n // Delegate to the request listener which handles all the Node.js <-> Web Standard conversion\n // including proper SSE streaming support\n await handler(req, res);\n }\n\n /**\n * Close an SSE stream for a specific request, triggering client reconnection.\n * Use this to implement polling behavior during long-running operations -\n * client will reconnect after the retry interval specified in the priming event.\n */\n closeSSEStream(requestId: RequestId): void {\n this._webStandardTransport.closeSSEStream(requestId);\n }\n\n /**\n * Close the standalone GET SSE stream, triggering client reconnection.\n * Use this to implement polling behavior for server-initiated notifications.\n */\n closeStandaloneSSEStream(): void {\n this._webStandardTransport.closeStandaloneSSEStream();\n }\n}\n","/**\n * `toNodeHandler` — adapt the web-standard {@linkcode McpHttpHandler} returned\n * by `createMcpHandler` to a Node.js `(req, res, parsedBody?)` request handler.\n *\n * The handler itself is web-standards-only (`{ fetch, close, notify, bus }` — the\n * shape Workers/Bun/Deno expect from `export default`). Node frameworks\n * (Express, Fastify, plain `node:http`) wrap it once with this helper:\n *\n * ```ts\n * import { createMcpHandler } from '@modelcontextprotocol/server';\n * import { toNodeHandler } from '@modelcontextprotocol/node';\n *\n * const handler = createMcpHandler(factory);\n * app.all('/mcp', toNodeHandler(handler));\n * // or, when a body parser already consumed the stream:\n * const node = toNodeHandler(handler);\n * app.all('/mcp', (req, res) => void node(req, res, req.body));\n * ```\n *\n * The Node→web `Request` conversion the adapter performs is also exported on\n * its own as {@linkcode toWebRequest}, for hand-wired compositions (for\n * example, routing on `isLegacyRequest`).\n *\n * The Node request/response shapes are duck-typed (kept structural so this\n * module stays free of `node:` imports); the conversion reads `req.auth`\n * (validated authentication info attached by upstream middleware) and forwards\n * it as the handler's pass-through `authInfo`.\n */\nimport type { AuthInfo, McpHandlerRequestOptions } from '@modelcontextprotocol/server';\n\n/**\n * Minimal duck-typed shape of a Node.js `IncomingMessage` accepted by\n * {@linkcode toNodeHandler}. Kept structural so the adapter stays free of\n * `node:` imports.\n */\nexport interface NodeIncomingMessageLike extends AsyncIterable<unknown> {\n method?: string;\n url?: string;\n headers: Record<string, string | string[] | undefined>;\n /** Validated authentication info attached by upstream middleware (pass-through). */\n auth?: AuthInfo;\n}\n\n/** Minimal duck-typed shape of a Node.js `ServerResponse` accepted by {@linkcode toNodeHandler}. */\nexport interface NodeServerResponseLike {\n writeHead(statusCode: number, headers?: Record<string, string>): unknown;\n write(chunk: string | Uint8Array): unknown;\n end(chunk?: string | Uint8Array): unknown;\n on(event: string, listener: (...args: unknown[]) => void): unknown;\n destroyed?: boolean;\n}\n\n/**\n * The web-standard fetch face of an `McpHttpHandler` (or any\n * fetch-shaped MCP handler) — the only surface {@linkcode toNodeHandler}\n * touches. Accepting the face structurally keeps the adapter usable with\n * hand-wired compositions that route over `isLegacyRequest` and produce a\n * `Response` directly.\n */\nexport interface FetchLikeMcpHandler {\n fetch: (request: Request, options?: McpHandlerRequestOptions) => Promise<Response>;\n}\n\n/**\n * A Node.js `(req, res, parsedBody?)` request handler produced by\n * {@linkcode toNodeHandler}. The third argument is an optional pre-parsed body\n * (`req.body` from `express.json()`); a function third argument (Express's\n * `next` when the handler is mounted as middleware) is ignored.\n */\nexport type NodeMcpRequestHandler = (req: NodeIncomingMessageLike, res: NodeServerResponseLike, parsedBody?: unknown) => Promise<void>;\n\n/** Options for {@linkcode toNodeHandler}. */\nexport interface ToNodeHandlerOptions {\n /**\n * Called when the adapter answers `500` because request conversion or\n * `handler.fetch` itself threw (e.g. a closed handler). Restores the\n * observability the removed `.node` face had via the entry's own\n * `onerror` — entry-internal failures are still reported through\n * `handler.fetch` and surface via the entry's `onerror` option as before.\n */\n onerror?: (error: Error) => void;\n}\n\n/**\n * Adapts a web-standard MCP handler (`handler.fetch`) to a Node.js\n * `(req, res, parsedBody?)` request handler. The returned function converts the\n * Node request to a web-standard `Request`, calls `handler.fetch`, then writes\n * the `Response` back to `res` (honoring write backpressure for streamed SSE\n * responses).\n *\n * `req.auth` is forwarded as the handler's pass-through `authInfo`. A function\n * third argument (Express's `next`) is ignored, never treated as a body.\n *\n * Pass `{ onerror }` to observe the adapter-level error fallback (request\n * conversion / `handler.fetch` throw) before the `500` response is written.\n */\nexport function toNodeHandler(handler: FetchLikeMcpHandler, opts?: ToNodeHandlerOptions): NodeMcpRequestHandler {\n return async (req, res, parsedBody) => {\n // Express passes (req, res, next) when the handler is mounted as a\n // middleware function; a function third argument is `next`, not a body.\n if (typeof parsedBody === 'function') {\n parsedBody = undefined;\n }\n\n let finished = false;\n const abort = new AbortController();\n res.on('close', () => {\n if (!finished) {\n abort.abort();\n }\n });\n if (res.destroyed === true) {\n abort.abort();\n }\n\n let response: Response;\n try {\n const request = await toWebRequest(req, parsedBody, { signal: abort.signal });\n response = await handler.fetch(request, {\n ...(req.auth !== undefined && { authInfo: req.auth }),\n ...(parsedBody !== undefined && { parsedBody })\n });\n } catch (error) {\n try {\n opts?.onerror?.(error instanceof Error ? error : new Error(String(error)));\n } catch {\n // Reporting must never alter the response.\n }\n response = internalServerErrorResponse(echoableRequestId(parsedBody));\n }\n\n const headers: Record<string, string> = {};\n for (const [name, value] of response.headers) {\n headers[name] = value;\n }\n res.writeHead(response.status, headers);\n if (response.body === null) {\n finished = true;\n res.end();\n return;\n }\n // Honor write backpressure: when write() reports a full buffer (Node's\n // `false` return), wait for the response to drain before pulling the\n // next chunk. The abort signal (wired to 'close' above, and seeded\n // from `res.destroyed` at entry to cover the pre-registration window\n // when 'close' already fired during async middleware) is the single\n // termination source — racing it against the drain wait means a\n // vanished client cannot park the loop, and breaking out of the async\n // iterator calls return() to cancel the upstream stream.\n let drainResolve: (() => void) | undefined;\n const releaseDrainWait = () => {\n drainResolve?.();\n drainResolve = undefined;\n };\n res.on('drain', releaseDrainWait);\n const closed = new Promise<void>(resolve => {\n abort.signal.addEventListener('abort', () => resolve(), { once: true });\n });\n try {\n for await (const chunk of response.body) {\n if (abort.signal.aborted) {\n break;\n }\n if (res.write(chunk) === false) {\n await Promise.race([\n new Promise<void>(resolve => {\n drainResolve = resolve;\n }),\n closed\n ]);\n }\n }\n } catch {\n // Stream aborted upstream; the abort signal already cancelled the exchange.\n }\n finished = true;\n res.end();\n };\n}\n\n/* ------------------------------------------------------------------------ *\n * Node request conversion — `toWebRequest` (duck-typed; no node: imports)\n * ------------------------------------------------------------------------ */\n\nfunction singleHeaderValue(value: string | string[] | undefined): string | undefined {\n return Array.isArray(value) ? value[0] : value;\n}\n\n/** Options for {@linkcode toWebRequest}. */\nexport interface ToWebRequestOptions {\n /** An `AbortSignal` to attach to the constructed `Request` (`request.signal`). */\n signal?: AbortSignal;\n}\n\n/**\n * Convert a Node.js `IncomingMessage` (duck-typed — an Express `req` works) to\n * the web-standard `Request` that `handler.fetch()` and `isLegacyRequest()`\n * take. This is the conversion {@linkcode toNodeHandler} performs internally,\n * exported for hand-wired compositions:\n *\n * ```ts\n * const probe = await toWebRequest(req, req.body);\n * await ((await isLegacyRequest(probe)) ? legacy(req, res) : modern(req, res, req.body));\n * ```\n *\n * With no `parsedBody` the Node stream is read to completion — read the body\n * from the returned `Request` afterwards, not from `req`. When a body parser\n * already consumed the stream (`express.json()`), pass the parsed value as\n * `parsedBody` and nothing is read from `req`.\n */\nexport async function toWebRequest(req: NodeIncomingMessageLike, parsedBody?: unknown, options?: ToWebRequestOptions): Promise<Request> {\n const method = (req.method ?? 'GET').toUpperCase();\n // HTTP/2 requests carry their authority as the `:authority` pseudo-header,\n // usually with no `host` entry at all (mirrors Node's `request.authority`).\n const host = singleHeaderValue(req.headers['host']) ?? singleHeaderValue(req.headers[':authority']) ?? 'localhost';\n const url = `http://${host}${req.url ?? '/'}`;\n\n const headers = new Headers();\n for (const [name, value] of Object.entries(req.headers)) {\n // HTTP/2 pseudo-headers (`:method`, `:path`, `:authority`, …) are\n // connection metadata, not header fields — `Headers` rejects their\n // names, so they are skipped rather than copied.\n if (value === undefined || name.startsWith(':')) {\n continue;\n }\n if (Array.isArray(value)) {\n for (const item of value) {\n headers.append(name, item);\n }\n } else {\n headers.set(name, value);\n }\n }\n\n // The body is carried as text: MCP request bodies are JSON, and a string\n // body keeps the constructed Request portable across runtime lib versions.\n let body: string | undefined;\n if (method !== 'GET' && method !== 'HEAD') {\n if (parsedBody === undefined) {\n const decoder = new TextDecoder();\n let collected = '';\n for await (const chunk of req) {\n collected += typeof chunk === 'string' ? chunk : decoder.decode(chunk as Uint8Array, { stream: true });\n }\n collected += decoder.decode();\n if (collected.length > 0) {\n body = collected;\n }\n } else {\n // The caller already consumed and parsed the Node stream (the\n // documented `(req, res, req.body)` mounting behind\n // `express.json()`), so the bytes cannot be re-read. Re-serialize\n // the parsed value so consumers of the forwarded Request — anything\n // on the legacy leg reading `request.json()`/`text()` instead of\n // the pass-through parsedBody — still receive the body, and replace\n // the entity headers that described the original raw bytes.\n const serialized: string | undefined = JSON.stringify(parsedBody);\n headers.delete('content-encoding');\n headers.delete('transfer-encoding');\n if (serialized === undefined) {\n headers.delete('content-length');\n } else {\n body = serialized;\n headers.set('content-length', String(new TextEncoder().encode(serialized).byteLength));\n }\n }\n }\n\n return new Request(url, {\n method,\n headers,\n ...(options?.signal !== undefined && { signal: options.signal }),\n ...(body !== undefined && { body })\n });\n}\n\n/* ------------------------------------------------------------------------ *\n * Adapter-level error fallback (request conversion failure / closed handler)\n * ------------------------------------------------------------------------ */\n\n/**\n * The JSON-RPC id to echo on an adapter-built error response: the body's `id`\n * when the body is a single JSON-RPC request whose id is a string or number,\n * `null` otherwise.\n */\nfunction echoableRequestId(body: unknown): string | number | null {\n if (body === null || typeof body !== 'object' || Array.isArray(body)) {\n return null;\n }\n const { method, id } = body as { method?: unknown; id?: unknown };\n if (typeof method !== 'string') {\n return null;\n }\n return typeof id === 'string' || typeof id === 'number' ? id : null;\n}\n\nfunction internalServerErrorResponse(id: string | number | null): Response {\n return Response.json({ jsonrpc: '2.0', error: { code: -32_603, message: 'Internal server error' }, id }, { status: 500 });\n}\n"],"mappings":"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"} |
+4
-11
| { | ||
| "name": "@modelcontextprotocol/node", | ||
| "version": "2.0.0-alpha.3", | ||
| "version": "2.0.0-alpha.4", | ||
| "description": "Model Context Protocol implementation for TypeScript - Node.js middleware", | ||
@@ -30,9 +30,2 @@ "license": "MIT", | ||
| "types": "./dist/index.d.mts", | ||
| "typesVersions": { | ||
| "*": { | ||
| "sse": [ | ||
| "dist/sse.d.mts" | ||
| ] | ||
| } | ||
| }, | ||
| "files": [ | ||
@@ -46,3 +39,3 @@ "dist" | ||
| "hono": "^4.11.4", | ||
| "@modelcontextprotocol/server": "^2.0.0-alpha.3" | ||
| "@modelcontextprotocol/server": "^2.0.0-alpha.4" | ||
| }, | ||
@@ -65,4 +58,4 @@ "peerDependenciesMeta": { | ||
| "vitest": "^4.0.15", | ||
| "@modelcontextprotocol/server": "^2.0.0-alpha.3", | ||
| "@modelcontextprotocol/core-internal": "^2.0.0-alpha.2", | ||
| "@modelcontextprotocol/server": "^2.0.0-alpha.4", | ||
| "@modelcontextprotocol/core-internal": "^2.0.0-alpha.3", | ||
| "@modelcontextprotocol/eslint-config": "^2.0.0", | ||
@@ -69,0 +62,0 @@ "@modelcontextprotocol/test-helpers": "^2.0.0-alpha.0", |
+5
-0
@@ -19,2 +19,7 @@ # `@modelcontextprotocol/node` | ||
| - `StreamableHTTPServerTransportOptions` (type alias for `WebStandardStreamableHTTPServerTransportOptions`) | ||
| - `toNodeHandler(handler, opts?)` — adapt a web-standard `{ fetch }` MCP handler to a Node `(req, res, parsedBody?)` handler | ||
| - `ToNodeHandlerOptions`, `FetchLikeMcpHandler`, `NodeMcpRequestHandler` (types for `toNodeHandler`) | ||
| - `toWebRequest(req, parsedBody?, opts?)` — the Node `IncomingMessage` → web-standard `Request` conversion `toNodeHandler` performs internally, exported on its own (for example to feed `isLegacyRequest()` from a hand-wired `(req, res)` handler) | ||
| - `ToWebRequestOptions` (options type for `toWebRequest`) | ||
| - `NodeIncomingMessageLike`, `NodeServerResponseLike` (structural Node request/response shapes) | ||
@@ -21,0 +26,0 @@ ## Usage |
Network access
Supply chain riskThis module accesses the network.
76397
108.51%391
162.42%61
8.93%3
200%