Skip to content

Commit 2ace039

Browse files
authored
Merge branch 'main' into groups/extensions-capability
2 parents 49fd0e7 + 108f2f3 commit 2ace039

7 files changed

Lines changed: 152 additions & 23 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@modelcontextprotocol/core": minor
3+
"@modelcontextprotocol/server": major
4+
---
5+
6+
Fix error handling for unknown tools and resources per MCP spec.
7+
8+
**Tools:** Unknown or disabled tool calls now return JSON-RPC protocol errors with
9+
code `-32602` (InvalidParams) instead of `CallToolResult` with `isError: true`.
10+
Callers who checked `result.isError` for unknown tools should catch rejected promises instead.
11+
12+
**Resources:** Unknown resource reads now return error code `-32002` (ResourceNotFound)
13+
instead of `-32602` (InvalidParams).
14+
15+
Added `ProtocolErrorCode.ResourceNotFound`.

packages/client/src/client/streamableHttp.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ export class StreamableHTTPClientTransport implements Transport {
222222
}
223223

224224
const response = await (this._fetch ?? fetch)(this._url, {
225+
...this._requestInit,
225226
method: 'GET',
226227
headers,
227228
signal: this._abortController?.signal

packages/client/test/client/authExtensions.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ describe('createPrivateKeyJwtAuth', () => {
305305

306306
const params = new URLSearchParams();
307307
await expect(addClientAuth(new Headers(), params, 'https://auth.example.com/token', undefined)).rejects.toThrow(
308-
/Invalid character/
308+
/cannot be part of a valid base64|Invalid character/
309309
);
310310
});
311311

packages/client/test/client/streamableHttp.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,34 @@ describe('StreamableHTTPClientTransport', () => {
425425
expect(headers.get('last-event-id')).toBe('test-event-id');
426426
});
427427

428+
it('should include requestInit options (credentials, mode, etc.) in GET SSE request', async () => {
429+
// Regression test for #895: POST and DELETE requests spread _requestInit but the
430+
// GET SSE request did not, so non-header options like credentials were dropped.
431+
vi.clearAllMocks();
432+
433+
transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
434+
requestInit: { credentials: 'include', mode: 'cors' }
435+
});
436+
437+
const fetchSpy = globalThis.fetch as Mock;
438+
fetchSpy.mockReset();
439+
fetchSpy.mockResolvedValue({
440+
ok: true,
441+
status: 200,
442+
headers: new Headers({ 'content-type': 'text/event-stream' }),
443+
body: new ReadableStream()
444+
});
445+
446+
await transport.start();
447+
await (transport as unknown as { _startOrAuthSse: (opts: StartSSEOptions) => Promise<void> })._startOrAuthSse({});
448+
449+
expect(fetchSpy).toHaveBeenCalled();
450+
const init = fetchSpy.mock.calls[0]![1];
451+
expect(init.method).toBe('GET');
452+
expect(init.credentials).toBe('include');
453+
expect(init.mode).toBe('cors');
454+
});
455+
428456
it('should throw error when invalid content-type is received', async () => {
429457
// Clear any previous state from other tests
430458
vi.clearAllMocks();

packages/core/src/types/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ export enum ProtocolErrorCode {
233233
InternalError = -32_603,
234234

235235
// MCP-specific error codes
236+
ResourceNotFound = -32_002,
236237
UrlElicitationRequired = -32_042
237238
}
238239

packages/server/src/server/mcp.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -166,15 +166,15 @@ export class McpServer {
166166
);
167167

168168
this.server.setRequestHandler('tools/call', async (request, ctx): Promise<CallToolResult | CreateTaskResult> => {
169-
try {
170-
const tool = this._registeredTools[request.params.name];
171-
if (!tool) {
172-
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Tool ${request.params.name} not found`);
173-
}
174-
if (!tool.enabled) {
175-
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Tool ${request.params.name} disabled`);
176-
}
169+
const tool = this._registeredTools[request.params.name];
170+
if (!tool) {
171+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Tool ${request.params.name} not found`);
172+
}
173+
if (!tool.enabled) {
174+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Tool ${request.params.name} disabled`);
175+
}
177176

177+
try {
178178
const isTaskRequest = !!request.params.task;
179179
const taskSupport = tool.execution?.taskSupport;
180180
const isTaskHandler = 'createTask' in (tool.handler as AnyToolHandler<AnySchema>);
@@ -506,7 +506,7 @@ export class McpServer {
506506
}
507507
}
508508

509-
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Resource ${uri} not found`);
509+
throw new ProtocolError(ProtocolErrorCode.ResourceNotFound, `Resource ${uri} not found`);
510510
});
511511

512512
this._resourceHandlersInitialized = true;

test/integration/test/server/mcp.test.ts

Lines changed: 97 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1831,22 +1831,59 @@ describe('Zod v4', () => {
18311831

18321832
await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]);
18331833

1834-
const result = await client.request({
1835-
method: 'tools/call',
1836-
params: {
1837-
name: 'nonexistent-tool'
1838-
}
1834+
await expect(
1835+
client.request({
1836+
method: 'tools/call',
1837+
params: {
1838+
name: 'nonexistent-tool'
1839+
}
1840+
})
1841+
).rejects.toMatchObject({
1842+
code: ProtocolErrorCode.InvalidParams,
1843+
message: expect.stringContaining('nonexistent-tool')
18391844
});
1845+
});
18401846

1841-
expect(result.isError).toBe(true);
1842-
expect(result.content).toEqual(
1843-
expect.arrayContaining([
1847+
/***
1848+
* Test: ProtocolError for Disabled Tool
1849+
*/
1850+
test('should throw ProtocolError for disabled tool', async () => {
1851+
const mcpServer = new McpServer({
1852+
name: 'test server',
1853+
version: '1.0'
1854+
});
1855+
1856+
const client = new Client({
1857+
name: 'test client',
1858+
version: '1.0'
1859+
});
1860+
1861+
const tool = mcpServer.registerTool('test-tool', {}, async () => ({
1862+
content: [
18441863
{
18451864
type: 'text',
1846-
text: expect.stringContaining('Tool nonexistent-tool not found')
1865+
text: 'Test response'
18471866
}
1848-
])
1849-
);
1867+
]
1868+
}));
1869+
1870+
tool.disable();
1871+
1872+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
1873+
1874+
await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]);
1875+
1876+
await expect(
1877+
client.request({
1878+
method: 'tools/call',
1879+
params: {
1880+
name: 'test-tool'
1881+
}
1882+
})
1883+
).rejects.toMatchObject({
1884+
code: ProtocolErrorCode.InvalidParams,
1885+
message: expect.stringContaining('disabled')
1886+
});
18501887
});
18511888

18521889
/***
@@ -2851,7 +2888,51 @@ describe('Zod v4', () => {
28512888
uri: 'test://nonexistent'
28522889
}
28532890
})
2854-
).rejects.toThrow(/Resource test:\/\/nonexistent not found/);
2891+
).rejects.toMatchObject({
2892+
code: ProtocolErrorCode.ResourceNotFound,
2893+
message: expect.stringContaining('not found')
2894+
});
2895+
});
2896+
2897+
/***
2898+
* Test: ProtocolError for Disabled Resource
2899+
*/
2900+
test('should throw ProtocolError for disabled resource', async () => {
2901+
const mcpServer = new McpServer({
2902+
name: 'test server',
2903+
version: '1.0'
2904+
});
2905+
const client = new Client({
2906+
name: 'test client',
2907+
version: '1.0'
2908+
});
2909+
2910+
const resource = mcpServer.registerResource('test', 'test://resource', {}, async () => ({
2911+
contents: [
2912+
{
2913+
uri: 'test://resource',
2914+
text: 'Test content'
2915+
}
2916+
]
2917+
}));
2918+
2919+
resource.disable();
2920+
2921+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
2922+
2923+
await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]);
2924+
2925+
await expect(
2926+
client.request({
2927+
method: 'resources/read',
2928+
params: {
2929+
uri: 'test://resource'
2930+
}
2931+
})
2932+
).rejects.toMatchObject({
2933+
code: ProtocolErrorCode.InvalidParams,
2934+
message: expect.stringContaining('disabled')
2935+
});
28552936
});
28562937

28572938
/***
@@ -3747,7 +3828,10 @@ describe('Zod v4', () => {
37473828
name: 'nonexistent-prompt'
37483829
}
37493830
})
3750-
).rejects.toThrow(/Prompt nonexistent-prompt not found/);
3831+
).rejects.toMatchObject({
3832+
code: ProtocolErrorCode.InvalidParams,
3833+
message: expect.stringContaining('not found')
3834+
});
37513835
});
37523836

37533837
/***

0 commit comments

Comments
 (0)