From ff1709c2da5c5df8037ab0d9180ef860b70b2442 Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Tue, 4 Aug 2026 22:47:50 +0200 Subject: [PATCH] fix(mcp): implement tools.discovery handler returning flat tool catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/app/api/mcp/route.test.ts | 22 ++++++++++++++++++++++ src/app/api/mcp/route.ts | 9 +++++++++ 2 files changed, 31 insertions(+) diff --git a/src/app/api/mcp/route.test.ts b/src/app/api/mcp/route.test.ts index a2bc70e..4b194b8 100644 --- a/src/app/api/mcp/route.test.ts +++ b/src/app/api/mcp/route.test.ts @@ -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" }, diff --git a/src/app/api/mcp/route.ts b/src/app/api/mcp/route.ts index d87dde7..838bb24 100644 --- a/src/app/api/mcp/route.ts +++ b/src/app/api/mcp/route.ts @@ -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}`); }