fix(mcp): implement tools.discovery handler returning flat tool catalog

The tools.discovery tool was declared in the catalog (line 1300) but no
tools/call dispatch branch existed for it — every call fell through to
'Unknown tool: tools.discovery'. Functionality was reachable only via
the parallel GET /api/mcp endpoint, which the JSON-RPC-only MCP clients
in n8n couldn't use.

Add a real handler that returns the same `tools` array as tools/list,
wrapped in the standard JSON-RPC content envelope. Mirrors GET /api/mcp
for JSON-RPC-only clients.

Also add a Vitest case that asserts the catalog shape (array of tools,
each with name+description+inputSchema, and tools.discovery in the list).

Deployment note: this fix is in source; the live apphub.co.za server
runs a built bundle and will keep returning 'Unknown tool' until rebuilt.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
SJCoetz 2026-08-04 22:47:50 +02:00
parent e132319910
commit ff1709c2da
2 changed files with 31 additions and 0 deletions

View file

@ -301,6 +301,28 @@ describe("POST /api/mcp", () => {
expect(body.error.message).toMatch(/Unknown tool/);
});
it("tools/call tools.discovery returns the flat tool catalog (JSON-RPC form of GET /api/mcp)", async () => {
const res = await POST(
mcpRequest({
method: "tools/call",
id: 8,
params: { name: "tools.discovery", arguments: {} },
})
);
expect(res.status).toBe(200);
const body = await res.json();
const payload = JSON.parse(body.result.content[0].text);
expect(Array.isArray(payload.tools)).toBe(true);
expect(payload.tools.length).toBeGreaterThan(60);
// Every entry has name + description + inputSchema
for (const t of payload.tools) {
expect(typeof t.name).toBe("string");
expect(typeof t.description).toBe("string");
expect(t.inputSchema).toBeDefined();
}
expect(payload.tools.map((t: { name: string }) => t.name)).toContain("tools.discovery");
});
it("tools/call quotes.create_from_email delegates to the service with the full payload", async () => {
createQuoteFromEmail.mockResolvedValueOnce({
quote: { id: "q1" },

View file

@ -2489,6 +2489,15 @@ export async function POST(req: NextRequest) {
return jsonRpcOk(body.id, { deleted: args.id });
}
// tools.discovery — flat tool catalog (no JSON-RPC envelope inside
// the text payload). Mirrors `GET /api/mcp` for clients that can
// only build a JSON-RPC request. Returns the same `tools` array as
// `tools/list` — handy when an agent wants a self-describing
// summary without paying the schema-drift tax of two surfaces.
if (toolName === "tools.discovery") {
return jsonRpcOk(body.id, { tools });
}
throw new Error(`Unknown tool: ${toolName}`);
}