Files
reverse-engineering-anything/src/server/registerBrowserTools.ts
T
morluto 3b698645a0 fix(evidence): retain completed observations on late cancellation
Validate completed reports before cancellation projection, retain valid
Evidence and captured command diagnostics, and convert raw cleanup partials
at their application owner. Record partial Evidence consistently across MCP
handlers. Keep page inspection separate from unfinished bundle analysis.

Refs #4261
2026-10-16 06:46:25 +08:00

214 lines
6.8 KiB
TypeScript

import type { ToolResultDelivery } from "./toolResult.js";
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import {
optionalProviderUnavailable,
type OptionalProviderLoadFailure,
} from "../application/OptionalObservationProviders.js";
import { err } from "../domain/result.js";
import type { ServerContext } from "@modelcontextprotocol/server";
import type { BrowserObservationPort } from "../application/BrowserObservationPort.js";
import {
analyzeWebBundle,
captureWebScreenshot,
compareWebCaptureEvidence,
compareWebScreenshotEvidence,
discoverWebMcpTools,
inspectWebPage,
listBrowserTargets,
observeWebSession,
} from "../application/BrowserObservationService.js";
import type { ProgressReporter } from "../application/ProgressReporter.js";
import { toolContract } from "../contracts/toolContracts.js";
import type { ToolContract } from "../contracts/toolContractTypes.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { Evidence } from "../domain/evidence.js";
import type { Result } from "../domain/result.js";
import 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";
import { deliverObservationEvidence } from "./observationEvidenceDelivery.js";
interface BrowserToolRegistration {
readonly logger: Logger;
readonly loadFailure?: OptionalProviderLoadFailure | undefined;
readonly browser: BrowserObservationPort | undefined;
readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined;
readonly withAdmittedAnalysis?: WithAdmittedAnalysis;
}
interface BrowserToolContext {
readonly signal: AbortSignal;
readonly progress: ProgressReporter;
}
/** Register browser tools with execution-time provider diagnostics. */
// oxlint-disable-next-line max-lines-per-function -- direct SDK calls retain each schema-handler type correlation.
export const registerBrowserTools = (
server: EvidenceMcpServer,
options: BrowserToolRegistration,
): void => {
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");
const sessionContract = toolContract("observe_web_session");
const webMcpContract = toolContract("discover_webmcp_tools");
const captureDiffContract = toolContract("compare_web_captures");
const screenshotContract = toolContract("capture_web_screenshot");
const screenshotDiffContract = toolContract("compare_web_screenshots");
server.registerTool(
listContract.name,
toolRegistrationOptions(listContract),
(input, context) =>
runBrowserTool(
registration,
listContract,
{ input, context },
(parsed, { signal }) =>
listBrowserTargets(options.browser, parsed, {
signal,
}),
),
);
server.registerTool(
inspectContract.name,
toolRegistrationOptions(inspectContract),
(input, context) =>
runBrowserTool(
registration,
inspectContract,
{ input, context },
(parsed, { signal, progress }) =>
inspectWebPage(options.browser, parsed, { signal, progress }),
),
);
server.registerTool(
analyzeContract.name,
toolRegistrationOptions(analyzeContract),
(input, context) =>
runBrowserTool(
registration,
analyzeContract,
{ input, context },
(parsed, { signal, progress }) =>
analyzeWebBundle(options.browser, parsed, {
signal,
progress,
}),
),
);
server.registerTool(
sessionContract.name,
toolRegistrationOptions(sessionContract),
(input, context) =>
runBrowserTool(
registration,
sessionContract,
{ input, context },
(parsed, { signal, progress }) =>
observeWebSession(options.browser, parsed, {
signal,
progress,
}),
),
);
server.registerTool(
webMcpContract.name,
toolRegistrationOptions(webMcpContract),
(input, context) =>
runBrowserTool(
registration,
webMcpContract,
{ input, context },
(parsed, { signal, progress }) =>
discoverWebMcpTools(options.browser, parsed, {
signal,
progress,
}),
),
);
server.registerTool(
captureDiffContract.name,
toolRegistrationOptions(captureDiffContract),
(input, context) =>
runBrowserTool(
registration,
captureDiffContract,
{ input, context },
(parsed) => compareWebCaptureEvidence(options.browser, parsed),
),
);
server.registerTool(
screenshotContract.name,
toolRegistrationOptions(screenshotContract),
(input, context) =>
runBrowserTool(
registration,
screenshotContract,
{ input, context },
(parsed, { signal, progress }) =>
captureWebScreenshot(options.browser, parsed, {
signal,
progress,
}),
),
);
server.registerTool(
screenshotDiffContract.name,
toolRegistrationOptions(screenshotDiffContract),
(input, context) =>
runBrowserTool(
registration,
screenshotDiffContract,
{ input, context },
(parsed) => compareWebScreenshotEvidence(options.browser, parsed),
),
);
};
const runBrowserTool = async <Input>(
options: BrowserToolRegistration & {
readonly delivery: ToolResultDelivery;
readonly server: EvidenceMcpServer;
},
contract: ToolContract,
request: { readonly input: Input; readonly context: ServerContext },
execute: (
input: Input,
context: BrowserToolContext,
) => Promise<Result<Evidence, AnalysisError>>,
) => {
const { input, context } = request;
if (options.loadFailure !== undefined)
return options.delivery.toCallToolResult(
err(optionalProviderUnavailable(options.loadFailure, contract.name)),
contract,
);
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),
}),
);
return deliverObservationEvidence(
options.server,
contract,
result,
options.recordEvidence,
);
},
);
};