fix(mcp): preserve retained evidence on oversized delivery

This commit is contained in:
morluto committed 2026-10-09 05:55:55 +08:00
1 parent 826583dd43
commit 0639a541a3
36 files changed
+339 -184

No files matched your search

+10 -1
View File
@@ -184,7 +184,9 @@ MCP SDK transport tests with recording providers remain integration tests.
They are useful for schema drift and failure projection but do not prove that
Hopper, Ghidra or another substituted engine works. `verify:package` proves
packaging/install behavior and fake-provider integration; use the corresponding
real-provider lanes for engine claims. Real Apple dispatch and Interface
real-provider lanes for engine claims. Packed-bridge checks verify shipped files
and Python syntax without deleted mock fixtures or source-spelling assertions.
Real Apple dispatch and Interface
Builder verifiers currently prove format integration through production readers.
`verify:hopper` exercises an installed Hopper through the production stdio MCP
@@ -354,6 +356,13 @@ workflow verifies leaf and qualified renames, repeated reuse of fully qualified
readback, lookup by the returned name, literal namespace-like leaf names,
rejection of empty qualified leaf names without changing comments, CLI
behavior, and independent CLI/MCP database ownership.
Large-result probes compile initialized host-native data sized from the pinned
MCP SDK receive budget. Real byte reads, annotation edits and function dossiers
exceed that budget while preserving the connection and active analysis run.
Each delivery error must identify its successfully retained Evidence record;
export must recover every source byte and complete annotation, with CLI parity
and an unchanged executable. The focused formatter check retains only the
separate case where recording was not acknowledged.
Malformed annotation readback, memory completeness, and inventory data remain separate
SDK/provider integration cases; success from a real
provider cannot establish rejection of a contradictory provider response.
+19
View File
@@ -7,10 +7,12 @@ import { setTimeout as delay } from "node:timers/promises";
import { promisify } from "node:util";
import Ajv from "ajv/dist/2020.js";
import addFormats from "ajv-formats";
import { analysisErrorProjectionSchema } from "../../dist/contracts/errorSchemas.js";
import { mcpTextValue, requireMcpResult } from "./mcp-verifier-results.mjs";
import { verifyLegacyGhidraReferenceSnapshot } from "./ghidra-reference-snapshot-e2e.mjs";
import { verifyGhidraSnapshotLifecycle } from "./real-ghidra-snapshot-lifecycle.mjs";
import { verifyGhidraTargetAdmission } from "./real-ghidra-target-admission.mjs";
import { verifyGhidraLargeResults } from "./real-ghidra-large-results.mjs";
import { verifyGhidraNamespaceAnnotations } from "./real-ghidra-namespace-annotations.mjs";
/** Probe real Ghidra location, annotation and error contracts through public adapters. */
@@ -53,6 +55,15 @@ export async function verifyGhidraBoundaries(
true,
`${name} accepted ${JSON.stringify(args)}`,
);
// SDK input-schema rejections are text-only; application errors use the
// canonical structured error projection, separately from success schemas.
if (reply.structuredContent !== undefined) {
analysisErrorProjectionSchema.parse(reply.structuredContent.error);
assert.deepEqual(
reply.structuredContent,
JSON.parse(mcpTextValue(reply)),
);
}
if (diagnostic !== undefined) {
const error = reply.structuredContent?.error;
assert.equal(error?.code, "invalid_request");
@@ -664,6 +675,13 @@ export async function verifyGhidraBoundaries(
entrypoint,
env,
});
await verifyGhidraLargeResults({
call,
reject: invalid,
target,
entrypoint,
env,
});
await assert.rejects(access(socketRoot), { code: "ENOENT" });
await assert.rejects(access(runtimeRoot), { code: "ENOENT" });
assert.equal(await call("procedure_address", { procedure: name }), address);
@@ -705,6 +723,7 @@ export async function verifyGhidraBoundaries(
imported_source_identity_retained: true,
equivalent_instruction_address_spellings: true,
qualified_annotation_name_roundtrip: true,
oversized_result_retention_and_complete_export: true,
source_immutable: true,
reopen_discards_edits: true,
long_tmpdir_private_socket_cleanup: true,
+178
View File
@@ -0,0 +1,178 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server";
/** Exercise complete oversized results and recovery through real CLI/MCP adapters. */
export async function verifyGhidraLargeResults({
call,
reject,
target,
entrypoint,
env,
}) {
const exec = promisify(execFile);
const compiler = process.env.REA_CC ?? "cc";
try {
await exec(compiler, ["--version"], { env, timeout: 30000 });
} catch (cause) {
throw new Error(
`Ghidra large-result verification requires a working host C compiler '${compiler}'.`,
{ cause },
);
}
// Hex encoding, Evidence projection and the two MCP representations multiply
// the source size. Derive the fixture from the pinned SDK's receive budget.
const length = Math.ceil(STDIO_DEFAULT_MAX_BUFFER_SIZE / 4);
const workspace = await mkdtemp(join(tmpdir(), "rea-ghidra-large-results-"));
const path = join(workspace, "large-results");
const source = fileURLToPath(
new URL("../../tests/conformance/ghidra/large-results.c", import.meta.url),
);
try {
await exec(
compiler,
["-O0", `-DREA_PAYLOAD_BYTES=${length}`, source, "-o", path],
{ env, timeout: 30000 },
);
const bytes = await readFile(path);
const sha256 = createHash("sha256").update(bytes).digest("hex");
await call("close_binary");
await call("open_binary", { path, provider_id: "ghidra" });
const names = await call("list_names");
const payload = names.find(({ value }) =>
/(?:^|_)rea_large_payload$/u.test(value),
);
const entry = names.find(({ value }) =>
/(?:^|_)rea_large_entry$/u.test(value),
);
assert.ok(payload);
assert.ok(entry);
const mapping = await call("address_to_file_offset", {
address: payload.address,
});
const expected = bytes.subarray(
mapping.file_offset,
mapping.file_offset + length,
);
assert.equal(expected.length, length);
assert.equal(expected[0], 0x5a);
assert.equal(expected.at(-1), 0xa5);
const before = await call("binary_session");
const readError = await reject("read_bytes", {
address: payload.address,
length,
});
const readId = retainedReference(readError, "read_bytes");
const comment =
"REA_BIG_COMMENT:" +
"x".repeat(Math.ceil(STDIO_DEFAULT_MAX_BUFFER_SIZE / 10));
const annotationError = await reject("annotate_native_function", {
procedure: entry.address,
name: "rea_frame_probe",
comment,
inline_comment: comment,
});
const annotationId = retainedReference(
annotationError,
"annotate_native_function",
);
assert.equal(
await call("procedure_address", { procedure: "rea_frame_probe" }),
entry.address,
"A delivery constraint must preserve the successfully committed edit",
);
const dossierError = await reject("analyze_function", {
procedure: "rea_frame_probe",
});
const dossierId = retainedReference(dossierError, "analyze_function");
const after = await call("binary_session");
assert.equal(after.analysis_run.run_id, before.analysis_run.run_id);
assert.equal(after.sha256, sha256);
assert.equal(
(await call("read_bytes", { address: payload.address, length: 1 }))
.bytes_hex,
"5a",
);
const lastAddress = `0x${(BigInt(payload.address) + BigInt(length - 1)).toString(16)}`;
assert.equal(
(await call("read_bytes", { address: lastAddress, length: 1 })).bytes_hex,
"a5",
);
const bundlePath = join(workspace, "bundle.json");
await call("export_evidence_bundle", { path: bundlePath });
const bundle = JSON.parse(await readFile(bundlePath, "utf8"));
const retained = (id) => {
const record = bundle.records.find(
({ evidence_id }) => evidence_id === id,
);
assert.ok(record, `Missing exported retained record ${id}`);
assert.equal(record.provider.id, "ghidra");
assert.equal(record.subject.digest.sha256, sha256);
return record;
};
const readRecord = retained(readId);
const read = readRecord.normalized_result;
assert.equal(read.requested_bytes, length);
assert.equal(read.returned_bytes, length);
assert.equal(read.complete, true);
assert.equal(read.bytes_hex, expected.toString("hex"));
assert.equal(readRecord.raw_result.bytes_hex, read.bytes_hex);
const annotationRecord = retained(annotationId);
const annotation = annotationRecord.normalized_result;
assert.equal(annotation.annotations.name, "rea_frame_probe");
assert.equal(annotation.annotations.comment, comment);
assert.equal(annotation.annotations.inline_comment, comment);
assert.equal(annotationRecord.raw_result.annotations.comment, comment);
assert.equal(
annotationRecord.raw_result.annotations.inline_comment,
comment,
);
const dossierRecord = retained(dossierId);
const dossier = dossierRecord.normalized_result;
assert.equal(dossier.procedure.name, "rea_frame_probe");
assert.ok(dossier.comments.some(({ text }) => text === comment));
assert.ok(
dossierRecord.raw_result.comments.some(({ text }) => text === comment),
);
const { stdout } = await exec(
process.execPath,
[
entrypoint,
"read-bytes",
path,
payload.address,
"--length",
String(length),
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 240000, maxBuffer: 72 * 1024 * 1024 },
);
assert.deepEqual(JSON.parse(stdout).normalized_result, read);
assert.deepEqual(await readFile(path), bytes);
await call("close_binary");
await call("open_binary", { path: target.path, provider_id: "ghidra" });
} finally {
await rm(workspace, { recursive: true, force: true });
}
}
function retainedReference(error, operation) {
assert.equal(error.code, "resource_constraint");
assert.equal(error.details.operation, operation);
assert.equal(error.details.resource, "transport");
const limits = error.details.reported_limits;
assert.equal(limits.boundary, "mcp-response");
assert.equal(limits.constraint, "receive-buffer");
assert.ok(limits.response_bytes_at_least > limits.result_budget_bytes);
assert.equal(limits.evidence_reference.kind, "retained-evidence");
assert.match(limits.evidence_reference.evidence_id, /^ev_[a-f0-9]{64}$/u);
return limits.evidence_reference.evidence_id;
}
+1 -1
View File
@@ -44,7 +44,7 @@ export const analysisErrorRemediationAction = (
return (
error.remediationAction ??
(error.resource === "transport"
? "Use the reported Evidence reference with a focused analysis tool when retained in this session, or export the session through export_evidence_bundle to a caller-selected path. Complete CLI JSON output is also available. The connection remains usable."
? "Export retained session evidence through export_evidence_bundle to a caller-selected path, or use complete CLI JSON output. Tools that accept retained-evidence references can inspect compatible records. The connection remains usable."
: error.resource === "cpu"
? "Review the reported worker CPU limits and observed signal. Retry with sufficient CPU time or a smaller artifact; REA retains tighter inherited limits."
: error.resource === "file-size"
+2 -4
View File
@@ -7,7 +7,7 @@ import type { AndroidOperation } from "../domain/android/androidAnalysis.js";
import type { Logger } from "../logger.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
/** Bind Android handlers to their exact named contracts through shared workflows. */
export const registerAndroidTools = (
@@ -24,9 +24,7 @@ export const registerAndroidTools = (
);
if (!result.ok) return toCallToolResult(result, contract);
const recorded = recordEvidence?.(result.value);
return recorded !== undefined && !recorded.ok
? toCallToolResult(recorded, contract)
: toCallToolResult(result, contract);
return toEvidenceToolResult(result.value, contract, recorded);
};
};
const packageContract = toolContract("inspect_android_package");
+2 -10
View File
@@ -1,6 +1,6 @@
import { APPLICATION_TOOL_CONTRACTS } from "../../contracts/applicationToolContracts.js";
import type { Evidence } from "../../domain/evidence.js";
import { toCallToolResult } from "../toolResult.js";
import { toEvidenceToolResult } from "../toolResult.js";
import type { ApplicationToolRegistration } from "./types.js";
export const recordResult = (
@@ -19,15 +19,7 @@ export const recordResult = (
evidence,
unknownRegistration(evidence, unknownKind),
);
if (recorded !== undefined && !recorded.ok)
return toCallToolResult(recorded, contract);
return toCallToolResult(
{ ok: true, value: evidence },
contract,
recorded === undefined
? undefined
: { retainedEvidenceId: evidence.evidence_id },
);
return toEvidenceToolResult(evidence, contract, recorded);
};
const unknownRegistration = (
+4 -3
View File
@@ -10,7 +10,7 @@ import { recordDerivedEvidence } from "./recordDerivedEvidence.js";
import { runDerivedOperation } from "./runDerivedOperation.js";
import { ARTIFACT_COMPARISON_PROVIDER } from "./sessionToolPolicies.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
/** Register Evidence-backed deterministic artifact comparison. */
export const registerArtifactComparisonTool = (
@@ -50,13 +50,14 @@ export const registerArtifactComparisonTool = (
limitations: comparison.limitations,
evidenceLinks: [...leftEvidenceIds, ...rightEvidenceIds],
});
return toCallToolResult(
return toEvidenceToolResult(
evidence,
contract,
recordDerivedEvidence(
session,
evidence,
artifactUnknownInput(left, right, comparison.status),
),
contract,
);
},
);
+2 -4
View File
@@ -5,7 +5,7 @@ import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "../logger.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
/** Bind offline layout inspection to its named contract and session Evidence owner. */
export const registerBinaryDiagnosticsTools = (
@@ -24,9 +24,7 @@ export const registerBinaryDiagnosticsTools = (
);
if (!result.ok) return toCallToolResult(result, contract);
const recorded = recordEvidence?.(result.value);
return recorded !== undefined && !recorded.ok
? toCallToolResult(recorded, contract)
: toCallToolResult(result, contract);
return toEvidenceToolResult(result.value, contract, recorded);
},
);
};
+2 -4
View File
@@ -13,7 +13,7 @@ import { browserScenarioSchema } from "../domain/browserScenario.js";
import type { Logger } from "../logger.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
interface BrowserScenarioToolRegistration {
readonly logger: Logger;
@@ -45,9 +45,7 @@ export const registerBrowserScenarioTool = (
);
if (!result.ok) return toCallToolResult(result, contract);
const recorded = options.recordEvidence?.(result.value);
return recorded !== undefined && !recorded.ok
? toCallToolResult(recorded, contract)
: toCallToolResult({ ok: true, value: result.value }, contract);
return toEvidenceToolResult(result.value, contract, recorded);
},
);
};
+2 -10
View File
@@ -28,7 +28,7 @@ import type { Logger } from "../logger.js";
import { mcpProgressReporter } from "./mcpProgress.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
interface BrowserToolRegistration {
readonly logger: Logger;
@@ -206,13 +206,5 @@ const evidenceResult = (
evidence: Evidence,
) => {
const recorded = options.recordEvidence?.(evidence);
return recorded !== undefined && !recorded.ok
? toCallToolResult(recorded, contract)
: toCallToolResult(
{ ok: true, value: evidence },
contract,
recorded === undefined
? undefined
: { retainedEvidenceId: evidence.evidence_id },
);
return toEvidenceToolResult(evidence, contract, recorded);
};
+2 -3
View File
@@ -6,11 +6,10 @@ import { toolContract } from "../contracts/toolContracts.js";
import { compareBundles } from "../domain/bundleComparison.js";
import { createEvidence } from "../domain/evidence.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { ok } from "../domain/result.js";
import { runDerivedOperation } from "./runDerivedOperation.js";
import { BUNDLE_COMPARISON_PROVIDER } from "./sessionToolPolicies.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
/** Register canonical Evidence bundle comparison. */
export const registerBundleComparisonTool = (
@@ -47,7 +46,7 @@ export const registerBundleComparisonTool = (
limitations: comparison.limitations,
});
const recorded = session.recordEvidence(evidence);
return toCallToolResult(recorded.ok ? ok(evidence) : recorded, contract);
return toEvidenceToolResult(evidence, contract, recorded);
},
);
};
+2 -10
View File
@@ -25,7 +25,7 @@ import type { Logger } from "../logger.js";
import { mcpProgressReporter } from "./mcpProgress.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
interface ElectronToolRegistration {
readonly logger: Logger;
@@ -166,13 +166,5 @@ const evidenceResult = (
evidence: Evidence,
) => {
const recorded = options.recordEvidence?.(evidence);
return recorded !== undefined && !recorded.ok
? toCallToolResult(recorded, contract)
: toCallToolResult(
{ ok: true, value: evidence },
contract,
recorded === undefined
? undefined
: { retainedEvidenceId: evidence.evidence_id },
);
return toEvidenceToolResult(evidence, contract, recorded);
};
+8 -8
View File
@@ -27,7 +27,7 @@ import type { Logger } from "../logger.js";
import { mcpProgressReporter } from "./mcpProgress.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
import { executeFunctionAnalysisEvidence } from "../application/FunctionAnalysisEvidence.js";
/** Optional session services used by enhanced tool registration. */
@@ -271,7 +271,7 @@ const executeEnhancedTool = async (
recordUnknown: registration.recordUnknown,
});
if (!unknowns.ok) return toCallToolResult(unknowns, contract);
return toCallToolResult({ ok: true, value: evidence }, contract);
return toEvidenceToolResult(evidence, contract, recorded);
}
return toCallToolResult(result, contract);
};
@@ -308,12 +308,12 @@ const executeFunctionTool = async (
message: result.ok ? "completed" : "failed",
terminal: true,
});
if (result.ok) {
const recorded = registration.recordEvidence?.(result.value);
if (recorded !== undefined && !recorded.ok)
return toCallToolResult(recorded, contract);
}
return toCallToolResult(result, contract);
if (!result.ok) return toCallToolResult(result, contract);
return toEvidenceToolResult(
result.value,
contract,
registration.recordEvidence?.(result.value),
);
};
const jsonParameters = (
+2 -4
View File
@@ -13,7 +13,7 @@ import type { Logger } from "../logger.js";
import { mcpProgressReporter } from "./mcpProgress.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
import { createArtifactExtractionDestination } from "../application/artifacts/ArtifactExtractionDestination.js";
interface EvidenceToolRegistration {
@@ -94,9 +94,7 @@ export const registerEvidenceTools = (
return toCallToolResult(sourceRecorded, contract);
}
const recorded = options.recordEvidence?.(evidence);
return recorded !== undefined && !recorded.ok
? toCallToolResult(recorded, contract)
: toCallToolResult({ ok: true, value: evidence }, contract);
return toEvidenceToolResult(evidence, contract, recorded);
},
);
}
+2 -4
View File
@@ -5,7 +5,7 @@ import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "../logger.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
/** Bind the named EVM contract to shared validation and session Evidence recording. */
export const registerEvmTools = (
@@ -24,9 +24,7 @@ export const registerEvmTools = (
);
if (!result.ok) return toCallToolResult(result, contract);
const recorded = recordEvidence?.(result.value);
return recorded !== undefined && !recorded.ok
? toCallToolResult(recorded, contract)
: toCallToolResult(result, contract);
return toEvidenceToolResult(result.value, contract, recorded);
},
);
};
+2 -4
View File
@@ -7,7 +7,7 @@ import type { FirmwareOperation } from "../domain/firmware/firmwareAnalysis.js";
import type { Logger } from "../logger.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
/** Bind firmware handlers to their exact named schemas. */
export const registerFirmwareTools = (
@@ -26,9 +26,7 @@ export const registerFirmwareTools = (
);
if (!result.ok) return toCallToolResult(result, contract);
const recorded = recordEvidence?.(result.value);
return recorded !== undefined && !recorded.ok
? toCallToolResult(recorded, contract)
: toCallToolResult(result, contract);
return toEvidenceToolResult(result.value, contract, recorded);
};
const inspect = toolContract("inspect_firmware_regions");
server.registerTool(
+2 -2
View File
@@ -17,7 +17,7 @@ import { recordSessionEvidenceSources } from "./sessionEvidence.js";
import { runDerivedOperation } from "./runDerivedOperation.js";
import { FUNCTION_COMPARISON_PROVIDER } from "./sessionToolPolicies.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
/** Register explicit Evidence-backed function comparison. */
export const registerFunctionComparisonTool = (
@@ -89,7 +89,7 @@ export const registerFunctionComparisonTool = (
rightIds,
}),
);
return toCallToolResult(recorded, contract);
return toEvidenceToolResult(evidence, contract, recorded);
},
);
};
+9 -7
View File
@@ -23,7 +23,7 @@ import {
STATIC_RUNTIME_PROVIDER,
} from "./sessionToolPolicies.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
/** Register Evidence-composed differential investigation workflows. */
export const registerInvestigationTools = (
@@ -101,7 +101,7 @@ const registerChangedBehavior = (
],
},
);
return toCallToolResult(recorded, contract);
return toEvidenceToolResult(evidence, contract, recorded);
},
);
};
@@ -158,7 +158,7 @@ const registerCallPath = (
],
},
);
return toCallToolResult(recorded, contract);
return toEvidenceToolResult(evidence, contract, recorded);
},
);
};
@@ -198,7 +198,9 @@ const registerStaticRuntime = (
limitations: result.limitations,
evidenceLinks: links,
});
return toCallToolResult(
return toEvidenceToolResult(
evidence,
contract,
recordWorkflowEvidence(
session,
evidence,
@@ -218,7 +220,6 @@ const registerStaticRuntime = (
],
},
),
contract,
);
},
);
@@ -255,7 +256,9 @@ const registerReconstruction = (
limitations: result.limitations,
evidenceLinks: links,
});
return toCallToolResult(
return toEvidenceToolResult(
evidence,
contract,
recordWorkflowEvidence(session, evidence, result.status === "unknown", {
question: "Does the reconstruction satisfy every declared claim?",
domain: "reconstruction-verification",
@@ -266,7 +269,6 @@ const registerReconstruction = (
rationale,
})),
}),
contract,
);
},
);
+2 -4
View File
@@ -5,7 +5,7 @@ import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "../logger.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
/** Bind optional recovery to its exact named contract without acquiring an engine. */
export const registerJavaScriptRecoveryTool = (
@@ -24,9 +24,7 @@ export const registerJavaScriptRecoveryTool = (
);
if (!result.ok) return toCallToolResult(result, contract);
const recorded = recordEvidence?.(result.value);
return recorded !== undefined && !recorded.ok
? toCallToolResult(recorded, contract)
: toCallToolResult(result, contract);
return toEvidenceToolResult(result.value, contract, recorded);
},
);
};
@@ -19,7 +19,7 @@ import { observeJavaScriptRuntimeInputSchema } from "../domain/javascript/javasc
import type { Logger } from "../logger.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
interface RuntimeToolRegistration {
readonly logger: Logger;
@@ -83,7 +83,5 @@ const runRuntimeTool = async <Input>(
);
if (!result.ok) return toCallToolResult(result, contract);
const recorded = options.recordEvidence?.(result.value);
return recorded !== undefined && !recorded.ok
? toCallToolResult(recorded, contract)
: toCallToolResult({ ok: true, value: result.value }, contract);
return toEvidenceToolResult(result.value, contract, recorded);
};
@@ -5,7 +5,7 @@ import { compareManagedMembersEvidenceValidated } from "../../application/manage
import { managedMemberComparisonResultSchema } from "../../domain/managed/managedMemberComparison.js";
import { logToolExecution } from "../toolLogging.js";
import { toolRegistrationOptions } from "../toolRegistrationOptions.js";
import { toCallToolResult } from "../toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "../toolResult.js";
import { managedWorkflowContract } from "./contract.js";
import { resolveManagedEvidence } from "./evidence.js";
import type { ManagedWorkflowToolRegistration } from "./types.js";
@@ -68,12 +68,7 @@ export const registerCompareManagedMembers = (
relationships: [],
})
: options.recordEvidence?.(result.value);
if (output !== undefined && !output.ok)
return toCallToolResult(output, compareContract);
return toCallToolResult(
{ ok: true, value: result.value },
compareContract,
);
return toEvidenceToolResult(result.value, compareContract, output);
},
);
};
@@ -3,7 +3,7 @@ import type { McpServer } from "@modelcontextprotocol/server";
import { importManagedReconstructionEvidenceValidated } from "../../application/managed/ManagedReconstructionService.js";
import { logToolExecution } from "../toolLogging.js";
import { toolRegistrationOptions } from "../toolRegistrationOptions.js";
import { toCallToolResult } from "../toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "../toolResult.js";
import { managedWorkflowContract } from "./contract.js";
import { resolveManagedEvidence } from "./evidence.js";
import type { ManagedWorkflowToolRegistration } from "./types.js";
@@ -41,9 +41,11 @@ export const registerImportManagedReconstruction = (
if (recordedSource !== undefined && !recordedSource.ok)
return toCallToolResult(recordedSource, reconstructionContract);
const recorded = options.recordEvidence?.(result.value);
if (recorded !== undefined && !recorded.ok)
return toCallToolResult(recorded, reconstructionContract);
return toCallToolResult(result, reconstructionContract);
return toEvidenceToolResult(
result.value,
reconstructionContract,
recorded,
);
},
);
};
@@ -4,7 +4,7 @@ import type { McpServer } from "@modelcontextprotocol/server";
import { projectManagedApplicationGraphEvidence } from "../../application/managed/ManagedApplicationGraphService.js";
import { logToolExecution } from "../toolLogging.js";
import { toolRegistrationOptions } from "../toolRegistrationOptions.js";
import { toCallToolResult } from "../toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "../toolResult.js";
import { managedWorkflowContract } from "./contract.js";
import {
resolveManagedArtifactEvidence,
@@ -63,9 +63,7 @@ export const registerProjectManagedApplicationGraph = (
if (!recordedSources.ok)
return toCallToolResult(recordedSources, graphContract);
const recorded = options.recordEvidence?.(result.value);
if (recorded !== undefined && !recorded.ok)
return toCallToolResult(recorded, graphContract);
return toCallToolResult(result, graphContract);
return toEvidenceToolResult(result.value, graphContract, recorded);
},
);
};
@@ -5,7 +5,7 @@ import { verifyManagedNativeBoundariesEvidence } from "../../application/managed
import { managedNativeVerificationResultSchema } from "../../domain/managed/managedNativeVerification.js";
import { logToolExecution } from "../toolLogging.js";
import { toolRegistrationOptions } from "../toolRegistrationOptions.js";
import { toCallToolResult } from "../toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "../toolResult.js";
import { managedWorkflowContract } from "./contract.js";
import {
resolveManagedBoundaryEvidence,
@@ -90,9 +90,11 @@ export const registerVerifyManagedNativeBoundaries = (
relationships: [],
})
: options.recordEvidence?.(result.value);
if (output !== undefined && !output.ok)
return toCallToolResult(output, nativeVerificationContract);
return toCallToolResult(result, nativeVerificationContract);
return toEvidenceToolResult(
result.value,
nativeVerificationContract,
output,
);
},
);
};
+2 -4
View File
@@ -22,7 +22,7 @@ import type { Logger } from "../logger.js";
import { mcpProgressReporter } from "./mcpProgress.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
/** Optional session services used by direct tool registration. */
export interface OfficialToolRegistration {
@@ -91,9 +91,7 @@ const registerOfficialTool = (
},
);
const recorded = registration.recordEvidence?.(evidence);
if (recorded !== undefined && !recorded.ok)
return toCallToolResult(recorded, contract);
return toCallToolResult({ ok: true, value: evidence }, contract);
return toEvidenceToolResult(evidence, contract, recorded);
},
);
};
+4 -3
View File
@@ -25,7 +25,7 @@ import { recordSessionEvidenceSources } from "./sessionEvidence.js";
import { runDerivedOperation } from "./runDerivedOperation.js";
import { PROCESS_PROVIDER } from "./sessionToolPolicies.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
const PROCESS_CAPTURE_EVIDENCE = {
operation: "capture_process_scenario",
@@ -143,7 +143,9 @@ export const registerProcessComparisonTool = (
);
if (!recordedSources.ok)
return toCallToolResult(recordedSources, contract);
return toCallToolResult(
return toEvidenceToolResult(
evidence,
contract,
recordDerivedEvidence(
session,
evidence,
@@ -156,7 +158,6 @@ export const registerProcessComparisonTool = (
comparison,
),
),
contract,
);
},
);
+2 -4
View File
@@ -5,7 +5,7 @@ import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "../logger.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
/** Bind recorded crash inspection to its named contract and session Evidence owner. */
export const registerRecordedCrashTools = (
@@ -24,9 +24,7 @@ export const registerRecordedCrashTools = (
);
if (!result.ok) return toCallToolResult(result, contract);
const recorded = recordEvidence?.(result.value);
return recorded !== undefined && !recorded.ok
? toCallToolResult(recorded, contract)
: toCallToolResult(result, contract);
return toEvidenceToolResult(result.value, contract, recorded);
},
);
};
+2 -2
View File
@@ -35,7 +35,7 @@ import {
} from "./sessionAvailabilityPolicy.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
const recordProcessResidualUnknowns = (
session: BinarySessionPort,
@@ -122,7 +122,7 @@ const registerProcessTools = ({
captured.value.residual_unknowns,
);
if (!unknowns.ok) return toCallToolResult(unknowns, captureContract);
return toCallToolResult(ok(evidence), captureContract);
return toEvidenceToolResult(evidence, captureContract, recorded);
},
);
};
+2 -4
View File
@@ -5,7 +5,7 @@ import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "../logger.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
/** Bind the module relationship workflow to its exact canonical contract. */
export const registerWebModuleTool = (
@@ -24,9 +24,7 @@ export const registerWebModuleTool = (
);
if (!result.ok) return toCallToolResult(result, contract);
const recorded = recordEvidence?.(result.value);
return recorded !== undefined && !recorded.ok
? toCallToolResult(recorded, contract)
: toCallToolResult(result, contract);
return toEvidenceToolResult(result.value, contract, recorded);
},
);
};
+2 -4
View File
@@ -8,7 +8,7 @@ import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "../logger.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
/** Bind historical inspection to its named contract and caller-owned Evidence writer. */
export const registerWebNetworkCaptureTool = (
@@ -38,9 +38,7 @@ export const registerWebNetworkCaptureTool = (
);
if (!result.ok) return toCallToolResult(result, contract);
const recorded = recordEvidence?.(result.value);
return recorded !== undefined && !recorded.ok
? toCallToolResult(recorded, contract)
: toCallToolResult(result, contract);
return toEvidenceToolResult(result.value, contract, recorded);
},
);
};
+3 -7
View File
@@ -6,7 +6,7 @@ import type { Logger } from "../logger.js";
import { mcpProgressReporter } from "./mcpProgress.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
/** Bind distinct runtime operations to named contracts and the session's Evidence owner. */
export const registerWebRuntimeTools = (
@@ -29,9 +29,7 @@ export const registerWebRuntimeTools = (
);
if (!result.ok) return toCallToolResult(result, execution);
const recorded = recordEvidence?.(result.value);
return recorded !== undefined && !recorded.ok
? toCallToolResult(recorded, execution)
: toCallToolResult(result, execution);
return toEvidenceToolResult(result.value, execution, recorded);
},
);
server.registerTool(
@@ -43,9 +41,7 @@ export const registerWebRuntimeTools = (
);
if (!result.ok) return toCallToolResult(result, listeners);
const recorded = recordEvidence?.(result.value);
return recorded !== undefined && !recorded.ok
? toCallToolResult(recorded, listeners)
: toCallToolResult(result, listeners);
return toEvidenceToolResult(result.value, listeners, recorded);
},
);
};
+2 -4
View File
@@ -6,7 +6,7 @@ import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "../logger.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
/** Register local script export without requiring a live browser provider. */
export const registerWebScriptTool = (
@@ -26,9 +26,7 @@ export const registerWebScriptTool = (
);
if (!result.ok) return toCallToolResult(result, contract);
const recorded = options.recordEvidence?.(result.value);
return recorded !== undefined && !recorded.ok
? toCallToolResult(recorded, contract)
: toCallToolResult(result, contract);
return toEvidenceToolResult(result.value, contract, recorded);
},
);
};
+2 -4
View File
@@ -5,7 +5,7 @@ import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "../logger.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { toCallToolResult } from "./toolResult.js";
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
/** Bind source point tracing to its exact named contract and session Evidence owner. */
export const registerWebSourceLocationTool = (
@@ -24,9 +24,7 @@ export const registerWebSourceLocationTool = (
);
if (!result.ok) return toCallToolResult(result, contract);
const recorded = recordEvidence?.(result.value);
return recorded !== undefined && !recorded.ok
? toCallToolResult(recorded, contract)
: toCallToolResult(result, contract);
return toEvidenceToolResult(result.value, contract, recorded);
},
);
};
+19 -2
View File
@@ -2,6 +2,7 @@ import type { CallToolResult } from "@modelcontextprotocol/server";
import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server";
import type { ToolContract } from "../contracts/toolContracts.js";
import type { Evidence } from "../domain/evidence.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { type AnalysisError } from "../domain/analysisErrorBase.js";
import type { JsonValue } from "../domain/jsonValue.js";
@@ -28,6 +29,22 @@ export const toCallToolResult = (
? successResult(result.value, contract, context)
: errorResult(result.error);
/** Deliver Evidence using the producer's explicit recording acknowledgment. */
export const toEvidenceToolResult = (
evidence: Evidence,
contract: ToolContract,
recorded: Result<unknown, AnalysisError> | undefined,
): CallToolResult =>
recorded !== undefined && !recorded.ok
? errorResult(recorded.error)
: toCallToolResult(
{ ok: true, value: evidence },
contract,
recorded === undefined
? undefined
: { retainedEvidenceId: evidence.evidence_id },
);
const errorResult = (error: AnalysisError): CallToolResult => {
const projected = projectAnalysisError(error);
const structuredContent = { error: projected };
@@ -66,8 +83,8 @@ const successResult = (
contract.name,
"transport",
encoded.constraint === "string-length"
? "The complete MCP response exceeds Node's single-string representation limit. Use a focused retained-Evidence query or export to consume the complete analysis."
: "The complete MCP response exceeds the stdio response budget. The analysis result was not truncated; use a focused retained-Evidence query or export to consume it.",
? "The operation completed, but its complete MCP response exceeds Node's single-string representation limit. Export retained evidence to consume the complete analysis, or use complete CLI JSON output."
: "The operation completed, but its complete MCP response exceeds the stdio response budget. The analysis result was not truncated; export retained evidence to consume it, or use complete CLI JSON output.",
{
boundary: "mcp-response",
default_receive_buffer_bytes: STDIO_DEFAULT_MAX_BUFFER_SIZE,
+19 -40
View File
@@ -1,14 +1,13 @@
import { describe, expect, it } from "vitest";
import { z } from "zod";
import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { ok } from "../../../src/domain/result.js";
import { toCallToolResult } from "../../../src/server/toolResult.js";
import { EvidenceLedger } from "../../../src/application/investigation/EvidenceLedger.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { toEvidenceToolResult } from "../../../src/server/toolResult.js";
describe("large complete Evidence MCP delivery", () => {
it("preserves retained content and returns an actionable framing constraint", () => {
it("does not claim retention without a recording acknowledgment", () => {
const normalized = {
payload: "x".repeat(Math.ceil(STDIO_DEFAULT_MAX_BUFFER_SIZE / 3)),
};
@@ -21,49 +20,29 @@ describe("large complete Evidence MCP delivery", () => {
result: normalized,
},
);
const ledger = new EvidenceLedger();
expect(ledger.record(evidence).ok).toBe(true);
const result = toCallToolResult(
ok(evidence),
const detached = toEvidenceToolResult(
evidence,
toolContract("analyze_javascript_application"),
{ retainedEvidenceId: evidence.evidence_id },
undefined,
);
expect(result.isError).toBe(true);
expect(result.structuredContent).toMatchObject({
error: {
code: "resource_constraint",
category: "resource_constraint",
details: {
resource: "transport",
reported_limits: {
boundary: "mcp-response",
constraint: "receive-buffer",
evidence_reference: {
kind: "retained-evidence",
evidence_id: evidence.evidence_id,
},
},
},
},
});
expect(Buffer.byteLength(JSON.stringify(result))).toBeLessThan(
expect(detached.isError).toBe(true);
expect(Buffer.byteLength(JSON.stringify(detached))).toBeLessThan(
STDIO_DEFAULT_MAX_BUFFER_SIZE,
);
expect(result.content).toEqual([
{ type: "text", text: JSON.stringify(result.structuredContent) },
]);
const retained = ledger.get(evidence.evidence_id);
expect(parseEvidence(retained)).toEqual(evidence);
expect(retained?.normalized_result).toEqual(normalized);
const detached = toCallToolResult(
ok(evidence),
toolContract("analyze_javascript_application"),
);
expect(detached.structuredContent).toMatchObject({
error: {
details: { reported_limits: { evidence_id: evidence.evidence_id } },
},
});
expect(JSON.stringify(detached)).not.toContain("retained-evidence");
const limits = z
.object({
error: z.object({
details: z.object({
reported_limits: z.record(z.string(), z.unknown()),
}),
}),
})
.parse(detached.structuredContent).error.details.reported_limits;
expect(limits).not.toHaveProperty("evidence_reference");
});
});
+9
View File
@@ -0,0 +1,9 @@
// A real initialized data range, with independently recognizable endpoints.
volatile unsigned char rea_large_payload[REA_PAYLOAD_BYTES] = {
[0] = 0x5a, [REA_PAYLOAD_BYTES - 1] = 0xa5};
__attribute__((noinline, used)) int rea_large_entry(void) {
return rea_large_payload[0] + rea_large_payload[REA_PAYLOAD_BYTES - 1];
}
int main(void) { return rea_large_entry(); }