Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`

Expand Down
9 changes: 5 additions & 4 deletions docs/migration-SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,16 +298,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
Expand Down Expand Up @@ -391,7 +392,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` |
Expand Down
10 changes: 7 additions & 3 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,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
Expand Down Expand Up @@ -512,7 +516,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` |
Expand All @@ -535,7 +539,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' }] };
Expand Down
7 changes: 3 additions & 4 deletions packages/core/src/shared/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import type {
RelatedTaskMetadata,
Request,
RequestId,
RequestInfo,
RequestMeta,
RequestMethod,
RequestTypeMap,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -392,7 +391,7 @@ export abstract class Protocol<ContextT extends BaseContext> {

/**
* 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;

Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/shared/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?: (<T extends JSONRPCMessage>(message: T, extra?: MessageExtraInfo) => void) | undefined;

Expand Down
14 changes: 2 additions & 12 deletions packages/core/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,24 +513,14 @@ export type ListChangedHandlers = {
resources?: ListChangedOptions<Resource>;
};

/**
* 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;
request?: globalThis.Request;

/**
* The authentication information.
Expand Down
59 changes: 54 additions & 5 deletions packages/middleware/node/test/streamableHttp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -406,10 +406,11 @@ describe('Zod v4', () => {
inputSchema: z.object({ name: z.string().describe('Name to greet') })
},
async ({ name }, ctx): Promise<CallToolResult> => {
// 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: [
Expand Down Expand Up @@ -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<CallToolResult> => {
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);

Expand Down
5 changes: 2 additions & 3 deletions packages/server/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,7 @@ export class Server extends Protocol<ServerContext> {

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;
const hasHttpInfo = ctx.http || transportInfo?.request || transportInfo?.closeSSEStream || transportInfo?.closeStandaloneSSEStream;
return {
...ctx,
mcpReq: {
Expand All @@ -168,7 +167,7 @@ export class Server extends Protocol<ServerContext> {
http: hasHttpInfo
? {
...ctx.http,
req: transportInfo?.requestInfo,
req: transportInfo?.request,
closeSSE: transportInfo?.closeSSEStream,
closeStandaloneSSE: transportInfo?.closeStandaloneSSEStream
}
Expand Down
13 changes: 5 additions & 8 deletions packages/server/src/server/streamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 request = req;

let rawMessage;
if (options?.parsedBody === undefined) {
Expand Down Expand Up @@ -707,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 });
}
Expand Down Expand Up @@ -741,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 });
}
});
}
Expand Down Expand Up @@ -811,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
Expand Down
Loading