From b5a7a316a5f745155f785f0fc1760a0ca8853e98 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 30 Mar 2026 20:07:14 +0300 Subject: [PATCH 1/3] Web standards Request object in ctx --- docs/migration-SKILL.md | 9 +-- docs/migration.md | 10 +++- packages/core/src/shared/protocol.ts | 5 +- packages/core/src/types/types.ts | 14 +---- .../node/test/streamableHttp.test.ts | 59 +++++++++++++++++-- packages/server/src/server/streamableHttp.ts | 7 +-- 6 files changed, 72 insertions(+), 32 deletions(-) diff --git a/docs/migration-SKILL.md b/docs/migration-SKILL.md index c2f42b5f5..af2de332f 100644 --- a/docs/migration-SKILL.md +++ b/docs/migration-SKILL.md @@ -297,16 +297,17 @@ Note: the third argument (`metadata`) is required — pass `{}` if no metadata. ## 7. Headers API -Transport constructors and `RequestInfo.headers` now use the Web Standard `Headers` object instead of plain objects. +Transport constructors now use the Web Standard `Headers` object instead of plain objects. The custom `RequestInfo` type has been replaced with the standard Web `Request` object, giving access to headers, URL, query parameters, and method. ```typescript -// v1: plain object, bracket access +// v1: plain object, bracket access, custom RequestInfo headers: { 'Authorization': 'Bearer token' } extra.requestInfo?.headers['mcp-session-id'] -// v2: Headers object, .get() access +// v2: Headers object, .get() access, standard Web Request headers: new Headers({ 'Authorization': 'Bearer token' }) ctx.http?.req?.headers.get('mcp-session-id') +new URL(ctx.http?.req?.url).searchParams.get('debug') ``` ## 8. Removed Server Features @@ -390,7 +391,7 @@ Request/notification params remain fully typed. Remove unused schema imports aft | `extra.sendNotification(...)` | `ctx.mcpReq.notify(...)` | | `extra.authInfo` | `ctx.http?.authInfo` | | `extra.sessionId` | `ctx.sessionId` | -| `extra.requestInfo` | `ctx.http?.req` (only `ServerContext`) | +| `extra.requestInfo` | `ctx.http?.req` (standard Web `Request`, only `ServerContext`) | | `extra.closeSSEStream` | `ctx.http?.closeSSE` (only `ServerContext`) | | `extra.closeStandaloneSSEStream` | `ctx.http?.closeStandaloneSSE` (only `ServerContext`) | | `extra.taskStore` | `ctx.task?.store` | diff --git a/docs/migration.md b/docs/migration.md index 5d7763cbe..066754755 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -154,8 +154,12 @@ const transport = new StreamableHTTPClientTransport(url, { } }); -// Reading headers in a request handler +// Reading headers in a request handler (ctx.http.req is the standard Web Request object) const sessionId = ctx.http?.req?.headers.get('mcp-session-id'); + +// Reading query parameters +const url = new URL(ctx.http!.req!.url); +const debug = url.searchParams.get('debug'); ``` ### `McpServer.tool()`, `.prompt()`, `.resource()` removed @@ -492,7 +496,7 @@ The `RequestHandlerExtra` type has been replaced with a structured context type | `extra.sendRequest(...)` | `ctx.mcpReq.send(...)` | | `extra.sendNotification(...)` | `ctx.mcpReq.notify(...)` | | `extra.authInfo` | `ctx.http?.authInfo` | -| `extra.requestInfo` | `ctx.http?.req` (only on `ServerContext`) | +| `extra.requestInfo` | `ctx.http?.req` (standard Web `Request`, only on `ServerContext`) | | `extra.closeSSEStream` | `ctx.http?.closeSSE` (only on `ServerContext`) | | `extra.closeStandaloneSSEStream` | `ctx.http?.closeStandaloneSSE` (only on `ServerContext`) | | `extra.sessionId` | `ctx.sessionId` | @@ -515,7 +519,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => { ```typescript server.setRequestHandler('tools/call', async (request, ctx) => { - const headers = ctx.http?.req?.headers; + const headers = ctx.http?.req?.headers; // standard Web Request object const taskStore = ctx.task?.store; await ctx.mcpReq.notify({ method: 'notifications/progress', params: { progressToken: 'abc', progress: 50, total: 100 } }); return { content: [{ type: 'text', text: 'result' }] }; diff --git a/packages/core/src/shared/protocol.ts b/packages/core/src/shared/protocol.ts index d6daf0172..13345b81b 100644 --- a/packages/core/src/shared/protocol.ts +++ b/packages/core/src/shared/protocol.ts @@ -24,7 +24,6 @@ import type { RelatedTaskMetadata, Request, RequestId, - RequestInfo, RequestMeta, RequestMethod, RequestTypeMap, @@ -257,9 +256,9 @@ export type ServerContext = BaseContext & { http?: { /** - * The original HTTP request information. + * The original HTTP request. */ - req?: RequestInfo; + req?: globalThis.Request; /** * Closes the SSE stream for this request, triggering client reconnection. diff --git a/packages/core/src/types/types.ts b/packages/core/src/types/types.ts index 342542b36..cd0ca0544 100644 --- a/packages/core/src/types/types.ts +++ b/packages/core/src/types/types.ts @@ -513,24 +513,14 @@ export type ListChangedHandlers = { resources?: ListChangedOptions; }; -/** - * Information about the incoming request. - */ -export interface RequestInfo { - /** - * The headers of the request. - */ - headers: Headers; -} - /** * Extra information about a message. */ export interface MessageExtraInfo { /** - * The request information. + * The original HTTP request. */ - requestInfo?: RequestInfo; + requestInfo?: globalThis.Request; /** * The authentication information. diff --git a/packages/middleware/node/test/streamableHttp.test.ts b/packages/middleware/node/test/streamableHttp.test.ts index ab3ba540f..460f72d93 100644 --- a/packages/middleware/node/test/streamableHttp.test.ts +++ b/packages/middleware/node/test/streamableHttp.test.ts @@ -396,7 +396,7 @@ describe('Zod v4', () => { /*** * Test: Tool With Request Info */ - it('should pass request info to tool callback', async () => { + it('should expose the full Request object to tool handlers', async () => { sessionId = await initializeServer(); mcpServer.registerTool( @@ -406,10 +406,11 @@ describe('Zod v4', () => { inputSchema: z.object({ name: z.string().describe('Name to greet') }) }, async ({ name }, ctx): Promise => { - // Convert Headers object to plain object for JSON serialization - // Headers is a Web API class that doesn't serialize with JSON.stringify + const req = ctx.http?.req; const serializedRequestInfo = { - headers: Object.fromEntries(ctx.http?.req?.headers ?? new Headers()) + headers: Object.fromEntries(req?.headers ?? new Headers()), + url: req?.url, + method: req?.method }; return { content: [ @@ -464,10 +465,58 @@ describe('Zod v4', () => { 'user-agent': expect.any(String), 'accept-encoding': expect.any(String), 'content-length': expect.any(String) - } + }, + url: expect.stringContaining(baseUrl.pathname), + method: 'POST' }); }); + it('should expose query parameters via the Request object', async () => { + sessionId = await initializeServer(); + + mcpServer.registerTool( + 'test-query-params', + { + description: 'A tool that reads query params', + inputSchema: z.object({}) + }, + async (_args, ctx): Promise => { + const req = ctx.http?.req; + const url = new URL(req!.url); + const params = Object.fromEntries(url.searchParams); + return { + content: [{ type: 'text', text: JSON.stringify(params) }] + }; + } + ); + + const toolCallMessage: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'tools/call', + params: { + name: 'test-query-params', + arguments: {} + }, + id: 'call-2' + }; + + // Send to a URL with query parameters + const urlWithParams = new URL(baseUrl.toString()); + urlWithParams.searchParams.set('foo', 'bar'); + urlWithParams.searchParams.set('debug', 'true'); + + const response = await sendPostRequest(urlWithParams, toolCallMessage, sessionId); + expect(response.status).toBe(200); + + const text = await readSSEEvent(response); + const dataLine = text.split('\n').find(line => line.startsWith('data:')); + expect(dataLine).toBeDefined(); + + const eventData = JSON.parse(dataLine!.slice(5)); + const queryParams = JSON.parse(eventData.result.content[0].text); + expect(queryParams).toEqual({ foo: 'bar', debug: 'true' }); + }); + it('should reject requests without a valid session ID', async () => { const response = await sendPostRequest(baseUrl, TEST_MESSAGES.toolsList); diff --git a/packages/server/src/server/streamableHttp.ts b/packages/server/src/server/streamableHttp.ts index 31053f35c..ee4a0784d 100644 --- a/packages/server/src/server/streamableHttp.ts +++ b/packages/server/src/server/streamableHttp.ts @@ -7,7 +7,7 @@ * For Node.js Express/HTTP compatibility, use {@linkcode @modelcontextprotocol/node!NodeStreamableHTTPServerTransport | NodeStreamableHTTPServerTransport} which wraps this transport. */ -import type { AuthInfo, JSONRPCMessage, MessageExtraInfo, RequestId, RequestInfo, Transport } from '@modelcontextprotocol/core'; +import type { AuthInfo, JSONRPCMessage, MessageExtraInfo, RequestId, Transport } from '@modelcontextprotocol/core'; import { DEFAULT_NEGOTIATED_PROTOCOL_VERSION, isInitializeRequest, @@ -634,10 +634,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport { return this.createJsonErrorResponse(415, -32_000, 'Unsupported Media Type: Content-Type must be application/json'); } - // Build request info from headers - const requestInfo: RequestInfo = { - headers: req.headers - }; + const requestInfo = req; let rawMessage; if (options?.parsedBody === undefined) { From 3649e330d8508fea07cf8228de0aaf1d863c8ab8 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 31 Mar 2026 13:49:03 +0300 Subject: [PATCH 2/3] MessageExtraInfo - rename requestInfo to request, update TSDocs --- CLAUDE.md | 2 +- packages/core/src/shared/protocol.ts | 2 +- packages/core/src/shared/transport.ts | 4 ++-- packages/core/src/types/types.ts | 2 +- packages/server/src/server/server.ts | 4 ++-- packages/server/src/server/streamableHttp.ts | 8 ++++---- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 472ee06ec..609c920cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -164,7 +164,7 @@ When a request arrives from the remote side: 3. **`Protocol._onrequest()`**: - Looks up handler in `_requestHandlers` map (keyed by method name) - Creates `BaseContext` with `signal`, `sessionId`, `sendNotification`, `sendRequest`, etc. - - Calls `buildContext()` to let subclasses enrich the context (e.g., Server adds `requestInfo`) + - Calls `buildContext()` to let subclasses enrich the context (e.g., Server adds HTTP request info) - Invokes handler, sends JSON-RPC response back via transport 4. **Handler** was registered via `setRequestHandler('method', handler)` diff --git a/packages/core/src/shared/protocol.ts b/packages/core/src/shared/protocol.ts index 593659ffa..57eab6932 100644 --- a/packages/core/src/shared/protocol.ts +++ b/packages/core/src/shared/protocol.ts @@ -391,7 +391,7 @@ export abstract class Protocol { /** * Builds the context object for request handlers. Subclasses must override - * to return the appropriate context type (e.g., ServerContext adds requestInfo). + * to return the appropriate context type (e.g., ServerContext adds HTTP request info). */ protected abstract buildContext(ctx: BaseContext, transportInfo?: MessageExtraInfo): ContextT; diff --git a/packages/core/src/shared/transport.ts b/packages/core/src/shared/transport.ts index 6f4432ae2..c606e2e3b 100644 --- a/packages/core/src/shared/transport.ts +++ b/packages/core/src/shared/transport.ts @@ -110,9 +110,9 @@ export interface Transport { /** * Callback for when a message (request or response) is received over the connection. * - * Includes the {@linkcode MessageExtraInfo.requestInfo | requestInfo} and {@linkcode MessageExtraInfo.authInfo | authInfo} if the transport is authenticated. + * Includes the {@linkcode MessageExtraInfo.request | request} and {@linkcode MessageExtraInfo.authInfo | authInfo} if the transport is authenticated. * - * The {@linkcode MessageExtraInfo.requestInfo | requestInfo} can be used to get the original request information (headers, etc.) + * The {@linkcode MessageExtraInfo.request | request} can be used to get the original request information (headers, etc.) */ onmessage?: ((message: T, extra?: MessageExtraInfo) => void) | undefined; diff --git a/packages/core/src/types/types.ts b/packages/core/src/types/types.ts index cd0ca0544..a92deec8e 100644 --- a/packages/core/src/types/types.ts +++ b/packages/core/src/types/types.ts @@ -520,7 +520,7 @@ export interface MessageExtraInfo { /** * The original HTTP request. */ - requestInfo?: globalThis.Request; + request?: globalThis.Request; /** * The authentication information. diff --git a/packages/server/src/server/server.ts b/packages/server/src/server/server.ts index f1a1851f4..14f205124 100644 --- a/packages/server/src/server/server.ts +++ b/packages/server/src/server/server.ts @@ -156,7 +156,7 @@ export class Server extends Protocol { protected override buildContext(ctx: BaseContext, transportInfo?: MessageExtraInfo): ServerContext { // Only create http when there's actual HTTP transport info or auth info const hasHttpInfo = - ctx.http || transportInfo?.requestInfo || transportInfo?.closeSSEStream || transportInfo?.closeStandaloneSSEStream; + ctx.http || transportInfo?.request || transportInfo?.closeSSEStream || transportInfo?.closeStandaloneSSEStream; return { ...ctx, mcpReq: { @@ -168,7 +168,7 @@ export class Server extends Protocol { http: hasHttpInfo ? { ...ctx.http, - req: transportInfo?.requestInfo, + req: transportInfo?.request, closeSSE: transportInfo?.closeSSEStream, closeStandaloneSSE: transportInfo?.closeStandaloneSSEStream } diff --git a/packages/server/src/server/streamableHttp.ts b/packages/server/src/server/streamableHttp.ts index ee4a0784d..edb07b004 100644 --- a/packages/server/src/server/streamableHttp.ts +++ b/packages/server/src/server/streamableHttp.ts @@ -634,7 +634,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport { return this.createJsonErrorResponse(415, -32_000, 'Unsupported Media Type: Content-Type must be application/json'); } - const requestInfo = req; + const request = req; let rawMessage; if (options?.parsedBody === undefined) { @@ -704,7 +704,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport { if (!hasRequests) { // if it only contains notifications or responses, return 202 for (const message of messages) { - this.onmessage?.(message, { authInfo: options?.authInfo, requestInfo }); + this.onmessage?.(message, { authInfo: options?.authInfo, request }); } return new Response(null, { status: 202 }); } @@ -738,7 +738,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport { } for (const message of messages) { - this.onmessage?.(message, { authInfo: options?.authInfo, requestInfo }); + this.onmessage?.(message, { authInfo: options?.authInfo, request }); } }); } @@ -808,7 +808,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport { }; } - this.onmessage?.(message, { authInfo: options?.authInfo, requestInfo, closeSSEStream, closeStandaloneSSEStream }); + this.onmessage?.(message, { authInfo: options?.authInfo, request, closeSSEStream, closeStandaloneSSEStream }); } // The server SHOULD NOT close the SSE stream before sending all JSON-RPC responses // This will be handled by the send() method when responses are ready From 9fb91081eb19c48e9f2306a52b02901943168050 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 31 Mar 2026 13:50:46 +0300 Subject: [PATCH 3/3] lint fix --- packages/server/src/server/server.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/server/src/server/server.ts b/packages/server/src/server/server.ts index 14f205124..0f9382691 100644 --- a/packages/server/src/server/server.ts +++ b/packages/server/src/server/server.ts @@ -155,8 +155,7 @@ export class Server extends Protocol { protected override buildContext(ctx: BaseContext, transportInfo?: MessageExtraInfo): ServerContext { // Only create http when there's actual HTTP transport info or auth info - const hasHttpInfo = - ctx.http || transportInfo?.request || transportInfo?.closeSSEStream || transportInfo?.closeStandaloneSSEStream; + const hasHttpInfo = ctx.http || transportInfo?.request || transportInfo?.closeSSEStream || transportInfo?.closeStandaloneSSEStream; return { ...ctx, mcpReq: {