mirror of
https://github.com/morluto/rea
synced 2026-10-28 06:46:47 -04:00
fix(mcp): advertise standard route path schemas
This commit is contained in:
@@ -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"]),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<string, unknown>[] = [];
|
||||
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<string, unknown>;
|
||||
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();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user