From 76eecf7fa175023b7315f40bc7582baea401c2e2 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 27 Oct 2026 11:23:39 +0800 Subject: [PATCH] fix(mcp): advertise standard route path schemas --- src/domain/javascript/javascriptRouteFacts.ts | 2 +- .../javascript/javascriptRouteInventory.ts | 2 +- .../JavaScriptRouteAdvertisedSchema.test.ts | 89 +++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 src/server/JavaScriptRouteAdvertisedSchema.test.ts diff --git a/src/domain/javascript/javascriptRouteFacts.ts b/src/domain/javascript/javascriptRouteFacts.ts index aae175a114..7962fa5ecf 100644 --- a/src/domain/javascript/javascriptRouteFacts.ts +++ b/src/domain/javascript/javascriptRouteFacts.ts @@ -16,7 +16,7 @@ export const javaScriptRouteSegmentSchema = z.strictObject({ origin: z.enum(["literal", "property-name", "next-app-path"]), }); export const javaScriptRouteCandidateSchema = z.strictObject({ - path: z.string().startsWith("/"), + path: z.string().regex(/^\//u), mechanism: javaScriptRouteMechanismSchema, confidence: z.enum(["low", "medium", "high"]), derivation: z.enum(["literal", "joined", "inferred"]), diff --git a/src/domain/javascript/javascriptRouteInventory.ts b/src/domain/javascript/javascriptRouteInventory.ts index f57abd8b63..e78d57ba4c 100644 --- a/src/domain/javascript/javascriptRouteInventory.ts +++ b/src/domain/javascript/javascriptRouteInventory.ts @@ -57,7 +57,7 @@ export const javaScriptRouteOccurrenceSchema = z.strictObject({ }); export const javaScriptInventoryRouteSchema = z.strictObject({ id: z.string().regex(/^jsr:[a-f0-9]{32}$/u), - path: z.string().startsWith("/"), + path: z.string().regex(/^\//u), classification: z.enum(["page", "system", "unknown"]), classification_basis: z.enum([ "next-app-page", diff --git a/src/server/JavaScriptRouteAdvertisedSchema.test.ts b/src/server/JavaScriptRouteAdvertisedSchema.test.ts new file mode 100644 index 0000000000..39215371e2 --- /dev/null +++ b/src/server/JavaScriptRouteAdvertisedSchema.test.ts @@ -0,0 +1,89 @@ +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import { Ajv2020 } from "ajv/dist/2020.js"; +import type { AnySchema } from "ajv"; +import { createRequire } from "node:module"; +import { expect, it } from "vitest"; + +import { toolContract } from "../contracts/toolContracts.js"; +import { toolOutputSchemaWithMetadata } from "../contracts/toolSchemaMetadata.js"; +import { javaScriptRouteCandidateSchema } from "../domain/javascript/javascriptRouteFacts.js"; +import { javaScriptInventoryRouteSchema } from "../domain/javascript/javascriptRouteInventory.js"; +import { RequestTrackingMcpServer } from "./RequestTrackingMcpServer.js"; + +it("advertises JavaScript route outputs with standard JSON Schema path constraints", async () => { + const server = new RequestTrackingMcpServer({ + name: "route-schema", + version: "1", + }); + const contract = toolContract("analyze_javascript_application"); + server.registerTool( + contract.name, + { + outputSchema: toolOutputSchemaWithMetadata(contract), + }, + () => ({ content: [] }), + ); + const client = new Client({ name: "route-schema-client", version: "1" }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + const advertised = (await client.listTools()).tools.find( + (tool) => tool.name === contract.name, + )?.outputSchema; + expect(advertised).toBeDefined(); + if (advertised === undefined) + throw new Error("Missing advertised route schema"); + const ajv = new Ajv2020({ + strictSchema: true, + strictTypes: false, + code: { optimize: false }, + }); + const require = createRequire(import.meta.url); + const addFormats: unknown = require("ajv-formats"); + if (typeof addFormats !== "function") + throw new Error("Pinned AJV formats plugin is unavailable"); + addFormats(ajv); + expect(() => ajv.compile(advertised as AnySchema)).not.toThrow(); + + // The inventory path is nested inside the advertised complete Evidence branch. + const patterns: Record[] = []; + const visit = (value: unknown): void => { + if (value === null || typeof value !== "object") return; + if (Array.isArray(value)) { + value.forEach(visit); + return; + } + const record = value as Record; + if (record.pattern === "^\\/") patterns.push(record); + Object.values(record).forEach(visit); + }; + visit(advertised); + expect(patterns.length).toBeGreaterThan(0); + for (const schema of patterns) { + const validate = ajv.compile(schema); + for (const path of ["/", "/users", "/users\nsettings", "/\r\n"]) { + expect(validate(path), path).toBe(true); + expect( + javaScriptRouteCandidateSchema.shape.path.safeParse(path).success, + ).toBe(true); + expect( + javaScriptInventoryRouteSchema.shape.path.safeParse(path).success, + ).toBe(true); + } + for (const path of ["", "users", "\n/users", "https://example.test/"]) { + expect(validate(path), path).toBe(false); + expect( + javaScriptRouteCandidateSchema.shape.path.safeParse(path).success, + ).toBe(false); + expect( + javaScriptInventoryRouteSchema.shape.path.safeParse(path).success, + ).toBe(false); + } + } + } finally { + await client.close(); + await server.close(); + } +});