Skip to content
Merged
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
1 change: 1 addition & 0 deletions src/mcp/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const MCP_ERROR_CODES = {
// paid_call allow-list — #487
ERR_URL_BLOCKED: 'ERR_URL_BLOCKED',
ERR_HOST_NOT_ALLOWED: 'ERR_HOST_NOT_ALLOWED',
ERR_UNKNOWN_TOOL: 'ERR_UNKNOWN_TOOL',
ERR_INTERNAL_FAILURE: 'ERR_INTERNAL_FAILURE'
} as const;

Expand Down
10 changes: 9 additions & 1 deletion src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ function hostOf(url: string): string {
}
}

export const VALID_TOOL_NAMES = ['bazaar_search', 'paid_call'] as const;
export type ValidToolName = (typeof VALID_TOOL_NAMES)[number];

export interface McpServerConfig {
fetchWithPayment: typeof fetch;
/**
Expand Down Expand Up @@ -479,7 +482,12 @@ export class BazaarMcpServer {
}
}

throw new Error(`Unknown tool: ${request.params.name}`);
const rawName = String(request.params.name ?? '');
const boundedName = rawName.slice(0, 64).replace(/[\r\n\t]/g, ' ');
return this.createErrorResponse(
'ERR_UNKNOWN_TOOL',
`Unknown tool '${boundedName}'. Valid tools are: ${VALID_TOOL_NAMES.join(', ')}.`
);
});
}

Expand Down
49 changes: 49 additions & 0 deletions tests/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,55 @@ describe('BazaarMcpServer', () => {
const parsedText = JSON.parse(response.content[0].text);
expect(parsedText.success).toBe(true);
});

it('should return structured ERR_UNKNOWN_TOOL error with valid tool names when unknown tool is called', async () => {
const server = new BazaarMcpServer({ fetchWithPayment: vi.fn() });
let handler: any = null;
(server as any).server.setRequestHandler = (schema: any, fn: any) => {
if (schema === CallToolRequestSchema) {
handler = fn;
}
};
(server as any).setupHandlers();

const response = await handler({
params: {
name: 'non_existent_tool_123',
arguments: {}
}
});

expect(response.isError).toBe(true);
const content = JSON.parse(response.content[0].text);
expect(content.code).toBe('ERR_UNKNOWN_TOOL');
expect(content.reason).toContain("Unknown tool 'non_existent_tool_123'");
expect(content.reason).toContain('bazaar_search, paid_call');
});

it('should bound and sanitize unknown tool name in error response', async () => {
const server = new BazaarMcpServer({ fetchWithPayment: vi.fn() });
let handler: any = null;
(server as any).server.setRequestHandler = (schema: any, fn: any) => {
if (schema === CallToolRequestSchema) {
handler = fn;
}
};
(server as any).setupHandlers();

const overlyLongName = 'a'.repeat(200);
const response = await handler({
params: {
name: overlyLongName,
arguments: {}
}
});

expect(response.isError).toBe(true);
const content = JSON.parse(response.content[0].text);
expect(content.code).toBe('ERR_UNKNOWN_TOOL');
expect(content.reason).toContain('a'.repeat(64));
expect(content.reason).not.toContain('a'.repeat(65));
});
});

describe('BazaarMcpServer bazaar_search SSRF guard (#407)', () => {
Expand Down