refactor(mcp): consolidate retained-summary inspection workflows

This commit is contained in:
morluto committed 2026-10-25 02:03:28 +08:00
1 parent 9a13c4f420
commit 08b8e62914
6 files changed
+161 -185

No files matched your search

+53 -1
View File
@@ -3,17 +3,21 @@ import type {
ServerContext,
} from "@modelcontextprotocol/server";
import type { Logger } from "pino";
import type { z } from "zod";
import type { ExecutionOptions } from "../application/AnalysisProvider.js";
import type { AnalysisViewRetention } from "../application/analysisView/AnalysisViewService.js";
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import type { ToolContract } from "../contracts/toolContractTypes.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { Evidence } from "../domain/evidence.js";
import type { Result } from "../domain/result.js";
import { err, type Result } from "../domain/result.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import { logToolExecution } from "./toolLogging.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { runAdmittedToolOperation } from "./admittedToolOperation.js";
import { retainedSummaryDelivery } from "./retainedSummaryDelivery.js";
/** Application service executing one named contract operation. */
export interface ContractToolService<TOperation extends string> {
@@ -62,6 +66,54 @@ export const createContractToolHandler =
withAdmittedAnalysis,
)(contract);
/** Validate delivery selection before inspection and retain its complete source for summaries. */
export const createRetainedSummaryToolHandler = <
Name extends string,
InputSchema extends z.ZodType<
Readonly<Record<string, unknown>> & {
readonly detail: "complete" | "summary";
}
>,
>(
server: EvidenceMcpServer,
contract: ToolContract<Name, InputSchema>,
inspect: (
input: Omit<z.output<InputSchema>, "detail">,
options: ExecutionOptions,
) => Promise<Result<Evidence, AnalysisError>>,
logger: Logger,
retention: AnalysisViewRetention,
withAdmittedAnalysis?: WithAdmittedAnalysis,
) => {
const delivery = retainedSummaryDelivery(new Set([contract.name]), retention);
return createEvidenceToolHandler(
server,
async (rawInput, options) => {
const parsed = contract.inputSchema.safeParse(rawInput);
if (!parsed.success)
return err(
analysisInputErrorFromIssues(
contract.name,
parsed.error.issues,
rawInput,
{ cause: parsed.error },
),
);
const { detail, ...input } = parsed.data;
const parameters = { detail };
const ready = delivery.preflight(contract.name, parameters);
if (!ready.ok) return ready;
const inspected = await inspect(input, options);
return inspected.ok
? delivery.projectEvidence(contract.name, parameters, inspected.value)
: inspected;
},
logger,
retention.recordEvidence,
withAdmittedAnalysis,
)(contract);
};
/** Bind a named Evidence-producing operation to shared logging and delivery. */
export const createEvidenceToolHandler =
(
+6 -31
View File
@@ -3,13 +3,10 @@ import type { DevicetreeService } from "../application/binaryDiagnostics/Devicet
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "pino";
import { createEvidenceToolHandler } from "./contractToolHandler.js";
import { createRetainedSummaryToolHandler } from "./contractToolHandler.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { applicationToolRegistrationOptions } from "./toolRegistrationOptions.js";
import type { EvidenceLookup } from "../application/EvidenceInputResolver.js";
import { retainedSummaryDelivery } from "./retainedSummaryDelivery.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { err } from "../domain/result.js";
/** Register the explicit-file devicetree workflow and retain its complete Evidence. */
export const registerDevicetreeTool = (
@@ -21,39 +18,17 @@ export const registerDevicetreeTool = (
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const contract = toolContract("inspect_devicetree");
const delivery = retainedSummaryDelivery(new Set([contract.name]), {
recordEvidence,
evidenceById,
});
const handler = createEvidenceToolHandler(
const handler = createRetainedSummaryToolHandler(
server,
async (rawInput, options) => {
const parsed = contract.inputSchema.safeParse(rawInput);
if (!parsed.success)
return err(
analysisInputErrorFromIssues(
contract.name,
parsed.error.issues,
rawInput,
{ cause: parsed.error },
),
);
const { detail, ...input } = parsed.data;
const parameters = { detail };
const ready = delivery.preflight(contract.name, parameters);
if (!ready.ok) return ready;
const inspected = await service.inspect(input, options);
return inspected.ok
? delivery.projectEvidence(contract.name, parameters, inspected.value)
: inspected;
},
contract,
(input, options) => service.inspect(input, options),
logger,
recordEvidence,
{ recordEvidence, evidenceById },
withAdmittedAnalysis,
);
server.registerTool(
contract.name,
applicationToolRegistrationOptions(contract),
handler(contract),
handler,
);
};
+6 -31
View File
@@ -3,13 +3,10 @@ import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import type { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js";
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import type { EvidenceLookup } from "../application/EvidenceInputResolver.js";
import { retainedSummaryDelivery } from "./retainedSummaryDelivery.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { err } from "../domain/result.js";
import { evmInterfaceModule } from "../composition/evm.js";
import type { Logger } from "pino";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { createEvidenceToolHandler } from "./contractToolHandler.js";
import { createRetainedSummaryToolHandler } from "./contractToolHandler.js";
import { applicationToolRegistrationOptions } from "./toolRegistrationOptions.js";
/** Bind the named EVM contract to shared validation and session Evidence recording. */
@@ -22,39 +19,17 @@ export const registerEvmTools = (
evidenceById?: EvidenceLookup,
): void => {
const contract = evmInterfaceModule.contract;
const delivery = retainedSummaryDelivery(new Set([contract.name]), {
recordEvidence,
evidenceById,
});
const handler = createEvidenceToolHandler(
const handler = createRetainedSummaryToolHandler(
server,
async (rawInput, options) => {
const parsed = contract.inputSchema.safeParse(rawInput);
if (!parsed.success)
return err(
analysisInputErrorFromIssues(
contract.name,
parsed.error.issues,
rawInput,
{ cause: parsed.error },
),
);
const { detail, ...input } = parsed.data;
const parameters = { detail };
const ready = delivery.preflight(contract.name, parameters);
if (!ready.ok) return ready;
const inspected = await service.inspect(input, options);
return inspected.ok
? delivery.projectEvidence(contract.name, parameters, inspected.value)
: inspected;
},
contract,
(input, options) => service.inspect(input, options),
logger,
recordEvidence,
{ recordEvidence, evidenceById },
withAdmittedAnalysis,
);
server.registerTool(
contract.name,
applicationToolRegistrationOptions(contract),
handler(contract),
handler,
);
};
+6 -31
View File
@@ -3,13 +3,10 @@ import type { NativeExportsService } from "../application/binaryDiagnostics/Nati
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "pino";
import { createEvidenceToolHandler } from "./contractToolHandler.js";
import { createRetainedSummaryToolHandler } from "./contractToolHandler.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { applicationToolRegistrationOptions } from "./toolRegistrationOptions.js";
import type { EvidenceLookup } from "../application/EvidenceInputResolver.js";
import { retainedSummaryDelivery } from "./retainedSummaryDelivery.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { err } from "../domain/result.js";
/** Register the explicit-file export-table workflow and retain its complete Evidence. */
export const registerNativeExportsTool = (
@@ -21,39 +18,17 @@ export const registerNativeExportsTool = (
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const contract = toolContract("inspect_native_exports");
const delivery = retainedSummaryDelivery(new Set([contract.name]), {
recordEvidence,
evidenceById,
});
const handler = createEvidenceToolHandler(
const handler = createRetainedSummaryToolHandler(
server,
async (rawInput, options) => {
const parsed = contract.inputSchema.safeParse(rawInput);
if (!parsed.success)
return err(
analysisInputErrorFromIssues(
contract.name,
parsed.error.issues,
rawInput,
{ cause: parsed.error },
),
);
const { detail, ...input } = parsed.data;
const parameters = { detail };
const ready = delivery.preflight(contract.name, parameters);
if (!ready.ok) return ready;
const inspected = await service.inspect(input, options);
return inspected.ok
? delivery.projectEvidence(contract.name, parameters, inspected.value)
: inspected;
},
contract,
(input, options) => service.inspect(input, options),
logger,
recordEvidence,
{ recordEvidence, evidenceById },
withAdmittedAnalysis,
);
server.registerTool(
contract.name,
applicationToolRegistrationOptions(contract),
handler(contract),
handler,
);
};
+9 -29
View File
@@ -7,12 +7,14 @@ import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import type { WasmArtifactService } from "../application/wasm/WasmArtifactService.js";
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import type { EvidenceLookup } from "../application/EvidenceInputResolver.js";
import { retainedSummaryDelivery } from "./retainedSummaryDelivery.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { err } from "../domain/result.js";
import { wasmArtifactModule } from "../composition/wasm.js";
import type { Logger } from "pino";
import { createEvidenceToolHandler } from "./contractToolHandler.js";
import {
createEvidenceToolHandler,
createRetainedSummaryToolHandler,
} from "./contractToolHandler.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import {
applicationToolRegistrationOptions,
@@ -61,39 +63,17 @@ export const registerWasmTools = (
compareHandler(comparison),
);
const contract = wasmArtifactModule.contract;
const delivery = retainedSummaryDelivery(new Set([contract.name]), {
recordEvidence,
evidenceById,
});
const handler = createEvidenceToolHandler(
const handler = createRetainedSummaryToolHandler(
server,
async (rawInput, options) => {
const parsed = contract.inputSchema.safeParse(rawInput);
if (!parsed.success)
return err(
analysisInputErrorFromIssues(
contract.name,
parsed.error.issues,
rawInput,
{ cause: parsed.error },
),
);
const { detail, ...input } = parsed.data;
const parameters = { detail };
const ready = delivery.preflight(contract.name, parameters);
if (!ready.ok) return ready;
const inspected = await service.inspect(input, options);
return inspected.ok
? delivery.projectEvidence(contract.name, parameters, inspected.value)
: inspected;
},
contract,
(input, options) => service.inspect(input, options),
logger,
recordEvidence,
{ recordEvidence, evidenceById },
withAdmittedAnalysis,
);
server.registerTool(
contract.name,
applicationToolRegistrationOptions(contract),
handler(contract),
handler,
);
};
+81 -62
View File
@@ -238,72 +238,91 @@ it("advertises exact valid schemas and records inline WASM interface evidence wi
expect(close).toHaveBeenCalled();
});
it("records complete WASM Evidence when an MCP request is cancelled during provider work", async () => {
const value = wasmArtifactFixture();
let releaseProvider: (() => void) | undefined;
let providerStarted: (() => void) | undefined;
const started = new Promise<void>((resolve) => {
providerStarted = resolve;
});
const providerGate = new Promise<void>((resolve) => {
releaseProvider = resolve;
});
const service = new WasmArtifactService({
close: () => Promise.resolve(),
inspect: async (_input, options) => {
providerStarted?.();
await providerGate;
expect(options?.signal?.aborted).toBe(true);
return ok(wasmArtifactExecution(value));
},
});
const inspect = vi.spyOn(service, "inspect");
const session = createTestBinarySession(() => {
throw new Error("deep provider must not start");
});
const harness = await createApplicationMcpHarness({
session,
serverOptions: { wasmArtifact: service },
});
onTestFinished(harness.close);
const controller = new AbortController();
const cancellation = harness.client.callTool(
{
name: "inspect_wasm_artifact",
arguments: { path: value.artifact.path },
},
{ signal: controller.signal },
);
await started;
controller.abort(new Error("cancel after provider work starts"));
releaseProvider?.();
await expect(cancellation).rejects.toThrow();
expect(inspect).toHaveBeenCalledOnce();
const result = await inspect.mock.results[0]?.value;
expect(result).toMatchObject({
ok: false,
error: {
_tag: "AnalysisCancelledError",
partialObservation: {
operation: "inspect_wasm_artifact",
normalized_result: value,
confidence: "observed",
it.each([undefined, "summary"] as const)(
"records complete WASM Evidence when an MCP request is cancelled during provider work (detail=%s)",
async (detail) => {
const value = wasmArtifactFixture();
let releaseProvider: (() => void) | undefined;
let providerStarted: (() => void) | undefined;
const started = new Promise<void>((resolve) => {
providerStarted = resolve;
});
const providerGate = new Promise<void>((resolve) => {
releaseProvider = resolve;
});
let serverSignal: AbortSignal | undefined;
const service = new WasmArtifactService({
close: () => Promise.resolve(),
inspect: async (_input, options) => {
serverSignal = options?.signal;
providerStarted?.();
await providerGate;
expect(options?.signal?.aborted).toBe(true);
return ok(wasmArtifactExecution(value));
},
},
});
await harness.client.ping();
await vi.waitFor(() => {
expect(session.exportEvidenceBundle().records).toEqual(
expect.arrayContaining([
expect.objectContaining({
});
const inspect = vi.spyOn(service, "inspect");
const session = createTestBinarySession(() => {
throw new Error("deep provider must not start");
});
const harness = await createApplicationMcpHarness({
session,
serverOptions: { wasmArtifact: service },
});
onTestFinished(harness.close);
const controller = new AbortController();
const cancellation = harness.client.callTool(
{
name: "inspect_wasm_artifact",
arguments: {
path: value.artifact.path,
...(detail === undefined ? {} : { detail }),
},
},
{ signal: controller.signal },
);
const rejected = expect(cancellation).rejects.toThrow();
await started;
controller.abort(new Error("cancel after provider work starts"));
let serverReason: unknown;
try {
await harness.client.ping();
serverReason = serverSignal?.reason;
} finally {
releaseProvider?.();
}
await rejected;
expect(inspect).toHaveBeenCalledOnce();
const result = await inspect.mock.results[0]?.value;
expect(result).toMatchObject({
ok: false,
error: {
_tag: "AnalysisCancelledError",
partialObservation: {
operation: "inspect_wasm_artifact",
normalized_result: value,
confidence: "observed",
}),
]),
);
});
});
},
},
});
if (result === undefined || result.ok)
throw new Error("Expected cancelled WASM Evidence");
expect(serverSignal?.aborted).toBe(true);
expect(result.error.cause).toBe(serverReason);
const partial = parseEvidence(result.error.partialObservation);
expect(partial.parameters).toEqual({
path: value.artifact.path,
glue_paths: [],
candidate_paths: [],
});
await harness.client.ping();
await vi.waitFor(() => {
// Failed summary delivery retains the complete producer, without a derived view.
expect(session.exportEvidenceBundle().records).toEqual([partial]);
});
expect(session.evidenceById(partial.evidence_id)).toEqual(partial);
},
);
it("attributes rejected WASM paths to their selected fields before provider work", async () => {
const value = wasmArtifactFixture();