refactor: declare auxiliary capabilities from source (#853)

This commit is contained in:
N0zoM1z0 2026-10-07 12:32:34 +08:00 committed by GitHub
parent c9f49db907
commit 4a6c17ce83
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 499 additions and 240 deletions

View File

@ -145,3 +145,26 @@ reads, Evidence identities and deterministic bundle transfer keep their existing
behavior. Snapshots still require an active target and concrete analysis profile;
extracting investigation ownership does not make them a target-free record store.
#721 remains responsible for any future lifetime-policy change.
## Auxiliary source declaration pilot
Artifact, macOS native and managed static metadata now belongs to each adapter's
`*ProviderMetadata.ts` module. The implementation and
`src/composition/auxiliaryAnalysisProviders.ts` reuse the same identity and exact
capability descriptors. The declarations reference existing AnalysisProvider
loaders; they cover these three disjoint auxiliary implementations only. Deep
candidate selection and the five typed observation ports remain separate.
The binary runtime reads these source declarations while auxiliary construction
stays lazy. The separate managed-only entrypoint retains direct construction for
its execution-free workflow. The MCP catalog generator reads declarations without
constructing implementations, using its canonical macOS platform; actual runtime
host restrictions remain explicit.
Discovery and binary operation-kind routing now consume canonical source contracts
rather than their generated JSON artifact. No declaration imports its generated
output. Generated catalog facts and public schemas stay unchanged.
All observation registrars resolve contracts by name. An isolated compiled-process
test reverses presentation arrays and performs actual MCP SDK calls to verify
advertised schemas and handler meaning. Reordering metadata must not select a
different operation, schema, provider or deep-provider priority.

View File

@ -7,9 +7,7 @@ import { McpServer } from "@modelcontextprotocol/server";
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../dist/contracts/managedWorkflowToolContracts.js";
import { ArtifactProvider } from "../dist/artifacts/ArtifactProvider.js";
import { ManagedStaticProvider } from "../dist/dotnet/ManagedStaticProvider.js";
import { NativeMacOSProvider } from "../dist/native/NativeMacOSProvider.js";
import { auxiliaryAnalysisProviderDeclarations } from "../dist/composition/auxiliaryAnalysisProviders.js";
import { toolRegistrationOptions } from "../dist/server/toolRegistrationOptions.js";
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
@ -88,14 +86,9 @@ async function sdkToolCatalog() {
await Promise.allSettled([client.close(), server.close()]);
}
}
const auxiliaryProviders = [
new ArtifactProvider(CATALOG_PLATFORM),
new NativeMacOSProvider(undefined, CATALOG_PLATFORM),
new ManagedStaticProvider(),
].map((provider) => ({
identity: provider.identity(),
capabilities: provider.capabilities(),
}));
const auxiliaryProviders = auxiliaryAnalysisProviderDeclarations(
CATALOG_PLATFORM,
).map(({ identity, capabilities }) => ({ identity, capabilities }));
const canonicalizeJson = (value) => {
if (Array.isArray(value)) return value.map(canonicalizeJson);

View File

@ -59,7 +59,7 @@ const SOURCE_PATHS = {
hopperProvider: "dist/hopper/HopperProvider.js",
ghidraProvider: "dist/ghidra/GhidraProvider.js",
idaProvider: "dist/ida/IdaProviderCapabilities.js",
nativeProvider: "dist/native/NativeMacOSProvider.js",
nativeProvider: "dist/native/NativeMacOSProviderMetadata.js",
artifactProviders: "dist/application/InvestigationProviders.js",
browserProvider: "dist/browser/CdpBrowserProvider.js",
browserScenarioProvider: "dist/browser/PlaywrightBrowserScenarioProvider.js",

View File

@ -18,7 +18,8 @@ import type {
ProviderIdentity,
} from "./AnalysisProvider.js";
import type { AnalysisOperation } from "./AnalysisProvider.js";
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
import { OFFICIAL_TOOL_CONTRACTS } from "../contracts/officialToolContracts.js";
import { ENHANCED_TOOL_CONTRACTS } from "../contracts/enhancedToolContracts.js";
import type { BinarySessionPort } from "./BinarySessionPort.js";
import {
SessionProviderRouter,
@ -39,14 +40,10 @@ import {
import { closeAnalysisClient } from "./AnalysisClientCleanup.js";
export type { BinarySessionPort } from "./BinarySessionPort.js";
const OFFICIAL_OPERATIONS: ReadonlySet<string> = new Set(
GENERATED_MCP_TOOL_CATALOG.filter(
({ kind }) => kind === "official-proxy",
).map(({ name }) => name),
OFFICIAL_TOOL_CONTRACTS.map(({ name }) => name),
);
const ENHANCED_OPERATIONS: ReadonlySet<string> = new Set(
GENERATED_MCP_TOOL_CATALOG.filter(({ kind }) => kind === "enhanced").map(
({ name }) => name,
),
ENHANCED_TOOL_CONTRACTS.map(({ name }) => name),
);
/**

View File

@ -14,7 +14,7 @@ import {
} from "../contracts/toolOutputSchemaPrimitives.js";
import type { ToolKind } from "../contracts/toolContractTypes.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
import { TOOL_CONTRACTS } from "../contracts/toolContracts.js";
import {
clientRequirementsFor,
NO_CLIENT_FEATURES,
@ -131,7 +131,7 @@ export const buildCapabilityInventory = (
const descriptors = new Map<string, ProviderDescriptor>(
status.capabilities.map((descriptor) => [descriptor.operation, descriptor]),
);
return GENERATED_MCP_TOOL_CATALOG.map((contract): ToolAvailability => {
return TOOL_CONTRACTS.map((contract): ToolAvailability => {
const availability = availabilityFor({
name: contract.name,
kind: contract.kind,

View File

@ -40,12 +40,12 @@ export class LazyAnalysisProvider implements AnalysisProvider {
this.#loadProvider = options.load;
}
/** Return generated provider identity without loading its implementation. */
/** Return source-declared provider identity without loading its implementation. */
identity(): ProviderIdentity {
return this.#providerIdentity;
}
/** Return generated capability metadata without loading its implementation. */
/** Return source-declared capability metadata without loading its implementation. */
capabilities(): readonly CapabilityDescriptor[] {
return this.#capabilityDescriptors;
}

View File

@ -4,10 +4,9 @@ import { HopperProvider } from "../hopper/HopperProvider.js";
import { GhidraProvider } from "../ghidra/GhidraProvider.js";
import { IdaProvider } from "../ida/IdaProvider.js";
import { silentLogger, type Logger } from "../logger.js";
import { GENERATED_AUXILIARY_PROVIDERS } from "../generatedMcpToolCatalog.js";
import { auxiliaryAnalysisProviderDeclarations } from "../composition/auxiliaryAnalysisProviders.js";
import { AnalysisProviderRegistry } from "./AnalysisProviderRegistry.js";
import { composeBinarySession } from "./BinarySessionComposition.js";
import { nativeHostCapabilities } from "../native/NativeHostCapabilities.js";
import { LazyAnalysisProvider } from "./LazyAnalysisProvider.js";
import { ManagedStaticProvider } from "../dotnet/ManagedStaticProvider.js";
import { SessionProviderRouter } from "./SessionProviderRouter.js";
@ -24,47 +23,14 @@ export const createBinarySession = (
const hopper = new HopperProvider(config, logger);
const ghidra = new GhidraProvider(config, logger);
const ida = new IdaProvider(config);
const auxiliary = new Map(
GENERATED_AUXILIARY_PROVIDERS.map((provider) => [
provider.identity.id,
provider,
]),
);
const lazyProvider = (
id: string,
load: ConstructorParameters<typeof LazyAnalysisProvider>[0]["load"],
) => {
const generated = auxiliary.get(id);
if (generated === undefined)
throw new TypeError(`Missing generated provider metadata for ${id}`);
return new LazyAnalysisProvider({
...generated,
capabilities: nativeHostCapabilities(generated.capabilities),
load,
});
};
return composeBinarySession(
new AnalysisProviderRegistry(
[hopper, ghidra, ida],
config.analysisProvider,
),
[
lazyProvider("rea-artifact-graph", async () => {
const { ArtifactProvider } =
await import("../artifacts/ArtifactProvider.js");
return new ArtifactProvider();
}),
lazyProvider("native-macos", async () => {
const { NativeMacOSProvider } =
await import("../native/NativeMacOSProvider.js");
return new NativeMacOSProvider();
}),
lazyProvider("rea-dotnet-static", async () => {
const { ManagedStaticProvider } =
await import("../dotnet/ManagedStaticProvider.js");
return new ManagedStaticProvider();
}),
],
auxiliaryAnalysisProviderDeclarations().map(
(declaration) => new LazyAnalysisProvider(declaration),
),
);
};

View File

@ -27,54 +27,20 @@ import type { JsonValue } from "../domain/jsonValue.js";
import { interfaceBuilderLimitsSchema } from "../domain/interfaceBuilderGraph.js";
import { err, ok } from "../domain/result.js";
import { ArtifactReaderFailure } from "./ArtifactReader.js";
import { ARTIFACT_GRAPH_PROVIDER } from "../application/InvestigationProviders.js";
import {
ARTIFACT_PROVIDER_IDENTITY as IDENTITY,
artifactCapabilities,
} from "./ArtifactProviderMetadata.js";
import { createEvidence } from "../domain/evidence.js";
import { createArtifactInspection } from "../domain/artifactInspection.js";
import { resolveArtifactIntegrityPolicy } from "../application/ArtifactInventory/policy.js";
const IDENTITY: ProviderIdentity = Object.freeze(ARTIFACT_GRAPH_PROVIDER);
/** Read-only inventory and exclusively owned extraction provider. */
export class ArtifactProvider implements AnalysisProvider {
readonly #capabilities: readonly CapabilityDescriptor[];
constructor(platform: NodeJS.Platform = process.platform) {
this.#capabilities = Object.freeze(
ARTIFACT_ANALYSIS_OPERATIONS.map((operation) => {
const common = {
provider: IDENTITY,
operation,
effects: Object.freeze({
mutatesArtifact: false,
launchesProcess:
operation !== "decode_interface_builder" &&
operation !== "inspect_keyed_archive",
mayShowUi: false,
mayAccessNetwork: false,
mayWriteFilesystem:
operation === "extract_artifact" ||
operation === "inspect_artifact" ||
operation === "inventory_artifact",
changesPermissions: false,
requiresRoot: false,
}),
limitations: Object.freeze([
"DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",
"ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only.",
]),
};
return Object.freeze(
operation === "inspect_asset_catalog" && platform !== "darwin"
? {
...common,
available: false as const,
availabilityCode: "unsupported_host" as const,
reason: "Apple asset catalogs require macOS assetutil.",
}
: { ...common, available: true as const, reason: null },
);
}),
);
this.#capabilities = artifactCapabilities(platform);
}
identity(): ProviderIdentity {

View File

@ -0,0 +1,53 @@
import type {
CapabilityDescriptor,
ProviderIdentity,
} from "../application/AnalysisProvider.js";
import { ARTIFACT_GRAPH_PROVIDER } from "../application/InvestigationProviders.js";
import { ARTIFACT_ANALYSIS_OPERATIONS } from "../contracts/artifactToolContracts.js";
/** Identity of artifact inventory and owned extraction observations. */
export const ARTIFACT_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze(
ARTIFACT_GRAPH_PROVIDER,
);
const IDENTITY = ARTIFACT_PROVIDER_IDENTITY;
/** Declare artifact coverage without constructing readers or acquiring resources. */
export const artifactCapabilities = (
platform: NodeJS.Platform = process.platform,
): readonly CapabilityDescriptor[] =>
Object.freeze(
ARTIFACT_ANALYSIS_OPERATIONS.map((operation) => {
const common = {
provider: IDENTITY,
operation,
effects: Object.freeze({
mutatesArtifact: false,
launchesProcess:
operation !== "decode_interface_builder" &&
operation !== "inspect_keyed_archive",
mayShowUi: false,
mayAccessNetwork: false,
mayWriteFilesystem:
operation === "extract_artifact" ||
operation === "inspect_artifact" ||
operation === "inventory_artifact",
changesPermissions: false,
requiresRoot: false,
}),
limitations: Object.freeze([
"DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",
"ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only.",
]),
};
return Object.freeze(
operation === "inspect_asset_catalog" && platform !== "darwin"
? {
...common,
available: false as const,
availabilityCode: "unsupported_host" as const,
reason: "Apple asset catalogs require macOS assetutil.",
}
: { ...common, available: true as const, reason: null },
);
}),
);

View File

@ -0,0 +1,50 @@
import type { LazyAnalysisProvider } from "../application/LazyAnalysisProvider.js";
import {
ARTIFACT_PROVIDER_IDENTITY,
artifactCapabilities,
} from "../artifacts/ArtifactProviderMetadata.js";
import {
MANAGED_STATIC_PROVIDER_IDENTITY,
managedStaticCapabilities,
} from "../dotnet/ManagedStaticProviderMetadata.js";
import {
NATIVE_MACOS_PROVIDER_IDENTITY,
nativeMacOSCapabilities,
} from "../native/NativeMacOSProviderMetadata.js";
type AuxiliaryProviderDeclaration = ConstructorParameters<
typeof LazyAnalysisProvider
>[0];
/** Wire the three existing auxiliary AnalysisProvider implementations lazily. */
export const auxiliaryAnalysisProviderDeclarations = (
platform: NodeJS.Platform = process.platform,
): readonly AuxiliaryProviderDeclaration[] => [
{
identity: ARTIFACT_PROVIDER_IDENTITY,
capabilities: artifactCapabilities(platform),
load: async () => {
const { ArtifactProvider } =
await import("../artifacts/ArtifactProvider.js");
return new ArtifactProvider(platform);
},
},
{
identity: NATIVE_MACOS_PROVIDER_IDENTITY,
capabilities: nativeMacOSCapabilities(platform),
load: async () => {
const { NativeMacOSProvider } =
await import("../native/NativeMacOSProvider.js");
return new NativeMacOSProvider(undefined, platform);
},
},
{
identity: MANAGED_STATIC_PROVIDER_IDENTITY,
capabilities: managedStaticCapabilities(),
load: async () => {
const { ManagedStaticProvider } =
await import("../dotnet/ManagedStaticProvider.js");
return new ManagedStaticProvider();
},
},
];

View File

@ -11,7 +11,10 @@ import {
type ExecutionOptions,
type ProviderIdentity,
} from "../application/AnalysisProvider.js";
import { MANAGED_STATIC_PROVIDER } from "../application/InvestigationProviders.js";
import {
MANAGED_STATIC_PROVIDER_IDENTITY as IDENTITY,
managedStaticCapabilities,
} from "./ManagedStaticProviderMetadata.js";
import {
MANAGED_TOOL_CONTRACTS,
managedArtifactInputSchema,
@ -38,30 +41,10 @@ import { inspectManagedArtifactBytes } from "./ManagedArtifactInspector.js";
import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
import { inspectManagedNativeBoundariesBytes } from "./ManagedNativeBoundaryInspector.js";
const IDENTITY: ProviderIdentity = Object.freeze(MANAGED_STATIC_PROVIDER);
/** Execution-free managed PE/CLI auxiliary provider. */
export class ManagedStaticProvider implements AnalysisProvider {
readonly #capabilities: readonly CapabilityDescriptor[] = Object.freeze(
MANAGED_TOOL_CONTRACTS.map((contract) =>
Object.freeze({
provider: IDENTITY,
operation: contract.name,
available: true as const,
reason: null,
effects: Object.freeze({
mutatesArtifact: false,
launchesProcess: false,
mayShowUi: false,
mayAccessNetwork: false,
mayWriteFilesystem: false,
changesPermissions: false,
requiresRoot: false,
}),
limitations: limitationsFor(contract.name),
}),
),
);
readonly #capabilities: readonly CapabilityDescriptor[] =
managedStaticCapabilities();
identity(): ProviderIdentity {
return IDENTITY;
@ -196,24 +179,6 @@ const isManagedOperation = (
): operation is ManagedToolName =>
MANAGED_TOOL_CONTRACTS.some(({ name }) => name === operation);
const limitationsFor = (operation: ManagedToolName): readonly string[] =>
Object.freeze(
operation === "inspect_managed_artifact"
? [
"This capability inventories PE/CLI identity only; inspect_managed_members admits metadata members, signatures, and CIL anchors.",
"It never loads the target assembly, resolves dependencies through a CLR, decompiles C#, or executes target code.",
]
: operation === "inspect_managed_native_boundaries"
? [
"This capability inventories managed declarations for native boundaries only; native exports, thunks, and addresses require separate native-provider evidence.",
"It never loads the target assembly, resolves dependencies through a CLR, decompiles C#, or executes target code.",
]
: [
"This capability decodes PE/CLI metadata members, signatures, and file-backed method bodies; decompiled C# reconstruction and cross-build member comparison are separate operations.",
"It never loads the target assembly, resolves dependencies through a CLR, decompiles C#, or executes target code.",
],
);
const inspectManagedOperation = (
operation: ManagedToolName,
parameters: Readonly<Record<string, JsonValue>>,

View File

@ -0,0 +1,56 @@
import type {
CapabilityDescriptor,
ProviderIdentity,
} from "../application/AnalysisProvider.js";
import { MANAGED_STATIC_PROVIDER } from "../application/InvestigationProviders.js";
import {
MANAGED_TOOL_CONTRACTS,
type ManagedToolName,
} from "../contracts/managedToolContracts.js";
/** Identity of execution-free managed metadata observations. */
export const MANAGED_STATIC_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze(
MANAGED_STATIC_PROVIDER,
);
const IDENTITY = MANAGED_STATIC_PROVIDER_IDENTITY;
/** Declare managed metadata coverage without opening or loading an assembly. */
export const managedStaticCapabilities = (): readonly CapabilityDescriptor[] =>
Object.freeze(
MANAGED_TOOL_CONTRACTS.map((contract) =>
Object.freeze({
provider: IDENTITY,
operation: contract.name,
available: true as const,
reason: null,
effects: Object.freeze({
mutatesArtifact: false,
launchesProcess: false,
mayShowUi: false,
mayAccessNetwork: false,
mayWriteFilesystem: false,
changesPermissions: false,
requiresRoot: false,
}),
limitations: limitationsFor(contract.name),
}),
),
);
const limitationsFor = (operation: ManagedToolName): readonly string[] =>
Object.freeze(
operation === "inspect_managed_artifact"
? [
"This capability inventories PE/CLI identity only; inspect_managed_members admits metadata members, signatures, and CIL anchors.",
"It never loads the target assembly, resolves dependencies through a CLR, decompiles C#, or executes target code.",
]
: operation === "inspect_managed_native_boundaries"
? [
"This capability inventories managed declarations for native boundaries only; native exports, thunks, and addresses require separate native-provider evidence.",
"It never loads the target assembly, resolves dependencies through a CLR, decompiles C#, or executes target code.",
]
: [
"This capability decodes PE/CLI metadata members, signatures, and file-backed method bodies; decompiled C# reconstruction and cross-build member comparison are separate operations.",
"It never loads the target assembly, resolves dependencies through a CLR, decompiles C#, or executes target code.",
],
);

View File

@ -1,29 +0,0 @@
import type { CapabilityDescriptor } from "../application/AnalysisProvider.js";
/** Apply native host restrictions to portable provider discovery metadata. */
export const nativeHostCapabilities = (
capabilities: readonly CapabilityDescriptor[],
platform: NodeJS.Platform = process.platform,
): readonly CapabilityDescriptor[] => {
if (platform === "darwin") return capabilities;
return capabilities.map((capability): CapabilityDescriptor => {
if (capability.provider.id === "native-macos")
return {
...capability,
available: false,
availabilityCode: "unsupported_host",
reason: "Native macOS utilities require macOS.",
};
if (
capability.provider.id === "rea-artifact-graph" &&
capability.operation === "inspect_asset_catalog"
)
return {
...capability,
available: false,
availabilityCode: "unsupported_host",
reason: "Apple asset catalogs require macOS assetutil.",
};
return capability;
});
};

View File

@ -1,3 +1,8 @@
import {
NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY,
nativeMacOSCapabilities,
} from "./NativeMacOSProviderMetadata.js";
export { NATIVE_MACOS_PROVIDER_IDENTITY } from "./NativeMacOSProviderMetadata.js";
import { inspectAppleDispatchMetadata } from "./AppleDispatchMetadata.js";
import { observeNativeUi } from "./NativeUiObservation.js";
import { dirname, isAbsolute, resolve } from "node:path";
@ -51,14 +56,6 @@ import {
inspectNativeMacho,
} from "./NativeMachoInspection.js";
/** Public identity committed by macOS-native inspection observations. */
export const NATIVE_MACOS_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
id: "native-macos",
name: "macOS native inspection utilities",
version: null,
});
const IDENTITY = NATIVE_MACOS_PROVIDER_IDENTITY;
/** Read-only semantic provider composed from Xcode command-line utilities. */
export class NativeMacOSProvider implements AnalysisProvider {
readonly #capabilities: readonly CapabilityDescriptor[];
@ -67,41 +64,7 @@ export class NativeMacOSProvider implements AnalysisProvider {
private readonly runner: NativeCommandRunner = new XcrunCommandRunner(),
platform: NodeJS.Platform = process.platform,
) {
const available = platform === "darwin";
this.#capabilities = Object.freeze(
[
...NATIVE_TOOL_CONTRACTS,
{ name: "inspect_native_dispatch_metadata" as const },
].map((contract): CapabilityDescriptor => {
const availability = available
? ({ available: true, reason: null } as const)
: ({
available: false,
availabilityCode: "unsupported_host",
reason: "Native macOS utilities require macOS.",
} as const);
return Object.freeze({
provider: IDENTITY,
operation: contract.name,
...availability,
effects: Object.freeze({
mutatesArtifact: false,
launchesProcess:
contract.name !== "inspect_native_dispatch_metadata",
mayShowUi: contract.name === "capture_native_ui_scenario",
mayAccessNetwork: contract.name === "capture_native_ui_scenario",
mayWriteFilesystem:
contract.name === "capture_native_ui_scenario" ||
contract.name === "observe_native_ui",
changesPermissions: false,
requiresRoot: false,
}),
limitations: Object.freeze([
"Availability and textual formats depend on the installed macOS/Xcode toolchain.",
]),
});
}),
);
this.#capabilities = nativeMacOSCapabilities(platform);
}
identity(): ProviderIdentity {

View File

@ -0,0 +1,53 @@
import type {
CapabilityDescriptor,
ProviderIdentity,
} from "../application/AnalysisProvider.js";
import { NATIVE_TOOL_CONTRACTS } from "../contracts/nativeToolContracts.js";
/** Public identity committed by macOS-native inspection observations. */
export const NATIVE_MACOS_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
id: "native-macos",
name: "macOS native inspection utilities",
version: null,
});
const IDENTITY = NATIVE_MACOS_PROVIDER_IDENTITY;
/** Declare native host coverage without creating a command runner. */
export const nativeMacOSCapabilities = (
platform: NodeJS.Platform = process.platform,
): readonly CapabilityDescriptor[] => {
const available = platform === "darwin";
return Object.freeze(
[
...NATIVE_TOOL_CONTRACTS,
{ name: "inspect_native_dispatch_metadata" as const },
].map((contract): CapabilityDescriptor => {
const availability = available
? ({ available: true, reason: null } as const)
: ({
available: false,
availabilityCode: "unsupported_host",
reason: "Native macOS utilities require macOS.",
} as const);
return Object.freeze({
provider: IDENTITY,
operation: contract.name,
...availability,
effects: Object.freeze({
mutatesArtifact: false,
launchesProcess: contract.name !== "inspect_native_dispatch_metadata",
mayShowUi: contract.name === "capture_native_ui_scenario",
mayAccessNetwork: contract.name === "capture_native_ui_scenario",
mayWriteFilesystem:
contract.name === "capture_native_ui_scenario" ||
contract.name === "observe_native_ui",
changesPermissions: false,
requiresRoot: false,
}),
limitations: Object.freeze([
"Availability and textual formats depend on the installed macOS/Xcode toolchain.",
]),
});
}),
);
};

View File

@ -8,7 +8,7 @@ import type { McpServer } from "@modelcontextprotocol/server";
import type { BrowserScenarioCapturePort } from "../application/BrowserScenarioCapturePort.js";
import { captureBrowserScenario } from "../application/BrowserScenarioCaptureService.js";
import { BROWSER_SCENARIO_TOOL_CONTRACTS } from "../contracts/browserScenarioToolContracts.js";
import { toolContract } from "../contracts/toolContracts.js";
import { browserScenarioSchema } from "../domain/browserScenario.js";
import type { Logger } from "../logger.js";
import { logToolExecution } from "./toolLogging.js";
@ -27,7 +27,7 @@ export const registerBrowserScenarioTool = (
server: McpServer,
options: BrowserScenarioToolRegistration,
): void => {
const [contract] = BROWSER_SCENARIO_TOOL_CONTRACTS;
const contract = toolContract("capture_browser_scenario");
server.registerTool(
contract.name,
toolRegistrationOptions(contract),

View File

@ -18,8 +18,7 @@ import {
observeWebSession,
} from "../application/BrowserObservationService.js";
import type { ProgressReporter } from "../application/ProgressReporter.js";
import { BROWSER_TOOL_CONTRACTS } from "../contracts/browserToolContracts.js";
import type { ToolContract } from "../contracts/toolContracts.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";
@ -49,16 +48,14 @@ export const registerBrowserTools = (
server: McpServer,
options: BrowserToolRegistration,
): void => {
const [
listContract,
inspectContract,
analyzeContract,
sessionContract,
webMcpContract,
captureDiffContract,
screenshotContract,
screenshotDiffContract,
] = BROWSER_TOOL_CONTRACTS;
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,

View File

@ -16,8 +16,7 @@ import {
import { analyzeJavaScriptApplicationValidated } from "../application/JavaScriptApplicationService.js";
import { reconcileJavaScriptRuntimeEvidenceValidated } from "../application/JavaScriptRuntimeReconciliationService.js";
import type { ProgressReporter } from "../application/ProgressReporter.js";
import { ELECTRON_TOOL_CONTRACTS } from "../contracts/electronToolContracts.js";
import type { ToolContract } from "../contracts/toolContracts.js";
import { toolContract, type ToolContract } from "../contracts/toolContracts.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { Evidence } from "../domain/evidence.js";
import type { Result } from "../domain/result.js";
@ -48,13 +47,11 @@ export const registerElectronTools = (
server: McpServer,
options: ElectronToolRegistration,
): void => {
const [
listContract,
inspectContract,
analyzeContract,
reconcileContract,
activeContract,
] = ELECTRON_TOOL_CONTRACTS;
const listContract = toolContract("list_electron_targets");
const inspectContract = toolContract("inspect_electron_page");
const analyzeContract = toolContract("analyze_javascript_application");
const reconcileContract = toolContract("reconcile_javascript_runtime");
const activeContract = toolContract("capture_electron_scenario");
server.registerTool(
listContract.name,

View File

@ -11,8 +11,7 @@ import {
listJavaScriptRuntimeTargets,
observeJavaScriptRuntime,
} from "../application/JavaScriptRuntimeObservationService.js";
import { JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS } from "../contracts/javascriptRuntimeObservationToolContracts.js";
import type { ToolContract } from "../contracts/toolContracts.js";
import { toolContract, type ToolContract } from "../contracts/toolContracts.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { Evidence } from "../domain/evidence.js";
import type { Result } from "../domain/result.js";
@ -34,8 +33,8 @@ export const registerJavaScriptRuntimeObservationTools = (
server: McpServer,
options: RuntimeToolRegistration,
): void => {
const [listContract, observeContract] =
JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS;
const listContract = toolContract("list_javascript_runtime_targets");
const observeContract = toolContract("observe_javascript_runtime");
server.registerTool(
listContract.name,
toolRegistrationOptions(listContract),

View File

@ -0,0 +1,63 @@
import { execFile } from "node:child_process";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { expect, it } from "vitest";
import { z } from "zod";
import { GENERATED_MCP_TOOL_CATALOG } from "../../src/generatedMcpToolCatalog.js";
const execute = promisify(execFile);
const fixture = fileURLToPath(
new URL("../fixtures/contractPresentation.mjs", import.meta.url),
);
const resultSchema = z.object({
catalog: z.array(
z.object({
name: z.string(),
inputSchema: z.record(z.string(), z.unknown()),
outputSchema: z.record(z.string(), z.unknown()),
}),
),
responses: z.array(
z.object({ name: z.string(), response: z.record(z.string(), z.unknown()) }),
),
});
it.each(["original", "reversed"])(
"preserves advertised schemas and handler meaning with %s presentation",
async (presentation) => {
const { stdout } = await execute(
process.execPath,
[fixture, presentation],
{
timeout: 20_000,
maxBuffer: 16 * 1024 * 1024,
},
);
const result = resultSchema.parse(JSON.parse(stdout));
expect(result.catalog).toHaveLength(GENERATED_MCP_TOOL_CATALOG.length);
expect(new Set(result.catalog.map(({ name }) => name))).toEqual(
new Set(GENERATED_MCP_TOOL_CATALOG.map(({ name }) => name)),
);
for (const advertised of result.catalog) {
const canonical = GENERATED_MCP_TOOL_CATALOG.find(
({ name }) => name === advertised.name,
);
expect(advertised.inputSchema).toEqual(canonical?.inputSchema);
expect(advertised.outputSchema).toEqual(canonical?.outputSchema);
}
for (const { name, response } of result.responses) {
if (name === "reconcile_javascript_runtime") {
expect(response.isError).not.toBe(true);
expect(response.structuredContent).toMatchObject({
evidence: { operation: name },
});
} else {
expect(response.isError, name).toBe(true);
expect(response.structuredContent, name).toMatchObject({
error: { details: { operation: name } },
});
}
}
},
);

View File

@ -0,0 +1,89 @@
import { expect, it } from "vitest";
import { auxiliaryAnalysisProviderDeclarations } from "../../../src/composition/auxiliaryAnalysisProviders.js";
import { LazyAnalysisProvider } from "../../../src/application/LazyAnalysisProvider.js";
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
import { GENERATED_AUXILIARY_PROVIDERS } from "../../../src/generatedMcpToolCatalog.js";
it.each(["darwin", "linux", "win32"] as const)(
"preserves canonical auxiliary metadata and host facts on %s",
async (platform) => {
const declarations = auxiliaryAnalysisProviderDeclarations(platform);
expect(declarations.map(({ identity }) => identity)).toEqual(
GENERATED_AUXILIARY_PROVIDERS.map(({ identity }) => identity),
);
const operations = new Set<string>();
for (const declaration of declarations) {
const canonical = GENERATED_AUXILIARY_PROVIDERS.find(
({ identity }) => identity.id === declaration.identity.id,
);
if (canonical === undefined)
throw new Error("Missing canonical provider");
const expected = canonical.capabilities.map((capability) => {
if (platform === "darwin") return capability;
if (capability.provider.id === "native-macos")
return {
...capability,
available: false,
availabilityCode: "unsupported_host",
reason: "Native macOS utilities require macOS.",
};
if (capability.operation === "inspect_asset_catalog")
return {
...capability,
available: false,
availabilityCode: "unsupported_host",
reason: "Apple asset catalogs require macOS assetutil.",
};
return capability;
});
expect(declaration.capabilities).toEqual(expected);
const first = await declaration.load();
const second = await declaration.load();
expect(first).not.toBe(second);
expect(first.identity()).toEqual(declaration.identity);
expect(first.capabilities()).toEqual(declaration.capabilities);
expect(second.capabilities()).toEqual(declaration.capabilities);
for (const capability of declaration.capabilities) {
expect(operations.has(capability.operation)).toBe(false);
operations.add(capability.operation);
expect(Object.isFrozen(capability)).toBe(true);
expect(Object.isFrozen(capability.effects)).toBe(true);
expect(Object.isFrozen(capability.limitations)).toBe(true);
}
}
expect(operations.has("inspect_native_dispatch_metadata")).toBe(true);
},
);
it("keeps declaration discovery and client creation lazy and independently owned", async () => {
const target = await parseBinaryTarget(process.execPath);
if (!target.ok) throw target.error;
for (const declaration of auxiliaryAnalysisProviderDeclarations()) {
let loads = 0;
const provider = new LazyAnalysisProvider({
...declaration,
load: async () => {
loads += 1;
return declaration.load();
},
});
expect(provider.identity()).toEqual(declaration.identity);
expect(provider.capabilities()).toEqual(declaration.capabilities);
const client = provider.createClient(target.value);
const unusedClient = provider.createClient(target.value);
await unusedClient.close();
expect(loads).toBe(0);
try {
expect(await client.execute("health", {})).toMatchObject({
ok: true,
value: { provider: declaration.identity },
});
expect(loads).toBe(1);
expect(await client.execute("health", {})).toMatchObject({ ok: true });
expect(loads).toBe(1);
} finally {
await client.close();
}
}
});

58
tests/fixtures/contractPresentation.mjs vendored Normal file
View File

@ -0,0 +1,58 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { BROWSER_TOOL_CONTRACTS } from "../../dist/contracts/browserToolContracts.js";
import { BROWSER_SCENARIO_TOOL_CONTRACTS } from "../../dist/contracts/browserScenarioToolContracts.js";
import { ELECTRON_TOOL_CONTRACTS } from "../../dist/contracts/electronToolContracts.js";
import { JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS } from "../../dist/contracts/javascriptRuntimeObservationToolContracts.js";
import { TOOL_CONTRACTS } from "../../dist/contracts/toolContracts.js";
import { parseConfig } from "../../dist/config.js";
import { createBinarySession } from "../../dist/application/runtime.js";
// Mutate only this child's presentation arrays, before importing registration.
if (process.argv[2] === "reversed") {
for (const contracts of [
BROWSER_TOOL_CONTRACTS,
BROWSER_SCENARIO_TOOL_CONTRACTS,
ELECTRON_TOOL_CONTRACTS,
JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS,
TOOL_CONTRACTS,
])
contracts.reverse();
}
const { createServer } = await import("../../dist/server/createServer.js");
const config = parseConfig({});
if (!config.ok) throw config.error;
const session = createBinarySession(config.value);
const server = createServer(session, session);
const client = new Client({ name: "contract-presentation", version: "1" });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const catalog = await client.listTools();
const responses = [];
for (const contract of [
...BROWSER_TOOL_CONTRACTS,
...BROWSER_SCENARIO_TOOL_CONTRACTS,
...ELECTRON_TOOL_CONTRACTS.filter(
({ name }) => name !== "analyze_javascript_application",
),
...JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS,
]) {
const example = contract.examples[0];
if (example === undefined)
throw new Error(`Missing example for ${contract.name}`);
responses.push({
name: contract.name,
response: await client.callTool({
name: contract.name,
arguments: example.input,
}),
});
}
process.stdout.write(JSON.stringify({ catalog: catalog.tools, responses }));
} finally {
await client.close();
await server.close();
await session.close();
}