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

+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,
}),
});
};