mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 14:26:07 +00:00
fix(mcp): isolate optional observation adapter failures (#838)
This commit is contained in:
1 parent
78f4848d70
commit
be3cfbffdf
31 files changed
+741
-102
No files matched your search
@@ -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}`,
|
||||
);
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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,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,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));
|
||||
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
});
|
||||
};
|
||||
Reference in new issue
Block a user