Files
rea/src/server/registerSessionTools.ts
T

271 lines
8.2 KiB
TypeScript

import type { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";
import type { BinarySessionPort } from "../application/BinarySession.js";
import { SESSION_TOOL_CONTRACTS } from "../contracts/toolContracts.js";
import { toCallToolResult } from "./toolResult.js";
import type { Logger } from "../logger.js";
import { logToolExecution } from "./toolLogging.js";
import { err, ok } from "../domain/result.js";
import { EvidenceLedgerError } from "../domain/errors.js";
import type { ProcessExecutionPolicy } from "../domain/processCapture.js";
import {
compareProcessCaptures,
processCaptureSchema,
processScenarioSchema,
} from "../domain/processCapture.js";
import { captureProcessScenario } from "../application/ProcessHarness.js";
import { createEvidence } from "../domain/evidence.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
const DENY_PROCESS_POLICY: ProcessExecutionPolicy = {
enabled: false,
executableRoots: [],
workingRoots: [],
allowedEnvironment: [],
};
const PROCESS_PROVIDER = {
id: "rea-process",
name: "REA deterministic process harness",
version: "1",
} as const;
interface ProcessToolRegistration {
readonly server: McpServer;
readonly session: BinarySessionPort;
readonly logger: Logger;
readonly processPolicy: ProcessExecutionPolicy;
readonly captureContract: (typeof SESSION_TOOL_CONTRACTS)[5];
readonly compareContract: (typeof SESSION_TOOL_CONTRACTS)[6];
}
const registerProcessTools = ({
server,
session,
logger,
processPolicy,
captureContract,
compareContract,
}: ProcessToolRegistration): void => {
server.registerTool(
captureContract.name,
{
description: captureContract.description,
inputSchema: captureContract.inputSchema,
outputSchema: captureContract.outputSchema,
annotations: captureContract.annotations,
},
async (input, context) => {
const scenario = processScenarioSchema.parse(input);
const captured = await logToolExecution(
logger,
captureContract.name,
() =>
captureProcessScenario(
scenario,
processPolicy,
context.mcpReq.signal,
),
);
if (!captured.ok) return toCallToolResult(captured, captureContract);
const evidence = createEvidence(undefined, PROCESS_PROVIDER, {
predicateType: "rea.process-capture/v1",
operation: captureContract.name,
parameters: {
executable_name:
scenario.executable.split("/").at(-1) ?? scenario.executable,
argument_count: scenario.arguments.length,
event_count: scenario.events.length,
filesystem_root_count: scenario.filesystem_roots.length,
},
result: jsonValueSchema.parse(captured.value),
confidence: "observed",
authority: "controlled-replay",
environment: {
id: `${process.platform}-${process.arch}`,
platform: process.platform,
architecture: process.arch,
isolation: "process",
},
limitations: captured.value.limitations,
});
session.recordEvidence(evidence);
return toCallToolResult(ok(evidence), captureContract);
},
);
server.registerTool(
compareContract.name,
{
description: compareContract.description,
inputSchema: compareContract.inputSchema,
outputSchema: compareContract.outputSchema,
annotations: compareContract.annotations,
},
(input) => {
const parsed = z
.object({
left_evidence_id: z.string(),
left: processCaptureSchema,
right_evidence_id: z.string(),
right: processCaptureSchema,
})
.parse(input);
const comparison = compareProcessCaptures(parsed.left, parsed.right);
const evidence = createEvidence(undefined, PROCESS_PROVIDER, {
predicateType: "rea.process-comparison/v1",
operation: compareContract.name,
parameters: {},
result: jsonValueSchema.parse(comparison),
confidence: "derived",
authority: "analyst-inference",
limitations: comparison.limitations,
evidenceLinks: [parsed.left_evidence_id, parsed.right_evidence_id],
});
session.recordEvidence(evidence);
return toCallToolResult(ok(evidence), compareContract);
},
);
};
interface EvidenceToolRegistration {
readonly server: McpServer;
readonly session: BinarySessionPort;
readonly exportContract: (typeof SESSION_TOOL_CONTRACTS)[3];
readonly importContract: (typeof SESSION_TOOL_CONTRACTS)[4];
}
const registerEvidenceTools = ({
server,
session,
exportContract,
importContract,
}: EvidenceToolRegistration): void => {
server.registerTool(
exportContract.name,
{
description: exportContract.description,
inputSchema: exportContract.inputSchema,
outputSchema: exportContract.outputSchema,
annotations: exportContract.annotations,
},
() => toCallToolResult(ok(session.exportEvidenceBundle()), exportContract),
);
server.registerTool(
importContract.name,
{
description: importContract.description,
inputSchema: importContract.inputSchema,
outputSchema: importContract.outputSchema,
annotations: importContract.annotations,
},
(input) => {
try {
const bundle = z.object({ bundle: z.unknown() }).parse(input).bundle;
const imported = session.importEvidenceBundle(bundle);
return toCallToolResult(
ok({
imported,
total: session.exportEvidenceBundle().records.length,
}),
importContract,
);
} catch (cause: unknown) {
return toCallToolResult(
err(
new EvidenceLedgerError("Evidence bundle validation failed", {
cause,
}),
),
importContract,
);
}
},
);
};
/** Register MCP-only target lifecycle operations on a long-lived session. */
export const registerSessionTools = (
server: McpServer,
session: BinarySessionPort,
logger: Logger,
processPolicy: ProcessExecutionPolicy = DENY_PROCESS_POLICY,
): void => {
const [
openContract,
closeContract,
statusContract,
exportContract,
importContract,
captureContract,
compareContract,
] = SESSION_TOOL_CONTRACTS;
server.registerTool(
openContract.name,
{
description: openContract.description,
inputSchema: openContract.inputSchema,
outputSchema: openContract.outputSchema,
annotations: openContract.annotations,
},
async (input, context) => {
const parsed = z.object({ path: z.string().min(1) }).parse(input);
const opened = await logToolExecution(logger, openContract.name, () =>
session.open(parsed.path, { signal: context.mcpReq.signal }),
);
return opened.ok
? toCallToolResult(
{
ok: true,
value: {
path: opened.value.path,
format: opened.value.format,
kind: opened.value.kind,
loaderArgs: [...opened.value.loaderArgs],
sha256: opened.value.sha256,
architecture: opened.value.architecture ?? null,
},
},
openContract,
)
: toCallToolResult(opened, openContract);
},
);
server.registerTool(
closeContract.name,
{
description: closeContract.description,
inputSchema: closeContract.inputSchema,
outputSchema: closeContract.outputSchema,
annotations: closeContract.annotations,
},
async () =>
toCallToolResult(
await logToolExecution(logger, closeContract.name, () =>
session.close(),
),
closeContract,
),
);
server.registerTool(
statusContract.name,
{
description: statusContract.description,
inputSchema: statusContract.inputSchema,
outputSchema: statusContract.outputSchema,
annotations: statusContract.annotations,
},
() =>
toCallToolResult({ ok: true, value: session.status() }, statusContract),
);
registerEvidenceTools({ server, session, exportContract, importContract });
registerProcessTools({
server,
session,
logger,
processPolicy,
captureContract,
compareContract,
});
};