mirror of
https://github.com/morluto/rea.git
synced 2026-11-03 21:36:13 +00:00
Project bounded JSON diagnostics using shared URI roles and source spans. Keep complete parser input unchanged and compute safe diagnostics lazily. Preserve byte facts, error classification, path data and cleanup ownership. Reuse JSON escape-coordinate mapping with IDA diagnostics. Fixes #7812 Assisted-by: Reimu (gensokyo-skills:reimu-incident-triage) Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing) Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
112 lines
3.8 KiB
TypeScript
112 lines
3.8 KiB
TypeScript
import {
|
|
Client,
|
|
type CallToolRequestOptions,
|
|
} from "@modelcontextprotocol/client";
|
|
import {
|
|
StdioClientTransport,
|
|
type StdioServerParameters,
|
|
} from "@modelcontextprotocol/client/stdio";
|
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
import type { ValidateFunction } from "ajv";
|
|
import { fileURLToPath } from "node:url";
|
|
import { expect, onTestFinished } from "vitest";
|
|
import { z } from "zod";
|
|
|
|
// validateSchema checks against the meta-schema without registering user $ids.
|
|
const schemaValidator = new Ajv2020({
|
|
strict: false,
|
|
validateFormats: false,
|
|
});
|
|
const validatedSchemas = new Set<string>();
|
|
const compiledValidators = new Map<string, ValidateFunction<unknown>>();
|
|
|
|
const compileValidator = (schema: unknown): ValidateFunction<unknown> => {
|
|
const schemaRecord = z.record(z.string(), z.unknown()).parse(schema);
|
|
const serialized = JSON.stringify(schemaRecord);
|
|
const cached = compiledValidators.get(serialized);
|
|
if (cached !== undefined) return cached;
|
|
|
|
// Keep a private Ajv registry per schema: distinct tool schemas can carry
|
|
// identical $id values, while this cache only reuses exact schema content.
|
|
const validate = new Ajv2020({
|
|
strict: false,
|
|
validateFormats: false,
|
|
}).compile<unknown>(schemaRecord);
|
|
compiledValidators.set(serialized, validate);
|
|
return validate;
|
|
};
|
|
|
|
/** Start the built production server without configuring deep binary providers. */
|
|
export async function connectLocalToolsMcp(
|
|
environment: Readonly<Record<string, string>> = {},
|
|
registerCleanup: (cleanup: () => Promise<void>) => void = onTestFinished,
|
|
transportOptions: Readonly<Pick<StdioServerParameters, "maxBufferSize">> = {},
|
|
) {
|
|
const transport = new StdioClientTransport({
|
|
...transportOptions,
|
|
command: process.execPath,
|
|
args: [
|
|
fileURLToPath(new URL("../../scripts/rea.mjs", import.meta.url)),
|
|
"mcp",
|
|
],
|
|
env: {
|
|
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
|
REA_LOG_LEVEL: "silent",
|
|
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
|
|
...environment,
|
|
},
|
|
stderr: "pipe",
|
|
});
|
|
const client = new Client({ name: "local-tools-e2e", version: "1" });
|
|
registerCleanup(async () => {
|
|
await client.close();
|
|
await transport.close();
|
|
});
|
|
await client.connect(transport);
|
|
const tools = (await client.listTools()).tools;
|
|
for (const tool of tools)
|
|
for (const [kind, schema] of [
|
|
["inputSchema", tool.inputSchema],
|
|
["outputSchema", tool.outputSchema],
|
|
] as const) {
|
|
if (schema === undefined) continue;
|
|
const serialized = JSON.stringify(schema);
|
|
if (validatedSchemas.has(serialized)) continue;
|
|
const schemaRecord = z.record(z.string(), z.unknown()).parse(schema);
|
|
const valid = schemaValidator.validateSchema(schemaRecord);
|
|
expect(
|
|
valid,
|
|
`${tool.name}.${kind}: ${schemaValidator.errorsText(schemaValidator.errors)}`,
|
|
).toBe(true);
|
|
validatedSchemas.add(serialized);
|
|
}
|
|
const definitions = new Map(tools.map((tool) => [tool.name, tool]));
|
|
const call = async (
|
|
name: string,
|
|
arguments_: Record<string, unknown>,
|
|
options?: Pick<CallToolRequestOptions, "signal" | "onprogress">,
|
|
) => {
|
|
const definition = definitions.get(name);
|
|
if (definition !== undefined) compileValidator(definition.inputSchema);
|
|
const response = await client.callTool(
|
|
{ name, arguments: arguments_ },
|
|
{
|
|
...options,
|
|
...(definition === undefined ? {} : { toolDefinition: definition }),
|
|
},
|
|
);
|
|
if (
|
|
response.structuredContent !== undefined &&
|
|
definition?.outputSchema !== undefined
|
|
) {
|
|
const validate = compileValidator(definition.outputSchema);
|
|
expect(
|
|
validate(response.structuredContent),
|
|
JSON.stringify(validate.errors),
|
|
).toBe(true);
|
|
}
|
|
return response;
|
|
};
|
|
return { client, call };
|
|
}
|