Files
rea/scripts/record-mcp-catalog.mjs
morlutoandCursor Agent 4afb6b79ae fix(mcp): guide retained evidence input recovery (#8761)
* fix(mcp): guide retained evidence input recovery

* style(mcp): format catalog and server types

* test(mcp): verify HTTP capacity recovery guidance

* test(mcp): model catalog recorder imports in cleanup fixtures

record-mcp-catalog.mjs loads tool contracts and the Evidence-input
contract before it registers cleanup owners. The cleanup fixture still
sentineled only the previous imports, so those loads failed before any
owner wrote cleanup.jsonl. Track the script's read() calls and replace
the new modules with the empty contract list this fault injection uses.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-11-05 03:34:26 +08:00

129 lines
4.5 KiB
JavaScript

import { createHash } from "node:crypto";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
// Runtime discovery is generated from the same canonical registrations, after
// default identity has been recorded and before bundling. Test catalogs stay separate.
const compiled = resolve(dirname(fileURLToPath(import.meta.url)), "../dist");
const read = (name) => import(pathToFileURL(join(compiled, name)).href);
const [
{ createServer },
{ createBinarySession },
{ parseConfig },
{ silentLogger },
{ PROMPT_CONTRACTS },
{ TOOL_CONTRACTS },
{ getEvidenceInputContract },
] = await Promise.all([
read("server/createServer.js"),
read("composition/binary.js"),
read("config/parseConfig.js"),
read("logger.js"),
read("contracts/promptContracts.js"),
read("contracts/toolContracts.js"),
read("domain/evidenceInputContract.js"),
]);
const recordCatalog = async (profile) => {
// Catalog generation is independent of the host account and user configuration.
const home = await mkdtemp(join(tmpdir(), "rea-mcp-catalog-"));
const environment = {
REA_MCP_INPUT_SCHEMA_PROFILE: profile,
HOME: home,
USERPROFILE: home,
XDG_CONFIG_HOME: join(home, "config"),
XDG_CACHE_HOME: join(home, "cache"),
XDG_STATE_HOME: join(home, "state"),
};
const cleanup = [];
let producerFailure;
try {
const config = parseConfig(environment);
if (!config.ok) throw config.error;
const session = createBinarySession(
config.value,
silentLogger,
environment,
);
cleanup.push(async () => {
const closed = await session.close({ retainProviderDocuments: true });
if (!closed.ok) throw closed.error;
});
const server = createServer({ kind: "session", session }, { environment });
cleanup.push(() => server.close());
const client = new Client({ name: "rea-catalog-recorder", version: "1" });
cleanup.push(() => client.close());
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
const tools = (await client.listTools()).tools;
const prompts = (await client.listPrompts()).prompts.map((prompt) => {
const contract = PROMPT_CONTRACTS.find(
(candidate) => candidate.name === prompt.name,
);
if (contract === undefined)
throw new Error(`Missing prompt ${prompt.name}`);
return { ...prompt, parameters: contract.arguments };
});
const catalog = {
version: 1,
info: client.getServerVersion(),
capabilities: client.getServerCapabilities(),
instructions: client.getInstructions(),
identity: server.catalogIdentity,
tools,
evidenceReferenceAlternatives: TOOL_CONTRACTS.filter(
(contract) =>
getEvidenceInputContract(contract.inputSchema)
.hasEvidenceReferenceAlternative,
).map((contract) => contract.name),
prompts,
};
const encoded = JSON.stringify(catalog);
const digest = createHash("sha256").update(encoded).digest("hex");
await writeFile(
join(
compiled,
profile === "full" ? "mcp-catalog-full.json" : "mcp-catalog.json",
),
encoded,
);
return digest;
} catch (cause) {
producerFailure = { cause };
throw cause;
} finally {
const results = await Promise.allSettled(
cleanup.reverse().map((close) => Promise.resolve().then(close)),
);
const failures = results.flatMap((result) =>
result.status === "rejected" ? [result.reason] : [],
);
if (failures.length === 0) {
try {
await rm(home, { recursive: true, force: true });
} catch (cause) {
failures.push(cause);
}
}
if (failures.length > 0)
throw new AggregateError(
producerFailure === undefined
? failures
: [producerFailure.cause, ...failures],
`Catalog cleanup failed; temporary home retained at ${home}`,
);
}
};
const digests = {};
for (const profile of ["compact", "full"])
digests[profile] = await recordCatalog(profile);
await writeFile(
join(compiled, "recordedMcpCatalog.js"),
`/** Recorded from the compiled runtime; do not edit. */\nexport const RECORDED_MCP_CATALOG_SHA256 = ${JSON.stringify(digests)};\n`,
);