refactor(server): admit complete evidence workflows and share handlers

Validation: integrated changes passed formatting, typecheck, lint, resolved module-boundary verification and git diff --check. Tests and real-provider workflows were not run.
This commit is contained in:
morluto 2026-10-11 00:03:24 +08:00
parent 5f30ea6ff2
commit c1b5ccf0a4
43 changed files with 1482 additions and 915 deletions

View File

@ -0,0 +1,22 @@
import type {
ServerContext,
CallToolResult,
} from "@modelcontextprotocol/server";
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
/** Run one complete tool workflow inside session admission when supplied. */
export const runAdmittedToolOperation = async (
server: EvidenceMcpServer,
admission: WithAdmittedAnalysis | undefined,
operationName: string,
signal: ServerContext["mcpReq"]["signal"],
operation: () => Promise<CallToolResult>,
): Promise<CallToolResult> => {
if (admission === undefined) return operation();
const admitted = await admission(operationName, signal, operation);
return admitted.ok
? admitted.value
: server.delivery.toErrorToolResult(admitted.error);
};

View File

@ -9,6 +9,8 @@ import type { Evidence } from "../domain/evidence.js";
import type { Result } from "../domain/result.js";
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import { logToolExecution } from "./toolLogging.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { runAdmittedToolOperation } from "./admittedToolOperation.js";
/** Application service executing one named contract operation. */
export interface ContractToolService<TOperation extends string> {
@ -29,19 +31,51 @@ export const createContractToolHandler =
service: ContractToolService<TOperation>,
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
) =>
(contract: ToolContract<TOperation>) => {
(contract: ToolContract<TOperation>) =>
createEvidenceToolHandler(
server,
(input, options) => service.execute(contract.name, input, options),
logger,
recordEvidence,
withAdmittedAnalysis,
)(contract);
/** Bind a named Evidence-producing operation to shared logging and delivery. */
export const createEvidenceToolHandler =
(
server: EvidenceMcpServer,
operation: (
input: unknown,
options: ExecutionOptions,
) => Promise<Result<Evidence, AnalysisError>>,
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
) =>
<TOperation extends string>(contract: ToolContract<TOperation>) => {
const name = contract.name;
return async (input: unknown, context: ServerContext) => {
const result = await logToolExecution(logger, name, () =>
service.execute(name, input, { signal: context.mcpReq.signal }),
);
if (!result.ok) return server.delivery.toCallToolResult(result, contract);
const recorded = recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
contract,
recorded,
const executeAndRecord = async () => {
const result = await logToolExecution(logger, name, () =>
operation(input, { signal: context.mcpReq.signal }),
);
if (!result.ok)
return server.delivery.toCallToolResult(result, contract);
const recorded = recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
contract,
recorded,
);
};
return runAdmittedToolOperation(
server,
withAdmittedAnalysis,
name,
context.mcpReq.signal,
executeAndRecord,
);
};
};

View File

