fix(mcp): isolate optional observation adapter failures (#838)

This commit is contained in:
N0zoM1z0 authored and GitHub committed 2026-10-07 11:45:17 +08:00
1 parent 78f4848d70
commit be3cfbffdf
31 files changed
+741 -102

No files matched your search

+28
View File
@@ -1,4 +1,8 @@
import { z } from "zod";
import type {
OptionalProviderLoadFailures,
OptionalProviderLoadFailure,
} from "./OptionalObservationProviders.js";
import {
providerCapability,
@@ -28,6 +32,7 @@ type ToolAvailabilityReason = "available" | ToolUnavailabilityReason;
type ProviderDescriptor = ProviderCapability;
export type AvailabilityPolicy = {
readonly processCaptureEnabled: boolean;
readonly optionalProviderLoadFailures?: OptionalProviderLoadFailures;
readonly firmwareInspectionEnabled?: boolean;
readonly firmwareExtractionEnabled?: boolean;
readonly androidAnalysisEnabled?: boolean;
@@ -186,6 +191,12 @@ const availabilityFor = (context: AvailabilityContext): Availability => {
return navigationContextAvailability(context.descriptors);
const javascriptApplication = javascriptApplicationAvailability(context);
if (javascriptApplication !== null) return javascriptApplication;
const failure = optionalProviderFailureFor(context);
if (failure !== undefined)
return {
reason: "provider_unavailable",
remediation: `Adapter ${failure.providerId} could not load: ${failure.reason}. Repair the adapter installation and restart REA; other capabilities remain available.`,
};
const workflowAvailability = workflowAvailabilityFor(context);
if (workflowAvailability !== null) return workflowAvailability;
const targetDecision = targetAvailability(context);
@@ -193,6 +204,23 @@ const availabilityFor = (context: AvailabilityContext): Availability => {
return providerAvailability(context);
};
const optionalProviderFailureFor = ({
name,
kind,
policy,
}: AvailabilityContext): OptionalProviderLoadFailure | undefined => {
const failures = policy.optionalProviderLoadFailures;
if (name === "capture_browser_scenario")
return failures?.browserScenarioCapture;
if (kind === "browser-provider") return failures?.browserObservation;
if (name === "capture_electron_scenario")
return failures?.electronActiveObservation;
if (kind === "electron-provider") return failures?.electronObservation;
if (kind === "runtime-provider")
return failures?.javascriptRuntimeObservation;
return undefined;
};
const javascriptApplicationAvailability = ({
name,
}: AvailabilityContext): Availability | null => {
@@ -0,0 +1,43 @@
import type { BrowserObservationPort } from "./BrowserObservationPort.js";
import type { BrowserScenarioCapturePort } from "./BrowserScenarioCapturePort.js";
import type { ElectronObservationPort } from "./ElectronObservationPort.js";
import type { ElectronActiveObservationPort } from "./ElectronActiveObservationPort.js";
import type { JavaScriptRuntimeObservationPort } from "./JavaScriptRuntimeObservationPort.js";
import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js";
/** Existing observation ports loaded independently at MCP startup. */
export interface OptionalObservationProviders {
readonly browserObservation?: BrowserObservationPort;
readonly browserScenarioCapture?: BrowserScenarioCapturePort;
readonly electronObservation?: ElectronObservationPort;
readonly electronActiveObservation?: ElectronActiveObservationPort;
readonly javascriptRuntimeObservation?: JavaScriptRuntimeObservationPort;
}
/** A module failed before its port could be constructed; no engine was opened. */
export interface OptionalProviderLoadFailure {
readonly providerId: string;
readonly reason: string;
}
export type OptionalProviderLoadFailures = Readonly<
Partial<
Record<keyof OptionalObservationProviders, OptionalProviderLoadFailure>
>
>;
/** Successful ports and failed peers from one independent loading attempt. */
export interface OptionalProviderLoadResult extends OptionalObservationProviders {
readonly optionalProviderLoadFailures?: OptionalProviderLoadFailures;
}
/** Preserve the module's failure reason through the existing tool error algebra. */
export const optionalProviderUnavailable = (
failure: OptionalProviderLoadFailure,
operation: string,
): AnalysisCapabilityUnavailableError =>
new AnalysisCapabilityUnavailableError(
failure.providerId,
operation,
`Adapter could not load: ${failure.reason}`,
);
+2 -5
View File
@@ -64,11 +64,8 @@ import { captureCdpScreenshot } from "./CdpScreenshot.js";
import { comparePngScreenshots } from "./PngVisualDiff.js";
/** Public identity committed by passive browser observations. */
export const CDP_BROWSER_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
id: "rea-cdp-browser",
name: "REA Chrome DevTools Protocol observation provider",
version: "2",
});
export { CDP_BROWSER_PROVIDER_IDENTITY } from "./providerIdentities.js";
import { CDP_BROWSER_PROVIDER_IDENTITY } from "./providerIdentities.js";
const IDENTITY = CDP_BROWSER_PROVIDER_IDENTITY;
const CLEANUP_DOMAINS = ["Network", "Debugger", "Runtime", "Page"] as const;
+2 -5
View File
@@ -30,11 +30,8 @@ import { inspectCdpElectronPage } from "./CdpElectronInspection.js";
import { authorizedElectronFile } from "./ElectronFileScope.js";
/** Public identity committed by passive Electron observations. */
export const CDP_ELECTRON_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
id: "rea-cdp-electron",
name: "REA Electron file-page CDP observation provider",
version: "1",
});
export { CDP_ELECTRON_PROVIDER_IDENTITY } from "./providerIdentities.js";
import { CDP_ELECTRON_PROVIDER_IDENTITY } from "./providerIdentities.js";
const IDENTITY = CDP_ELECTRON_PROVIDER_IDENTITY;
/** Passive Electron provider for local file pages exposed by loopback CDP. */
@@ -41,12 +41,8 @@ const lazyPlaywrightFactory: BrowserScenarioSessionFactory = {
},
};
export const PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY: ProviderIdentity =
Object.freeze({
id: "rea-playwright-browser-scenario",
name: "REA Playwright browser scenario capture provider",
version: "1",
});
export { PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY } from "./providerIdentities.js";
import { PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY } from "./providerIdentities.js";
type SnapshotKind = BrowserScenario["capture"]["after_each_step"][number];
const SNAPSHOT_KINDS = [
@@ -42,12 +42,8 @@ const OPERATION = "capture_electron_scenario" as const;
const STARTUP_TIMEOUT_MS = 60_000;
/** Public identity for provider-owned Electron runtime experiments. */
export const PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY: ProviderIdentity =
Object.freeze({
id: "rea-playwright-electron-active",
name: "REA Playwright active Electron observation provider",
version: "1",
});
export { PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY } from "./providerIdentities.js";
import { PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY } from "./providerIdentities.js";
const hookPath = fileURLToPath(
new URL("../../scripts/electron-active-hook.cjs", import.meta.url),
+2 -5
View File
@@ -39,11 +39,8 @@ import {
} from "./V8InspectorEndpoint.js";
/** Public identity committed by passive V8 Inspector observations. */
export const V8_INSPECTOR_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
id: "rea-v8-inspector",
name: "REA passive Node/Electron V8 Inspector provider",
version: "1",
});
export { V8_INSPECTOR_PROVIDER_IDENTITY } from "./providerIdentities.js";
import { V8_INSPECTOR_PROVIDER_IDENTITY } from "./providerIdentities.js";
export interface ScriptDraft {
readonly rawUrl: string;
+38
View File
@@ -0,0 +1,38 @@
import type { ProviderIdentity } from "../application/AnalysisProvider.js";
/** Identity available without loading the CdpBrowserProvider implementation. */
export const CDP_BROWSER_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
id: "rea-cdp-browser",
name: "REA Chrome DevTools Protocol observation provider",
version: "2",
});
/** Identity available without loading the PlaywrightBrowserScenarioProvider implementation. */
export const PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY: ProviderIdentity =
Object.freeze({
id: "rea-playwright-browser-scenario",
name: "REA Playwright browser scenario capture provider",
version: "1",
});
/** Identity available without loading the CdpElectronProvider implementation. */
export const CDP_ELECTRON_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
id: "rea-cdp-electron",
name: "REA Electron file-page CDP observation provider",
version: "1",
});
/** Identity available without loading the PlaywrightElectronActiveProvider implementation. */
export const PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY: ProviderIdentity =
Object.freeze({
id: "rea-playwright-electron-active",
name: "REA Playwright active Electron observation provider",
version: "1",
});
/** Identity available without loading the V8InspectorProvider implementation. */
export const V8_INSPECTOR_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
id: "rea-v8-inspector",
name: "REA passive Node/Electron V8 Inspector provider",
version: "1",
});
+7 -12
View File
@@ -6,8 +6,6 @@ import {
} from "../application/ElectronObservationService.js";
import { captureElectronScenario } from "../application/ElectronActiveObservationService.js";
import { reconcileJavaScriptRuntimeEvidence } from "../application/JavaScriptRuntimeReconciliationService.js";
import { CdpElectronProvider } from "../browser/CdpElectronProvider.js";
import { PlaywrightElectronActiveProvider } from "../browser/PlaywrightElectronActiveProvider.js";
import { logCliCommand } from "../cliLogging.js";
import {
inspectElectronPageInputSchema,
@@ -61,9 +59,10 @@ const registerElectronActiveCommand = (
input.value,
);
if (!parsed.success) return inputError("capture_electron_scenario");
const context = await electronContext();
const { createElectronScenarioProvider } =
await import("../composition/electronScenario.js");
const result = await captureElectronScenario(
context.activeProvider,
createElectronScenarioProvider(),
parsed.data,
);
return result.ok ? result.value : cliError(result.error);
@@ -181,16 +180,12 @@ const registerJavaScriptApplicationCommand = (
});
};
const electronContext = async () => {
return {
ok: true as const,
provider: new CdpElectronProvider(),
activeProvider: new PlaywrightElectronActiveProvider(),
};
const electronObservationContext = async () => {
const { createElectronObservationProvider } =
await import("../composition/electronObservation.js");
return { provider: createElectronObservationProvider() };
};
const electronObservationContext = electronContext;
const inputError = (operation: string): JsonValue =>
cliError(new AnalysisInputError(operation));
+4 -2
View File
@@ -1,9 +1,11 @@
import { CdpBrowserProvider } from "./browser/CdpBrowserProvider.js";
import { createBrowserObservationProvider } from "./composition/browserObservation.js";
import { projectAnalysisError } from "./domain/analysisErrorProjection.js";
import type { JsonValue } from "./domain/jsonValue.js";
/** Build the passive browser provider; scope is supplied on each request. */
export const browserContext = () => ({ provider: new CdpBrowserProvider() });
export const browserContext = () => ({
provider: createBrowserObservationProvider(),
});
/** Project a browser operation failure consistently across CLI command groups. */
export const browserCliError = (
+2 -2
View File
@@ -2,7 +2,7 @@ import { Cli } from "incur";
import { z } from "zod";
import { captureBrowserScenario } from "./application/BrowserScenarioCaptureService.js";
import { PlaywrightBrowserScenarioProvider } from "./browser/PlaywrightBrowserScenarioProvider.js";
import { createBrowserScenarioProvider } from "./composition/browserScenario.js";
import { CLI_COMMANDS } from "./cliCommandNames.js";
import { parseCliJsonInput } from "./cliJsonInput.js";
import { logCliCommand } from "./cliLogging.js";
@@ -35,7 +35,7 @@ export const registerBrowserScenarioCommands = (
if (!scenario.success)
return cliError(new AnalysisInputError(OPERATION));
const result = await captureBrowserScenario(
new PlaywrightBrowserScenarioProvider(),
createBrowserScenarioProvider(),
scenario.data,
);
return result.ok ? result.value : cliError(result.error);
+4 -2
View File
@@ -4,7 +4,7 @@ import {
listJavaScriptRuntimeTargets,
observeJavaScriptRuntime,
} from "./application/JavaScriptRuntimeObservationService.js";
import { V8InspectorProvider } from "./browser/V8InspectorProvider.js";
import { createJavaScriptRuntimeObservationProvider } from "./composition/javascriptRuntimeObservation.js";
import { CLI_COMMANDS } from "./cliCommandNames.js";
import { logCliCommand } from "./cliLogging.js";
import {
@@ -89,7 +89,9 @@ export const registerJavaScriptRuntimeObservationCommands = (
});
};
const runtimeContext = () => ({ provider: new V8InspectorProvider() });
const runtimeContext = () => ({
provider: createJavaScriptRuntimeObservationProvider(),
});
const inputError = (operation: string): JsonValue =>
cliError(new AnalysisInputError(operation));
+6
View File
@@ -0,0 +1,6 @@
import type { BrowserObservationPort } from "../application/BrowserObservationPort.js";
import { CdpBrowserProvider } from "../browser/CdpBrowserProvider.js";
/** Construct a fresh provider without opening a target or acquiring an engine. */
export const createBrowserObservationProvider = (): BrowserObservationPort =>
new CdpBrowserProvider();
+6
View File
@@ -0,0 +1,6 @@
import type { BrowserScenarioCapturePort } from "../application/BrowserScenarioCapturePort.js";
import { PlaywrightBrowserScenarioProvider } from "../browser/PlaywrightBrowserScenarioProvider.js";
/** Construct a fresh provider without opening a target or acquiring an engine. */
export const createBrowserScenarioProvider = (): BrowserScenarioCapturePort =>
new PlaywrightBrowserScenarioProvider();
+6
View File
@@ -0,0 +1,6 @@
import type { ElectronObservationPort } from "../application/ElectronObservationPort.js";
import { CdpElectronProvider } from "../browser/CdpElectronProvider.js";
/** Construct a fresh provider without opening a target or acquiring an engine. */
export const createElectronObservationProvider = (): ElectronObservationPort =>
new CdpElectronProvider();
+6
View File
@@ -0,0 +1,6 @@
import type { ElectronActiveObservationPort } from "../application/ElectronActiveObservationPort.js";
import { PlaywrightElectronActiveProvider } from "../browser/PlaywrightElectronActiveProvider.js";
/** Construct a fresh provider without opening a target or acquiring an engine. */
export const createElectronScenarioProvider =
(): ElectronActiveObservationPort => new PlaywrightElectronActiveProvider();
@@ -0,0 +1,6 @@
import type { JavaScriptRuntimeObservationPort } from "../application/JavaScriptRuntimeObservationPort.js";
import { V8InspectorProvider } from "../browser/V8InspectorProvider.js";
/** Construct a fresh provider without opening a target or acquiring an engine. */
export const createJavaScriptRuntimeObservationProvider =
(): JavaScriptRuntimeObservationPort => new V8InspectorProvider();
@@ -0,0 +1,97 @@
import type {
OptionalObservationProviders,
OptionalProviderLoadFailure,
OptionalProviderLoadResult,
} from "../application/OptionalObservationProviders.js";
import {
CDP_BROWSER_PROVIDER_IDENTITY,
PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY,
CDP_ELECTRON_PROVIDER_IDENTITY,
PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY,
V8_INSPECTOR_PROVIDER_IDENTITY,
} from "../browser/providerIdentities.js";
import { err, ok, type Result } from "../domain/result.js";
/** Precisely typed startup factories for the five existing observation ports. */
export type OptionalObservationFactories = {
readonly [Port in keyof OptionalObservationProviders]-?: () => Promise<
NonNullable<OptionalObservationProviders[Port]>
>;
};
const productionFactories: OptionalObservationFactories = {
browserObservation: async () =>
(
await import("./browserObservation.js")
).createBrowserObservationProvider(),
browserScenarioCapture: async () =>
(await import("./browserScenario.js")).createBrowserScenarioProvider(),
electronObservation: async () =>
(
await import("./electronObservation.js")
).createElectronObservationProvider(),
electronActiveObservation: async () =>
(await import("./electronScenario.js")).createElectronScenarioProvider(),
javascriptRuntimeObservation: async () =>
(
await import("./javascriptRuntimeObservation.js")
).createJavaScriptRuntimeObservationProvider(),
};
const loadPort = async <Port>(
factory: () => Promise<Port>,
providerId: string,
): Promise<Result<Port, OptionalProviderLoadFailure>> => {
try {
return ok(await factory());
} catch (cause: unknown) {
return err({
providerId,
reason: cause instanceof Error ? cause.message : String(cause),
});
}
};
/** Load each adapter independently, retaining healthy peers and exact failures. */
export const loadOptionalObservationProviders = async (
factories: OptionalObservationFactories = productionFactories,
): Promise<OptionalProviderLoadResult> => {
const [browser, scenario, electron, electronActive, runtime] =
await Promise.all([
loadPort(factories.browserObservation, CDP_BROWSER_PROVIDER_IDENTITY.id),
loadPort(
factories.browserScenarioCapture,
PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY.id,
),
loadPort(
factories.electronObservation,
CDP_ELECTRON_PROVIDER_IDENTITY.id,
),
loadPort(
factories.electronActiveObservation,
PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY.id,
),
loadPort(
factories.javascriptRuntimeObservation,
V8_INSPECTOR_PROVIDER_IDENTITY.id,
),
]);
return {
...(browser.ok ? { browserObservation: browser.value } : {}),
...(scenario.ok ? { browserScenarioCapture: scenario.value } : {}),
...(electron.ok ? { electronObservation: electron.value } : {}),
...(electronActive.ok
? { electronActiveObservation: electronActive.value }
: {}),
...(runtime.ok ? { javascriptRuntimeObservation: runtime.value } : {}),
optionalProviderLoadFailures: {
...(browser.ok ? {} : { browserObservation: browser.error }),
...(scenario.ok ? {} : { browserScenarioCapture: scenario.error }),
...(electron.ok ? {} : { electronObservation: electron.error }),
...(electronActive.ok
? {}
: { electronActiveObservation: electronActive.error }),
...(runtime.ok ? {} : { javascriptRuntimeObservation: runtime.error }),
},
};
};
+2 -23
View File
@@ -14,6 +14,7 @@ import { SERVER_START_FAILED } from "./main/messages.js";
import { createRuntimeState } from "./main/state.js";
import { openInitialTarget } from "./main/startup.js";
import { startMcpTransport } from "./main/transport.js";
import { loadOptionalObservationProviders } from "./composition/optionalObservationProviders.js";
import { registerConfigReload } from "./main/reload.js";
import { createShutdown } from "./main/shutdown.js";
@@ -70,29 +71,7 @@ export const run = async (
logger,
serverLogger,
loadOptionalProviders:
dependencies.loadOptionalProviders ??
(async () => {
const [
{ CdpBrowserProvider },
{ PlaywrightBrowserScenarioProvider },
{ CdpElectronProvider },
{ PlaywrightElectronActiveProvider },
{ V8InspectorProvider },
] = await Promise.all([
import("./browser/CdpBrowserProvider.js"),
import("./browser/PlaywrightBrowserScenarioProvider.js"),
import("./browser/CdpElectronProvider.js"),
import("./browser/PlaywrightElectronActiveProvider.js"),
import("./browser/V8InspectorProvider.js"),
]);
return {
browserObservation: new CdpBrowserProvider(),
browserScenarioCapture: new PlaywrightBrowserScenarioProvider(),
electronObservation: new CdpElectronProvider(),
electronActiveObservation: new PlaywrightElectronActiveProvider(),
javascriptRuntimeObservation: new V8InspectorProvider(),
};
}),
dependencies.loadOptionalProviders ?? loadOptionalObservationProviders,
});
if (!transport.ok) return 1;
const unregisterReload = registerConfigReload({
+11 -12
View File
@@ -2,25 +2,16 @@ import type { StdioServerHandle } from "@modelcontextprotocol/server/stdio";
import type { BinarySession } from "../application/BinarySession.js";
import type { Logger } from "../logger.js";
import {
createServer,
type CreateServerOptions,
} from "../server/createServer.js";
import { createServer } from "../server/createServer.js";
import type { RuntimeDependencies } from "./types.js";
import type { OptionalProviderLoadResult } from "../application/OptionalObservationProviders.js";
import {
MCP_CONNECTION_LOST,
MCP_CONNECTION_START_FAILED,
} from "./messages.js";
/** Optional adapters whose absence must not prevent the core MCP server. */
export type OptionalProviders = Pick<
CreateServerOptions,
| "browserObservation"
| "browserScenarioCapture"
| "electronObservation"
| "electronActiveObservation"
| "javascriptRuntimeObservation"
>;
export type OptionalProviders = OptionalProviderLoadResult;
interface ServerContext {
readonly logger: Logger;
@@ -51,6 +42,14 @@ export const startMcpTransport = async (
"Optional MCP providers could not load; affected tools remain unavailable",
);
}
for (const failure of Object.values(
optionalProviders.optionalProviderLoadFailures ?? {},
)) {
serverLogger.warn(
{ providerId: failure.providerId, error: failure.reason },
"Optional MCP adapter could not load; its peers remain available",
);
}
let handle: StdioServerHandle;
try {
handle = dependencies.serve(
+11
View File
@@ -8,6 +8,7 @@ import type { BrowserScenarioCapturePort } from "../application/BrowserScenarioC
import type { ElectronActiveObservationPort } from "../application/ElectronActiveObservationPort.js";
import type { ElectronObservationPort } from "../application/ElectronObservationPort.js";
import type { JavaScriptRuntimeObservationPort } from "../application/JavaScriptRuntimeObservationPort.js";
import type { OptionalProviderLoadFailures } from "../application/OptionalObservationProviders.js";
import { PRODUCT_IDENTITY } from "../identity.js";
import { silentLogger, type Logger } from "../logger.js";
import { registerApplicationTools } from "./registerApplicationTools.js";
@@ -51,6 +52,7 @@ export interface CreateServerOptions {
readonly electronActiveObservation?: ElectronActiveObservationPort;
readonly javascriptRuntimeObservation?: JavaScriptRuntimeObservationPort;
readonly availabilityPolicy?: () => SessionAvailability;
readonly optionalProviderLoadFailures?: OptionalProviderLoadFailures;
}
const installSessionToolAvailability = (
@@ -60,6 +62,7 @@ const installSessionToolAvailability = (
) => {
if (session === undefined) return undefined;
const policy = sessionAvailabilityPolicy(options.availabilityPolicy, {
optionalProviderLoadFailures: options.optionalProviderLoadFailures,
optionalFeatures: {
firmwareInspectionEnabled:
options.firmwareAnalysis !== undefined ||
@@ -259,19 +262,27 @@ const registerObservationTools = ({
registerBrowserTools(server, {
...common,
browser: options.browserObservation,
loadFailure: options.optionalProviderLoadFailures?.browserObservation,
});
registerBrowserScenarioTool(server, {
...common,
provider: options.browserScenarioCapture,
loadFailure: options.optionalProviderLoadFailures?.browserScenarioCapture,
});
registerElectronTools(server, {
...common,
electron: options.electronObservation,
electronActive: options.electronActiveObservation,
observationLoadFailure:
options.optionalProviderLoadFailures?.electronObservation,
activeLoadFailure:
options.optionalProviderLoadFailures?.electronActiveObservation,
});
registerJavaScriptRuntimeObservationTools(server, {
...common,
runtime: options.javascriptRuntimeObservation,
loadFailure:
options.optionalProviderLoadFailures?.javascriptRuntimeObservation,
});
registerApplicationTools(server, {
evidenceById,
+11
View File
@@ -1,3 +1,8 @@
import {
optionalProviderUnavailable,
type OptionalProviderLoadFailure,
} from "../application/OptionalObservationProviders.js";
import { err } from "../domain/result.js";
import type { McpServer } from "@modelcontextprotocol/server";
import type { BinarySessionPort } from "../application/BinarySession.js";
@@ -12,6 +17,7 @@ import { toCallToolResult } from "./toolResult.js";
interface BrowserScenarioToolRegistration {
readonly logger: Logger;
readonly loadFailure?: OptionalProviderLoadFailure | undefined;
readonly provider: BrowserScenarioCapturePort | undefined;
readonly recordEvidence: BinarySessionPort["recordEvidence"] | undefined;
}
@@ -27,6 +33,11 @@ export const registerBrowserScenarioTool = (
toolRegistrationOptions(contract),
async (input, context) => {
const scenario = browserScenarioSchema.parse(input);
if (options.loadFailure !== undefined)
return toCallToolResult(
err(optionalProviderUnavailable(options.loadFailure, contract.name)),
contract,
);
const result = await logToolExecution(options.logger, contract.name, () =>
captureBrowserScenario(options.provider, scenario, {
signal: context.mcpReq.signal,
+11
View File
@@ -1,3 +1,8 @@
import {
optionalProviderUnavailable,
type OptionalProviderLoadFailure,
} from "../application/OptionalObservationProviders.js";
import { err } from "../domain/result.js";
import type { McpServer, ServerContext } from "@modelcontextprotocol/server";
import type { BinarySessionPort } from "../application/BinarySession.js";
@@ -28,6 +33,7 @@ import { toCallToolResult } from "./toolResult.js";
interface BrowserToolRegistration {
readonly logger: Logger;
readonly loadFailure?: OptionalProviderLoadFailure | undefined;
readonly browser: BrowserObservationPort | undefined;
readonly recordEvidence: BinarySessionPort["recordEvidence"] | undefined;
}
@@ -182,6 +188,11 @@ const runBrowserTool = async <Input>(
) => 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,
+19
View File
@@ -1,3 +1,8 @@
import {
optionalProviderUnavailable,
type OptionalProviderLoadFailure,
} from "../application/OptionalObservationProviders.js";
import { err } from "../domain/result.js";
import type { McpServer, ServerContext } from "@modelcontextprotocol/server";
import type { BinarySessionPort } from "../application/BinarySession.js";
@@ -25,6 +30,8 @@ import { toCallToolResult } from "./toolResult.js";
interface ElectronToolRegistration {
readonly logger: Logger;
readonly observationLoadFailure?: OptionalProviderLoadFailure | undefined;
readonly activeLoadFailure?: OptionalProviderLoadFailure | undefined;
readonly electron: ElectronObservationPort | undefined;
readonly electronActive: ElectronActiveObservationPort | undefined;
readonly recordEvidence: BinarySessionPort["recordEvidence"] | undefined;
@@ -134,6 +141,18 @@ const runElectronTool = async <Input>(
) => Promise<Result<Evidence, AnalysisError>>,
) => {
const { input, context } = request;
const failure =
contract.name === "capture_electron_scenario"
? options.activeLoadFailure
: contract.name === "list_electron_targets" ||
contract.name === "inspect_electron_page"
? options.observationLoadFailure
: undefined;
if (failure !== undefined)
return toCallToolResult(
err(optionalProviderUnavailable(failure, contract.name)),
contract,
);
const result = await logToolExecution(options.logger, contract.name, () =>
execute(input, {
signal: context.mcpReq.signal,
@@ -1,3 +1,8 @@
import {
optionalProviderUnavailable,
type OptionalProviderLoadFailure,
} from "../application/OptionalObservationProviders.js";
import { err } from "../domain/result.js";
import type { McpServer, ServerContext } from "@modelcontextprotocol/server";
import type { BinarySessionPort } from "../application/BinarySession.js";
@@ -19,6 +24,7 @@ import { toCallToolResult } from "./toolResult.js";
interface RuntimeToolRegistration {
readonly logger: Logger;
readonly loadFailure?: OptionalProviderLoadFailure | undefined;
readonly runtime: JavaScriptRuntimeObservationPort | undefined;
readonly recordEvidence: BinarySessionPort["recordEvidence"] | undefined;
}
@@ -68,6 +74,11 @@ const runRuntimeTool = async <Input>(
) => 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, context.mcpReq.signal),
);
+35 -21
View File
@@ -1,9 +1,13 @@
import type { AvailabilityPolicy } from "../application/CapabilityInventory.js";
import type { OptionalProviderLoadFailures } from "../application/OptionalObservationProviders.js";
import { platform } from "node:process";
export type SessionAvailability = AvailabilityPolicy;
export interface SessionAvailabilityDefaults {
readonly optionalProviderLoadFailures?:
| OptionalProviderLoadFailures
| undefined;
readonly optionalFeatures?: Pick<
SessionAvailability,
| "browserObservationEnabled"
@@ -21,24 +25,34 @@ export interface SessionAvailabilityDefaults {
export const sessionAvailabilityPolicy = (
configured: (() => SessionAvailability) | undefined,
defaults: SessionAvailabilityDefaults,
): (() => SessionAvailability) =>
configured ??
(() => ({
processCaptureEnabled: platform !== "win32",
firmwareInspectionEnabled:
defaults.optionalFeatures?.firmwareInspectionEnabled ?? false,
firmwareExtractionEnabled:
defaults.optionalFeatures?.firmwareExtractionEnabled ?? false,
androidAnalysisEnabled:
defaults.optionalFeatures?.androidAnalysisEnabled ?? false,
browserObservationEnabled:
defaults.optionalFeatures?.browserObservationEnabled ?? false,
browserScenarioEnabled:
defaults.optionalFeatures?.browserScenarioEnabled ?? false,
electronObservationEnabled:
defaults.optionalFeatures?.electronObservationEnabled ?? false,
electronAutomationEnabled:
defaults.optionalFeatures?.electronAutomationEnabled ?? false,
v8InspectorObservationEnabled:
defaults.optionalFeatures?.v8InspectorObservationEnabled ?? false,
}));
): (() => SessionAvailability) => {
const policy =
configured ??
(() => ({
processCaptureEnabled: platform !== "win32",
firmwareInspectionEnabled:
defaults.optionalFeatures?.firmwareInspectionEnabled ?? false,
firmwareExtractionEnabled:
defaults.optionalFeatures?.firmwareExtractionEnabled ?? false,
androidAnalysisEnabled:
defaults.optionalFeatures?.androidAnalysisEnabled ?? false,
browserObservationEnabled:
defaults.optionalFeatures?.browserObservationEnabled ?? false,
browserScenarioEnabled:
defaults.optionalFeatures?.browserScenarioEnabled ?? false,
electronObservationEnabled:
defaults.optionalFeatures?.electronObservationEnabled ?? false,
electronAutomationEnabled:
defaults.optionalFeatures?.electronAutomationEnabled ?? false,
v8InspectorObservationEnabled:
defaults.optionalFeatures?.v8InspectorObservationEnabled ?? false,
}));
return () => ({
...policy(),
...(defaults.optionalProviderLoadFailures === undefined
? {}
: {
optionalProviderLoadFailures: defaults.optionalProviderLoadFailures,
}),
});
};