From 28c774fec77c6a783003d34ed878a5cf79c29059 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Tue, 9 Jun 2026 21:34:55 +0100 Subject: [PATCH 1/5] fix(server): return -32602 for nonexistent resources (SEP-2164) Per SEP-2164, the draft spec upgrades resource-not-found handling from SHOULD return -32002 (2025-11-25) to MUST return -32602 (Invalid params). The requested URI is included in the error's data.uri so clients can still distinguish not-found from other invalid-params errors. ProtocolErrorCode.ResourceNotFound (-32002) is deprecated but remains exported, since clients SHOULD still accept it from older servers. Closes #2194 --- .changeset/sep-2164-resource-not-found-invalid-params.md | 6 ++++++ packages/core-internal/src/types/enums.ts | 7 +++++++ packages/server/src/server/mcp.ts | 4 +++- test/e2e/requirements.ts | 3 ++- test/e2e/scenarios/resources.test.ts | 6 ++++-- test/integration/test/server/mcp.test.ts | 6 ++++-- 6 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 .changeset/sep-2164-resource-not-found-invalid-params.md diff --git a/.changeset/sep-2164-resource-not-found-invalid-params.md b/.changeset/sep-2164-resource-not-found-invalid-params.md new file mode 100644 index 0000000000..d2689a8cf8 --- /dev/null +++ b/.changeset/sep-2164-resource-not-found-invalid-params.md @@ -0,0 +1,6 @@ +--- +'@modelcontextprotocol/server': patch +'@modelcontextprotocol/core': patch +--- + +Resource not found now returns `-32602` (Invalid params) per SEP-2164; `-32002` (`ProtocolErrorCode.ResourceNotFound`) is deprecated. The error includes the requested URI in `data.uri` so clients can still distinguish not-found from other invalid-params errors. Clients SHOULD continue to accept legacy `-32002` from older servers. diff --git a/packages/core-internal/src/types/enums.ts b/packages/core-internal/src/types/enums.ts index 0e3b65f9f0..49ee6a62ae 100644 --- a/packages/core-internal/src/types/enums.ts +++ b/packages/core-internal/src/types/enums.ts @@ -11,6 +11,13 @@ export enum ProtocolErrorCode { InternalError = -32_603, // MCP-specific error codes + /** + * Legacy error code for reads of nonexistent resources. + * + * @deprecated Per SEP-2164, servers MUST return {@link ProtocolErrorCode.InvalidParams} + * (`-32602`, with the requested URI in `data.uri`) for nonexistent resources. This code + * remains exported because clients SHOULD still accept `-32002` from older servers. + */ ResourceNotFound = -32_002, /** * Processing the request requires a capability the client did not declare diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index be18a7c7cd..7e43828add 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -426,7 +426,9 @@ export class McpServer { } } - throw new ProtocolError(ProtocolErrorCode.ResourceNotFound, `Resource ${uri} not found`); + // SEP-2164: nonexistent resources MUST return -32602 (Invalid params); the + // requested URI is included in `data` so clients can distinguish not-found. + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Resource ${uri} not found`, { uri: uri.toString() }); }); this._resourceHandlersInitialized = true; diff --git a/test/e2e/requirements.ts b/test/e2e/requirements.ts index a338864298..06bb7b98fd 100644 --- a/test/e2e/requirements.ts +++ b/test/e2e/requirements.ts @@ -531,7 +531,8 @@ export const REQUIREMENTS: Record = { }, 'resources:read:unknown-uri': { source: 'https://modelcontextprotocol.io/specification/2025-11-25/server/resources#error-handling', - behavior: 'resources/read for an unknown URI returns JSON-RPC error -32002 (resource not found).' + behavior: 'resources/read for an unknown URI returns JSON-RPC error -32602 (Invalid params) with the URI in error data.', + note: 'SEP-2164: the draft spec upgrades this to MUST -32602 (2025-11-25 said SHOULD -32002); clients SHOULD still accept legacy -32002 from older servers.' }, 'resources:subscribe:capability-required': { source: 'https://modelcontextprotocol.io/specification/2025-11-25/server/resources#capabilities', diff --git a/test/e2e/scenarios/resources.test.ts b/test/e2e/scenarios/resources.test.ts index 826066cdf9..7fec0f16df 100644 --- a/test/e2e/scenarios/resources.test.ts +++ b/test/e2e/scenarios/resources.test.ts @@ -203,8 +203,10 @@ verifies('resources:read:unknown-uri', async ({ transport }: TestArgs) => { await using _ = await wire(transport, makeServer, client); await expect(client.readResource({ uri: 'file:///no-such-resource' })).rejects.toMatchObject({ - code: -32_002, - message: expect.stringMatching(/not found|unknown/i) + // SEP-2164: nonexistent resources return -32602 (Invalid params) with the URI in `data` + code: -32_602, + message: expect.stringMatching(/not found|unknown/i), + data: { uri: 'file:///no-such-resource' } }); }); diff --git a/test/integration/test/server/mcp.test.ts b/test/integration/test/server/mcp.test.ts index 7bc22f2652..8567b5e5b3 100644 --- a/test/integration/test/server/mcp.test.ts +++ b/test/integration/test/server/mcp.test.ts @@ -2734,8 +2734,10 @@ describe('Zod v4', () => { } }) ).rejects.toMatchObject({ - code: ProtocolErrorCode.ResourceNotFound, - message: expect.stringContaining('not found') + // SEP-2164: nonexistent resources return -32602 (Invalid params) with the URI in `data` + code: ProtocolErrorCode.InvalidParams, + message: expect.stringContaining('not found'), + data: { uri: 'test://nonexistent' } }); }); From bbe2f97b1d2047917969d9d5d9218c79e3ef62f5 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Thu, 25 Jun 2026 11:03:38 +0200 Subject: [PATCH 2/5] fix(node): forward supported protocol versions --- ...-2164-resource-not-found-invalid-params.md | 6 ++- .../middleware/node/src/streamableHttp.ts | 8 +++ .../node/test/streamableHttp.test.ts | 53 ++++++++++++++++++- test/conformance/expected-failures.yaml | 4 +- test/conformance/src/everythingServer.ts | 4 +- 5 files changed, 71 insertions(+), 4 deletions(-) diff --git a/.changeset/sep-2164-resource-not-found-invalid-params.md b/.changeset/sep-2164-resource-not-found-invalid-params.md index d2689a8cf8..13a1978e94 100644 --- a/.changeset/sep-2164-resource-not-found-invalid-params.md +++ b/.changeset/sep-2164-resource-not-found-invalid-params.md @@ -1,6 +1,10 @@ --- '@modelcontextprotocol/server': patch '@modelcontextprotocol/core': patch +'@modelcontextprotocol/node': patch --- -Resource not found now returns `-32602` (Invalid params) per SEP-2164; `-32002` (`ProtocolErrorCode.ResourceNotFound`) is deprecated. The error includes the requested URI in `data.uri` so clients can still distinguish not-found from other invalid-params errors. Clients SHOULD continue to accept legacy `-32002` from older servers. +Resource not found now returns `-32602` (Invalid params) per SEP-2164; `-32002` (`ProtocolErrorCode.ResourceNotFound`) is deprecated. The error includes the requested URI in `data.uri` so clients can still distinguish not-found from other invalid-params errors. Clients SHOULD +continue to accept legacy `-32002` from older servers. + +`NodeStreamableHTTPServerTransport` now forwards server-configured supported protocol versions to the underlying web-standard transport, so custom version lists passed to `McpServer` are also honored by HTTP header validation. diff --git a/packages/middleware/node/src/streamableHttp.ts b/packages/middleware/node/src/streamableHttp.ts index c101fcd0af..6ceac2211f 100644 --- a/packages/middleware/node/src/streamableHttp.ts +++ b/packages/middleware/node/src/streamableHttp.ts @@ -158,6 +158,14 @@ export class NodeStreamableHTTPServerTransport implements Transport { return this._webStandardTransport.send(message, options); } + /** + * Sets the supported protocol versions for header validation. + * Called by the server during connect() to pass its supported versions. + */ + setSupportedProtocolVersions(versions: string[]): void { + this._webStandardTransport.setSupportedProtocolVersions(versions); + } + /** * Handles an incoming HTTP request, whether `GET` or `POST`. * diff --git a/packages/middleware/node/test/streamableHttp.test.ts b/packages/middleware/node/test/streamableHttp.test.ts index 140717c6bb..fa384ec77c 100644 --- a/packages/middleware/node/test/streamableHttp.test.ts +++ b/packages/middleware/node/test/streamableHttp.test.ts @@ -40,6 +40,7 @@ async function getFreePort() { interface TestServerConfig { sessionIdGenerator: (() => string) | undefined; enableJsonResponse?: boolean; + supportedProtocolVersions?: string[]; customRequestHandler?: (req: IncomingMessage, res: ServerResponse, parsedBody?: unknown) => Promise; eventStore?: EventStore; onsessioninitialized?: ((sessionId: string) => void | Promise) | undefined; @@ -159,7 +160,13 @@ describe('Zod v4', () => { baseUrl: URL; }> { config ??= { sessionIdGenerator: () => randomUUID() }; - const mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: { logging: {} } }); + const mcpServer = new McpServer( + { name: 'test-server', version: '1.0.0' }, + { + capabilities: { logging: {} }, + ...(config.supportedProtocolVersions ? { supportedProtocolVersions: config.supportedProtocolVersions } : {}) + } + ); mcpServer.registerTool( 'greet', @@ -1001,6 +1008,50 @@ describe('Zod v4', () => { expect(response.status).toBe(200); }); + it('should accept protocol versions configured on the connected server', async () => { + const customVersion = '2026-07-28'; + const { + server: customServer, + transport: customTransport, + baseUrl: customBaseUrl + } = await createTestServer({ + sessionIdGenerator: () => randomUUID(), + supportedProtocolVersions: [customVersion, '2025-11-25'] + }); + + try { + const initResponse = await sendPostRequest(customBaseUrl, { + jsonrpc: '2.0', + method: 'initialize', + params: { + clientInfo: { name: 'test-client', version: '1.0' }, + protocolVersion: customVersion, + capabilities: {} + }, + id: 'init-custom-version' + } as JSONRPCMessage); + + expect(initResponse.status).toBe(200); + const customSessionId = initResponse.headers.get('mcp-session-id'); + expect(customSessionId).toBeDefined(); + + const response = await fetch(customBaseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', + 'mcp-session-id': customSessionId!, + 'mcp-protocol-version': customVersion + }, + body: JSON.stringify(TEST_MESSAGES.toolsList) + }); + + expect(response.status).toBe(200); + } finally { + await stopTestServer({ server: customServer, transport: customTransport }); + } + }); + it('should reject unsupported protocol version on GET requests', async () => { sessionId = await initializeServer(); diff --git a/test/conformance/expected-failures.yaml b/test/conformance/expected-failures.yaml index abfb3751d3..5abac5a5e6 100644 --- a/test/conformance/expected-failures.yaml +++ b/test/conformance/expected-failures.yaml @@ -85,7 +85,9 @@ server: - http-custom-header-server-validation # WARNING-only entries: these scenarios emit no FAILURE checks, only SHOULD-level # WARNINGs, but the expected-failures evaluator counts WARNINGs as failures. - # SEP-2164: server returns -32002 without the requested URI in error.data. + # SEP-2164: the draft conformance runner exercises resources/read through the + # 2026-07-28 stateless HTTP path; this fixture is still stateful-only, so the + # request does not reach McpServer's resource-not-found handler yet. - sep-2164-resource-not-found # SEP-2322 SHOULD-level behaviours (re-request missing inputResponses, ignore # unrecognized inputResponses keys). diff --git a/test/conformance/src/everythingServer.ts b/test/conformance/src/everythingServer.ts index 387054f0b1..b3eb3418e5 100644 --- a/test/conformance/src/everythingServer.ts +++ b/test/conformance/src/everythingServer.ts @@ -12,7 +12,7 @@ import { randomUUID } from 'node:crypto'; import { localhostHostValidation } from '@modelcontextprotocol/express'; import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; import type { CallToolResult, EventId, EventStore, GetPromptResult, ReadResourceResult, StreamId } from '@modelcontextprotocol/server'; -import { isInitializeRequest, McpServer, ResourceTemplate } from '@modelcontextprotocol/server'; +import { isInitializeRequest, McpServer, ResourceTemplate, SUPPORTED_PROTOCOL_VERSIONS } from '@modelcontextprotocol/server'; import cors from 'cors'; import type { Request, Response } from 'express'; import express from 'express'; @@ -63,6 +63,7 @@ const TEST_IMAGE_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQ // Sample base64 encoded minimal WAV file for testing const TEST_AUDIO_BASE64 = 'UklGRiYAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQIAAAA='; +const DRAFT_PROTOCOL_VERSION = '2026-07-28'; // Function to create a new MCP server instance (one per session) function createMcpServer() { @@ -72,6 +73,7 @@ function createMcpServer() { version: '1.0.0' }, { + supportedProtocolVersions: [...new Set([DRAFT_PROTOCOL_VERSION, ...SUPPORTED_PROTOCOL_VERSIONS])], capabilities: { tools: { listChanged: true From 7451c7808697121b9cd2b92dd2477e54f737cc4d Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Thu, 25 Jun 2026 11:41:19 +0200 Subject: [PATCH 3/5] fix(server): echo requested URI in not-found data --- ...-2164-resource-not-found-invalid-params.md | 2 + packages/server/src/server/mcp.ts | 2 +- test/integration/test/server/mcp.test.ts | 37 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/.changeset/sep-2164-resource-not-found-invalid-params.md b/.changeset/sep-2164-resource-not-found-invalid-params.md index 13a1978e94..a87128daac 100644 --- a/.changeset/sep-2164-resource-not-found-invalid-params.md +++ b/.changeset/sep-2164-resource-not-found-invalid-params.md @@ -7,4 +7,6 @@ Resource not found now returns `-32602` (Invalid params) per SEP-2164; `-32002` (`ProtocolErrorCode.ResourceNotFound`) is deprecated. The error includes the requested URI in `data.uri` so clients can still distinguish not-found from other invalid-params errors. Clients SHOULD continue to accept legacy `-32002` from older servers. +This supersedes the earlier 2.0.0-alpha.1 / #1389 resource error-code change that moved unknown resource reads to `-32002`. + `NodeStreamableHTTPServerTransport` now forwards server-configured supported protocol versions to the underlying web-standard transport, so custom version lists passed to `McpServer` are also honored by HTTP header validation. diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 7e43828add..0a33c75665 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -428,7 +428,7 @@ export class McpServer { // SEP-2164: nonexistent resources MUST return -32602 (Invalid params); the // requested URI is included in `data` so clients can distinguish not-found. - throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Resource ${uri} not found`, { uri: uri.toString() }); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Resource ${uri} not found`, { uri: request.params.uri }); }); this._resourceHandlersInitialized = true; diff --git a/test/integration/test/server/mcp.test.ts b/test/integration/test/server/mcp.test.ts index 8567b5e5b3..2babd937a3 100644 --- a/test/integration/test/server/mcp.test.ts +++ b/test/integration/test/server/mcp.test.ts @@ -2741,6 +2741,43 @@ describe('Zod v4', () => { }); }); + test('should echo the exact requested URI for nonexistent resources', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + const client = new Client({ + name: 'test client', + version: '1.0' + }); + const requestedUri = 'HTTP://example.com:80/docs/../missing file.txt'; + mcpServer.registerResource('test', 'test://resource', {}, async () => ({ + contents: [ + { + uri: 'test://resource', + text: 'Test content' + } + ] + })); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); + + await expect( + client.request({ + method: 'resources/read', + params: { + uri: requestedUri + } + }) + ).rejects.toMatchObject({ + code: ProtocolErrorCode.InvalidParams, + message: expect.stringContaining('not found'), + data: { uri: requestedUri } + }); + }); + /*** * Test: ProtocolError for Disabled Resource */ From b3c60d348ae70d02fd79d1ffd0c334f8c924d701 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Thu, 25 Jun 2026 23:47:26 +0200 Subject: [PATCH 4/5] chore(codemod): update generated package versions --- packages/codemod/src/generated/versions.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/codemod/src/generated/versions.ts b/packages/codemod/src/generated/versions.ts index 4fa12a1a87..196a367508 100644 --- a/packages/codemod/src/generated/versions.ts +++ b/packages/codemod/src/generated/versions.ts @@ -1,9 +1,9 @@ // AUTO-GENERATED — do not edit. Run `pnpm run generate:versions` to regenerate. export const V2_PACKAGE_VERSIONS: Record = { - '@modelcontextprotocol/client': '^2.0.0-alpha.2', - '@modelcontextprotocol/server': '^2.0.0-alpha.2', - '@modelcontextprotocol/node': '^2.0.0-alpha.2', - '@modelcontextprotocol/express': '^2.0.0-alpha.2', - '@modelcontextprotocol/server-legacy': '^2.0.0-alpha.2', - '@modelcontextprotocol/core': '^2.0.0-alpha.0' + '@modelcontextprotocol/client': '^2.0.0-alpha.3', + '@modelcontextprotocol/server': '^2.0.0-alpha.3', + '@modelcontextprotocol/node': '^2.0.0-alpha.3', + '@modelcontextprotocol/express': '^2.0.0-alpha.3', + '@modelcontextprotocol/server-legacy': '^2.0.0-alpha.3', + '@modelcontextprotocol/core': '^2.0.0-alpha.1' }; From 5215acf832d8ceb22cd4199ba754578c31ac4923 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Fri, 26 Jun 2026 00:09:56 +0200 Subject: [PATCH 5/5] fix(server): reject malformed resource URIs --- ...-2164-resource-not-found-invalid-params.md | 2 +- packages/server/src/server/mcp.ts | 9 ++++- test/integration/test/server/mcp.test.ts | 37 +++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/.changeset/sep-2164-resource-not-found-invalid-params.md b/.changeset/sep-2164-resource-not-found-invalid-params.md index a87128daac..af7473d1ca 100644 --- a/.changeset/sep-2164-resource-not-found-invalid-params.md +++ b/.changeset/sep-2164-resource-not-found-invalid-params.md @@ -1,6 +1,6 @@ --- '@modelcontextprotocol/server': patch -'@modelcontextprotocol/core': patch +'@modelcontextprotocol/core-internal': patch '@modelcontextprotocol/node': patch --- diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 0a33c75665..b8cf9356f2 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -407,7 +407,14 @@ export class McpServer { }); this.server.setRequestHandler('resources/read', async (request, ctx) => { - const uri = new URL(request.params.uri); + let uri: URL; + try { + uri = new URL(request.params.uri); + } catch { + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Resource URI ${request.params.uri} is invalid`, { + uri: request.params.uri + }); + } // First check for exact resource match const resource = this._registeredResources[uri.toString()]; diff --git a/test/integration/test/server/mcp.test.ts b/test/integration/test/server/mcp.test.ts index 2babd937a3..96a161c8b3 100644 --- a/test/integration/test/server/mcp.test.ts +++ b/test/integration/test/server/mcp.test.ts @@ -2778,6 +2778,43 @@ describe('Zod v4', () => { }); }); + test('should return invalid params for syntactically invalid resource URIs', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + const client = new Client({ + name: 'test client', + version: '1.0' + }); + const requestedUri = 'not a valid URI'; + mcpServer.registerResource('test', 'test://resource', {}, async () => ({ + contents: [ + { + uri: 'test://resource', + text: 'Test content' + } + ] + })); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); + + await expect( + client.request({ + method: 'resources/read', + params: { + uri: requestedUri + } + }) + ).rejects.toMatchObject({ + code: ProtocolErrorCode.InvalidParams, + message: expect.stringContaining('invalid'), + data: { uri: requestedUri } + }); + }); + /*** * Test: ProtocolError for Disabled Resource */