@ -204,7 +204,23 @@ export const createServer = (
const analysis = source.kind === "session" ? source.session : source.analysis;
const environment = snapshotEnvironment(options.environment ?? process.env);
const delivery = selectToolResultDelivery(environment, options.delivery);
const selectedOptions = { ...options, environment, delivery };
const evmInterface =
options.evmInterface ?? createEvmInterfaceService(environment);
const webSourceLocation =
options.webSourceLocation ?? createWebSourceLocationService(environment);
const javascriptRecovery =
options.javascriptRecovery ?? createJavaScriptRecoveryProvider(environment);
const firmwareAnalysis = new FirmwareAnalysisService(
options.firmwareAnalysis ?? createFirmwareAnalysisProvider(environment),
);
const selectedOptions = {
...options,
environment,
delivery,
evmInterface,
webSourceLocation,
javascriptRecovery,
};
const startedAt = new Date().toISOString();
const logger = options.logger ?? silentLogger;
const server = createMcpServer(
@ -220,9 +236,34 @@ export const createServer = (
const jeb = options.jebAnalysis ?? createJebAnalysisProvider(environment);
const adbDevice =
options.adbDeviceAnalysis ?? createAdbDeviceAnalysisProvider(environment);
let providerClose: Promise<void> | undefined;
const closeProviders = (): Promise<void> => {
providerClose ??= Promise.allSettled([
android.close(),
apktool.close(),
jeb.close(),
evmInterface.close(),
webSourceLocation.close(),
javascriptRecovery.close?.() ?? Promise.resolve(),
firmwareAnalysis.close(),
options.browserScenarioCapture?.close?.() ?? Promise.resolve(),
])
.then((results) => {
const failures = results.flatMap((result) =>
result.status === "rejected" ? [result.reason] : [],
);
if (failures.length > 0)
throw new AggregateError(failures, "REA provider cleanup failed");
})
.catch((cause: unknown) => {
providerClose = undefined;
throw cause;
});
return providerClose;
};
const availability = installSessionToolAvailability(
session,
selectedOptions,
options,
environment,
);
const toolLogger = logger.child({ layer: "server" });
@ -232,6 +273,7 @@ export const createServer = (
recordEvidence,
recordEvidenceWithUnknown,
} = createSessionRecorders(server, session);
const analysisAdmission = withAdmittedAnalysis(source);
const toolContext: ServerToolContext = {
server,
session,
@ -242,54 +284,39 @@ export const createServer = (
activeTarget,
recordEvidence,
recordEvidenceWithUnknown,
withAdmittedAnalysis: withAdmittedAnalysis(source),
withAdmittedAnalysis: analysisAdmission,
};
registerBinaryAnalysisTools(toolContext);
const previousOnclose = server.server.onclose;
server.server.onclose = () => {
previousOnclose?.();
void android.close().catch((cause: unknown) => {
void closeProviders().catch((cause: unknown) => {
logger.error(
{ error: cause instanceof Error ? cause.message : String(cause) },
"Android provider cleanup failed",
);
});
void apktool.close().catch((cause: unknown) => {
logger.error(
{ error: cause instanceof Error ? cause.message : String(cause) },
"Apktool provider cleanup failed",
);
});
void jeb.close().catch((cause: unknown) => {
logger.error(
{ error: cause instanceof Error ? cause.message : String(cause) },
"JEB provider cleanup failed",
"Provider cleanup failed during server shutdown",
);
});
};
const closeServer = server.close.bind(server);
server.close = async () => {
const results = await Promise.allSettled([
closeServer(),
android.close(),
apktool.close(),
jeb.close(),
]);
const results = await Promise.allSettled([closeServer(), closeProviders()]);
for (const result of results)
if (result.status === "rejected") throw result.reason;
};
registerConfiguredAnalysisTools(toolContext, android, jeb);
registerConfiguredAnalysisTools(toolContext, android, jeb, firmwareAnalysis);
registerAdbTools(
server,
new AdbDeviceAnalysisService(adbDevice),
toolLogger,
recordEvidence,
analysisAdmission,
);
registerApktoolTools(
server,
new ApktoolResourceAnalysisService(apktool),
toolLogger,
recordEvidence,
analysisAdmission,
);
registerObservationTools(toolContext);
registerGuidedPrompts(server, analysis, session);
@ -339,21 +366,25 @@ const registerConfiguredAnalysisTools = (
logger: toolLogger,
evidenceById,
recordEvidence,
withAdmittedAnalysis,
}: ServerToolContext,
android: AndroidAnalysisPort,
jeb: JebAnalysisPort,
firmwareAnalysis: FirmwareAnalysisService,
): void => {
registerAndroidTools(
server,
new AndroidAnalysisService(android),
toolLogger,
recordEvidence,
withAdmittedAnalysis,
);
registerJebTools(
server,
new JebAnalysisService(jeb),
toolLogger,
recordEvidence,
withAdmittedAnalysis,
);
registerBinaryDiagnosticsTools(
server,
@ -361,57 +392,70 @@ const registerConfiguredAnalysisTools = (
toolLogger,
recordEvidence,
evidenceById,
withAdmittedAnalysis,
);
registerAnalysisViewTool(
server,
toolLogger,
evidenceById,
recordEvidence,
withAdmittedAnalysis,
);
registerAnalysisViewTool(server, toolLogger, evidenceById, recordEvidence);
registerPeResourcesTool(
server,
createPeResourcesService(),
toolLogger,
recordEvidence,
withAdmittedAnalysis,
);
registerEvmTools(
server,
options.evmInterface ?? createEvmInterfaceService(environment),
toolLogger,
recordEvidence,
withAdmittedAnalysis,
);
registerRecordedCrashTools(
server,
options.recordedCrash ?? createRecordedCrashService(environment),
toolLogger,
recordEvidence,
withAdmittedAnalysis,
);
registerFirmwareTools(
server,
new FirmwareAnalysisService(
options.firmwareAnalysis ?? createFirmwareAnalysisProvider(environment),
),
firmwareAnalysis,
toolLogger,
recordEvidence,
withAdmittedAnalysis,
);
registerWebModuleTool(
server,
options.webModuleTrace ?? createWebModuleTraceService(environment),
toolLogger,
recordEvidence,
withAdmittedAnalysis,
);
registerWebSourceLocationTool(
server,
options.webSourceLocation ?? createWebSourceLocationService(environment),
toolLogger,
recordEvidence,
withAdmittedAnalysis,
);
registerWebRuntimeTools(
server,
options.webRuntime ?? createWebRuntimeService(),
toolLogger,
recordEvidence,
withAdmittedAnalysis,
);
registerWebNetworkCaptureTool(
server,
options.webNetworkCapture ?? createWebNetworkCaptureService(environment),
toolLogger,
recordEvidence,
withAdmittedAnalysis,
);
registerJavaScriptRecoveryTool(
server,
@ -421,6 +465,7 @@ const registerConfiguredAnalysisTools = (
),
toolLogger,
recordEvidence,
withAdmittedAnalysis,
);
};
@ -509,6 +554,7 @@ const registerBinaryAnalysisTools = ({
recordEvidence,
recordEvidenceWithUnknown,
session,
withAdmittedAnalysis,
});
};
@ -519,8 +565,9 @@ const registerObservationTools = ({
logger,
recordEvidence,
recordEvidenceWithUnknown,
withAdmittedAnalysis,
}: ServerToolContext): void => {
const common = { logger, recordEvidence };
const common = { logger, recordEvidence, withAdmittedAnalysis };
registerWebScriptTool(server, common);
registerBrowserTools(server, {
...common,
@ -553,5 +600,6 @@ const registerObservationTools = ({
logger,
recordEvidence,
recordEvidenceWithUnknown,
withAdmittedAnalysis,
});
};

View File

@ -5,6 +5,7 @@ import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "pino";
import { createContractToolHandler } from "./contractToolHandler.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
/** Bind ADB handlers to their exact named contracts through shared workflows. */
export const registerAdbTools = (
@ -12,12 +13,14 @@ export const registerAdbTools = (
service: AdbDeviceAnalysisService,
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const handler = createContractToolHandler(
server,
service,
logger,
recordEvidence,
withAdmittedAnalysis,
);
for (const contract of [
toolContract("inspect_adb_client"),

View File

@ -5,7 +5,8 @@ import type { EvidenceLookup } from "../application/EvidenceInputResolver.js";
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "pino";
import { logToolExecution } from "./toolLogging.js";
import { createEvidenceToolHandler } from "./contractToolHandler.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
/** Bind selected-view projection to its named contract and session Evidence owner. */
@ -14,22 +15,19 @@ export const registerAnalysisViewTool = (
logger: Logger,
evidenceById?: EvidenceLookup,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const contract = toolContract("inspect_analysis_view");
const handler = createEvidenceToolHandler(
server,
(input) => Promise.resolve(inspectAnalysisView(input, evidenceById)),
logger,
recordEvidence,
withAdmittedAnalysis,
);
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async (input) => {
const result = await logToolExecution(logger, contract.name, () =>
Promise.resolve(inspectAnalysisView(input, evidenceById)),
);
if (!result.ok) return server.delivery.toCallToolResult(result, contract);
const recorded = recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
contract,
recorded,
);
},
handler(contract),
);
};

View File

@ -5,6 +5,7 @@ import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "pino";
import { createContractToolHandler } from "./contractToolHandler.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
/** Bind Android handlers to their exact named contracts through shared workflows. */
export const registerAndroidTools = (
@ -12,12 +13,14 @@ export const registerAndroidTools = (
service: AndroidAnalysisService,
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const handler = createContractToolHandler(
server,
service,
logger,
recordEvidence,
withAdmittedAnalysis,
);
const packageContract = toolContract("inspect_android_package");
server.registerTool(

View File

@ -5,6 +5,7 @@ import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "pino";
import { createContractToolHandler } from "./contractToolHandler.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
/** Bind Apktool handlers to their exact named contracts. */
export const registerApktoolTools = (
@ -12,12 +13,14 @@ export const registerApktoolTools = (
service: ApktoolResourceAnalysisService,
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const handler = createContractToolHandler(
server,
service,
logger,
recordEvidence,
withAdmittedAnalysis,
);
for (const contract of [
toolContract("inspect_apktool_client"),

View File

@ -1,4 +1,5 @@
import type { EvidenceMcpServer } from "../EvidenceMcpServer.js";
import { runAdmittedToolOperation } from "../admittedToolOperation.js";
import { resolvePairedEvidenceRequest } from "../../application/EvidenceInputResolver.js";
import { recordSessionEvidenceSources } from "../sessionEvidence.js";
@ -20,36 +21,49 @@ export const registerCompareJavaScriptExportShapesTool = (
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async (input) => {
const resolved = resolvePairedEvidenceRequest(
input,
options.evidenceById,
);
if (!resolved.ok)
return server.delivery.toCallToolResult(resolved, contract);
const parsed = resolved.value;
const result = await logToolExecution(options.logger, contract.name, () =>
Promise.resolve(compareJavaScriptExportShapesEvidenceValidated(parsed)),
);
if (!result.ok) return server.delivery.toCallToolResult(result, contract);
const recorded = recordSessionEvidenceSources(options.recordEvidence, [
parsed.left,
parsed.right,
]);
if (!recorded.ok)
return server.delivery.toCallToolResult(recorded, contract);
const comparison = javaScriptExportShapeComparisonResultSchema.parse(
result.value.normalized_result,
);
const unknown =
comparison.summary.unknown > 0 ||
comparison.coverage.status !== "complete-within-inputs";
return recordResult(
{ ...options, delivery: server.delivery },
contract,
result.value,
unknown ? "javascript-export-shape" : undefined,
);
},
async (input, context) =>
runAdmittedToolOperation(
server,
options.withAdmittedAnalysis,
contract.name,
context.mcpReq.signal,
async () => {
const resolved = resolvePairedEvidenceRequest(
input,
options.evidenceById,
);
if (!resolved.ok)
return server.delivery.toCallToolResult(resolved, contract);
const parsed = resolved.value;
const result = await logToolExecution(
options.logger,
contract.name,
() =>
Promise.resolve(
compareJavaScriptExportShapesEvidenceValidated(parsed),
),
);
if (!result.ok)
return server.delivery.toCallToolResult(result, contract);
const recorded = recordSessionEvidenceSources(
options.recordEvidence,
[parsed.left, parsed.right],
);
if (!recorded.ok)
return server.delivery.toCallToolResult(recorded, contract);
const comparison = javaScriptExportShapeComparisonResultSchema.parse(
result.value.normalized_result,
);
const unknown =
comparison.summary.unknown > 0 ||
comparison.coverage.status !== "complete-within-inputs";
return recordResult(
{ ...options, delivery: server.delivery },
contract,
result.value,
unknown ? "javascript-export-shape" : undefined,
);
},
),
);
};

View File

@ -1,4 +1,5 @@
import type { EvidenceMcpServer } from "../EvidenceMcpServer.js";
import { runAdmittedToolOperation } from "../admittedToolOperation.js";
import { resolveApplicationEvidenceRequest } from "../../application/EvidenceInputResolver.js";
import { recordSessionEvidenceSources } from "../sessionEvidence.js";
@ -20,34 +21,46 @@ export const registerCompareSourceToBundleTool = (
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async (input) => {
const resolved = resolveApplicationEvidenceRequest(
input,
options.evidenceById,
);
if (!resolved.ok)
return server.delivery.toCallToolResult(resolved, contract);
const parsed = resolved.value;
const result = await logToolExecution(options.logger, contract.name, () =>
Promise.resolve(compareSourceToBundleEvidenceValidated(parsed)),
);
if (!result.ok) return server.delivery.toCallToolResult(result, contract);
const recorded = recordSessionEvidenceSources(options.recordEvidence, [
parsed.application,
]);
if (!recorded.ok)
return server.delivery.toCallToolResult(recorded, contract);
const comparison = sourceToBundleComparisonResultSchema.parse(
result.value.normalized_result,
);
return recordResult(
{ ...options, delivery: server.delivery },
contract,
result.value,
comparison.summary.unknown > 0
? "source-to-bundle-comparison"
: undefined,
);
},
async (input, context) =>
runAdmittedToolOperation(
server,
options.withAdmittedAnalysis,
contract.name,
context.mcpReq.signal,
async () => {
const resolved = resolveApplicationEvidenceRequest(
input,
options.evidenceById,
);
if (!resolved.ok)
return server.delivery.toCallToolResult(resolved, contract);
const parsed = resolved.value;
const result = await logToolExecution(
options.logger,
contract.name,
() =>
Promise.resolve(compareSourceToBundleEvidenceValidated(parsed)),
);
if (!result.ok)
return server.delivery.toCallToolResult(result, contract);
const recorded = recordSessionEvidenceSources(
options.recordEvidence,
[parsed.application],
);
if (!recorded.ok)
return server.delivery.toCallToolResult(recorded, contract);
const comparison = sourceToBundleComparisonResultSchema.parse(
result.value.normalized_result,
);
return recordResult(
{ ...options, delivery: server.delivery },
contract,
result.value,
comparison.summary.unknown > 0
? "source-to-bundle-comparison"
: undefined,
);
},
),
);
};

View File

@ -1,4 +1,5 @@
import type { EvidenceMcpServer } from "../EvidenceMcpServer.js";
import { runAdmittedToolOperation } from "../admittedToolOperation.js";
import { resolvePairedEvidenceRequest } from "../../application/EvidenceInputResolver.js";
import { recordSessionEvidenceSources } from "../sessionEvidence.js";
@ -20,44 +21,53 @@ export const registerCompareApplicationVersionsTool = (
server.registerTool(
compareContract.name,
toolRegistrationOptions(compareContract),
async (input) => {
const resolved = resolvePairedEvidenceRequest(
input,
options.evidenceById,
);
if (!resolved.ok)
return server.delivery.toCallToolResult(resolved, compareContract);
const parsed = resolved.value;
const result = await logToolExecution(
options.logger,
async (input, context) =>
runAdmittedToolOperation(
server,
options.withAdmittedAnalysis,
compareContract.name,
() =>
Promise.resolve(compareApplicationVersionsEvidenceValidated(parsed)),
);
if (!result.ok)
return server.delivery.toCallToolResult(result, compareContract);
const sources = [
parsed.left,
parsed.right,
...parsed.left_native_observations,
...parsed.right_native_observations,
];
const recorded = recordSessionEvidenceSources(
options.recordEvidence,
sources,
);
if (!recorded.ok)
return server.delivery.toCallToolResult(recorded, compareContract);
const comparison = applicationVersionComparisonResultSchema.parse(
result.value.normalized_result,
);
const unknown = comparison.summary.unknown > 0;
return recordResult(
{ ...options, delivery: server.delivery },
compareContract,
result.value,
unknown ? "application-version-comparison" : undefined,
);
},
context.mcpReq.signal,
async () => {
const resolved = resolvePairedEvidenceRequest(
input,
options.evidenceById,
);
if (!resolved.ok)
return server.delivery.toCallToolResult(resolved, compareContract);
const parsed = resolved.value;
const result = await logToolExecution(
options.logger,
compareContract.name,
() =>
Promise.resolve(
compareApplicationVersionsEvidenceValidated(parsed),
),
);
if (!result.ok)
return server.delivery.toCallToolResult(result, compareContract);
const sources = [
parsed.left,
parsed.right,
...parsed.left_native_observations,
...parsed.right_native_observations,
];
const recorded = recordSessionEvidenceSources(
options.recordEvidence,
sources,
);
if (!recorded.ok)
return server.delivery.toCallToolResult(recorded, compareContract);
const comparison = applicationVersionComparisonResultSchema.parse(
result.value.normalized_result,
);
const unknown = comparison.summary.unknown > 0;
return recordResult(
{ ...options, delivery: server.delivery },
compareContract,
result.value,
unknown ? "application-version-comparison" : undefined,
);
},
),
);
};

View File

@ -1,4 +1,5 @@
import type { EvidenceMcpServer } from "../EvidenceMcpServer.js";
import { runAdmittedToolOperation } from "../admittedToolOperation.js";
import { evaluateReconstructionCoverage } from "../../application/ReconstructionCoverageService.js";
import { applicationToolContract } from "../../contracts/applicationToolContracts.js";
@ -16,11 +17,20 @@ export const registerCoverageTools = (
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async (input) => {
const result = await logToolExecution(options.logger, contract.name, () =>
Promise.resolve(evaluateReconstructionCoverage(input)),
);
return server.delivery.toCallToolResult(result, contract);
},
async (input, context) =>
runAdmittedToolOperation(
server,
options.withAdmittedAnalysis,
contract.name,
context.mcpReq.signal,
async () => {
const result = await logToolExecution(
options.logger,
contract.name,
() => Promise.resolve(evaluateReconstructionCoverage(input)),
);
return server.delivery.toCallToolResult(result, contract);
},
),
);
};

View File

@ -1,4 +1,5 @@
import type { EvidenceMcpServer } from "../EvidenceMcpServer.js";
import { runAdmittedToolOperation } from "../admittedToolOperation.js";
import { recordSessionEvidenceSources } from "../sessionEvidence.js";
import {
@ -23,27 +24,40 @@ export const registerReconstructionObligationLedgerTool = (
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async (input) => {
const resolved = resolveReconstructionObligationLedgerRequest(input);
if (!resolved.ok)
return server.delivery.toCallToolResult(resolved, contract);
const result = await logToolExecution(options.logger, contract.name, () =>
Promise.resolve(
buildReconstructionObligationLedgerEvidenceValidated(resolved.value),
),
);
if (!result.ok) return server.delivery.toCallToolResult(result, contract);
const recorded = recordSessionEvidenceSources(
options.recordEvidence,
resolved.value.evidence_bundle.records,
);
if (!recorded.ok)
return server.delivery.toCallToolResult(recorded, contract);
return recordResult(
{ ...options, delivery: server.delivery },
contract,
result.value,
);
},
async (input, context) =>
runAdmittedToolOperation(
server,
options.withAdmittedAnalysis,
contract.name,
context.mcpReq.signal,
async () => {
const resolved = resolveReconstructionObligationLedgerRequest(input);
if (!resolved.ok)
return server.delivery.toCallToolResult(resolved, contract);
const result = await logToolExecution(
options.logger,
contract.name,
() =>
Promise.resolve(
buildReconstructionObligationLedgerEvidenceValidated(
resolved.value,
),
),
);
if (!result.ok)
return server.delivery.toCallToolResult(result, contract);
const recorded = recordSessionEvidenceSources(
options.recordEvidence,
resolved.value.evidence_bundle.records,
);
if (!recorded.ok)
return server.delivery.toCallToolResult(recorded, contract);
return recordResult(
{ ...options, delivery: server.delivery },
contract,
result.value,
);
},
),
);
};

View File

@ -1,4 +1,6 @@
import type { EvidenceMcpServer } from "../EvidenceMcpServer.js";
import type { ServerContext } from "@modelcontextprotocol/server";
import { runAdmittedToolOperation } from "../admittedToolOperation.js";
import { projectAndroidApplicationEvidence } from "../../application/android/AndroidApplicationService.js";
import { projectAppleApplicationEvidence } from "../../application/apple/AppleApplicationService.js";
@ -44,23 +46,33 @@ const registerProjection = (context: {
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async (input: unknown) => {
const result = await logToolExecution(options.logger, contract.name, () =>
Promise.resolve(project(input)),
);
if (!result.ok) return server.delivery.toCallToolResult(result, contract);
const recordedSources = recordSessionEvidenceSources(
options.recordEvidence,
inventoryEvidenceSources(input),
);
if (!recordedSources.ok)
return server.delivery.toCallToolResult(recordedSources, contract);
return recordResult(
{ ...options, delivery: server.delivery },
contract,
result.value,
);
},
async (input: unknown, request: ServerContext) =>
runAdmittedToolOperation(
server,
options.withAdmittedAnalysis,
contract.name,
request.mcpReq.signal,
async () => {
const result = await logToolExecution(
options.logger,
contract.name,
() => Promise.resolve(project(input)),
);
if (!result.ok)
return server.delivery.toCallToolResult(result, contract);
const recordedSources = recordSessionEvidenceSources(
options.recordEvidence,
inventoryEvidenceSources(input),
);
if (!recordedSources.ok)
return server.delivery.toCallToolResult(recordedSources, contract);
return recordResult(
{ ...options, delivery: server.delivery },
contract,
result.value,
);
},
),
);
};

View File

@ -1,4 +1,5 @@
import type { EvidenceMcpServer } from "../EvidenceMcpServer.js";
import { runAdmittedToolOperation } from "../admittedToolOperation.js";
import { resolveApplicationEvidenceRequest } from "../../application/EvidenceInputResolver.js";
import { recordSessionEvidenceSources } from "../sessionEvidence.js";
@ -19,33 +20,41 @@ export const registerTraceFeatureTool = (
server.registerTool(
traceContract.name,
toolRegistrationOptions(traceContract),
async (input) => {
const resolved = resolveApplicationEvidenceRequest(
input,
options.evidenceById,
);
if (!resolved.ok)
return server.delivery.toCallToolResult(resolved, traceContract);
const parsed = resolved.value;
const result = await logToolExecution(
options.logger,
async (input, context) =>
runAdmittedToolOperation(
server,
options.withAdmittedAnalysis,
traceContract.name,
() => Promise.resolve(traceApplicationFeatureEvidenceValidated(parsed)),
);
if (!result.ok)
return server.delivery.toCallToolResult(result, traceContract);
const sources = [parsed.application, ...parsed.native_observations];
const recorded = recordSessionEvidenceSources(
options.recordEvidence,
sources,
);
if (!recorded.ok)
return server.delivery.toCallToolResult(recorded, traceContract);
return recordResult(
{ ...options, delivery: server.delivery },
traceContract,
result.value,
);
},
context.mcpReq.signal,
async () => {
const resolved = resolveApplicationEvidenceRequest(
input,
options.evidenceById,
);
if (!resolved.ok)
return server.delivery.toCallToolResult(resolved, traceContract);
const parsed = resolved.value;
const result = await logToolExecution(
options.logger,
traceContract.name,
() =>
Promise.resolve(traceApplicationFeatureEvidenceValidated(parsed)),
);
if (!result.ok)
return server.delivery.toCallToolResult(result, traceContract);
const sources = [parsed.application, ...parsed.native_observations];
const recorded = recordSessionEvidenceSources(
options.recordEvidence,
sources,
);
if (!recorded.ok)
return server.delivery.toCallToolResult(recorded, traceContract);
return recordResult(
{ ...options, delivery: server.delivery },
traceContract,
result.value,
);
},
),
);
};

View File

@ -1,4 +1,5 @@
import type { EvidenceMcpServer } from "../EvidenceMcpServer.js";
import { runAdmittedToolOperation } from "../admittedToolOperation.js";
import { resolveApplicationEvidenceRequest } from "../../application/EvidenceInputResolver.js";
import { recordSessionEvidenceSources } from "../sessionEvidence.js";
@ -19,28 +20,42 @@ export const registerTraceJavaScriptSemanticsTool = (
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async (input) => {
const resolved = resolveApplicationEvidenceRequest(
input,
options.evidenceById,
);
if (!resolved.ok)
return server.delivery.toCallToolResult(resolved, contract);
const parsed = resolved.value;
const result = await logToolExecution(options.logger, contract.name, () =>
Promise.resolve(traceJavaScriptSemanticsEvidenceValidated(parsed)),
);
if (!result.ok) return server.delivery.toCallToolResult(result, contract);
const recorded = recordSessionEvidenceSources(options.recordEvidence, [
parsed.application,
]);
if (!recorded.ok)
return server.delivery.toCallToolResult(recorded, contract);
return recordResult(
{ ...options, delivery: server.delivery },
contract,
result.value,
);
},
async (input, context) =>
runAdmittedToolOperation(
server,
options.withAdmittedAnalysis,
contract.name,
context.mcpReq.signal,
async () => {
const resolved = resolveApplicationEvidenceRequest(
input,
options.evidenceById,
);
if (!resolved.ok)
return server.delivery.toCallToolResult(resolved, contract);
const parsed = resolved.value;
const result = await logToolExecution(
options.logger,
contract.name,
() =>
Promise.resolve(
traceJavaScriptSemanticsEvidenceValidated(parsed),
),
);
if (!result.ok)
return server.delivery.toCallToolResult(result, contract);
const recorded = recordSessionEvidenceSources(
options.recordEvidence,
[parsed.application],
);
if (!recorded.ok)
return server.delivery.toCallToolResult(recorded, contract);
return recordResult(
{ ...options, delivery: server.delivery },
contract,
result.value,
);
},
),
);
};

View File

@ -4,9 +4,11 @@ import type {
EvidenceUnknownWriter,
} from "../../application/investigation/InvestigationRecordPort.js";
import type { Logger } from "pino";
import type { WithAdmittedAnalysis } from "../analysisAdmission.js";
/** Shared services for registering JavaScript application graph workflows. */
export interface ApplicationToolRegistration {
readonly withAdmittedAnalysis?: WithAdmittedAnalysis;
readonly logger: Logger;
readonly evidenceById: EvidenceReader["evidenceById"] | undefined;
readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined;

View File

@ -10,6 +10,8 @@ import { recordDerivedEvidence } from "./recordDerivedEvidence.js";
import { runDerivedOperation } from "./runDerivedOperation.js";
import { ARTIFACT_COMPARISON_PROVIDER } from "../application/InvestigationProviders.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { withAdmittedAnalysis } from "./analysisAdmission.js";
import { runAdmittedToolOperation } from "./admittedToolOperation.js";
/** Register Evidence-backed deterministic artifact comparison. */
export const registerArtifactComparisonTool = (
@ -17,54 +19,70 @@ export const registerArtifactComparisonTool = (
session: BinarySessionPort,
contract: ReturnType<typeof toolContract<"compare_artifacts">>,
): void => {
const admission = withAdmittedAnalysis({ kind: "session", session });
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async (input, context) => {
const computed = await runDerivedOperation(context, contract.name, () => {
const left = parseEvidence(input.left);
const right = parseEvidence(input.right);
return { left, right, comparison: compareArtifacts(left, right) };
});
if (!computed.ok)
return server.delivery.toCallToolResult(computed, contract);
const { left, right, comparison } = computed.value;
const sources = [left, right];
for (const source of sources) {
const recordedSource = session.recordEvidence(source);
if (!recordedSource.ok)
return server.delivery.toCallToolResult(recordedSource, contract);
}
const leftEvidenceIds = [left.evidence_id];
const rightEvidenceIds = [right.evidence_id];
const evidence = createEvidence(undefined, ARTIFACT_COMPARISON_PROVIDER, {
predicateType: "rea.artifact-comparison",
operation: contract.name,
parameters: {
left_evidence_ids: leftEvidenceIds,
right_evidence_ids: rightEvidenceIds,
async (input, context) =>
runAdmittedToolOperation(
server,
admission,
contract.name,
context.mcpReq.signal,
async () => {
const computed = await runDerivedOperation(
context,
contract.name,
() => {
const left = parseEvidence(input.left);
const right = parseEvidence(input.right);
return { left, right, comparison: compareArtifacts(left, right) };
},
);
if (!computed.ok)
return server.delivery.toCallToolResult(computed, contract);
const { left, right, comparison } = computed.value;
const sources = [left, right];
for (const source of sources) {
const recordedSource = session.recordEvidence(source);
if (!recordedSource.ok)
return server.delivery.toCallToolResult(recordedSource, contract);
}
const leftEvidenceIds = [left.evidence_id];
const rightEvidenceIds = [right.evidence_id];
const evidence = createEvidence(
undefined,
ARTIFACT_COMPARISON_PROVIDER,
{
predicateType: "rea.artifact-comparison",
operation: contract.name,
parameters: {
left_evidence_ids: leftEvidenceIds,
right_evidence_ids: rightEvidenceIds,
},
result: jsonValueSchema.parse(comparison),
confidence: "derived",
authority: "analyst-inference",
limitations: comparison.limitations,
evidenceLinks: [...leftEvidenceIds, ...rightEvidenceIds],
},
);
return server.delivery.toEvidenceToolResult(
evidence,
contract,
recordDerivedEvidence(
session,
evidence,
artifactUnknownInput(
left,
right,
comparison.status,
evidence.evidence_id,
),
),
);
},
result: jsonValueSchema.parse(comparison),
confidence: "derived",
authority: "analyst-inference",
limitations: comparison.limitations,
evidenceLinks: [...leftEvidenceIds, ...rightEvidenceIds],
});
return server.delivery.toEvidenceToolResult(
evidence,
contract,
recordDerivedEvidence(
session,
evidence,
artifactUnknownInput(
left,
right,
comparison.status,
evidence.evidence_id,
),
),
);
},
),
);
};

View File

@ -8,6 +8,8 @@ import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "pino";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { runAdmittedToolOperation } from "./admittedToolOperation.js";
/** Bind offline layout inspection to its named contract and session Evidence owner. */
export const registerBinaryDiagnosticsTools = (
@ -16,30 +18,43 @@ export const registerBinaryDiagnosticsTools = (
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
evidenceById?: EvidenceLookup,
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const contract = toolContract("inspect_binary_layout");
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async ({ detail, ...request }, context) => {
const result = await logToolExecution(logger, contract.name, async () => {
const inspected = await service.inspect(request, {
signal: context.mcpReq.signal,
});
return detail === "summary" && inspected.ok
? summarizeRetainedAnalysis(inspected.value, {
recordEvidence,
evidenceById,
})
: inspected;
});
if (!result.ok) return server.delivery.toCallToolResult(result, contract);
const recorded = recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
contract,
recorded,
);
},
async ({ detail, ...request }, context) =>
runAdmittedToolOperation(
server,
withAdmittedAnalysis,
contract.name,
context.mcpReq.signal,
async () => {
const result = await logToolExecution(
logger,
contract.name,
async () => {
const inspected = await service.inspect(request, {
signal: context.mcpReq.signal,
});
return detail === "summary" && inspected.ok
? summarizeRetainedAnalysis(inspected.value, {
recordEvidence,
evidenceById,
})
: inspected;
},
);
if (!result.ok)
return server.delivery.toCallToolResult(result, contract);
const recorded = recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
contract,
recorded,
);
},
),
);
};

View File

@ -13,12 +13,15 @@ import { browserScenarioSchema } from "../domain/browserScenario.js";
import type { Logger } from "pino";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { runAdmittedToolOperation } from "./admittedToolOperation.js";
interface BrowserScenarioToolRegistration {
readonly logger: Logger;
readonly loadFailure?: OptionalProviderLoadFailure | undefined;
readonly provider: BrowserScenarioCapturePort | undefined;
readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined;
readonly withAdmittedAnalysis?: WithAdmittedAnalysis;
}
/** Register the browser scenario tool with execution-time provider diagnostics. */
@ -37,17 +40,29 @@ export const registerBrowserScenarioTool = (
err(optionalProviderUnavailable(options.loadFailure, contract.name)),
contract,
);
const result = await logToolExecution(options.logger, contract.name, () =>
captureBrowserScenario(options.provider, scenario, {
signal: context.mcpReq.signal,
}),
);
if (!result.ok) return server.delivery.toCallToolResult(result, contract);
const recorded = options.recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
contract,
recorded,
return runAdmittedToolOperation(
server,
options.withAdmittedAnalysis,
contract.name,
context.mcpReq.signal,
async () => {
const result = await logToolExecution(
options.logger,
contract.name,
() =>
captureBrowserScenario(options.provider, scenario, {
signal: context.mcpReq.signal,
}),
);
if (!result.ok)
return server.delivery.toCallToolResult(result, contract);
const recorded = options.recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
contract,
recorded,
);
},
);
},
);

View File

@ -31,12 +31,15 @@ import type { Logger } from "pino";
import { mcpProgressReporter } from "./mcpProgress.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { runAdmittedToolOperation } from "./admittedToolOperation.js";
interface BrowserToolRegistration {
readonly logger: Logger;
readonly loadFailure?: OptionalProviderLoadFailure | undefined;
readonly browser: BrowserObservationPort | undefined;
readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined;
readonly withAdmittedAnalysis?: WithAdmittedAnalysis;
}
interface BrowserToolContext {
@ -50,7 +53,7 @@ export const registerBrowserTools = (
server: EvidenceMcpServer,
options: BrowserToolRegistration,
): void => {
const registration = { ...options, delivery: server.delivery };
const registration = { ...options, delivery: server.delivery, server };
const listContract = toolContract("list_browser_targets");
const inspectContract = toolContract("inspect_web_page");
const analyzeContract = toolContract("analyze_web_bundle");
@ -179,7 +182,10 @@ export const registerBrowserTools = (
};
const runBrowserTool = async <Input>(
options: BrowserToolRegistration & { readonly delivery: ToolResultDelivery },
options: BrowserToolRegistration & {
readonly delivery: ToolResultDelivery;
readonly server: EvidenceMcpServer;
},
contract: ToolContract,
request: { readonly input: Input; readonly context: ServerContext },
execute: (
@ -193,17 +199,26 @@ const runBrowserTool = async <Input>(
err(optionalProviderUnavailable(options.loadFailure, contract.name)),
contract,
);
const result = await logToolExecution(options.logger, contract.name, () =>
execute(input, {
signal: context.mcpReq.signal,
progress: mcpProgressReporter(context),
}),
);
if (!result.ok) return options.delivery.toCallToolResult(result, contract);
const recorded = options.recordEvidence?.(result.value);
return options.delivery.toEvidenceToolResult(
result.value,
contract,
recorded,
return runAdmittedToolOperation(
options.server,
options.withAdmittedAnalysis,
contract.name,
context.mcpReq.signal,
async () => {
const result = await logToolExecution(options.logger, contract.name, () =>
execute(input, {
signal: context.mcpReq.signal,
progress: mcpProgressReporter(context),
}),
);
if (!result.ok)
return options.delivery.toCallToolResult(result, contract);
const recorded = options.recordEvidence?.(result.value);
return options.delivery.toEvidenceToolResult(
result.value,
contract,
recorded,
);
},
);
};

View File

@ -7,8 +7,11 @@ import { compareBundles } from "../domain/bundleComparison.js";
import { createEvidence } from "../domain/evidence.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { runDerivedOperation } from "./runDerivedOperation.js";
import { recordSessionEvidenceSources } from "./sessionEvidence.js";
import { BUNDLE_COMPARISON_PROVIDER } from "../application/InvestigationProviders.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { withAdmittedAnalysis } from "./analysisAdmission.js";
import { runAdmittedToolOperation } from "./admittedToolOperation.js";
/** Register canonical Evidence bundle comparison. */
export const registerBundleComparisonTool = (
@ -16,37 +19,73 @@ export const registerBundleComparisonTool = (
session: BinarySessionPort,
contract: ReturnType<typeof toolContract<"compare_bundles">>,
): void => {
const admission = withAdmittedAnalysis({ kind: "session", session });
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async (input, context) => {
const [left, right] = await Promise.all([
readEvidenceBundle(input.left_bundle_path),
readEvidenceBundle(input.right_bundle_path),
]);
if (!left.ok) return server.delivery.toCallToolResult(left, contract);
if (!right.ok) return server.delivery.toCallToolResult(right, contract);
const computed = await runDerivedOperation(context, contract.name, () =>
compareBundles(left.value, right.value, input.record_pairs),
);
if (!computed.ok)
return server.delivery.toCallToolResult(computed, contract);
const comparison = computed.value;
const evidence = createEvidence(undefined, BUNDLE_COMPARISON_PROVIDER, {
predicateType: "rea.bundle-comparison",
operation: contract.name,
parameters: {
left_bundle_sha256: comparison.left_bundle_sha256,
right_bundle_sha256: comparison.right_bundle_sha256,
record_pairs: input.record_pairs,
async (input, context) =>
runAdmittedToolOperation(
server,
admission,
contract.name,
context.mcpReq.signal,
async () => {
const [left, right] = await Promise.all([
readEvidenceBundle(input.left_bundle_path),
readEvidenceBundle(input.right_bundle_path),
]);
if (!left.ok) return server.delivery.toCallToolResult(left, contract);
if (!right.ok)
return server.delivery.toCallToolResult(right, contract);
const computed = await runDerivedOperation(
context,
contract.name,
() => compareBundles(left.value, right.value, input.record_pairs),
);
if (!computed.ok)
return server.delivery.toCallToolResult(computed, contract);
const comparison = computed.value;
const sourceRecords = [
...new Map(
[...left.value.records, ...right.value.records].map((record) => [
record.evidence_id,
record,
]),
).values(),
];
const recordedSources = recordSessionEvidenceSources(
(evidence) => session.recordEvidence(evidence),
sourceRecords,
);
if (!recordedSources.ok)
return server.delivery.toCallToolResult(recordedSources, contract);
const evidence = createEvidence(
undefined,
BUNDLE_COMPARISON_PROVIDER,
{
predicateType: "rea.bundle-comparison",
operation: contract.name,
parameters: {
left_bundle_sha256: comparison.left_bundle_sha256,
right_bundle_sha256: comparison.right_bundle_sha256,
record_pairs: input.record_pairs,
},
result: jsonValueSchema.parse(comparison),
confidence: "derived",
authority: "analyst-inference",
limitations: comparison.limitations,
evidenceLinks: sourceRecords.map(
({ evidence_id }) => evidence_id,
),
},
);
const recorded = session.recordEvidence(evidence);
return server.delivery.toEvidenceToolResult(
evidence,
contract,
recorded,
);
},
result: jsonValueSchema.parse(comparison),
confidence: "derived",
authority: "analyst-inference",
limitations: comparison.limitations,
});
const recorded = session.recordEvidence(evidence);
return server.delivery.toEvidenceToolResult(evidence, contract, recorded);
},
),
);
};

View File

@ -3,6 +3,8 @@ import {
reportLifecycleStart,
} from "./lifecycleProgress.js";
import { ok } from "../domain/result.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { Result } from "../domain/result.js";
import { mcpProgressReporter } from "./mcpProgress.js";
import type { LifecycleToolRegistration } from "./registerSessionTools.js";
import { logToolExecution } from "./toolLogging.js";
@ -14,7 +16,46 @@ export const registerCloseLifecycleTool = ({
session,
logger,
closeContract,
closeProcessResources,
}: LifecycleToolRegistration): void => {
const closeWithResources = async <Value>(
closeSession: () => Promise<Result<Value, AnalysisError>>,
): Promise<Result<Value, AnalysisError>> => {
const [sessionClose, resourceClose] = await Promise.allSettled([
closeSession(),
closeProcessResources(),
]);
if (sessionClose.status === "rejected") {
if (resourceClose.status === "rejected")
logger.warn(
{
error:
resourceClose.reason instanceof Error
? resourceClose.reason.message
: String(resourceClose.reason),
},
"Process capture cleanup threw while closing the session",
);
else if (!resourceClose.value.ok)
logger.warn(
{ error: resourceClose.value.error.message },
"Process capture cleanup failed while closing the session",
);
throw sessionClose.reason;
}
if (resourceClose.status === "rejected") throw resourceClose.reason;
const closed = sessionClose.value;
const resources = resourceClose.value;
if (!closed.ok) {
if (!resources.ok)
logger.warn(
{ error: resources.error.message },
"Process capture cleanup failed while closing the session",
);
return closed;
}
return resources.ok ? closed : resources;
};
server.registerTool(
closeContract.name,
toolRegistrationOptions(closeContract),
@ -25,7 +66,7 @@ export const registerCloseLifecycleTool = ({
// Enqueue the lifecycle transition before awaiting progress transport so
// a later open request cannot overtake this close.
const closing = logToolExecution(logger, closeContract.name, () =>
session.close({ progress }),
closeWithResources(() => session.close({ progress })),
);
await reportLifecycleStart(progress, closeContract.name);
const closed = await closing;
@ -33,7 +74,11 @@ export const registerCloseLifecycleTool = ({
return server.delivery.toCallToolResult(closed, closeContract);
}
const closing = logToolExecution(logger, closeContract.name, () =>
session.closeWithSnapshot(snapshotPath, input.overwrite, { progress }),
closeWithResources(() =>
session.closeWithSnapshot(snapshotPath, input.overwrite, {
progress,
}),
),
);
await reportLifecycleStart(
progress,

View File

@ -30,6 +30,8 @@ import type { Logger } from "pino";
import { mcpProgressReporter } from "./mcpProgress.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { runAdmittedToolOperation } from "./admittedToolOperation.js";
interface ElectronToolRegistration {
readonly logger: Logger;
@ -39,6 +41,7 @@ interface ElectronToolRegistration {
readonly electronActive: ElectronActiveObservationPort | undefined;
readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined;
readonly evidenceById: EvidenceLookup | undefined;
readonly withAdmittedAnalysis?: WithAdmittedAnalysis;
}
interface ElectronToolContext {
@ -52,7 +55,7 @@ export const registerElectronTools = (
server: EvidenceMcpServer,
options: ElectronToolRegistration,
): void => {
const registration = { ...options, delivery: server.delivery };
const registration = { ...options, delivery: server.delivery, server };
const listContract = toolContract("list_electron_targets");
const inspectContract = toolContract("inspect_electron_page");
const analyzeContract = toolContract("analyze_javascript_application");
@ -139,7 +142,10 @@ export const registerElectronTools = (
};
const runElectronTool = async <Input>(
options: ElectronToolRegistration & { readonly delivery: ToolResultDelivery },
options: ElectronToolRegistration & {
readonly delivery: ToolResultDelivery;
readonly server: EvidenceMcpServer;
},
contract: ToolContract,
request: {
readonly input: Input;
@ -163,17 +169,26 @@ const runElectronTool = async <Input>(
err(optionalProviderUnavailable(failure, contract.name)),
contract,
);
const result = await logToolExecution(options.logger, contract.name, () =>
execute(input, {
signal: context.mcpReq.signal,
progress: mcpProgressReporter(context),
}),
);
if (!result.ok) return options.delivery.toCallToolResult(result, contract);
const recorded = options.recordEvidence?.(result.value);
return options.delivery.toEvidenceToolResult(
result.value,
contract,
recorded,
return runAdmittedToolOperation(
options.server,
options.withAdmittedAnalysis,
contract.name,
context.mcpReq.signal,
async () => {
const result = await logToolExecution(options.logger, contract.name, () =>
execute(input, {
signal: context.mcpReq.signal,
progress: mcpProgressReporter(context),
}),
);
if (!result.ok)
return options.delivery.toCallToolResult(result, contract);
const recorded = options.recordEvidence?.(result.value);
return options.delivery.toEvidenceToolResult(
result.value,
contract,
recorded,
);
},
);
};

View File

@ -4,7 +4,8 @@ import type { EvmInterfaceService } from "../application/evm/EvmInterfaceService
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "pino";
import { logToolExecution } from "./toolLogging.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { createEvidenceToolHandler } from "./contractToolHandler.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
/** Bind the named EVM contract to shared validation and session Evidence recording. */
@ -13,22 +14,19 @@ export const registerEvmTools = (
service: EvmInterfaceService,
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const contract = toolContract("inspect_evm_interface");
const handler = createEvidenceToolHandler(
server,
(input, options) => service.inspect(input, options),
logger,
recordEvidence,
withAdmittedAnalysis,
);
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async (input, context) => {
const result = await logToolExecution(logger, contract.name, () =>
service.inspect(input, { signal: context.mcpReq.signal }),
);
if (!result.ok) return server.delivery.toCallToolResult(result, contract);
const recorded = recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
contract,
recorded,
);
},
handler(contract),
);
};

View File

@ -1,12 +1,11 @@
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import type { ServerContext } from "@modelcontextprotocol/server";
import type { FirmwareAnalysisService } from "../application/firmware/FirmwareAnalysisService.js";
import { toolContract } from "../contracts/toolContracts.js";
import type { ToolContract } from "../contracts/toolContractTypes.js";
import type { FirmwareOperation } from "../domain/firmware/firmwareAnalysis.js";
import type { Logger } from "pino";
import { logToolExecution } from "./toolLogging.js";
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { createContractToolHandler } from "./contractToolHandler.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
/** Bind firmware handlers to their exact named schemas. */
@ -15,23 +14,15 @@ export const registerFirmwareTools = (
service: FirmwareAnalysisService,
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const handler =
(contract: ToolContract<FirmwareOperation>) =>
async (input: unknown, context: ServerContext) => {
const result = await logToolExecution(logger, contract.name, () =>
service.execute(contract.name, input, {
signal: context.mcpReq.signal,
}),
);
if (!result.ok) return server.delivery.toCallToolResult(result, contract);
const recorded = recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
contract,
recorded,
);
};
const handler = createContractToolHandler(
server,
service,
logger,
recordEvidence,
withAdmittedAnalysis,
);
const inspect = toolContract("inspect_firmware_regions");
server.registerTool(
inspect.name,

View File

@ -16,6 +16,8 @@ import { recordSessionEvidenceSources } from "./sessionEvidence.js";
import { runDerivedOperation } from "./runDerivedOperation.js";
import { FUNCTION_COMPARISON_PROVIDER } from "../application/InvestigationProviders.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { withAdmittedAnalysis } from "./analysisAdmission.js";
import { runAdmittedToolOperation } from "./admittedToolOperation.js";
import { compareFunctions } from "../domain/functionComparison.js";
/** Register explicit Evidence-backed function comparison. */
@ -24,74 +26,96 @@ export const registerFunctionComparisonTool = (
session: BinarySessionPort,
contract: ReturnType<typeof toolContract<"compare_functions">>,
): void => {
const admission = withAdmittedAnalysis({ kind: "session", session });
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async (input, context) => {
let leftEvidence: Evidence;
let rightEvidence: Evidence;
try {
leftEvidence = parseEvidence(input.left);
rightEvidence = parseEvidence(input.right);
} catch (cause: unknown) {
return server.delivery.toCallToolResult(
err(
new EvidenceIntegrityError(
cause instanceof Error ? cause.message : "Invalid Evidence",
),
),
contract,
);
}
if (
leftEvidence.operation !== "analyze_function" ||
rightEvidence.operation !== "analyze_function" ||
leftEvidence.predicate_type !== "rea.analysis" ||
rightEvidence.predicate_type !== "rea.analysis"
)
return server.delivery.toCallToolResult(
err(new EvidenceIntegrityError("Expected analyze_function Evidence")),
contract,
);
const leftIds = [leftEvidence.evidence_id];
const rightIds = [rightEvidence.evidence_id];
const computed = await runDerivedOperation(context, contract.name, () =>
compareFunctions(leftEvidence, rightEvidence),
);
if (!computed.ok)
return server.delivery.toCallToolResult(computed, contract);
const comparison = computed.value;
const recordedSources = recordSessionEvidenceSources(
(evidence) => session.recordEvidence(evidence),
[leftEvidence, rightEvidence],
);
if (!recordedSources.ok)
return server.delivery.toCallToolResult(recordedSources, contract);
const evidence = createEvidence(undefined, FUNCTION_COMPARISON_PROVIDER, {
predicateType: "rea.function-comparison",
operation: contract.name,
parameters: {
left_evidence_id: leftEvidence.evidence_id,
right_evidence_id: rightEvidence.evidence_id,
async (input, context) =>
runAdmittedToolOperation(
server,
admission,
contract.name,
context.mcpReq.signal,
async () => {
let leftEvidence: Evidence;
let rightEvidence: Evidence;
try {
leftEvidence = parseEvidence(input.left);
rightEvidence = parseEvidence(input.right);
} catch (cause: unknown) {
return server.delivery.toCallToolResult(
err(
new EvidenceIntegrityError(
cause instanceof Error ? cause.message : "Invalid Evidence",
),
),
contract,
);
}
if (
leftEvidence.operation !== "analyze_function" ||
rightEvidence.operation !== "analyze_function" ||
leftEvidence.predicate_type !== "rea.analysis" ||
rightEvidence.predicate_type !== "rea.analysis"
)
return server.delivery.toCallToolResult(
err(
new EvidenceIntegrityError(
"Expected analyze_function Evidence",
),
),
contract,
);
const leftIds = [leftEvidence.evidence_id];
const rightIds = [rightEvidence.evidence_id];
const computed = await runDerivedOperation(
context,
contract.name,
() => compareFunctions(leftEvidence, rightEvidence),
);
if (!computed.ok)
return server.delivery.toCallToolResult(computed, contract);
const comparison = computed.value;
const recordedSources = recordSessionEvidenceSources(
(evidence) => session.recordEvidence(evidence),
[leftEvidence, rightEvidence],
);
if (!recordedSources.ok)
return server.delivery.toCallToolResult(recordedSources, contract);
const evidence = createEvidence(
undefined,
FUNCTION_COMPARISON_PROVIDER,
{
predicateType: "rea.function-comparison",
operation: contract.name,
parameters: {
left_evidence_id: leftEvidence.evidence_id,
right_evidence_id: rightEvidence.evidence_id,
},
result: jsonValueSchema.parse(comparison),
confidence: "derived",
authority: "analyst-inference",
limitations: comparison.limitations,
evidenceLinks: [...leftIds, ...rightIds],
},
);
const recorded = recordDerivedEvidence(
session,
evidence,
functionUnknownInput({
status: comparison.status,
leftIds,
rightIds,
comparisonEvidenceId: evidence.evidence_id,
}),
);
return server.delivery.toEvidenceToolResult(
evidence,
contract,
recorded,
);
},
result: jsonValueSchema.parse(comparison),
confidence: "derived",
authority: "analyst-inference",
limitations: comparison.limitations,
evidenceLinks: [...leftIds, ...rightIds],
});
const recorded = recordDerivedEvidence(
session,
evidence,
functionUnknownInput({
status: comparison.status,
leftIds,
rightIds,
comparisonEvidenceId: evidence.evidence_id,
}),
);
return server.delivery.toEvidenceToolResult(evidence, contract, recorded);
},
),
);
};

View File

@ -4,7 +4,8 @@ import type { JavaScriptRecoveryService } from "../application/javascript/JavaSc
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "pino";
import { logToolExecution } from "./toolLogging.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { createEvidenceToolHandler } from "./contractToolHandler.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
/** Bind optional recovery to its exact named contract without acquiring an engine. */
@ -13,22 +14,19 @@ export const registerJavaScriptRecoveryTool = (
service: JavaScriptRecoveryService,
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const contract = toolContract("recover_javascript_sources");
const handler = createEvidenceToolHandler(
server,
(input, options) => service.recover(input, options),
logger,
recordEvidence,
withAdmittedAnalysis,
);
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async (input, context) => {
const result = await logToolExecution(logger, contract.name, () =>
service.recover(input, { signal: context.mcpReq.signal }),
);
if (!result.ok) return server.delivery.toCallToolResult(result, contract);
const recorded = recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
contract,
recorded,
);
},
handler(contract),
);
};

View File

@ -22,12 +22,15 @@ import { observeJavaScriptRuntimeInputSchema } from "../domain/javascript/javasc
import type { Logger } from "pino";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { runAdmittedToolOperation } from "./admittedToolOperation.js";
interface RuntimeToolRegistration {
readonly logger: Logger;
readonly loadFailure?: OptionalProviderLoadFailure | undefined;
readonly runtime: JavaScriptRuntimeObservationPort | undefined;
readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined;
readonly withAdmittedAnalysis?: WithAdmittedAnalysis;
}
/** Register passive Inspector tools even when policy keeps them unavailable. */
@ -35,7 +38,7 @@ export const registerJavaScriptRuntimeObservationTools = (
server: EvidenceMcpServer,
options: RuntimeToolRegistration,
): void => {
const registration = { ...options, delivery: server.delivery };
const registration = { ...options, delivery: server.delivery, server };
const listContract = toolContract("list_javascript_runtime_targets");
const observeContract = toolContract("observe_javascript_runtime");
server.registerTool(
@ -67,7 +70,10 @@ export const registerJavaScriptRuntimeObservationTools = (
};
const runRuntimeTool = async <Input>(
options: RuntimeToolRegistration & { readonly delivery: ToolResultDelivery },
options: RuntimeToolRegistration & {
readonly delivery: ToolResultDelivery;
readonly server: EvidenceMcpServer;
},
contract: ToolContract,
request: { readonly input: Input; readonly context: ServerContext },
execute: (
@ -81,14 +87,23 @@ const runRuntimeTool = async <Input>(
err(optionalProviderUnavailable(options.loadFailure, contract.name)),
contract,
);
const result = await logToolExecution(options.logger, contract.name, () =>
execute(input, context.mcpReq.signal),
);
if (!result.ok) return options.delivery.toCallToolResult(result, contract);
const recorded = options.recordEvidence?.(result.value);
return options.delivery.toEvidenceToolResult(
result.value,
contract,
recorded,
return runAdmittedToolOperation(
options.server,
options.withAdmittedAnalysis,
contract.name,
context.mcpReq.signal,
async () => {
const result = await logToolExecution(options.logger, contract.name, () =>
execute(input, context.mcpReq.signal),
);
if (!result.ok)
return options.delivery.toCallToolResult(result, contract);
const recorded = options.recordEvidence?.(result.value);
return options.delivery.toEvidenceToolResult(
result.value,
contract,
recorded,
);
},
);
};

View File

@ -5,6 +5,7 @@ import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "pino";
import { createContractToolHandler } from "./contractToolHandler.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
/** Bind JEB handlers to their exact named contracts through shared workflows. */
export const registerJebTools = (
@ -12,12 +13,14 @@ export const registerJebTools = (
service: JebAnalysisService,
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const handler = createContractToolHandler(
server,
service,
logger,
recordEvidence,
withAdmittedAnalysis,
);
const clientContract = toolContract("inspect_jeb_client");
server.registerTool(

View File

@ -1,4 +1,5 @@
import type { EvidenceMcpServer } from "../EvidenceMcpServer.js";
import { runAdmittedToolOperation } from "../admittedToolOperation.js";
import { recordSessionEvidenceSources } from "../sessionEvidence.js";
import { compareManagedMembersEvidenceValidated } from "../../application/managed/ManagedMemberComparisonService.js";
@ -19,61 +20,77 @@ export const registerCompareManagedMembers = (
server.registerTool(
compareContract.name,
toolRegistrationOptions(compareContract),
async (input) => {
const leftResolved = resolveManagedEvidence(input.left);
const rightResolved = resolveManagedEvidence(input.right);
if (!leftResolved.ok)
return server.delivery.toCallToolResult(leftResolved, compareContract);
if (!rightResolved.ok)
return server.delivery.toCallToolResult(rightResolved, compareContract);
const [left] = leftResolved.value;
const [right] = rightResolved.value;
if (left === undefined || right === undefined)
throw new TypeError("Managed comparison Evidence resolution failed");
const parsed = { left, right };
const result = await logToolExecution(
options.logger,
async (input, context) =>
runAdmittedToolOperation(
server,
options.withAdmittedAnalysis,
compareContract.name,
() => Promise.resolve(compareManagedMembersEvidenceValidated(parsed)),
);
if (!result.ok)
return server.delivery.toCallToolResult(result, compareContract);
const recorded = recordSessionEvidenceSources(options.recordEvidence, [
parsed.left,
parsed.right,
]);
if (!recorded.ok)
return server.delivery.toCallToolResult(recorded, compareContract);
const comparison = managedMemberComparisonResultSchema.parse(
result.value.normalized_result,
);
const unknown = comparison.summary.unknown > 0;
const output = unknown
? options.recordEvidenceWithUnknown?.(result.value, {
question:
"Which managed members remain unmatched or ambiguous across these versions?",
severity: "medium",
domain: "managed-member-comparison",
supporting_evidence_ids: [result.value.evidence_id],
contradicting_evidence_ids: [],
required_authority: "shipped-artifact",
required_confidence: "observed",
required_environment: null,
recommended_probes: [
{
operation: "inspect_managed_members",
rationale:
"Review the inspection Evidence coverage and limit diagnostics; this operation has no page override, so unresolved members must remain unknown.",
},
],
relationships: [],
})
: options.recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
compareContract,
output,
);
},
context.mcpReq.signal,
async () => {
const leftResolved = resolveManagedEvidence(input.left);
const rightResolved = resolveManagedEvidence(input.right);
if (!leftResolved.ok)
return server.delivery.toCallToolResult(
leftResolved,
compareContract,
);
if (!rightResolved.ok)
return server.delivery.toCallToolResult(
rightResolved,
compareContract,
);
const [left] = leftResolved.value;
const [right] = rightResolved.value;
if (left === undefined || right === undefined)
throw new TypeError(
"Managed comparison Evidence resolution failed",
);
const parsed = { left, right };
const result = await logToolExecution(
options.logger,
compareContract.name,
() =>
Promise.resolve(compareManagedMembersEvidenceValidated(parsed)),
);
if (!result.ok)
return server.delivery.toCallToolResult(result, compareContract);
const recorded = recordSessionEvidenceSources(
options.recordEvidence,
[parsed.left, parsed.right],
);
if (!recorded.ok)
return server.delivery.toCallToolResult(recorded, compareContract);
const comparison = managedMemberComparisonResultSchema.parse(
result.value.normalized_result,
);
const unknown = comparison.summary.unknown > 0;
const output = unknown
? options.recordEvidenceWithUnknown?.(result.value, {
question:
"Which managed members remain unmatched or ambiguous across these versions?",
severity: "medium",
domain: "managed-member-comparison",
supporting_evidence_ids: [result.value.evidence_id],
contradicting_evidence_ids: [],
required_authority: "shipped-artifact",
required_confidence: "observed",
required_environment: null,
recommended_probes: [
{
operation: "inspect_managed_members",
rationale:
"Review the inspection Evidence coverage and limit diagnostics; this operation has no page override, so unresolved members must remain unknown.",
},
],
relationships: [],
})
: options.recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
compareContract,
output,
);
},
),
);
};

View File

@ -1,4 +1,5 @@
import type { EvidenceMcpServer } from "../EvidenceMcpServer.js";
import { runAdmittedToolOperation } from "../admittedToolOperation.js";
import { importManagedReconstructionEvidenceValidated } from "../../application/managed/ManagedReconstructionService.js";
import { logToolExecution } from "../toolLogging.js";
@ -19,39 +20,53 @@ export const registerImportManagedReconstruction = (
server.registerTool(
reconstructionContract.name,
toolRegistrationOptions(reconstructionContract),
async (input) => {
const resolved = resolveManagedEvidence(input.static_members);
if (!resolved.ok)
return server.delivery.toCallToolResult(
resolved,
reconstructionContract,
);
const staticMembers = resolved.value[0];
if (staticMembers === undefined)
throw new TypeError(
"Managed reconstruction Evidence resolution failed",
);
const parsed = { ...input, static_members: staticMembers };
const result = await logToolExecution(
options.logger,
async (input, context) =>
runAdmittedToolOperation(
server,
options.withAdmittedAnalysis,
reconstructionContract.name,
() =>
Promise.resolve(importManagedReconstructionEvidenceValidated(parsed)),
);
if (!result.ok)
return server.delivery.toCallToolResult(result, reconstructionContract);
const recordedSource = options.recordEvidence?.(parsed.static_members);
if (recordedSource !== undefined && !recordedSource.ok)
return server.delivery.toCallToolResult(
recordedSource,
reconstructionContract,
);
const recorded = options.recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
reconstructionContract,
recorded,
);
},
context.mcpReq.signal,
async () => {
const resolved = resolveManagedEvidence(input.static_members);
if (!resolved.ok)
return server.delivery.toCallToolResult(
resolved,
reconstructionContract,
);
const staticMembers = resolved.value[0];
if (staticMembers === undefined)
throw new TypeError(
"Managed reconstruction Evidence resolution failed",
);
const parsed = { ...input, static_members: staticMembers };
const result = await logToolExecution(
options.logger,
reconstructionContract.name,
() =>
Promise.resolve(
importManagedReconstructionEvidenceValidated(parsed),
),
);
if (!result.ok)
return server.delivery.toCallToolResult(
result,
reconstructionContract,
);
const recordedSource = options.recordEvidence?.(
parsed.static_members,
);
if (recordedSource !== undefined && !recordedSource.ok)
return server.delivery.toCallToolResult(
recordedSource,
reconstructionContract,
);
const recorded = options.recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
reconstructionContract,
recorded,
);
},
),
);
};

View File

@ -1,4 +1,5 @@
import type { EvidenceMcpServer } from "../EvidenceMcpServer.js";
import { runAdmittedToolOperation } from "../admittedToolOperation.js";
import { recordSessionEvidenceSources } from "../sessionEvidence.js";
import { projectManagedApplicationGraphEvidence } from "../../application/managed/ManagedApplicationGraphService.js";
@ -25,52 +26,69 @@ export const registerProjectManagedApplicationGraph = (
server.registerTool(
graphContract.name,
toolRegistrationOptions(graphContract),
async (input) => {
const managedArtifact =
input.managed_artifact === undefined
? undefined
: resolveManagedArtifactEvidence(input.managed_artifact);
if (managedArtifact !== undefined && !managedArtifact.ok)
return server.delivery.toCallToolResult(managedArtifact, graphContract);
const managedMembers =
input.managed_members === undefined
? undefined
: resolveManagedEvidence(input.managed_members);
if (managedMembers !== undefined && !managedMembers.ok)
return server.delivery.toCallToolResult(managedMembers, graphContract);
const managedBoundaries =
input.managed_native_boundaries === undefined
? undefined
: resolveManagedBoundaryEvidence(input.managed_native_boundaries);
if (managedBoundaries !== undefined && !managedBoundaries.ok)
return server.delivery.toCallToolResult(
managedBoundaries,
graphContract,
);
const parsed = {
managed_artifact: managedArtifact?.value[0],
managed_members: managedMembers?.value[0],
managed_native_boundaries: managedBoundaries?.value[0],
};
const result = await logToolExecution(
options.logger,
async (input, context) =>
runAdmittedToolOperation(
server,
options.withAdmittedAnalysis,
graphContract.name,
() => Promise.resolve(projectManagedApplicationGraphEvidence(parsed)),
);
if (!result.ok)
return server.delivery.toCallToolResult(result, graphContract);
const recordedSources = recordSessionEvidenceSources(
options.recordEvidence,
sourceEvidence(parsed),
);
if (!recordedSources.ok)
return server.delivery.toCallToolResult(recordedSources, graphContract);
const recorded = options.recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
graphContract,
recorded,
);
},
context.mcpReq.signal,
async () => {
const managedArtifact =
input.managed_artifact === undefined
? undefined
: resolveManagedArtifactEvidence(input.managed_artifact);
if (managedArtifact !== undefined && !managedArtifact.ok)
return server.delivery.toCallToolResult(
managedArtifact,
graphContract,
);
const managedMembers =
input.managed_members === undefined
? undefined
: resolveManagedEvidence(input.managed_members);
if (managedMembers !== undefined && !managedMembers.ok)
return server.delivery.toCallToolResult(
managedMembers,
graphContract,
);
const managedBoundaries =
input.managed_native_boundaries === undefined
? undefined
: resolveManagedBoundaryEvidence(input.managed_native_boundaries);
if (managedBoundaries !== undefined && !managedBoundaries.ok)
return server.delivery.toCallToolResult(
managedBoundaries,
graphContract,
);
const parsed = {
managed_artifact: managedArtifact?.value[0],
managed_members: managedMembers?.value[0],
managed_native_boundaries: managedBoundaries?.value[0],
};
const result = await logToolExecution(
options.logger,
graphContract.name,
() =>
Promise.resolve(projectManagedApplicationGraphEvidence(parsed)),
);
if (!result.ok)
return server.delivery.toCallToolResult(result, graphContract);
const recordedSources = recordSessionEvidenceSources(
options.recordEvidence,
sourceEvidence(parsed),
);
if (!recordedSources.ok)
return server.delivery.toCallToolResult(
recordedSources,
graphContract,
);
const recorded = options.recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
graphContract,
recorded,
);
},
),
);
};

View File

@ -4,9 +4,11 @@ import type {
} from "../../application/investigation/InvestigationRecordPort.js";
import type { BinarySessionPort } from "../../application/binary/BinarySessionPort.js";
import type { Logger } from "pino";
import type { WithAdmittedAnalysis } from "../analysisAdmission.js";
/** Shared services for registering managed-code workflow tools. */
export interface ManagedWorkflowToolRegistration {
readonly withAdmittedAnalysis?: WithAdmittedAnalysis;
readonly logger: Logger;
readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined;
readonly recordEvidenceWithUnknown:

View File

@ -1,4 +1,5 @@
import type { EvidenceMcpServer } from "../EvidenceMcpServer.js";
import { runAdmittedToolOperation } from "../admittedToolOperation.js";
import { recordSessionEvidenceSources } from "../sessionEvidence.js";
import { verifyManagedNativeBoundariesEvidence } from "../../application/managed/ManagedNativeVerificationService.js";
@ -24,88 +25,96 @@ export const registerVerifyManagedNativeBoundaries = (
server.registerTool(
nativeVerificationContract.name,
toolRegistrationOptions(nativeVerificationContract),
async (input) => {
const managedBoundaries = resolveManagedBoundaryEvidence(
input.managed_boundaries,
);
if (!managedBoundaries.ok)
return server.delivery.toCallToolResult(
managedBoundaries,
nativeVerificationContract,
);
const nativeObservations = resolveNativeEvidence(
input.native_observations,
);
if (!nativeObservations.ok)
return server.delivery.toCallToolResult(
nativeObservations,
nativeVerificationContract,
);
const managedBoundary = managedBoundaries.value[0];
if (managedBoundary === undefined)
throw new TypeError("Managed boundary Evidence resolution failed");
const parsed = {
...input,
managed_boundaries: managedBoundary,
native_observations: nativeObservations.value,
};
const result = await logToolExecution(
options.logger,
async (input, context) =>
runAdmittedToolOperation(
server,
options.withAdmittedAnalysis,
nativeVerificationContract.name,
() => Promise.resolve(verifyManagedNativeBoundariesEvidence(parsed)),
);
if (!result.ok)
return server.delivery.toCallToolResult(
result,
nativeVerificationContract,
);
const recordedSources = recordSessionEvidenceSources(
options.recordEvidence,
[parsed.managed_boundaries, ...parsed.native_observations],
);
if (!recordedSources.ok)
return server.delivery.toCallToolResult(
recordedSources,
nativeVerificationContract,
);
const verification = managedNativeVerificationResultSchema.parse(
result.value.normalized_result,
);
const unknown =
verification.summary.unresolved > 0 ||
verification.summary.contradicted > 0 ||
verification.summary.native_body_unresolved > 0;
const output = unknown
? options.recordEvidenceWithUnknown?.(result.value, {
question:
"Which managed/native boundaries remain unresolved or contradicted by the supplied native Evidence?",
severity: "medium",
domain: "managed-native-verification",
supporting_evidence_ids: [result.value.evidence_id],
contradicting_evidence_ids: [],
required_authority: "shipped-artifact",
required_confidence: "observed",
required_environment: null,
recommended_probes: [
{
operation: "inspect_managed_native_boundaries",
rationale:
"Review the inspection Evidence coverage and limit diagnostics; this operation has no page override, so unresolved declarations must remain unknown.",
},
{
operation: "analyze_function",
rationale:
"Analyze the provider-resolved native function candidate for the declared export.",
},
],
relationships: [],
})
: options.recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
nativeVerificationContract,
output,
);
},
context.mcpReq.signal,
async () => {
const managedBoundaries = resolveManagedBoundaryEvidence(
input.managed_boundaries,
);
if (!managedBoundaries.ok)
return server.delivery.toCallToolResult(
managedBoundaries,
nativeVerificationContract,
);
const nativeObservations = resolveNativeEvidence(
input.native_observations,
);
if (!nativeObservations.ok)
return server.delivery.toCallToolResult(
nativeObservations,
nativeVerificationContract,
);
const managedBoundary = managedBoundaries.value[0];
if (managedBoundary === undefined)
throw new TypeError("Managed boundary Evidence resolution failed");
const parsed = {
...input,
managed_boundaries: managedBoundary,
native_observations: nativeObservations.value,
};
const result = await logToolExecution(
options.logger,
nativeVerificationContract.name,
() =>
Promise.resolve(verifyManagedNativeBoundariesEvidence(parsed)),
);
if (!result.ok)
return server.delivery.toCallToolResult(
result,
nativeVerificationContract,
);
const recordedSources = recordSessionEvidenceSources(
options.recordEvidence,
[parsed.managed_boundaries, ...parsed.native_observations],
);
if (!recordedSources.ok)
return server.delivery.toCallToolResult(
recordedSources,
nativeVerificationContract,
);
const verification = managedNativeVerificationResultSchema.parse(
result.value.normalized_result,
);
const unknown =
verification.summary.unresolved > 0 ||
verification.summary.contradicted > 0 ||
verification.summary.native_body_unresolved > 0;
const output = unknown
? options.recordEvidenceWithUnknown?.(result.value, {
question:
"Which managed/native boundaries remain unresolved or contradicted by the supplied native Evidence?",
severity: "medium",
domain: "managed-native-verification",
supporting_evidence_ids: [result.value.evidence_id],
contradicting_evidence_ids: [],
required_authority: "shipped-artifact",
required_confidence: "observed",
required_environment: null,
recommended_probes: [
{
operation: "inspect_managed_native_boundaries",
rationale:
"Review the inspection Evidence coverage and limit diagnostics; this operation has no page override, so unresolved declarations must remain unknown.",
},
{
operation: "analyze_function",
rationale:
"Analyze the provider-resolved native function candidate for the declared export.",
},
],
relationships: [],
})
: options.recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
nativeVerificationContract,
output,
);
},
),
);
};

View File

@ -3,7 +3,8 @@ import type { PeResourcesService } from "../application/binaryDiagnostics/PeReso
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "pino";
import { logToolExecution } from "./toolLogging.js";
import { createEvidenceToolHandler } from "./contractToolHandler.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
/** Register the explicit-file PE resource workflow and retain its complete Evidence. */
@ -12,21 +13,19 @@ export const registerPeResourcesTool = (
service: PeResourcesService,
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const contract = toolContract("inspect_pe_resources");
const handler = createEvidenceToolHandler(
server,
(input, options) => service.inspect(input, options),
logger,
recordEvidence,
withAdmittedAnalysis,
);
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async (input, context) => {
const result = await logToolExecution(logger, contract.name, () =>
service.inspect(input, { signal: context.mcpReq.signal }),
);
if (!result.ok) return server.delivery.toCallToolResult(result, contract);
return server.delivery.toEvidenceToolResult(
result.value,
contract,
recordEvidence?.(result.value),
);
},
handler(contract),
);
};

View File

@ -25,6 +25,8 @@ import { recordSessionEvidenceSources } from "./sessionEvidence.js";
import { runDerivedOperation } from "./runDerivedOperation.js";
import { PROCESS_PROVIDER } from "../domain/process/processEvidenceProvider.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { withAdmittedAnalysis } from "./analysisAdmission.js";
import { runAdmittedToolOperation } from "./admittedToolOperation.js";
const PROCESS_CAPTURE_EVIDENCE = {
operation: "capture_process_scenario",
@ -94,72 +96,87 @@ export const registerProcessComparisonTool = (
contract: ReturnType<typeof toolContract<"compare_process_captures">>,
now: () => number = Date.now,
): void => {
const admission = withAdmittedAnalysis({ kind: "session", session });
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async (input, context) => {
const left = parseCaptureSide(contract.name, "left", input.left);
if (!left.ok) return server.delivery.toCallToolResult(left, contract);
const right = parseCaptureSide(contract.name, "right", input.right);
if (!right.ok) return server.delivery.toCallToolResult(right, contract);
const { record: leftRecord, capture: leftCapture } = left.value;
const { record: rightRecord, capture: rightCapture } = right.value;
const computed = await runDerivedOperation(context, contract.name, () =>
compareProcessCaptures(leftCapture, rightCapture, {
...(input.max_capture_age_ms === undefined
? {}
: { maxCaptureAgeMs: input.max_capture_age_ms }),
...(input.trace_spec === undefined
? {}
: { traceSpecification: input.trace_spec }),
now,
}),
);
if (!computed.ok)
return server.delivery.toCallToolResult(computed, contract);
const comparison = computed.value;
const evidence = createEvidence(undefined, PROCESS_PROVIDER, {
predicateType: "rea.process-comparison",
operation: contract.name,
parameters: {
left_evidence_id: leftRecord.evidence_id,
right_evidence_id: rightRecord.evidence_id,
left_normalization: leftCapture.normalization,
right_normalization: rightCapture.normalization,
...(input.trace_spec === undefined
? {}
: { trace_spec: jsonValueSchema.parse(input.trace_spec) }),
},
result: jsonValueSchema.parse(comparison),
confidence: "derived",
authority: "analyst-inference",
limitations: comparison.limitations,
locations: sourceLocations(leftRecord.locations, rightRecord.locations),
evidenceLinks: [leftRecord.evidence_id, rightRecord.evidence_id],
});
const recordedSources = recordSessionEvidenceSources(
(evidence) => session.recordEvidence(evidence),
[leftRecord, rightRecord],
);
if (!recordedSources.ok)
return server.delivery.toCallToolResult(recordedSources, contract);
return server.delivery.toEvidenceToolResult(
evidence,
contract,
recordDerivedEvidence(
session,
evidence,
comparisonUnknownInput(
{
async (input, context) =>
runAdmittedToolOperation(
server,
admission,
contract.name,
context.mcpReq.signal,
async () => {
const left = parseCaptureSide(contract.name, "left", input.left);
if (!left.ok) return server.delivery.toCallToolResult(left, contract);
const right = parseCaptureSide(contract.name, "right", input.right);
if (!right.ok)
return server.delivery.toCallToolResult(right, contract);
const { record: leftRecord, capture: leftCapture } = left.value;
const { record: rightRecord, capture: rightCapture } = right.value;
const computed = await runDerivedOperation(
context,
contract.name,
() =>
compareProcessCaptures(leftCapture, rightCapture, {
...(input.max_capture_age_ms === undefined
? {}
: { maxCaptureAgeMs: input.max_capture_age_ms }),
...(input.trace_spec === undefined
? {}
: { traceSpecification: input.trace_spec }),
now,
}),
);
if (!computed.ok)
return server.delivery.toCallToolResult(computed, contract);
const comparison = computed.value;
const evidence = createEvidence(undefined, PROCESS_PROVIDER, {
predicateType: "rea.process-comparison",
operation: contract.name,
parameters: {
left_evidence_id: leftRecord.evidence_id,
right_evidence_id: rightRecord.evidence_id,
comparison_evidence_id: evidence.evidence_id,
left_normalization: leftCapture.normalization,
right_normalization: rightCapture.normalization,
...(input.trace_spec === undefined
? {}
: { trace_spec: jsonValueSchema.parse(input.trace_spec) }),
},
comparison,
),
),
);
},
result: jsonValueSchema.parse(comparison),
confidence: "derived",
authority: "analyst-inference",
limitations: comparison.limitations,
locations: sourceLocations(
leftRecord.locations,
rightRecord.locations,
),
evidenceLinks: [leftRecord.evidence_id, rightRecord.evidence_id],
});
const recordedSources = recordSessionEvidenceSources(
(evidence) => session.recordEvidence(evidence),
[leftRecord, rightRecord],
);
if (!recordedSources.ok)
return server.delivery.toCallToolResult(recordedSources, contract);
return server.delivery.toEvidenceToolResult(
evidence,
contract,
recordDerivedEvidence(
session,
evidence,
comparisonUnknownInput(
{
left_evidence_id: leftRecord.evidence_id,
right_evidence_id: rightRecord.evidence_id,
comparison_evidence_id: evidence.evidence_id,
},
comparison,
),
),
);
},
),
);
};

View File

@ -4,7 +4,8 @@ import type { RecordedCrashService } from "../application/binaryDiagnostics/Reco
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "pino";
import { logToolExecution } from "./toolLogging.js";
import { createEvidenceToolHandler } from "./contractToolHandler.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
/** Bind recorded crash inspection to its named contract and session Evidence owner. */
@ -13,22 +14,19 @@ export const registerRecordedCrashTools = (
service: RecordedCrashService,
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const contract = toolContract("inspect_recorded_crash");
const handler = createEvidenceToolHandler(
server,
(input, options) => service.inspect(input, options),
logger,
recordEvidence,
withAdmittedAnalysis,
);
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async (input, context) => {
const result = await logToolExecution(logger, contract.name, () =>
service.inspect(input, { signal: context.mcpReq.signal }),
);
if (!result.ok) return server.delivery.toCallToolResult(result, contract);
const recorded = recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
contract,
recorded,
);
},
handler(contract),
);
};

View File

@ -8,13 +8,15 @@ import { readAnalysisSnapshot } from "../application/binary/AnalysisSnapshotFile
import type { BinarySessionPort } from "../application/binary/BinarySessionPort.js";
import { createProcessCaptureEvidence } from "../application/process/ProcessEvidence.js";
import { captureProcessScenario } from "../process/capture/ProcessHarness.js";
import { ProcessCaptureResourceScope } from "../process/capture/ProcessCaptureLifecycle.js";
import { ProcessCaptureError } from "../process/capture/ProcessCaptureError.js";
import { toolContract } from "../contracts/toolContracts.js";
import type { AnalysisSnapshot } from "../domain/analysisSnapshot.js";
import { UnknownRegistryError } from "../domain/unknownRegistryError.js";
import { type AnalysisError } from "../domain/analysisErrorBase.js";
import type { Evidence } from "../domain/evidence.js";
import type { ProcessCapture } from "../domain/process/processCaptureParsing.js";
import { ok, type Result } from "../domain/result.js";
import { err, ok, type Result } from "../domain/result.js";
import type { Logger } from "pino";
import type { ProviderAvailability } from "../application/AnalysisProvider.js";
import { mcpProgressReporter } from "./mcpProgress.js";
@ -33,6 +35,8 @@ import { sessionAvailabilityPolicy } from "./sessionAvailabilityPolicy.js";
import type { AvailabilityPolicy } from "../application/CapabilityInventory.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { withAdmittedAnalysis } from "./analysisAdmission.js";
import { runAdmittedToolOperation } from "./admittedToolOperation.js";
const recordProcessResidualUnknowns = (
session: BinarySessionPort,
@ -77,6 +81,7 @@ interface ProcessToolRegistration {
readonly captureContract: ReturnType<
typeof toolContract<"capture_process_scenario">
>;
readonly resourceScope: ProcessCaptureResourceScope;
}
const registerProcessTools = ({
@ -84,45 +89,55 @@ const registerProcessTools = ({
session,
logger,
captureContract,
resourceScope,
}: ProcessToolRegistration): void => {
const admission = withAdmittedAnalysis({ kind: "session", session });
server.registerTool(
captureContract.name,
toolRegistrationOptions(captureContract),
async (input, context) => {
const progress = mcpProgressReporter(context);
const captured = await logToolExecution(
logger,
async (input, context) =>
runAdmittedToolOperation(
server,
admission,
captureContract.name,
() =>
captureProcessScenario(
input,
context.mcpReq.signal,
process.platform,
process.env,
undefined,
undefined,
progress,
),
);
if (!captured.ok)
return server.delivery.toCallToolResult(captured, captureContract);
const evidence = createProcessCaptureEvidence(input, captured.value);
const recorded = session.recordEvidence(evidence);
if (!recorded.ok)
return server.delivery.toCallToolResult(recorded, captureContract);
const unknowns = recordProcessResidualUnknowns(
session,
evidence,
captured.value.residual_unknowns,
);
if (!unknowns.ok)
return server.delivery.toCallToolResult(unknowns, captureContract);
return server.delivery.toEvidenceToolResult(
evidence,
captureContract,
recorded,
);
},
context.mcpReq.signal,
async () => {
const progress = mcpProgressReporter(context);
const captured = await logToolExecution(
logger,
captureContract.name,
() =>
captureProcessScenario(
input,
context.mcpReq.signal,
process.platform,
process.env,
undefined,
undefined,
progress,
resourceScope,
),
);
if (!captured.ok)
return server.delivery.toCallToolResult(captured, captureContract);
const evidence = createProcessCaptureEvidence(input, captured.value);
const recorded = session.recordEvidence(evidence);
if (!recorded.ok)
return server.delivery.toCallToolResult(recorded, captureContract);
const unknowns = recordProcessResidualUnknowns(
session,
evidence,
captured.value.residual_unknowns,
);
if (!unknowns.ok)
return server.delivery.toCallToolResult(unknowns, captureContract);
return server.delivery.toEvidenceToolResult(
evidence,
captureContract,
recorded,
);
},
),
);
};
@ -138,6 +153,7 @@ export interface LifecycleToolRegistration {
readonly androidAnalysisAvailability: (
signal: AbortSignal,
) => Promise<ProviderAvailability>;
readonly closeProcessResources: () => Promise<Result<null, AnalysisError>>;
}
const registerLifecycleTools = (
@ -279,6 +295,7 @@ export const registerSessionTools = (
const compareFunctionsContract = toolContract("compare_functions");
const compareBundlesContract = toolContract("compare_bundles");
const snapshotContract = toolContract("get_evidence_bundle");
const processResources = new ProcessCaptureResourceScope();
registerLifecycleTools({
server,
session,
@ -293,6 +310,27 @@ export const registerSessionTools = (
),
androidAnalysisAvailability:
options.androidAnalysisAvailability ?? missingAndroidAvailability,
closeProcessResources: async () => {
try {
await processResources.retryCleanup();
return ok(null);
} catch (cause: unknown) {
return err(
new ProcessCaptureError(
"process capture cleanup remains incomplete",
{
cause,
reason: "cleanup_incomplete",
cleanupResources: ["process_capture_resources"],
executionFailure:
cause instanceof Error
? cause.message || cause.name
: String(cause),
},
),
);
}
},
});
registerEvidenceTools({
server,
@ -306,7 +344,33 @@ export const registerSessionTools = (
session,
logger,
captureContract,
resourceScope: processResources,
});
const closeServer = server.close.bind(server);
server.close = async () => {
const results = await Promise.allSettled([
closeServer(),
processResources.close(),
]);
const failures = results.flatMap((result) =>
result.status === "rejected" ? [result.reason] : [],
);
if (failures.length > 0)
throw new AggregateError(failures, "REA session cleanup failed");
};
const previousOnclose = server.server.onclose;
server.server.onclose = () => {
try {
previousOnclose?.();
} finally {
void processResources.close().catch((cause: unknown) => {
logger.error(
{ error: cause instanceof Error ? cause.message : String(cause) },
"Process capture cleanup failed during server shutdown",
);
});
}
};
registerProcessComparisonTool(server, session, compareContract);
registerArtifactComparisonTool(server, session, compareArtifactsContract);
registerFunctionComparisonTool(server, session, compareFunctionsContract);

View File

@ -4,7 +4,8 @@ import type { WebModuleTraceService } from "../application/WebModuleTraceService
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "pino";
import { logToolExecution } from "./toolLogging.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { createEvidenceToolHandler } from "./contractToolHandler.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
/** Bind the module relationship workflow to its exact canonical contract. */
@ -13,22 +14,19 @@ export const registerWebModuleTool = (
service: WebModuleTraceService,
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const contract = toolContract("trace_web_module_imports");
const handler = createEvidenceToolHandler(
server,
(input, options) => service.trace(input, options),
logger,
recordEvidence,
withAdmittedAnalysis,
);
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async (input, context) => {
const result = await logToolExecution(logger, contract.name, () =>
service.trace(input, { signal: context.mcpReq.signal }),
);
if (!result.ok) return server.delivery.toCallToolResult(result, contract);
const recorded = recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
contract,
recorded,
);
},
handler(contract),
);
};

View File

@ -4,7 +4,8 @@ import type { WebNetworkCaptureService } from "../application/WebNetworkCaptureS
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "pino";
import { logToolExecution } from "./toolLogging.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { createEvidenceToolHandler } from "./contractToolHandler.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
/** Bind historical inspection to its named contract and caller-owned Evidence writer. */
@ -13,6 +14,7 @@ export const registerWebNetworkCaptureTool = (
service: WebNetworkCaptureService,
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const contract = toolContract("inspect_web_network_capture");
const registration = toolRegistrationOptions(contract);
@ -26,20 +28,16 @@ export const registerWebNetworkCaptureTool = (
validate: (value: unknown) => ({ value }),
},
};
const handler = createEvidenceToolHandler(
server,
(input, options) => service.inspect(input, options),
logger,
recordEvidence,
withAdmittedAnalysis,
);
server.registerTool(
contract.name,
{ ...registration, inputSchema },
async (input, context) => {
const result = await logToolExecution(logger, contract.name, () =>
service.inspect(input, { signal: context.mcpReq.signal }),
);
if (!result.ok) return server.delivery.toCallToolResult(result, contract);
const recorded = recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
contract,
recorded,
);
},
handler(contract),
);
};

View File

@ -7,6 +7,8 @@ import type { Logger } from "pino";
import { mcpProgressReporter } from "./mcpProgress.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { runAdmittedToolOperation } from "./admittedToolOperation.js";
/** Bind distinct runtime operations to named contracts and the session's Evidence owner. */
export const registerWebRuntimeTools = (
@ -14,44 +16,59 @@ export const registerWebRuntimeTools = (
service: WebRuntimeService,
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const execution = toolContract("observe_web_execution");
const listeners = toolContract("inspect_web_event_listeners");
server.registerTool(
execution.name,
toolRegistrationOptions(execution),
async (input, context) => {
const result = await logToolExecution(logger, execution.name, () =>
service.observe(input, {
signal: context.mcpReq.signal,
progress: mcpProgressReporter(context),
}),
);
if (!result.ok)
return server.delivery.toCallToolResult(result, execution);
const recorded = recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
execution,
recorded,
);
},
async (input, context) =>
runAdmittedToolOperation(
server,
withAdmittedAnalysis,
execution.name,
context.mcpReq.signal,
async () => {
const result = await logToolExecution(logger, execution.name, () =>
service.observe(input, {
signal: context.mcpReq.signal,
progress: mcpProgressReporter(context),
}),
);
if (!result.ok)
return server.delivery.toCallToolResult(result, execution);
const recorded = recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
execution,
recorded,
);
},
),
);
server.registerTool(
listeners.name,
toolRegistrationOptions(listeners),
async (input, context) => {
const result = await logToolExecution(logger, listeners.name, () =>
service.inspect(input, { signal: context.mcpReq.signal }),
);
if (!result.ok)
return server.delivery.toCallToolResult(result, listeners);
const recorded = recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
listeners,
recorded,
);
},
async (input, context) =>
runAdmittedToolOperation(
server,
withAdmittedAnalysis,
listeners.name,
context.mcpReq.signal,
async () => {
const result = await logToolExecution(logger, listeners.name, () =>
service.inspect(input, { signal: context.mcpReq.signal }),
);
if (!result.ok)
return server.delivery.toCallToolResult(result, listeners);
const recorded = recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
listeners,
recorded,
);
},
),
);
};

View File

@ -6,6 +6,8 @@ import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "pino";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { runAdmittedToolOperation } from "./admittedToolOperation.js";
/** Register local script export without requiring a live browser provider. */
export const registerWebScriptTool = (
@ -13,23 +15,37 @@ export const registerWebScriptTool = (
options: {
readonly logger: Logger;
readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined;
readonly withAdmittedAnalysis?: WithAdmittedAnalysis;
},
): void => {
const contract = toolContract("export_web_scripts");
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async (input, context) => {
const result = await logToolExecution(options.logger, contract.name, () =>
exportWebScriptsValidated(input, { signal: context.mcpReq.signal }),
);
if (!result.ok) return server.delivery.toCallToolResult(result, contract);
const recorded = options.recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
contract,
recorded,
);
},
async (input, context) =>
runAdmittedToolOperation(
server,
options.withAdmittedAnalysis,
contract.name,
context.mcpReq.signal,
async () => {
const result = await logToolExecution(
options.logger,
contract.name,
() =>
exportWebScriptsValidated(input, {
signal: context.mcpReq.signal,
}),
);
if (!result.ok)
return server.delivery.toCallToolResult(result, contract);
const recorded = options.recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
contract,
recorded,
);
},
),
);
};

View File

@ -4,7 +4,8 @@ import type { WebSourceLocationService } from "../application/WebSourceLocationS
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "pino";
import { logToolExecution } from "./toolLogging.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { createEvidenceToolHandler } from "./contractToolHandler.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
/** Bind source point tracing to its exact named contract and session Evidence owner. */
@ -13,22 +14,19 @@ export const registerWebSourceLocationTool = (
service: WebSourceLocationService,
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const contract = toolContract("trace_web_source_location");
const handler = createEvidenceToolHandler(
server,
(input, options) => service.trace(input, options),
logger,
recordEvidence,
withAdmittedAnalysis,
);
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async (input, context) => {
const result = await logToolExecution(logger, contract.name, () =>
service.trace(input, { signal: context.mcpReq.signal }),
);
if (!result.ok) return server.delivery.toCallToolResult(result, contract);
const recorded = recordEvidence?.(result.value);
return server.delivery.toEvidenceToolResult(
result.value,
contract,
recorded,
);
},
handler(contract),
);
};