mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
a1ed4232de
Retain complete authenticated application Evidence when the repeated MCP representation exceeds the default receive frame or Node string limit. Return a typed transport constraint, keep useful followup queries available, and stream full canonical bundles to atomic destinations. Verify complete Notion and Obsidian workflows on Linux and native Windows, full CLI/Evidence parity, and real large Chrome screenshot recovery. Preserve complete success contracts and caller-selected evidence paths. Refs #1065, #1053.
219 lines
6.8 KiB
TypeScript
219 lines
6.8 KiB
TypeScript
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
|
|
import {
|
|
optionalProviderUnavailable,
|
|
type OptionalProviderLoadFailure,
|
|
} from "../application/OptionalObservationProviders.js";
|
|
import { err } from "../domain/result.js";
|
|
import type { McpServer, 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, type ToolContract } from "../contracts/toolContracts.js";
|
|
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
|
import type { Evidence } from "../domain/evidence.js";
|
|
import { analyzeWebBundleInputSchema } from "../domain/webBundleAnalysis.js";
|
|
import { inspectWebPageInputSchema } from "../domain/browserObservation.js";
|
|
import type { Result } from "../domain/result.js";
|
|
import type { Logger } from "../logger.js";
|
|
import { mcpProgressReporter } from "./mcpProgress.js";
|
|
import { logToolExecution } from "./toolLogging.js";
|
|
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
|
|
import { toCallToolResult } from "./toolResult.js";
|
|
|
|
interface BrowserToolRegistration {
|
|
readonly logger: Logger;
|
|
readonly loadFailure?: OptionalProviderLoadFailure | undefined;
|
|
readonly browser: BrowserObservationPort | undefined;
|
|
readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined;
|
|
}
|
|
|
|
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: McpServer,
|
|
options: BrowserToolRegistration,
|
|
): void => {
|
|
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(
|
|
options,
|
|
listContract,
|
|
{ input, context },
|
|
(parsed, { signal }) =>
|
|
listBrowserTargets(options.browser, parsed, {
|
|
signal,
|
|
}),
|
|
),
|
|
);
|
|
server.registerTool(
|
|
inspectContract.name,
|
|
toolRegistrationOptions(inspectContract),
|
|
(input, context) =>
|
|
runBrowserTool(
|
|
options,
|
|
inspectContract,
|
|
{ input, context },
|
|
(parsed, { signal, progress }) =>
|
|
inspectWebPage(
|
|
options.browser,
|
|
inspectWebPageInputSchema.parse(parsed),
|
|
{ signal, progress },
|
|
),
|
|
),
|
|
);
|
|
server.registerTool(
|
|
analyzeContract.name,
|
|
toolRegistrationOptions(analyzeContract),
|
|
(input, context) =>
|
|
runBrowserTool(
|
|
options,
|
|
analyzeContract,
|
|
{ input, context },
|
|
(parsed, { signal, progress }) =>
|
|
analyzeWebBundle(
|
|
options.browser,
|
|
analyzeWebBundleInputSchema.parse(parsed),
|
|
{
|
|
signal,
|
|
progress,
|
|
},
|
|
),
|
|
),
|
|
);
|
|
server.registerTool(
|
|
sessionContract.name,
|
|
toolRegistrationOptions(sessionContract),
|
|
(input, context) =>
|
|
runBrowserTool(
|
|
options,
|
|
sessionContract,
|
|
{ input, context },
|
|
(parsed, { signal, progress }) =>
|
|
observeWebSession(options.browser, parsed, {
|
|
signal,
|
|
progress,
|
|
}),
|
|
),
|
|
);
|
|
server.registerTool(
|
|
webMcpContract.name,
|
|
toolRegistrationOptions(webMcpContract),
|
|
(input, context) =>
|
|
runBrowserTool(
|
|
options,
|
|
webMcpContract,
|
|
{ input, context },
|
|
(parsed, { signal, progress }) =>
|
|
discoverWebMcpTools(options.browser, parsed, {
|
|
signal,
|
|
progress,
|
|
}),
|
|
),
|
|
);
|
|
server.registerTool(
|
|
captureDiffContract.name,
|
|
toolRegistrationOptions(captureDiffContract),
|
|
(input, context) =>
|
|
runBrowserTool(
|
|
options,
|
|
captureDiffContract,
|
|
{ input, context },
|
|
(parsed) => compareWebCaptureEvidence(options.browser, parsed),
|
|
),
|
|
);
|
|
server.registerTool(
|
|
screenshotContract.name,
|
|
toolRegistrationOptions(screenshotContract),
|
|
(input, context) =>
|
|
runBrowserTool(
|
|
options,
|
|
screenshotContract,
|
|
{ input, context },
|
|
(parsed, { signal, progress }) =>
|
|
captureWebScreenshot(options.browser, parsed, {
|
|
signal,
|
|
progress,
|
|
}),
|
|
),
|
|
);
|
|
server.registerTool(
|
|
screenshotDiffContract.name,
|
|
toolRegistrationOptions(screenshotDiffContract),
|
|
(input, context) =>
|
|
runBrowserTool(
|
|
options,
|
|
screenshotDiffContract,
|
|
{ input, context },
|
|
(parsed) => compareWebScreenshotEvidence(options.browser, parsed),
|
|
),
|
|
);
|
|
};
|
|
|
|
const runBrowserTool = async <Input>(
|
|
options: BrowserToolRegistration,
|
|
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 toCallToolResult(
|
|
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 toCallToolResult(result, contract);
|
|
return evidenceResult(options, contract, result.value);
|
|
};
|
|
|
|
const evidenceResult = (
|
|
options: BrowserToolRegistration,
|
|
contract: ToolContract,
|
|
evidence: Evidence,
|
|
) => {
|
|
const recorded = options.recordEvidence?.(evidence);
|
|
return recorded !== undefined && !recorded.ok
|
|
? toCallToolResult(recorded, contract)
|
|
: toCallToolResult(
|
|
{ ok: true, value: evidence },
|
|
contract,
|
|
recorded === undefined
|
|
? undefined
|
|
: { retainedEvidenceId: evidence.evidence_id },
|
|
);
|
|
};
|