mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
* fix(mcp): deliver tool errors within advertised schema contracts Retain complete typed JSON error content while omitting non-conforming structured errors at the public transport boundary. Recover oversized diagnostics into Evidence before removing their private structured carrier. Update schema-validated client readers and cover actual stdio delivery. Closes #1261 Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing) * test(mcp): read typed errors in canonical Evidence regressions * test(mcp): validate public error diagnostics in acceptance workflows
300 lines
9.4 KiB
JavaScript
300 lines
9.4 KiB
JavaScript
import { requireMcpToolError } from "./lib/mcp-verifier-results.mjs";
|
|
import assert from "node:assert/strict";
|
|
import { join } from "node:path";
|
|
import { execFile } from "node:child_process";
|
|
import { promisify } from "node:util";
|
|
import { readFile, rm } from "node:fs/promises";
|
|
|
|
import { Client } from "@modelcontextprotocol/client";
|
|
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
import { MCP_STARTUP_POLICY } from "../dist/mcpStartupPolicy.js";
|
|
import { parseEvidence } from "../dist/domain/evidence.js";
|
|
import * as prompts from "./verify-package-prompts.mjs";
|
|
import { json, verifyCompleteToolCatalog } from "./lib/verify-package-core.mjs";
|
|
|
|
const execute = promisify(execFile);
|
|
|
|
const verifyMcpToolsAndPrompts = async (client, mcpOptions) => {
|
|
await verifyCompleteToolCatalog(client, mcpOptions);
|
|
await prompts.verifyPromptCatalog(client, mcpOptions, prompts.names);
|
|
await prompts.verifyPromptCompletion(client, mcpOptions, false);
|
|
};
|
|
|
|
const verifyMcpTargetFree = async (client, mcpOptions) => {
|
|
const status = await client.callTool(
|
|
{ name: "binary_session", arguments: {} },
|
|
mcpOptions,
|
|
);
|
|
const currentDocument =
|
|
status.structuredContent?.result?.tool_availability?.find(
|
|
({ name }) => name === "current_document",
|
|
);
|
|
if (
|
|
currentDocument?.available !== false ||
|
|
currentDocument.reason !== "target_required"
|
|
)
|
|
throw new Error(
|
|
"packaged target-free MCP omitted target-bound availability metadata",
|
|
);
|
|
};
|
|
|
|
const verifyMcpUnknownProvider = async (client, mcpOptions) => {
|
|
const unknownProvider = await client.callTool(
|
|
{
|
|
name: "open_binary",
|
|
arguments: {
|
|
path: process.execPath,
|
|
provider_id: "missing-provider",
|
|
},
|
|
},
|
|
mcpOptions,
|
|
);
|
|
if (
|
|
unknownProvider.isError !== true ||
|
|
requireMcpToolError(unknownProvider)?.details?.selection_reason !==
|
|
"unknown_provider"
|
|
)
|
|
throw new Error("packaged MCP accepted an unknown analysis provider");
|
|
};
|
|
|
|
const verifyMcpOpenAndBind = async (client, mcpOptions) => {
|
|
const opened = await client.callTool(
|
|
{
|
|
name: "open_binary",
|
|
arguments: { path: process.execPath, provider_id: "hopper" },
|
|
},
|
|
mcpOptions,
|
|
);
|
|
if (opened.isError === true)
|
|
throw new Error("packaged MCP could not open a binary");
|
|
const providerStatusEnvelope = json(
|
|
prompts.mcpText(
|
|
await client.callTool(
|
|
{ name: "binary_session", arguments: {} },
|
|
mcpOptions,
|
|
),
|
|
),
|
|
);
|
|
const providerStatus = providerStatusEnvelope.result;
|
|
if (
|
|
providerStatus.analysis_provider_binding?.provider?.id !== "hopper" ||
|
|
providerStatus.analysis_provider_binding?.selection_source !== "request"
|
|
)
|
|
throw new Error("packaged MCP omitted its explicit Hopper binding");
|
|
};
|
|
|
|
const verifyMcpLinuxToolAvailability = async (client, mcpOptions) => {
|
|
const current = await client.callTool(
|
|
{ name: "current_document", arguments: {} },
|
|
mcpOptions,
|
|
);
|
|
if (current.isError !== true)
|
|
throw new Error("packaged Linux MCP executed an unavailable Hopper tool");
|
|
};
|
|
|
|
const verifyMcpNonLinuxCurrentDocument = async (
|
|
client,
|
|
mcpOptions,
|
|
current,
|
|
) => {
|
|
if (json(prompts.mcpText(current)).normalized_result !== "fixture")
|
|
throw new Error("packaged MCP bridge call failed");
|
|
const batch = await client.callTool(
|
|
{
|
|
name: "batch_decompile",
|
|
arguments: { addresses: ["0x1000"] },
|
|
},
|
|
mcpOptions,
|
|
);
|
|
const batchResult = json(prompts.mcpText(batch)).normalized_result;
|
|
if (
|
|
batch.isError === true ||
|
|
batchResult?.total !== 1 ||
|
|
batchResult?.succeeded !== 1 ||
|
|
batchResult?.failed !== 0 ||
|
|
batchResult?.items?.[0]?.status !== "ok" ||
|
|
batchResult?.items?.[0]?.pseudocode !== "return 0;"
|
|
)
|
|
throw new Error("packaged MCP structured batch result failed");
|
|
};
|
|
|
|
const verifyMcpBinaryLifecycle = async (client, mcpOptions) => {
|
|
await verifyMcpOpenAndBind(client, mcpOptions);
|
|
await prompts.verifyPromptCompletion(
|
|
client,
|
|
mcpOptions,
|
|
process.platform !== "linux",
|
|
);
|
|
if (process.platform === "linux") {
|
|
await verifyMcpLinuxToolAvailability(client, mcpOptions);
|
|
} else {
|
|
const current = await client.callTool(
|
|
{ name: "current_document", arguments: {} },
|
|
mcpOptions,
|
|
);
|
|
await verifyMcpNonLinuxCurrentDocument(client, mcpOptions, current);
|
|
}
|
|
const closed = await client.callTool(
|
|
{ name: "close_binary", arguments: {} },
|
|
mcpOptions,
|
|
);
|
|
if (closed.isError === true)
|
|
throw new Error("packaged MCP could not close its binary");
|
|
await prompts.verifyPromptCompletion(client, mcpOptions, false);
|
|
};
|
|
|
|
const verifyMcpEvidenceBundle = async (client, mcpOptions, evidenceRoot) => {
|
|
const mcpBundlePath = join(evidenceRoot, "mcp.json");
|
|
const mcpExport = await client.callTool(
|
|
{ name: "export_evidence_bundle", arguments: { path: mcpBundlePath } },
|
|
mcpOptions,
|
|
);
|
|
if (mcpExport.isError === true)
|
|
throw new Error("packaged MCP evidence export failed");
|
|
const mcpImport = await client.callTool(
|
|
{ name: "import_evidence_bundle", arguments: { path: mcpBundlePath } },
|
|
mcpOptions,
|
|
);
|
|
if (mcpImport.isError === true)
|
|
throw new Error("packaged MCP evidence import failed");
|
|
};
|
|
|
|
const verifyMcpInlineArtifactEvidence = async (
|
|
client,
|
|
mcpOptions,
|
|
artifactArchive,
|
|
) => {
|
|
const opened = await client.callTool(
|
|
{ name: "open_binary", arguments: { path: artifactArchive } },
|
|
mcpOptions,
|
|
);
|
|
assert.notEqual(opened.isError, true, JSON.stringify(opened));
|
|
let outputRoot;
|
|
try {
|
|
const inspected = await client.callTool(
|
|
{ name: "inspect_artifact", arguments: {} },
|
|
mcpOptions,
|
|
);
|
|
assert.notEqual(inspected.isError, true, JSON.stringify(inspected));
|
|
const source = parseEvidence(inspected.structuredContent);
|
|
assert.deepEqual(
|
|
source.normalized_result,
|
|
inspected.structuredContent?.normalized_result,
|
|
);
|
|
assert.deepEqual(
|
|
json(prompts.mcpText(inspected)),
|
|
inspected.structuredContent,
|
|
);
|
|
const compared = await client.callTool(
|
|
{
|
|
name: "compare_artifacts",
|
|
arguments: {
|
|
left: inspected.structuredContent,
|
|
right: inspected.structuredContent,
|
|
},
|
|
},
|
|
mcpOptions,
|
|
);
|
|
assert.notEqual(compared.isError, true, JSON.stringify(compared));
|
|
const comparison = parseEvidence(compared.structuredContent);
|
|
assert.deepEqual(
|
|
comparison.normalized_result,
|
|
compared.structuredContent?.normalized_result,
|
|
);
|
|
assert.ok(comparison.evidence_links.includes(source.evidence_id));
|
|
const bundle = await client.callTool(
|
|
{ name: "get_evidence_bundle", arguments: {} },
|
|
mcpOptions,
|
|
);
|
|
assert.notEqual(bundle.isError, true, JSON.stringify(bundle));
|
|
for (const evidence of [source, comparison]) {
|
|
assert.deepEqual(
|
|
bundle.structuredContent?.result?.records.find(
|
|
(record) => record.evidence_id === evidence.evidence_id,
|
|
),
|
|
evidence,
|
|
);
|
|
}
|
|
const extracted = await client.callTool(
|
|
{ name: "extract_artifact", arguments: {} },
|
|
mcpOptions,
|
|
);
|
|
assert.notEqual(extracted.isError, true, JSON.stringify(extracted));
|
|
const extraction = parseEvidence(extracted.structuredContent);
|
|
assert.deepEqual(
|
|
extraction.normalized_result,
|
|
extracted.structuredContent?.normalized_result,
|
|
);
|
|
const materialized = extracted.structuredContent?.normalized_result;
|
|
assert.equal(typeof materialized?.output_root, "string");
|
|
outputRoot = materialized.output_root;
|
|
assert.equal(materialized.containment_verified, true);
|
|
assert.equal(
|
|
await readFile(join(outputRoot, "app/main.js"), "utf8"),
|
|
"main();",
|
|
);
|
|
} finally {
|
|
try {
|
|
const closed = await client.callTool(
|
|
{ name: "close_binary", arguments: {} },
|
|
mcpOptions,
|
|
);
|
|
assert.notEqual(closed.isError, true, JSON.stringify(closed));
|
|
} finally {
|
|
if (outputRoot !== undefined)
|
|
await rm(outputRoot, { recursive: true, force: true });
|
|
}
|
|
}
|
|
};
|
|
|
|
/** Connect to the packaged MCP server and verify catalog and Evidence composition. */
|
|
export async function verifyPackageMcp({
|
|
cli,
|
|
environment,
|
|
evidenceRoot,
|
|
artifactArchive,
|
|
}) {
|
|
const diagnosed = json(
|
|
(
|
|
await execute(cli, ["mcp", "doctor", "--json"], {
|
|
env: environment,
|
|
timeout: MCP_STARTUP_POLICY.doctorDeadlineMs,
|
|
})
|
|
).stdout,
|
|
);
|
|
if (
|
|
diagnosed.healthy !== true ||
|
|
diagnosed.inventory?.tools?.observed !==
|
|
diagnosed.inventory?.tools?.expected
|
|
)
|
|
throw new Error("packaged production MCP doctor failed");
|
|
const transport = new StdioClientTransport({
|
|
command: cli,
|
|
args: ["mcp"],
|
|
env: {
|
|
...environment,
|
|
},
|
|
stderr: "pipe",
|
|
});
|
|
let mcpStderr = "";
|
|
transport.stderr?.on("data", (chunk) => {
|
|
mcpStderr += chunk.toString();
|
|
});
|
|
const client = new Client({ name: "package-smoke", version: "1.0.0" });
|
|
try {
|
|
await client.connect(transport);
|
|
const mcpOptions = { timeout: 15_000 };
|
|
await verifyMcpToolsAndPrompts(client, mcpOptions);
|
|
await verifyMcpTargetFree(client, mcpOptions);
|
|
await verifyMcpUnknownProvider(client, mcpOptions);
|
|
await verifyMcpBinaryLifecycle(client, mcpOptions);
|
|
await verifyMcpInlineArtifactEvidence(client, mcpOptions, artifactArchive);
|
|
await verifyMcpEvidenceBundle(client, mcpOptions, evidenceRoot);
|
|
} catch (cause) {
|
|
throw new Error(`packaged MCP smoke failed: ${mcpStderr}`, { cause });
|
|
} finally {
|
|
await client.close();
|
|
await transport.close();
|
|
}
|
|
}
|