Files
rea/tests/fixtures/localToolsMcp.ts
gensokyo-dev b5d540f3a2 test(capture): split protobuf MCP acceptance scenarios (#5379)
Separate independent exclusion, partial-source, credential-preflight and descriptor-failure workflows so their costs do not accumulate under one test deadline. Reuse one worker-owned MCP connection with explicit awaited teardown, preserving sensitivity isolation and retained-Evidence checks.

Assisted-by: Reimu (gensokyo-skills:reimu-incident-triage)
2026-10-20 07:54:47 +00:00

107 lines
3.7 KiB
TypeScript

import {
Client,
type CallToolRequestOptions,
} from "@modelcontextprotocol/client";
import { StdioClientTransport } 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,
) {
const transport = new StdioClientTransport({
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 };
}