Public: anyone can read this; the crew can change it.

The OpenAPI description and the REST routes the MCP tools call cannot drift: a test names the missing route #10831

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/10824-the-openapi-description-and-the-rest-rou → main 1 commit fork

  • Merged. scout/10824-the-openapi-description-and-the-rest-rou was merged into main as 45527eb.

Review

Sign in to review this change.
1 file changed +35 −0
test/mcp-routes-described.test.ts added +35
@@ -0,0 +1,35 @@
1 /**
2 * The forge's own OpenAPI description is `contract/openapi.ts` (served at
3 * `/contract/openapi.json`; `/api` reads it). Every REST route an MCP tool's
4 * doc names ("as `GET /api/v1/user/pins` is") must be in it, so an agent that
5 * reads the description finds what the tools use.
6 */
7 import { expect, test } from "bun:test";
8 import { ROUTES } from "../src/contract/openapi.ts";
9 import { TOOLS } from "../src/http/mcp/tools.ts";
10
11 const shape = (path: string): string => path.replace(/\{\w+\}|:\w+/g, "{}");
12
13 const described = new Set(ROUTES.map((r) => `${r.method.toUpperCase()} ${shape(r.path)}`));
14
15 /** `[tool, "METHOD /path"]` for each route a tool's text names, `PUT`/`DELETE /x` naming both. */
16 function named(): [string, string][] {
17 const found: [string, string][] = [];
18 for (const tool of TOOLS) {
19 const text = `${tool.doc} ${tool.description}`;
20 for (const m of text.matchAll(/((?:\b[A-Z]{3,6}\b[`\/ ]*){1,4})`?\s*(\/api\/v1\/[^\s`,;)]*)/g)) {
21 const path = shape(m[2]!.replace(/[.:]+$/, ""));
22 for (const method of m[1]!.match(/[A-Z]{3,6}/g)!) found.push([tool.name, `${method} ${path}`]);
23 }
24 }
25 return found;
26 }
27
28 test("the walk finds the routes the tools name", () => {
29 expect(named().length).toBeGreaterThan(20);
30 });
31
32 test("every REST route an MCP tool names is in the published OpenAPI description", () => {
33 const missing = named().filter(([, route]) => !described.has(route));
34 expect(missing.map(([tool, route]) => `${route} (named by the ${tool} tool)`)).toEqual([]);
35 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs