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
70 changes: 70 additions & 0 deletions src/filesystem/__tests__/unknown-tool.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
49 changes: 49 additions & 0 deletions src/filesystem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -702,6 +703,54 @@ server.registerTool(
}
);

type RequestHandler = (request: unknown, extra: unknown) => unknown;
type RequestHandlerRegistry = {
_requestHandlers: Map<string, RequestHandler>;
};
type RegisteredToolRegistry = {
_registeredTools: Record<string, unknown>;
};

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);
Expand Down
Loading