From 47f416dbacd6f9530b20bedb6a45854d0037ed55 Mon Sep 17 00:00:00 2001 From: laplace young Date: Fri, 15 May 2026 14:55:01 +0800 Subject: [PATCH] fix(filesystem): return protocol error for unknown tools --- src/filesystem/__tests__/unknown-tool.test.ts | 70 +++++++++++++++++++ src/filesystem/index.ts | 49 +++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 src/filesystem/__tests__/unknown-tool.test.ts diff --git a/src/filesystem/__tests__/unknown-tool.test.ts b/src/filesystem/__tests__/unknown-tool.test.ts new file mode 100644 index 0000000000..80635596dc --- /dev/null +++ b/src/filesystem/__tests__/unknown-tool.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import * as fs from 'fs/promises'; +import * as path from 'path'; +import * as os from 'os'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { ErrorCode } from '@modelcontextprotocol/sdk/types.js'; + +describe('unknown tool handling', () => { + let client: Client; + let transport: StdioClientTransport; + let testDir: string; + + beforeEach(async () => { + testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-fs-unknown-tool-')); + + const serverPath = path.resolve(__dirname, '../dist/index.js'); + transport = new StdioClientTransport({ + command: 'node', + args: [serverPath, testDir], + }); + + client = new Client({ + name: 'test-client', + version: '1.0.0', + }, { + capabilities: {} + }); + + await client.connect(transport); + }); + + afterEach(async () => { + await client?.close(); + await fs.rm(testDir, { recursive: true, force: true }); + }); + + it('returns a JSON-RPC error for unknown tools', async () => { + const unknownToolName = '__mcp_test_unknown_tool_read_file__'; + let error: unknown; + + try { + await client.callTool({ + name: unknownToolName, + arguments: {} + }); + } catch (caughtError) { + error = caughtError; + } + + expect(error).toMatchObject({ + code: ErrorCode.InvalidParams + }); + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain(`Unknown tool: ${unknownToolName}`); + }); + + it('keeps validation failures for known tools as tool errors', async () => { + const result = await client.callTool({ + name: 'read_text_file', + arguments: {} + }); + + expect(result.isError).toBe(true); + expect(result.content[0]).toMatchObject({ + type: 'text' + }); + expect((result.content[0] as { text: string }).text).toContain('Input validation error'); + }); +}); diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 7b67e63e58..5f9b4979ba 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -4,6 +4,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolResult, + ErrorCode, RootsListChangedNotificationSchema, type Root, } from "@modelcontextprotocol/sdk/types.js"; @@ -702,6 +703,54 @@ server.registerTool( } ); +type RequestHandler = (request: unknown, extra: unknown) => unknown; +type RequestHandlerRegistry = { + _requestHandlers: Map; +}; +type RegisteredToolRegistry = { + _registeredTools: Record; +}; + +class JsonRpcError extends Error { + constructor( + readonly code: ErrorCode, + message: string + ) { + super(message); + } +} + +function preserveUnknownToolProtocolErrors() { + const requestHandlers = ( + server.server as unknown as RequestHandlerRegistry + )._requestHandlers; + const callToolHandler = requestHandlers.get("tools/call"); + + if (!callToolHandler) { + return; + } + + // The SDK CallTool handler converts tool execution failures to tool results. + // Unknown tools are protocol errors, so intercept them before the handler runs. + requestHandlers.set("tools/call", (request: unknown, extra: unknown) => { + const toolName = (request as { params?: { name?: unknown } }).params?.name; + const registeredTools = ( + server as unknown as RegisteredToolRegistry + )._registeredTools; + + if (typeof toolName === "string" && !registeredTools[toolName]) { + throw new JsonRpcError( + ErrorCode.InvalidParams, + `Unknown tool: ${toolName}` + ); + } + + return callToolHandler(request, extra); + }); +} + +preserveUnknownToolProtocolErrors(); + // Updates allowed directories based on MCP client roots async function updateAllowedDirectoriesFromRoots(requestedRoots: Root[]) { const validatedRootDirs = await getValidRootDirectories(requestedRoots);