From b8cb15289ceb82645fc6d2990affbb76942a65c5 Mon Sep 17 00:00:00 2001 From: Val Date: Tue, 13 Jan 2026 18:26:33 +0100 Subject: [PATCH] feat: add url property to RequestInfo interface --- .changeset/add-url-to-request-info.md | 5 +++++ src/server/sse.ts | 11 ++++++++++- src/server/webStandardStreamableHttp.ts | 5 +++-- src/types.ts | 4 ++++ test/server/sse.test.ts | 16 ++++++++++++---- test/server/streamableHttp.test.ts | 3 ++- 6 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 .changeset/add-url-to-request-info.md diff --git a/.changeset/add-url-to-request-info.md b/.changeset/add-url-to-request-info.md new file mode 100644 index 000000000..cefeb3d14 --- /dev/null +++ b/.changeset/add-url-to-request-info.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/sdk': minor +--- + +Add `url` property to `RequestInfo` interface as a `URL` type, exposing the full request URL to server handlers. The URL is unified across all HTTP transports (SSE and Streamable HTTP) to always provide the complete URL including protocol, host, and path. diff --git a/src/server/sse.ts b/src/server/sse.ts index b7450a09e..4931beae6 100644 --- a/src/server/sse.ts +++ b/src/server/sse.ts @@ -1,5 +1,6 @@ import { randomUUID } from 'node:crypto'; import { IncomingMessage, ServerResponse } from 'node:http'; +import { TLSSocket } from 'node:tls'; import { Transport } from '../shared/transport.js'; import { JSONRPCMessage, JSONRPCMessageSchema, MessageExtraInfo, RequestInfo } from '../types.js'; import getRawBody from 'raw-body'; @@ -149,7 +150,15 @@ export class SSEServerTransport implements Transport { } const authInfo: AuthInfo | undefined = req.auth; - const requestInfo: RequestInfo = { headers: req.headers }; + + const host = req.headers.host; + const protocol = req.socket instanceof TLSSocket ? 'https' : 'http'; + const fullUrl = host && req.url ? new URL(req.url, `${protocol}://${host}`) : undefined; + + const requestInfo: RequestInfo = { + headers: req.headers, + url: fullUrl + }; let body: string | unknown; try { diff --git a/src/server/webStandardStreamableHttp.ts b/src/server/webStandardStreamableHttp.ts index 3ae9846c2..85a399b88 100644 --- a/src/server/webStandardStreamableHttp.ts +++ b/src/server/webStandardStreamableHttp.ts @@ -597,9 +597,10 @@ export class WebStandardStreamableHTTPServerTransport implements Transport { return this.createJsonErrorResponse(415, -32000, 'Unsupported Media Type: Content-Type must be application/json'); } - // Build request info from headers + // Build request info from headers and URL const requestInfo: RequestInfo = { - headers: Object.fromEntries(req.headers.entries()) + headers: Object.fromEntries(req.headers.entries()), + url: new URL(req.url) }; let rawMessage; diff --git a/src/types.ts b/src/types.ts index 6bec5190c..bdd2dfed0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2361,6 +2361,10 @@ export interface RequestInfo { * The headers of the request. */ headers: IsomorphicHeaders; + /** + * The full URL of the request. + */ + url?: URL; } /** diff --git a/test/server/sse.test.ts b/test/server/sse.test.ts index 4686f2ba9..0e996d1d6 100644 --- a/test/server/sse.test.ts +++ b/test/server/sse.test.ts @@ -19,10 +19,15 @@ const createMockResponse = () => { return res as unknown as Mocked; }; -const createMockRequest = ({ headers = {}, body }: { headers?: Record; body?: string } = {}) => { +const createMockRequest = ({ + headers = {}, + body, + url = '/messages' +}: { headers?: Record; body?: string; url?: string } = {}) => { const mockReq = { headers, body: body ? body : undefined, + url, auth: { token: 'test-token' }, @@ -312,7 +317,8 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { 'user-agent': 'node', 'accept-encoding': 'gzip, deflate', 'content-length': '124' - } + }, + url: `http://127.0.0.1:${serverPort}/?sessionId=${sessionId}` }) } ] @@ -387,7 +393,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { id: 1 }); const mockReq = createMockRequest({ - headers: { 'content-type': 'application/json' }, + headers: { host: 'localhost', 'content-type': 'application/json' }, body: validMessage }); const mockRes = createMockResponse(); @@ -416,8 +422,10 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { }, requestInfo: { headers: { + host: 'localhost', 'content-type': 'application/json' - } + }, + url: new URL('http://localhost/messages') } } ); diff --git a/test/server/streamableHttp.test.ts b/test/server/streamableHttp.test.ts index 36a12ca9c..38da8e8af 100644 --- a/test/server/streamableHttp.test.ts +++ b/test/server/streamableHttp.test.ts @@ -443,7 +443,8 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { 'user-agent': expect.any(String), 'accept-encoding': expect.any(String), 'content-length': expect.any(String) - } + }, + url: baseUrl.toString() }); });