refactor(mcp): share entropy summary delivery

This commit is contained in:
morluto
2026-10-27 10:55:00 +08:00
parent 1fdd3df7b4
commit f8f7ac96de
2 changed files with 66 additions and 64 deletions
+9 -22
View File
@@ -1,12 +1,11 @@
import type { ByteEntropyService } from "../application/binaryDiagnostics/ByteEntropyService.js";
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import { retainedSummaryDelivery } from "./retainedSummaryDelivery.js";
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 { runEvidenceToolOperation } from "./contractToolHandler.js";
import { createRetainedSummaryToolHandler } from "./contractToolHandler.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
@@ -20,28 +19,16 @@ export const registerByteEntropyTool = (
withAdmittedAnalysis: WithAdmittedAnalysis | undefined,
): void => {
const contract = toolContract("inspect_byte_entropy");
const delivery = retainedSummaryDelivery(contract.name, {
recordEvidence,
evidenceById,
});
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async ({ detail, ...request }, context) =>
runEvidenceToolOperation(
{ server, logger, recordEvidence, withAdmittedAnalysis },
contract,
undefined,
{ input: request, context },
async (input, { signal }) => {
const parameters = { detail };
const ready = delivery.preflight(parameters);
if (!ready.ok) return ready;
const inspected = await entropyService.inspect(input, { signal });
return inspected.ok
? delivery.projectEvidence(parameters, inspected.value)
: inspected;
},
),
createRetainedSummaryToolHandler(
server,
contract,
(input, options) => entropyService.inspect(input, options),
logger,
{ recordEvidence, evidenceById },
withAdmittedAnalysis,
),
);
};
+57 -42
View File
@@ -33,48 +33,63 @@ import { parseNativeExports } from "../../../src/native/exports/NativeExportPars
import { createNativeExportsService } from "../../../src/composition/binaryDiagnostics.js";
import { registerNativeExportsTool } from "../../../src/server/registerNativeExportsTool.js";
it("refuses a native exports summary before file admission when retention is unavailable", async () => {
const service = createNativeExportsService();
const server = new EvidenceMcpServer(
{ name: "exports-preflight", version: "1" },
{},
undefined,
createToolResultDelivery(undefined),
);
registerNativeExportsTool(server, service, silentLogger);
const client = new Client({ name: "exports-preflight-client", version: "1" });
onTestFinished(async () => {
await client.close();
await server.close();
await service.close();
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
const schemas = await advertisedMcpToolSchemas(
client,
"inspect_native_exports",
);
expect(
schemas.ajv.validate(schemas.inputSchema, {
path: "/absent/native-library.so",
detail: "summary",
}),
).toBe(true);
const refused = await client.callTool({
name: "inspect_native_exports",
arguments: { path: "/absent/native-library.so", detail: "summary" },
});
expect(parseMcpToolError(refused).error).toMatchObject({
code: "capability_unavailable",
});
const complete = await client.callTool({
name: "inspect_native_exports",
arguments: { path: "/absent/native-library.so" },
});
expect(parseMcpToolError(complete).error.code).toBe("invalid_request");
});
it.each(["inspect_native_exports", "inspect_byte_entropy"] as const)(
"refuses %s summary before file admission when retention is unavailable",
async (name) => {
const service =
name === "inspect_native_exports"
? createNativeExportsService()
: new ByteEntropyService();
const server = new EvidenceMcpServer(
{ name: "exports-preflight", version: "1" },
{},
undefined,
createToolResultDelivery(undefined),
);
if (service instanceof ByteEntropyService)
registerByteEntropyTool(
server,
service,
silentLogger,
undefined,
undefined,
undefined,
);
else registerNativeExportsTool(server, service, silentLogger);
const client = new Client({
name: "exports-preflight-client",
version: "1",
});
onTestFinished(async () => {
await client.close();
await server.close();
await service.close();
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
const schemas = await advertisedMcpToolSchemas(client, name);
expect(
schemas.ajv.validate(schemas.inputSchema, {
path: "/absent/native-library.so",
detail: "summary",
}),
).toBe(true);
const refused = await client.callTool({
name,
arguments: { path: "/absent/native-library.so", detail: "summary" },
});
expect(parseMcpToolError(refused).error).toMatchObject({
code: "capability_unavailable",
});
const complete = await client.callTool({
name,
arguments: { path: "/absent/native-library.so" },
});
expect(parseMcpToolError(complete).error.code).toBe("invalid_request");
},
);
it.each([false, true])(
"preserves a completed byte report through MCP cleanup failure and retry (retained=%s)",