Skip to content
Open
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
12 changes: 12 additions & 0 deletions .changeset/sep-2164-resource-not-found-invalid-params.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@modelcontextprotocol/server': patch
'@modelcontextprotocol/core-internal': patch
'@modelcontextprotocol/node': patch
---
Comment thread
mattzcarey marked this conversation as resolved.

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.
Comment thread
mattzcarey marked this conversation as resolved.

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.
12 changes: 6 additions & 6 deletions packages/codemod/src/generated/versions.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// AUTO-GENERATED — do not edit. Run `pnpm run generate:versions` to regenerate.
export const V2_PACKAGE_VERSIONS: Record<string, string> = {
'@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'
};
7 changes: 7 additions & 0 deletions packages/core-internal/src/types/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions packages/middleware/node/src/streamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
*
Expand Down
53 changes: 52 additions & 1 deletion packages/middleware/node/test/streamableHttp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ async function getFreePort() {
interface TestServerConfig {
sessionIdGenerator: (() => string) | undefined;
enableJsonResponse?: boolean;
supportedProtocolVersions?: string[];
customRequestHandler?: (req: IncomingMessage, res: ServerResponse, parsedBody?: unknown) => Promise<void>;
eventStore?: EventStore;
onsessioninitialized?: ((sessionId: string) => void | Promise<void>) | undefined;
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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();

Expand Down
13 changes: 11 additions & 2 deletions packages/server/src/server/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()];
Expand All @@ -426,7 +433,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: request.params.uri });
Comment thread
mattzcarey marked this conversation as resolved.
});

this._resourceHandlersInitialized = true;
Expand Down
4 changes: 3 additions & 1 deletion test/conformance/expected-failures.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
4 changes: 3 additions & 1 deletion test/conformance/src/everythingServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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() {
Expand All @@ -72,6 +73,7 @@ function createMcpServer() {
version: '1.0.0'
},
{
supportedProtocolVersions: [...new Set([DRAFT_PROTOCOL_VERSION, ...SUPPORTED_PROTOCOL_VERSIONS])],
capabilities: {
tools: {
listChanged: true
Expand Down
3 changes: 2 additions & 1 deletion test/e2e/requirements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,8 @@ export const REQUIREMENTS: Record<string, Requirement> = {
},
'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',
Expand Down
6 changes: 4 additions & 2 deletions test/e2e/scenarios/resources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
});
});

Expand Down
80 changes: 78 additions & 2 deletions test/integration/test/server/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2734,8 +2734,84 @@ 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' }
});
});

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('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 }
});
});

Expand Down
Loading