Merge pull request #442 from morluto/fix/mcp-cold-start
perf(mcp): reduce cold-start work
This commit is contained in:
commit
8ec9bb1bfe
|
|
@ -55,6 +55,8 @@
|
|||
"deps:check": "node scripts/check-dependency-install.mjs",
|
||||
"metadata:generate": "node scripts/generate-package-metadata.mjs",
|
||||
"metadata:check": "node scripts/generate-package-metadata.mjs --check",
|
||||
"mcp-catalog:generate": "npm run build:cached && node scripts/generate-mcp-tool-catalog.mjs",
|
||||
"mcp-catalog:check": "node scripts/generate-mcp-tool-catalog.mjs --check",
|
||||
"prebuild": "npm run deps:check && npm run metadata:check",
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"precheck": "npm run deps:check",
|
||||
|
|
@ -124,9 +126,9 @@
|
|||
"evidence:check": "npm run build:cached && node scripts/generate-completion-ledger.mjs --check",
|
||||
"docs:api": "typedoc",
|
||||
"docs:api:cached": "turbo run docs:api",
|
||||
"docs:generate": "npm run metadata:generate && npm run build:cached && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && node scripts/generate-completion-ledger.mjs && npm run docs:api:cached && node scripts/generate-error-schema.mjs",
|
||||
"docs:generate": "npm run metadata:generate && npm run build:cached && node scripts/generate-mcp-tool-catalog.mjs && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && node scripts/generate-completion-ledger.mjs && npm run docs:api:cached && node scripts/generate-error-schema.mjs",
|
||||
"docs:check": "turbo run docs:check:built",
|
||||
"docs:check:built": "node scripts/generate-package-metadata.mjs --check && node scripts/generate-skill-metadata.mjs --check && node scripts/generate-product-catalog.mjs --check && node scripts/generate-completion-ledger.mjs --check && node scripts/check-doc-facts.mjs && node scripts/generate-error-schema.mjs --check",
|
||||
"docs:check:built": "node scripts/generate-package-metadata.mjs --check && node scripts/generate-mcp-tool-catalog.mjs --check && node scripts/generate-skill-metadata.mjs --check && node scripts/generate-product-catalog.mjs --check && node scripts/generate-completion-ledger.mjs --check && node scripts/check-doc-facts.mjs && node scripts/generate-error-schema.mjs --check",
|
||||
"docs:error-schema": "npm run build:cached && node scripts/generate-error-schema.mjs",
|
||||
"prepack": "node scripts/prepack.mjs",
|
||||
"prepare": "node scripts/prepare.mjs"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { format } from "prettier";
|
||||
|
||||
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 { toolRegistrationOptions } from "../dist/server/toolRegistrationOptions.js";
|
||||
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
|
||||
|
||||
const arguments_ = new Set(process.argv.slice(2));
|
||||
for (const argument of arguments_)
|
||||
if (argument !== "--check")
|
||||
throw new Error(`Unknown MCP tool catalog option: ${argument}`);
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const sessionToolNames = new Set([
|
||||
...TOOL_CONTRACTS.filter(({ kind }) => kind === "session").map(
|
||||
({ name }) => name,
|
||||
),
|
||||
...MANAGED_WORKFLOW_TOOL_CONTRACTS.map(({ name }) => name),
|
||||
]);
|
||||
const catalog = TOOL_CONTRACTS.map((contract) => {
|
||||
const options = toolRegistrationOptions(contract);
|
||||
return {
|
||||
name: contract.name,
|
||||
analysisOperation: [
|
||||
"official-proxy",
|
||||
"enhanced",
|
||||
"native-provider",
|
||||
"artifact-provider",
|
||||
"managed-provider",
|
||||
].includes(contract.kind)
|
||||
? contract.name
|
||||
: null,
|
||||
title: options.title,
|
||||
description: options.description,
|
||||
kind: contract.kind,
|
||||
requiresSession: sessionToolNames.has(contract.name),
|
||||
inputSchema: options.inputSchema["~standard"].jsonSchema.input({
|
||||
target: "draft-2020-12",
|
||||
}),
|
||||
outputSchema: options.outputSchema["~standard"].jsonSchema.output({
|
||||
target: "draft-2020-12",
|
||||
}),
|
||||
annotations: options.annotations,
|
||||
effects: contract.effects,
|
||||
};
|
||||
});
|
||||
const auxiliaryProviders = [
|
||||
new ArtifactProvider(false, false),
|
||||
new NativeMacOSProvider(),
|
||||
new ManagedStaticProvider(),
|
||||
].map((provider) => ({
|
||||
identity: provider.identity(),
|
||||
capabilities: provider.capabilities(),
|
||||
}));
|
||||
const payloadJson = JSON.stringify({ catalog, auxiliaryProviders });
|
||||
const source = await format(
|
||||
`import type { ToolAnnotations } from "@modelcontextprotocol/server";
|
||||
import type {
|
||||
CapabilityDescriptor,
|
||||
ProviderIdentity,
|
||||
} from "./application/AnalysisProvider.js";
|
||||
import type { ToolKind } from "./contracts/toolContractTypes.js";
|
||||
import type { ToolEffects } from "./contracts/toolEffects.js";
|
||||
|
||||
interface GeneratedTool {
|
||||
readonly name: string;
|
||||
readonly analysisOperation: CapabilityDescriptor["operation"] | null;
|
||||
readonly title: string;
|
||||
readonly description: string;
|
||||
readonly kind: ToolKind;
|
||||
readonly requiresSession: boolean;
|
||||
readonly inputSchema: Readonly<Record<string, unknown>>;
|
||||
readonly outputSchema: Readonly<Record<string, unknown>>;
|
||||
readonly annotations: ToolAnnotations;
|
||||
readonly effects: ToolEffects;
|
||||
}
|
||||
|
||||
interface GeneratedProvider {
|
||||
readonly identity: ProviderIdentity;
|
||||
readonly capabilities: readonly CapabilityDescriptor[];
|
||||
}
|
||||
|
||||
interface GeneratedPayload {
|
||||
readonly catalog: readonly GeneratedTool[];
|
||||
readonly auxiliaryProviders: readonly GeneratedProvider[];
|
||||
}
|
||||
|
||||
const GENERATED_PAYLOAD_JSON = ${JSON.stringify(payloadJson)};
|
||||
const generatedPayload: unknown = JSON.parse(GENERATED_PAYLOAD_JSON);
|
||||
if (!isGeneratedPayload(generatedPayload))
|
||||
throw new TypeError("Generated MCP catalog payload is invalid");
|
||||
|
||||
/** Generated from TOOL_CONTRACTS; do not edit. */
|
||||
export const GENERATED_MCP_TOOL_CATALOG = generatedPayload.catalog;
|
||||
|
||||
/** Generated lightweight metadata for lazily loaded auxiliary providers. */
|
||||
export const GENERATED_AUXILIARY_PROVIDERS = generatedPayload.auxiliaryProviders;
|
||||
|
||||
function isGeneratedPayload(value: unknown): value is GeneratedPayload {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
Array.isArray(value.catalog) &&
|
||||
value.catalog.every(isGeneratedTool) &&
|
||||
Array.isArray(value.auxiliaryProviders) &&
|
||||
value.auxiliaryProviders.every(isGeneratedProvider)
|
||||
);
|
||||
}
|
||||
|
||||
function isGeneratedTool(value: unknown): value is GeneratedTool {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
typeof value.name === "string" &&
|
||||
(typeof value.analysisOperation === "string" ||
|
||||
value.analysisOperation === null) &&
|
||||
typeof value.title === "string" &&
|
||||
typeof value.description === "string" &&
|
||||
typeof value.kind === "string" &&
|
||||
typeof value.requiresSession === "boolean" &&
|
||||
isRecord(value.inputSchema) &&
|
||||
isRecord(value.outputSchema) &&
|
||||
isRecord(value.annotations) &&
|
||||
isRecord(value.effects)
|
||||
);
|
||||
}
|
||||
|
||||
function isGeneratedProvider(value: unknown): value is GeneratedProvider {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
isRecord(value.identity) &&
|
||||
Array.isArray(value.capabilities)
|
||||
);
|
||||
}
|
||||
|
||||
function isRecord(
|
||||
value: unknown,
|
||||
): value is Readonly<Record<string, unknown>> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
`,
|
||||
{ parser: "typescript" },
|
||||
);
|
||||
|
||||
await ensureGeneratedFile({
|
||||
path: join(root, "src/generatedMcpToolCatalog.ts"),
|
||||
source,
|
||||
check: arguments_.has("--check"),
|
||||
generateCommand: "npm run mcp-catalog:generate",
|
||||
});
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
interface McpStartupPolicy {
|
||||
readonly initializeBudgetMs: number;
|
||||
readonly toolsListBudgetMs: number;
|
||||
readonly firstCatalogBudgetMs: number;
|
||||
readonly doctorDeadlineMs: number;
|
||||
}
|
||||
|
||||
interface McpProcessOptions {
|
||||
readonly command: string;
|
||||
readonly args: readonly string[];
|
||||
readonly environment: Readonly<Record<string, string | undefined>>;
|
||||
readonly policy: McpStartupPolicy;
|
||||
}
|
||||
|
||||
interface McpModuleProfileOptions extends McpProcessOptions {
|
||||
readonly packageName: string;
|
||||
}
|
||||
|
||||
interface RssMeasurement {
|
||||
readonly supported: boolean;
|
||||
readonly method: string | null;
|
||||
readonly peak_bytes: number | null;
|
||||
}
|
||||
|
||||
/** Measure the first usable MCP catalog through a child-process boundary. */
|
||||
export function measureMcpStartup(options: McpProcessOptions): Promise<{
|
||||
readonly process_started_to_initialized_ms: number;
|
||||
readonly initialized_to_tools_list_ms: number;
|
||||
readonly process_started_to_tools_list_ms: number;
|
||||
readonly listed_tool_count: number;
|
||||
readonly rss: RssMeasurement;
|
||||
}>;
|
||||
|
||||
/** Profile ESM file loading in a separate diagnostic MCP child. */
|
||||
export function profileMcpModuleLoading(
|
||||
options: McpModuleProfileOptions,
|
||||
): Promise<{
|
||||
readonly supported: true;
|
||||
readonly method: "node-debug-esm";
|
||||
readonly unique_file_modules: number;
|
||||
readonly unique_package_modules: number;
|
||||
readonly unique_dependency_modules: number;
|
||||
readonly debug_lines: number;
|
||||
readonly truncated: boolean;
|
||||
}>;
|
||||
|
||||
/** Parse Linux procfs VmRSS text into bytes. */
|
||||
export function parseLinuxVmRssBytes(status: string): number | null;
|
||||
|
||||
/** Collect unique file-backed ESM module URLs from Node diagnostics. */
|
||||
export function createEsmModuleProfileCollector(packageName?: string): {
|
||||
append(chunk: string): void;
|
||||
result(): {
|
||||
readonly supported: true;
|
||||
readonly method: "node-debug-esm";
|
||||
readonly unique_file_modules: number;
|
||||
readonly unique_package_modules: number;
|
||||
readonly unique_dependency_modules: number;
|
||||
readonly debug_lines: number;
|
||||
readonly truncated: boolean;
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { performance } from "node:perf_hooks";
|
||||
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
|
||||
const RSS_SAMPLE_INTERVAL_MS = 10;
|
||||
const MAXIMUM_PROFILE_BUFFER_BYTES = 2 * 1_024 * 1_024;
|
||||
const MAXIMUM_PROFILE_MODULES = 10_000;
|
||||
|
||||
/**
|
||||
* Measure the first usable MCP catalog through the installed process boundary.
|
||||
*/
|
||||
export async function measureMcpStartup({
|
||||
command,
|
||||
args,
|
||||
environment,
|
||||
policy,
|
||||
}) {
|
||||
const transport = new StdioClientTransport({
|
||||
command,
|
||||
args,
|
||||
env: environment,
|
||||
stderr: "pipe",
|
||||
});
|
||||
let stderr = "";
|
||||
transport.stderr?.on("data", (chunk) => {
|
||||
stderr = boundedAppend(stderr, chunk.toString());
|
||||
});
|
||||
const rss = createLinuxRssSampler(() => transport.pid);
|
||||
const client = new Client({
|
||||
name: "package-startup-probe",
|
||||
version: "1.0.0",
|
||||
});
|
||||
const startedAt = performance.now();
|
||||
rss.start();
|
||||
try {
|
||||
await client.connect(transport, { timeout: policy.initializeBudgetMs });
|
||||
const initializedAt = performance.now();
|
||||
const tools = await client.listTools(undefined, {
|
||||
timeout: policy.toolsListBudgetMs,
|
||||
});
|
||||
const listedAt = performance.now();
|
||||
rss.stop();
|
||||
const measurement = {
|
||||
process_started_to_initialized_ms: elapsed(startedAt, initializedAt),
|
||||
initialized_to_tools_list_ms: elapsed(initializedAt, listedAt),
|
||||
process_started_to_tools_list_ms: elapsed(startedAt, listedAt),
|
||||
listed_tool_count: tools.tools.length,
|
||||
rss: rss.result(),
|
||||
};
|
||||
assertStartupBudgets(measurement, policy);
|
||||
return measurement;
|
||||
} catch (cause) {
|
||||
throw new Error(`packaged MCP startup probe failed: ${stderr}`, { cause });
|
||||
} finally {
|
||||
rss.stop();
|
||||
await Promise.allSettled([client.close(), transport.close()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Profile ESM file loading in a separate diagnostic child.
|
||||
*/
|
||||
export async function profileMcpModuleLoading({
|
||||
command,
|
||||
args,
|
||||
environment,
|
||||
policy,
|
||||
packageName,
|
||||
}) {
|
||||
const profile = createEsmModuleProfileCollector(packageName);
|
||||
const transport = new StdioClientTransport({
|
||||
command,
|
||||
args,
|
||||
env: { ...environment, NODE_DEBUG: "esm" },
|
||||
stderr: "pipe",
|
||||
});
|
||||
transport.stderr?.on("data", (chunk) => profile.append(chunk.toString()));
|
||||
const client = new Client({
|
||||
name: "package-module-profile",
|
||||
version: "1.0.0",
|
||||
});
|
||||
try {
|
||||
await client.connect(transport, {
|
||||
timeout: policy.doctorDeadlineMs,
|
||||
});
|
||||
await client.listTools(undefined, {
|
||||
timeout: policy.toolsListBudgetMs,
|
||||
});
|
||||
return profile.result();
|
||||
} finally {
|
||||
await Promise.allSettled([client.close(), transport.close()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Linux procfs VmRSS text into bytes.
|
||||
*/
|
||||
export const parseLinuxVmRssBytes = (status) => {
|
||||
const match = /^VmRSS:\s+(\d+)\s+kB$/mu.exec(status);
|
||||
return match === null ? null : Number(match[1]) * 1_024;
|
||||
};
|
||||
|
||||
/**
|
||||
* Collect unique file-backed ESM module URLs from Node's diagnostic stream.
|
||||
*/
|
||||
export const createEsmModuleProfileCollector = (packageName = "rea-agents") => {
|
||||
const modules = new Set();
|
||||
let buffer = "";
|
||||
let debugLines = 0;
|
||||
let truncated = false;
|
||||
const consume = (line) => {
|
||||
if (line.startsWith("ESM ")) debugLines += 1;
|
||||
for (const match of line.matchAll(/file:\/\/\/[^\s'",}\])]+/gu)) {
|
||||
if (modules.size >= MAXIMUM_PROFILE_MODULES) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
modules.add(match[0]);
|
||||
}
|
||||
};
|
||||
return {
|
||||
append(chunk) {
|
||||
buffer += chunk;
|
||||
if (Buffer.byteLength(buffer) > MAXIMUM_PROFILE_BUFFER_BYTES) {
|
||||
buffer = buffer.slice(-MAXIMUM_PROFILE_BUFFER_BYTES);
|
||||
truncated = true;
|
||||
}
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() ?? "";
|
||||
for (const line of lines) consume(line);
|
||||
},
|
||||
result() {
|
||||
if (buffer.length > 0) consume(buffer);
|
||||
const values = [...modules];
|
||||
const packageSegment = `/node_modules/${packageName}/`;
|
||||
return {
|
||||
supported: true,
|
||||
method: "node-debug-esm",
|
||||
unique_file_modules: values.length,
|
||||
unique_package_modules: values.filter((value) =>
|
||||
value.includes(packageSegment),
|
||||
).length,
|
||||
unique_dependency_modules: values.filter(
|
||||
(value) =>
|
||||
value.includes("/node_modules/") && !value.includes(packageSegment),
|
||||
).length,
|
||||
debug_lines: debugLines,
|
||||
truncated,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const createLinuxRssSampler = (pid) => {
|
||||
if (process.platform !== "linux")
|
||||
return {
|
||||
start() {},
|
||||
stop() {},
|
||||
result: () => ({
|
||||
supported: false,
|
||||
method: null,
|
||||
peak_bytes: null,
|
||||
}),
|
||||
};
|
||||
let peakBytes = null;
|
||||
let timer;
|
||||
const sample = () => {
|
||||
const currentPid = pid();
|
||||
if (currentPid === null) return;
|
||||
try {
|
||||
const current = parseLinuxVmRssBytes(
|
||||
readFileSync(`/proc/${String(currentPid)}/status`, "utf8"),
|
||||
);
|
||||
if (current !== null && (peakBytes === null || current > peakBytes))
|
||||
peakBytes = current;
|
||||
} catch {
|
||||
// The short-lived child may exit between resolving its PID and reading.
|
||||
}
|
||||
};
|
||||
return {
|
||||
start() {
|
||||
sample();
|
||||
timer = setInterval(sample, RSS_SAMPLE_INTERVAL_MS);
|
||||
timer.unref();
|
||||
},
|
||||
stop() {
|
||||
if (timer !== undefined) clearInterval(timer);
|
||||
sample();
|
||||
},
|
||||
result: () => ({
|
||||
supported: true,
|
||||
method: "linux-proc-status-vmrss-sampled",
|
||||
peak_bytes: peakBytes,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const assertStartupBudgets = (measurement, policy) => {
|
||||
if (measurement.process_started_to_initialized_ms > policy.initializeBudgetMs)
|
||||
throw new Error("packaged MCP exceeded the initialize regression budget");
|
||||
if (measurement.initialized_to_tools_list_ms > policy.toolsListBudgetMs)
|
||||
throw new Error("packaged MCP exceeded the tools/list regression budget");
|
||||
if (
|
||||
measurement.process_started_to_tools_list_ms > policy.firstCatalogBudgetMs
|
||||
)
|
||||
throw new Error(
|
||||
"packaged MCP exceeded the first-catalog regression budget",
|
||||
);
|
||||
};
|
||||
|
||||
const elapsed = (startedAt, completedAt) =>
|
||||
Math.round((completedAt - startedAt) * 100) / 100;
|
||||
|
||||
const boundedAppend = (current, chunk) =>
|
||||
`${current}${chunk}`.slice(-64 * 1_024);
|
||||
|
|
@ -8,6 +8,7 @@ import { fileURLToPath } from "node:url";
|
|||
import { createPackage } from "@electron/asar";
|
||||
|
||||
import { evaluateCodexEvents } from "../dist/evaluation/CodexAgentEval.js";
|
||||
import { MCP_STARTUP_POLICY } from "../dist/mcpStartupPolicy.js";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
|
||||
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
|
@ -346,7 +347,7 @@ async function runCodex(prompt, investigationRoot) {
|
|||
"-c",
|
||||
`mcp_servers.rea.args=${JSON.stringify([join(repositoryRoot, "scripts/rea.mjs"), "mcp"])}`,
|
||||
"-c",
|
||||
"mcp_servers.rea.startup_timeout_sec=30",
|
||||
`mcp_servers.rea.startup_timeout_sec=${String(MCP_STARTUP_POLICY.codexStartupTimeoutSeconds)}`,
|
||||
"-c",
|
||||
mcpEnvironmentOverride,
|
||||
...(optionalModel === undefined ? [] : ["--model", optionalModel]),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { promisify } from "node:util";
|
|||
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { MCP_STARTUP_POLICY } from "../dist/mcpStartupPolicy.js";
|
||||
import * as prompts from "./verify-package-prompts.mjs";
|
||||
import {
|
||||
json,
|
||||
|
|
@ -180,7 +181,7 @@ export async function verifyPackageMcp({
|
|||
(
|
||||
await execute(cli, ["mcp", "doctor", "--json"], {
|
||||
env: environment,
|
||||
timeout: 30_000,
|
||||
timeout: MCP_STARTUP_POLICY.doctorDeadlineMs,
|
||||
})
|
||||
).stdout,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { join } from "node:path";
|
|||
|
||||
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
|
||||
import { PRODUCT_IDENTITY } from "../dist/identity.js";
|
||||
import { MCP_STARTUP_POLICY } from "../dist/mcpStartupPolicy.js";
|
||||
import * as prompts from "./verify-package-prompts.mjs";
|
||||
import { verifyPackagedInvestigation } from "./verify-package-investigation.mjs";
|
||||
import { verifyPackageArtifactAndElectron } from "./verify-package-artifact.mjs";
|
||||
|
|
@ -19,6 +20,10 @@ import { verifyPackageSetup } from "./verify-package-setup.mjs";
|
|||
import { verifyManaged } from "./verify-package-managed.mjs";
|
||||
import { verifyUnknownProvider } from "./verify-package-unknown-provider.mjs";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import {
|
||||
measureMcpStartup,
|
||||
profileMcpModuleLoading,
|
||||
} from "./lib/mcp-startup-probe.mjs";
|
||||
|
||||
const root = process.cwd();
|
||||
const verifierRun = createVerifierRun();
|
||||
|
|
@ -26,6 +31,8 @@ const workspace = await mkdtemp(join(tmpdir(), "rea-package-"));
|
|||
const evidenceRoot = join(workspace, "evidence");
|
||||
const referenceRoot = join(workspace, "reference-source");
|
||||
let tarball;
|
||||
let mcpStartup;
|
||||
let mcpModuleLoading;
|
||||
|
||||
try {
|
||||
({ tarball } = await verifyPackagePack({ root, workspace }));
|
||||
|
|
@ -42,6 +49,25 @@ try {
|
|||
workspace,
|
||||
environment: environmentData.environment,
|
||||
});
|
||||
mcpStartup = await measureMcpStartup({
|
||||
command: cli,
|
||||
args: ["mcp"],
|
||||
environment: {
|
||||
...environmentData.environment,
|
||||
REA_INVESTIGATION_INPUT_ROOTS_JSON: JSON.stringify([]),
|
||||
},
|
||||
policy: MCP_STARTUP_POLICY,
|
||||
});
|
||||
mcpModuleLoading = await profileMcpModuleLoading({
|
||||
command: cli,
|
||||
args: ["mcp"],
|
||||
environment: {
|
||||
...environmentData.environment,
|
||||
REA_INVESTIGATION_INPUT_ROOTS_JSON: JSON.stringify([]),
|
||||
},
|
||||
policy: MCP_STARTUP_POLICY,
|
||||
packageName: PRODUCT_IDENTITY.packageName,
|
||||
});
|
||||
const { supportedSetupHost } = await verifyPackageDiscovery({
|
||||
cli,
|
||||
environment: environmentData.environment,
|
||||
|
|
@ -102,7 +128,7 @@ try {
|
|||
investigationReplay,
|
||||
});
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), cli: true, analysisCli: true, artifactCli: true, managedCli: true, managedReconstructionCli: true, managedNativeVerificationCli: true, managedRuntimePlanCli: true, managedApplicationGraphCli: true, evidenceCli: true, incurMcpCommand: PRODUCT_IDENTITY.mcpCommand, lifecycleScriptsRequired: false, doctor: "platform-appropriate", setup: supportedSetupHost ? "planned-then-idempotent" : "unsupported-host-rejected", setupPlanReadOnly: supportedSetupHost, existingHopperPreserved: supportedSetupHost, clients: supportedSetupHost ? 3 : 0, backupReadback: supportedSetupHost, failureRecovery: supportedSetupHost, configSymlinkLifecycle: supportedSetupHost, skill: supportedSetupHost, skillReferences: supportedSetupHost, mcpTools: TOOL_CONTRACTS.length, mcpPrompts: prompts.names.length, promptCompletion: true, promptCompletionLifecycle: true, evidenceMcp: true, targetFree: true, targetLifecycle: true, boundedRegexBridge: true })}\n`,
|
||||
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), cli: true, analysisCli: true, artifactCli: true, managedCli: true, managedReconstructionCli: true, managedNativeVerificationCli: true, managedRuntimePlanCli: true, managedApplicationGraphCli: true, evidenceCli: true, incurMcpCommand: PRODUCT_IDENTITY.mcpCommand, lifecycleScriptsRequired: false, doctor: "platform-appropriate", setup: supportedSetupHost ? "planned-then-idempotent" : "unsupported-host-rejected", setupPlanReadOnly: supportedSetupHost, existingHopperPreserved: supportedSetupHost, clients: supportedSetupHost ? 3 : 0, backupReadback: supportedSetupHost, failureRecovery: supportedSetupHost, configSymlinkLifecycle: supportedSetupHost, skill: supportedSetupHost, skillReferences: supportedSetupHost, mcpTools: TOOL_CONTRACTS.length, mcpPrompts: prompts.names.length, promptCompletion: true, promptCompletionLifecycle: true, evidenceMcp: true, targetFree: true, targetLifecycle: true, boundedRegexBridge: true, mcpStartup, mcpModuleLoading })}\n`,
|
||||
);
|
||||
} finally {
|
||||
if (tarball) await rm(join(root, tarball), { force: true });
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import type {
|
|||
AnalysisOperation,
|
||||
CapabilityDescriptor,
|
||||
} from "./AnalysisProvider.js";
|
||||
import { OFFICIAL_TOOL_CONTRACTS } from "../contracts/toolContracts.js";
|
||||
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
|
||||
|
||||
const STATEFUL_OPERATIONS: ReadonlySet<AnalysisOperation> = new Set([
|
||||
"health",
|
||||
|
|
@ -47,13 +47,15 @@ const CURSOR_DEFAULT_OPERATIONS: ReadonlySet<AnalysisOperation> = new Set([
|
|||
"xrefs",
|
||||
]);
|
||||
|
||||
const DOCUMENT_SCOPED_OPERATIONS: ReadonlySet<AnalysisOperation> = new Set(
|
||||
OFFICIAL_TOOL_CONTRACTS.map(({ name }) => name).filter(
|
||||
(name) =>
|
||||
name !== "current_document" &&
|
||||
name !== "list_documents" &&
|
||||
name !== "set_current_document",
|
||||
),
|
||||
const DOCUMENT_SCOPED_OPERATIONS: ReadonlySet<string> = new Set(
|
||||
GENERATED_MCP_TOOL_CATALOG.filter(({ kind }) => kind === "official-proxy")
|
||||
.map(({ name }) => name)
|
||||
.filter(
|
||||
(name) =>
|
||||
name !== "current_document" &&
|
||||
name !== "list_documents" &&
|
||||
name !== "set_current_document",
|
||||
),
|
||||
);
|
||||
|
||||
/** Whether an operation is immutable and independent of provider UI state. */
|
||||
|
|
|
|||
|
|
@ -24,8 +24,7 @@ import type {
|
|||
ProviderIdentity,
|
||||
} from "./AnalysisProvider.js";
|
||||
import type { AnalysisOperation } from "./AnalysisProvider.js";
|
||||
import { enhancedToolNameSchema } from "../contracts/enhancedInputs.js";
|
||||
import { OFFICIAL_TOOL_CONTRACTS } from "../contracts/toolContracts.js";
|
||||
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
|
||||
import type { BinarySessionPort } from "./BinarySessionPort.js";
|
||||
import {
|
||||
SessionProviderRouter,
|
||||
|
|
@ -41,7 +40,14 @@ import { prepareSessionExecution } from "./BinarySessionExecution.js";
|
|||
import { closeAnalysisClient } from "./AnalysisClientCleanup.js";
|
||||
export type { BinarySessionPort } from "./BinarySessionPort.js";
|
||||
const OFFICIAL_OPERATIONS: ReadonlySet<string> = new Set(
|
||||
OFFICIAL_TOOL_CONTRACTS.map(({ name }) => name),
|
||||
GENERATED_MCP_TOOL_CATALOG.filter(
|
||||
({ kind }) => kind === "official-proxy",
|
||||
).map(({ name }) => name),
|
||||
);
|
||||
const ENHANCED_OPERATIONS: ReadonlySet<string> = new Set(
|
||||
GENERATED_MCP_TOOL_CATALOG.filter(({ kind }) => kind === "enhanced").map(
|
||||
({ name }) => name,
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
@ -100,7 +106,7 @@ export class BinarySession
|
|||
if (operation !== undefined) {
|
||||
const exact = route.capabilities?.get(operation)?.provider;
|
||||
if (exact !== undefined) selected = exact;
|
||||
else if (enhancedToolNameSchema.safeParse(operation).success) {
|
||||
else if (ENHANCED_OPERATIONS.has(operation)) {
|
||||
const providers = new Map<string, ProviderIdentity>();
|
||||
for (const descriptor of route.capabilities?.values() ?? [])
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { TOOL_CONTRACTS } from "../contracts/toolContracts.js";
|
||||
import type { ClientFeatureAvailability } 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";
|
||||
|
||||
const statusSchema = z.object({
|
||||
open: z.boolean(),
|
||||
|
|
@ -28,7 +29,6 @@ type ToolAvailabilityReason =
|
|||
| "unsupported_host"
|
||||
| "policy_disabled";
|
||||
|
||||
type ToolKind = (typeof TOOL_CONTRACTS)[number]["kind"];
|
||||
type ProviderDescriptor = {
|
||||
readonly available: boolean;
|
||||
readonly reason: string | null;
|
||||
|
|
@ -128,7 +128,7 @@ export const buildCapabilityInventory = (
|
|||
},
|
||||
]),
|
||||
);
|
||||
return TOOL_CONTRACTS.map((contract) => {
|
||||
return GENERATED_MCP_TOOL_CATALOG.map((contract) => {
|
||||
const availability = availabilityFor({
|
||||
name: contract.name,
|
||||
kind: contract.kind,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { parse as parseToml } from "smol-toml";
|
|||
import { z } from "zod";
|
||||
|
||||
import { PRODUCT_IDENTITY } from "../identity.js";
|
||||
import { MCP_STARTUP_POLICY } from "../mcpStartupPolicy.js";
|
||||
import { supportedClients } from "./SupportedClients.js";
|
||||
|
||||
export interface ClientRegistrationStatus {
|
||||
|
|
@ -80,7 +81,11 @@ const registrationAligned = (
|
|||
currentCommandPath: string,
|
||||
): boolean => {
|
||||
const command = [registration.command, ...registration.args];
|
||||
if (client === "codex" && registration.startup_timeout_sec !== 30)
|
||||
if (
|
||||
client === "codex" &&
|
||||
registration.startup_timeout_sec !==
|
||||
MCP_STARTUP_POLICY.codexStartupTimeoutSeconds
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
command.length === 4 &&
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTarget.js";
|
||||
import type {
|
||||
AnalysisClient,
|
||||
AnalysisClientContext,
|
||||
AnalysisProvider,
|
||||
CapabilityDescriptor,
|
||||
ProviderIdentity,
|
||||
ProviderRequestActivitySnapshot,
|
||||
ProviderRuntimeLineageSnapshot,
|
||||
} from "./AnalysisProvider.js";
|
||||
|
||||
type LoadAnalysisProvider = () => Promise<AnalysisProvider>;
|
||||
|
||||
/**
|
||||
* Preserve synchronous provider discovery while loading implementation modules
|
||||
* only when a target-bound client first executes.
|
||||
*/
|
||||
export class LazyAnalysisProvider implements AnalysisProvider {
|
||||
readonly #providerIdentity: ProviderIdentity;
|
||||
readonly #capabilityDescriptors: readonly CapabilityDescriptor[];
|
||||
readonly #loadProvider: LoadAnalysisProvider;
|
||||
#provider: Promise<AnalysisProvider> | undefined;
|
||||
|
||||
constructor(options: {
|
||||
readonly identity: ProviderIdentity;
|
||||
readonly capabilities: readonly CapabilityDescriptor[];
|
||||
readonly load: LoadAnalysisProvider;
|
||||
}) {
|
||||
this.#providerIdentity = options.identity;
|
||||
this.#capabilityDescriptors = options.capabilities;
|
||||
this.#loadProvider = options.load;
|
||||
}
|
||||
|
||||
/** Return generated provider identity without loading its implementation. */
|
||||
identity(): ProviderIdentity {
|
||||
return this.#providerIdentity;
|
||||
}
|
||||
|
||||
/** Return generated capability metadata without loading its implementation. */
|
||||
capabilities(): readonly CapabilityDescriptor[] {
|
||||
return this.#capabilityDescriptors;
|
||||
}
|
||||
|
||||
/** Create a client whose implementation is loaded on first execution. */
|
||||
createClient(
|
||||
target: BinaryTarget,
|
||||
profile?: AnalysisProfileCommitment,
|
||||
context?: AnalysisClientContext,
|
||||
): AnalysisClient {
|
||||
return new LazyAnalysisClient(async () => {
|
||||
const provider = await this.#load();
|
||||
return provider.createClient(target, profile, context);
|
||||
});
|
||||
}
|
||||
|
||||
async #load(): Promise<AnalysisProvider> {
|
||||
this.#provider ??= this.#loadProvider().catch((cause: unknown) => {
|
||||
this.#provider = undefined;
|
||||
throw cause;
|
||||
});
|
||||
return this.#provider;
|
||||
}
|
||||
}
|
||||
|
||||
class LazyAnalysisClient implements AnalysisClient {
|
||||
readonly #loadClient: () => Promise<AnalysisClient>;
|
||||
#client: AnalysisClient | undefined;
|
||||
#loading: Promise<AnalysisClient> | undefined;
|
||||
#closed = false;
|
||||
|
||||
constructor(loadClient: () => Promise<AnalysisClient>) {
|
||||
this.#loadClient = loadClient;
|
||||
}
|
||||
|
||||
execute: AnalysisClient["execute"] = async (
|
||||
operation,
|
||||
parameters,
|
||||
options,
|
||||
) => {
|
||||
const client = await this.#load();
|
||||
return client.execute(operation, parameters, options);
|
||||
};
|
||||
|
||||
runtimeLineageSnapshots(): readonly ProviderRuntimeLineageSnapshot[] {
|
||||
return this.#client?.runtimeLineageSnapshots?.() ?? [];
|
||||
}
|
||||
|
||||
requestActivitySnapshots(): readonly ProviderRequestActivitySnapshot[] {
|
||||
return this.#client?.requestActivitySnapshots?.() ?? [];
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
this.#closed = true;
|
||||
if (this.#loading === undefined) return;
|
||||
const client = await this.#loading;
|
||||
await client.close();
|
||||
}
|
||||
|
||||
async #load(): Promise<AnalysisClient> {
|
||||
if (this.#closed) throw new Error("Lazy analysis client is closed");
|
||||
this.#loading ??= this.#loadClient()
|
||||
.then((client) => {
|
||||
this.#client = client;
|
||||
return client;
|
||||
})
|
||||
.catch((cause: unknown) => {
|
||||
this.#loading = undefined;
|
||||
throw cause;
|
||||
});
|
||||
return this.#loading;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import writeFileAtomic from "write-file-atomic";
|
|||
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
||||
|
||||
import { PRODUCT_IDENTITY } from "../identity.js";
|
||||
import { MCP_STARTUP_POLICY } from "../mcpStartupPolicy.js";
|
||||
import { resolveClientConfigTransactionPath } from "./ClientConfigPath.js";
|
||||
import type {
|
||||
ClientConfigurationInspection,
|
||||
|
|
@ -21,8 +22,6 @@ const defaultCommand = (): readonly string[] => [
|
|||
"mcp",
|
||||
];
|
||||
|
||||
const CODEX_STARTUP_TIMEOUT_SECONDS = 30;
|
||||
|
||||
/** Back up, atomically update, and semantically read back one JSON MCP configuration. */
|
||||
export const configureJsonClient = async (
|
||||
client: SetupClient,
|
||||
|
|
@ -299,7 +298,9 @@ const clientConfigurationDesired = (
|
|||
command: command[0] ?? PRODUCT_IDENTITY.cliBinary,
|
||||
args: command.slice(1),
|
||||
...(client.name === "codex"
|
||||
? { startup_timeout_sec: CODEX_STARTUP_TIMEOUT_SECONDS }
|
||||
? {
|
||||
startup_timeout_sec: MCP_STARTUP_POLICY.codexStartupTimeoutSeconds,
|
||||
}
|
||||
: {}),
|
||||
...(Object.keys(environment).length === 0 ? {} : { env: environment }),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,11 +2,10 @@ import type { AppConfig } from "../config.js";
|
|||
import { BinarySession } from "./BinarySession.js";
|
||||
import { HopperProvider } from "../hopper/HopperProvider.js";
|
||||
import { GhidraProvider } from "../ghidra/GhidraProvider.js";
|
||||
import { NativeMacOSProvider } from "../native/NativeMacOSProvider.js";
|
||||
import { ArtifactProvider } from "../artifacts/ArtifactProvider.js";
|
||||
import { ManagedStaticProvider } from "../dotnet/ManagedStaticProvider.js";
|
||||
import { silentLogger, type Logger } from "../logger.js";
|
||||
import { GENERATED_AUXILIARY_PROVIDERS } from "../generatedMcpToolCatalog.js";
|
||||
import { AnalysisProviderRegistry } from "./AnalysisProviderRegistry.js";
|
||||
import { LazyAnalysisProvider } from "./LazyAnalysisProvider.js";
|
||||
import { SessionProviderRouter } from "./SessionProviderRouter.js";
|
||||
|
||||
/**
|
||||
|
|
@ -20,16 +19,46 @@ export const createBinarySession = (
|
|||
): BinarySession => {
|
||||
const hopper = new HopperProvider(config, logger);
|
||||
const ghidra = new GhidraProvider(config, logger);
|
||||
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, load });
|
||||
};
|
||||
return new BinarySession(
|
||||
SessionProviderRouter.selectable(
|
||||
new AnalysisProviderRegistry([hopper, ghidra], config.analysisProvider),
|
||||
[
|
||||
new ArtifactProvider(
|
||||
config.artifactNativeMountEnabled,
|
||||
config.artifactIntegrityContinueEnabled,
|
||||
),
|
||||
new NativeMacOSProvider(),
|
||||
new ManagedStaticProvider(),
|
||||
lazyProvider("rea-artifact-graph", async () => {
|
||||
const { ArtifactProvider } = await import(
|
||||
"../artifacts/ArtifactProvider.js"
|
||||
);
|
||||
return new ArtifactProvider(
|
||||
config.artifactNativeMountEnabled,
|
||||
config.artifactIntegrityContinueEnabled,
|
||||
);
|
||||
}),
|
||||
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();
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { TOOL_CONTRACTS } from "./toolContracts.js";
|
||||
import type { TOOL_CONTRACTS } from "./toolContracts.js";
|
||||
|
||||
type ToolName = (typeof TOOL_CONTRACTS)[number]["name"];
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export const TOOL_KINDS = [
|
|||
] as const;
|
||||
|
||||
/** Adapter family responsible for implementing a public MCP tool. */
|
||||
type ToolKind = (typeof TOOL_KINDS)[number];
|
||||
export type ToolKind = (typeof TOOL_KINDS)[number];
|
||||
|
||||
/** Single source of truth for a public MCP tool. */
|
||||
export interface ToolContract<Name extends string = string> {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -2,10 +2,7 @@ import type {
|
|||
CapabilityDescriptor,
|
||||
ProviderIdentity,
|
||||
} from "../application/AnalysisProvider.js";
|
||||
import {
|
||||
ENHANCED_TOOL_CONTRACTS,
|
||||
OFFICIAL_TOOL_CONTRACTS,
|
||||
} from "../contracts/toolContracts.js";
|
||||
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
|
||||
import {
|
||||
GHIDRA_DECOMPILE_REQUEST_TIMEOUT_MS,
|
||||
GHIDRA_MAX_LINE_BYTES,
|
||||
|
|
@ -22,10 +19,9 @@ export const GHIDRA_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
|
|||
});
|
||||
|
||||
const providerContractByName = new Map(
|
||||
[...OFFICIAL_TOOL_CONTRACTS, ...ENHANCED_TOOL_CONTRACTS].map((contract) => [
|
||||
contract.name,
|
||||
contract,
|
||||
]),
|
||||
GENERATED_MCP_TOOL_CATALOG.filter(
|
||||
({ kind }) => kind === "official-proxy" || kind === "enhanced",
|
||||
).map((contract) => [contract.name, contract]),
|
||||
);
|
||||
|
||||
/** Provider-neutral read-only contracts implemented by the Ghidra adapter. */
|
||||
|
|
@ -165,10 +161,13 @@ export const limitationsFor = (operation: string): readonly string[] => {
|
|||
|
||||
/** Provider-neutral capabilities advertised by every non-Windows Ghidra session. */
|
||||
export const CAPABILITIES: readonly CapabilityDescriptor[] = Object.freeze(
|
||||
GHIDRA_PROVIDER_TOOL_CONTRACTS.map((contract) =>
|
||||
Object.freeze({
|
||||
GHIDRA_PROVIDER_TOOL_CONTRACTS.map((contract) => {
|
||||
const operation = contract.analysisOperation;
|
||||
if (operation === null)
|
||||
throw new TypeError(`Missing analysis operation for ${contract.name}`);
|
||||
return Object.freeze({
|
||||
provider: GHIDRA_PROVIDER_IDENTITY,
|
||||
operation: contract.name,
|
||||
operation,
|
||||
inputContractVersion: 1,
|
||||
outputContractVersion: 1,
|
||||
available: true,
|
||||
|
|
@ -198,8 +197,8 @@ export const CAPABILITIES: readonly CapabilityDescriptor[] = Object.freeze(
|
|||
: GHIDRA_REQUEST_TIMEOUT_MS,
|
||||
}),
|
||||
limitations: Object.freeze(limitationsFor(contract.name)),
|
||||
}),
|
||||
),
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
/** Capabilities advertised for the experimental Windows x64 P0 boundary. */
|
||||
|
|
|
|||
|
|
@ -18,10 +18,7 @@ import type { BinaryTarget } from "../domain/binaryTarget.js";
|
|||
import type { Logger } from "../logger.js";
|
||||
import { AnalysisCapabilityUnavailableError } from "../domain/errors.js";
|
||||
import { err } from "../domain/result.js";
|
||||
import {
|
||||
ENHANCED_TOOL_CONTRACTS,
|
||||
OFFICIAL_TOOL_CONTRACTS,
|
||||
} from "../contracts/toolContracts.js";
|
||||
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
|
||||
import { HopperApplicationLauncher } from "./BridgeLauncher.js";
|
||||
import {
|
||||
hopperLoaderArgsForTarget,
|
||||
|
|
@ -47,22 +44,29 @@ const MUTATING_OPERATIONS = new Set([
|
|||
|
||||
/** Tool contracts implemented directly by the Hopper adapter. */
|
||||
export const HOPPER_PROVIDER_TOOL_CONTRACTS = Object.freeze([
|
||||
...OFFICIAL_TOOL_CONTRACTS,
|
||||
...ENHANCED_TOOL_CONTRACTS.filter(
|
||||
(contract) => contract.name === "analyze_function",
|
||||
...GENERATED_MCP_TOOL_CATALOG.filter(({ kind }) => kind === "official-proxy"),
|
||||
...GENERATED_MCP_TOOL_CATALOG.filter(
|
||||
({ name }) => name === "analyze_function",
|
||||
),
|
||||
]);
|
||||
|
||||
const CAPABILITIES: readonly CapabilityDescriptor[] = Object.freeze(
|
||||
HOPPER_PROVIDER_TOOL_CONTRACTS.map((contract) =>
|
||||
Object.freeze({
|
||||
HOPPER_PROVIDER_TOOL_CONTRACTS.map((contract) => {
|
||||
const operation = contract.analysisOperation;
|
||||
if (operation === null)
|
||||
throw new TypeError(`Missing analysis operation for ${contract.name}`);
|
||||
return Object.freeze({
|
||||
provider: IDENTITY,
|
||||
operation: contract.name,
|
||||
operation,
|
||||
inputContractVersion: 1,
|
||||
outputContractVersion: 1,
|
||||
available: true,
|
||||
reason: null,
|
||||
pagination: "offset" in contract.inputSchema.shape ? "offset" : "none",
|
||||
pagination:
|
||||
isRecord(contract.inputSchema.properties) &&
|
||||
"offset" in contract.inputSchema.properties
|
||||
? "offset"
|
||||
: "none",
|
||||
exhaustive: false,
|
||||
effects: Object.freeze({
|
||||
mutatesArtifact: MUTATING_OPERATIONS.has(contract.name),
|
||||
|
|
@ -81,10 +85,14 @@ const CAPABILITIES: readonly CapabilityDescriptor[] = Object.freeze(
|
|||
limitations: Object.freeze([
|
||||
"Results depend on Hopper's completed static analysis.",
|
||||
]),
|
||||
}),
|
||||
),
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
/** Concrete analysis provider backed by REA's private Hopper bridge. */
|
||||
export class HopperProvider implements AnalysisProviderCandidate {
|
||||
constructor(
|
||||
|
|
|
|||
31
src/main.ts
31
src/main.ts
|
|
@ -10,10 +10,6 @@ import { createBinarySession } from "./application/runtime.js";
|
|||
import { createLogger } from "./logger.js";
|
||||
import { projectAnalysisError } from "./domain/errors.js";
|
||||
import { loadConfiguredPermissionAuthority } from "./application/PermissionConfiguration.js";
|
||||
import { CdpBrowserProvider } from "./browser/CdpBrowserProvider.js";
|
||||
import { CdpElectronProvider } from "./browser/CdpElectronProvider.js";
|
||||
import { V8InspectorProvider } from "./browser/V8InspectorProvider.js";
|
||||
import { PlaywrightBrowserScenarioProvider } from "./browser/PlaywrightBrowserScenarioProvider.js";
|
||||
import type { RuntimeDependencies } from "./main/types.js";
|
||||
import { SERVER_START_FAILED } from "./main/messages.js";
|
||||
import { createRuntimeState } from "./main/state.js";
|
||||
|
|
@ -73,10 +69,6 @@ export const run = async (
|
|||
}
|
||||
|
||||
const session = createBinarySession(config.value, logger);
|
||||
const browserObservation = new CdpBrowserProvider();
|
||||
const browserScenarioCapture = new PlaywrightBrowserScenarioProvider();
|
||||
const electronObservation = new CdpElectronProvider();
|
||||
const javascriptRuntimeObservation = new V8InspectorProvider();
|
||||
const opened = await openInitialTarget(
|
||||
session,
|
||||
config.value,
|
||||
|
|
@ -88,10 +80,25 @@ export const run = async (
|
|||
const transport = await startMcpTransport(dependencies, session, {
|
||||
logger,
|
||||
serverLogger,
|
||||
browserObservation,
|
||||
browserScenarioCapture,
|
||||
electronObservation,
|
||||
javascriptRuntimeObservation,
|
||||
loadOptionalProviders: async () => {
|
||||
const [
|
||||
{ CdpBrowserProvider },
|
||||
{ PlaywrightBrowserScenarioProvider },
|
||||
{ CdpElectronProvider },
|
||||
{ V8InspectorProvider },
|
||||
] = await Promise.all([
|
||||
import("./browser/CdpBrowserProvider.js"),
|
||||
import("./browser/PlaywrightBrowserScenarioProvider.js"),
|
||||
import("./browser/CdpElectronProvider.js"),
|
||||
import("./browser/V8InspectorProvider.js"),
|
||||
]);
|
||||
return {
|
||||
browserObservation: new CdpBrowserProvider(),
|
||||
browserScenarioCapture: new PlaywrightBrowserScenarioProvider(),
|
||||
electronObservation: new CdpElectronProvider(),
|
||||
javascriptRuntimeObservation: new V8InspectorProvider(),
|
||||
};
|
||||
},
|
||||
permissionAuthority: permissionAuthority.value,
|
||||
runtimeState,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import type { StdioServerHandle } from "@modelcontextprotocol/server/stdio";
|
||||
|
||||
import type { BinarySession } from "../application/BinarySession.js";
|
||||
import type { CdpBrowserProvider } from "../browser/CdpBrowserProvider.js";
|
||||
import type { CdpElectronProvider } from "../browser/CdpElectronProvider.js";
|
||||
import type { V8InspectorProvider } from "../browser/V8InspectorProvider.js";
|
||||
import type { PlaywrightBrowserScenarioProvider } from "../browser/PlaywrightBrowserScenarioProvider.js";
|
||||
import type { PermissionAuthority } from "../application/PermissionAuthority.js";
|
||||
import type { Logger } from "../logger.js";
|
||||
import { createServer } from "../server/createServer.js";
|
||||
import {
|
||||
createServer,
|
||||
type CreateServerOptions,
|
||||
} from "../server/createServer.js";
|
||||
import type { RuntimeDependencies } from "./types.js";
|
||||
import type { RuntimeState } from "./state.js";
|
||||
import {
|
||||
|
|
@ -20,10 +19,9 @@ type McpServerInstance = ReturnType<typeof createServer>;
|
|||
interface ServerContext {
|
||||
readonly logger: Logger;
|
||||
readonly serverLogger: Logger;
|
||||
readonly browserObservation: CdpBrowserProvider;
|
||||
readonly browserScenarioCapture: PlaywrightBrowserScenarioProvider;
|
||||
readonly electronObservation: CdpElectronProvider;
|
||||
readonly javascriptRuntimeObservation: V8InspectorProvider;
|
||||
readonly loadOptionalProviders: NonNullable<
|
||||
CreateServerOptions["loadOptionalProviders"]
|
||||
>;
|
||||
readonly permissionAuthority: PermissionAuthority;
|
||||
readonly runtimeState: RuntimeState;
|
||||
}
|
||||
|
|
@ -58,11 +56,7 @@ export const startMcpTransport = async (
|
|||
analysisSnapshotFilePolicy:
|
||||
serverContext.runtimeState.snapshotPolicy,
|
||||
permissionAuthority: serverContext.permissionAuthority,
|
||||
browserObservation: serverContext.browserObservation,
|
||||
browserScenarioCapture: serverContext.browserScenarioCapture,
|
||||
electronObservation: serverContext.electronObservation,
|
||||
javascriptRuntimeObservation:
|
||||
serverContext.javascriptRuntimeObservation,
|
||||
loadOptionalProviders: serverContext.loadOptionalProviders,
|
||||
artifactIntegrityContinueEnabled: () =>
|
||||
serverContext.runtimeState.currentConfig
|
||||
.artifactIntegrityContinueEnabled,
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import {
|
|||
} from "./catalogIdentity.js";
|
||||
import { PROMPT_CONTRACTS } from "./contracts/promptContracts.js";
|
||||
import { PRODUCT_IDENTITY } from "./identity.js";
|
||||
import { MCP_STARTUP_POLICY } from "./mcpStartupPolicy.js";
|
||||
|
||||
const DEFAULT_DEADLINE_MS = 20_000;
|
||||
const STDERR_LIMIT_BYTES = 64 * 1_024;
|
||||
const OUTPUT_FORMATS = ["toon", "json", "yaml", "md", "jsonl"] as const;
|
||||
type OutputFormat = (typeof OUTPUT_FORMATS)[number];
|
||||
|
|
@ -40,7 +40,8 @@ interface McpDoctorCheck {
|
|||
|
||||
/** Diagnose the actual production stdio adapter with one bounded SDK session. */
|
||||
export const runProductionMcpDoctor = async (options: McpDoctorOptions) => {
|
||||
const deadline = Date.now() + (options.deadlineMs ?? DEFAULT_DEADLINE_MS);
|
||||
const deadline =
|
||||
Date.now() + (options.deadlineMs ?? MCP_STARTUP_POLICY.doctorDeadlineMs);
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(
|
||||
() => controller.abort(new Error("Production MCP doctor deadline expired")),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* Supported MCP startup deadlines and regression budgets.
|
||||
*
|
||||
* The Codex timeout is caller-facing configuration. The smaller phase budgets
|
||||
* preserve operational headroom, while the doctor deadline covers additional
|
||||
* inventory requests after initialization.
|
||||
*/
|
||||
export const MCP_STARTUP_POLICY = {
|
||||
codexStartupTimeoutSeconds: 30,
|
||||
initializeBudgetMs: 15_000,
|
||||
toolsListBudgetMs: 5_000,
|
||||
firstCatalogBudgetMs: 20_000,
|
||||
doctorDeadlineMs: 30_000,
|
||||
} as const;
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
import type {
|
||||
AnyToolHandler,
|
||||
JsonSchemaValidator,
|
||||
jsonSchemaValidator,
|
||||
McpServer,
|
||||
RegisteredTool,
|
||||
StandardSchemaWithJSON,
|
||||
} from "@modelcontextprotocol/server";
|
||||
import {
|
||||
fromJsonSchema,
|
||||
isCallToolResult,
|
||||
isInputRequiredResult,
|
||||
} from "@modelcontextprotocol/server";
|
||||
|
||||
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
|
||||
import type { ToolKind } from "../contracts/toolContractTypes.js";
|
||||
|
||||
type HydrateToolRegistrations = (kind: ToolKind) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Advertise the generated MCP catalog immediately and hydrate its callbacks
|
||||
* through the existing registrars on first execution.
|
||||
*/
|
||||
export class LazyToolCatalog {
|
||||
readonly #server: McpServer;
|
||||
readonly #hydrateRegistrations: HydrateToolRegistrations;
|
||||
readonly #includeSessionTools: boolean;
|
||||
readonly #tools = new Map<string, RegisteredTool>();
|
||||
readonly #hydrations = new Map<ToolKind, Promise<void>>();
|
||||
#hydrationQueue: Promise<void> = Promise.resolve();
|
||||
#closed = false;
|
||||
|
||||
constructor(
|
||||
server: McpServer,
|
||||
hydrateRegistrations: HydrateToolRegistrations,
|
||||
includeSessionTools = true,
|
||||
) {
|
||||
this.#server = server;
|
||||
this.#hydrateRegistrations = hydrateRegistrations;
|
||||
this.#includeSessionTools = includeSessionTools;
|
||||
}
|
||||
|
||||
/** Register every generated tool with a lazy callback. */
|
||||
register(): void {
|
||||
for (const contract of GENERATED_MCP_TOOL_CATALOG) {
|
||||
if (contract.requiresSession && !this.#includeSessionTools) continue;
|
||||
const inputSchema = advertisedSchema(contract.inputSchema);
|
||||
const outputSchema = advertisedSchema(contract.outputSchema);
|
||||
let registered: RegisteredTool | undefined;
|
||||
const callback: AnyToolHandler<StandardSchemaWithJSON> = async (
|
||||
input,
|
||||
context,
|
||||
) => {
|
||||
await this.#hydrate(contract.kind);
|
||||
const handler = registered?.handler;
|
||||
if (handler === undefined || handler === callback)
|
||||
throw new Error(`MCP tool ${contract.name} was not hydrated`);
|
||||
const result: unknown = await Reflect.apply(handler, undefined, [
|
||||
input,
|
||||
context,
|
||||
]);
|
||||
if (isCallToolResult(result) || isInputRequiredResult(result))
|
||||
return result;
|
||||
throw new TypeError(
|
||||
`MCP tool ${contract.name} returned an invalid result`,
|
||||
);
|
||||
};
|
||||
registered = this.#server.registerTool(
|
||||
contract.name,
|
||||
{
|
||||
title: contract.title,
|
||||
description: contract.description,
|
||||
inputSchema,
|
||||
outputSchema,
|
||||
annotations: contract.annotations,
|
||||
},
|
||||
callback,
|
||||
);
|
||||
this.#tools.set(contract.name, registered);
|
||||
}
|
||||
}
|
||||
|
||||
/** Wait for in-flight hydration and reject future first-use loading. */
|
||||
async close(): Promise<void> {
|
||||
this.#closed = true;
|
||||
await Promise.allSettled(this.#hydrations.values());
|
||||
}
|
||||
|
||||
async #hydrate(kind: ToolKind): Promise<void> {
|
||||
if (this.#closed) throw new Error("MCP tool catalog is closed");
|
||||
const existing = this.#hydrations.get(kind);
|
||||
if (existing !== undefined) {
|
||||
await existing;
|
||||
return;
|
||||
}
|
||||
const hydration = this.#hydrationQueue
|
||||
.catch(() => undefined)
|
||||
.then(() => this.#runHydration(kind));
|
||||
this.#hydrationQueue = hydration;
|
||||
this.#hydrations.set(kind, hydration);
|
||||
try {
|
||||
await hydration;
|
||||
} catch (cause: unknown) {
|
||||
if (this.#hydrations.get(kind) === hydration)
|
||||
this.#hydrations.delete(kind);
|
||||
throw cause;
|
||||
}
|
||||
}
|
||||
|
||||
async #runHydration(kind: ToolKind): Promise<void> {
|
||||
if (this.#closed) throw new Error("MCP tool catalog is closed");
|
||||
const expected = GENERATED_MCP_TOOL_CATALOG.filter(
|
||||
(contract) =>
|
||||
contract.kind === kind &&
|
||||
(this.#includeSessionTools || !contract.requiresSession),
|
||||
).map((contract) => contract.name);
|
||||
const expectedNames = new Set(expected);
|
||||
const hydrated = new Set<string>();
|
||||
const originalRegisterTool = this.#server.registerTool;
|
||||
const hydrationRegisterTool = (...args: unknown[]): unknown => {
|
||||
const [name, config, callback] = args;
|
||||
if (
|
||||
typeof name !== "string" ||
|
||||
!isRecord(config) ||
|
||||
typeof callback !== "function"
|
||||
)
|
||||
throw new TypeError("Invalid lazy MCP tool registration");
|
||||
const registered = this.#tools.get(name);
|
||||
if (registered === undefined || !expectedNames.has(name))
|
||||
throw new TypeError(
|
||||
`MCP tool ${name} is not in the ${kind} catalog family`,
|
||||
);
|
||||
if (hydrated.has(name))
|
||||
throw new TypeError(`MCP tool ${name} was hydrated more than once`);
|
||||
hydrated.add(name);
|
||||
Reflect.apply(registered.update, registered, [
|
||||
{
|
||||
title: config.title,
|
||||
description: config.description,
|
||||
paramsSchema: config.inputSchema,
|
||||
outputSchema: config.outputSchema,
|
||||
annotations: config.annotations,
|
||||
callback,
|
||||
},
|
||||
]);
|
||||
return registered;
|
||||
};
|
||||
if (!Reflect.set(this.#server, "registerTool", hydrationRegisterTool))
|
||||
throw new TypeError("Unable to install lazy MCP tool hydration");
|
||||
try {
|
||||
await this.#hydrateRegistrations(kind);
|
||||
} finally {
|
||||
Reflect.set(this.#server, "registerTool", originalRegisterTool);
|
||||
}
|
||||
const missing = expected.filter((name) => !hydrated.has(name));
|
||||
if (missing.length > 0)
|
||||
throw new TypeError(
|
||||
`Generated MCP tools were not hydrated: ${missing.join(", ")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const advertisedSchema = (
|
||||
jsonSchema: Readonly<Record<string, unknown>>,
|
||||
): StandardSchemaWithJSON =>
|
||||
fromJsonSchema(jsonSchema, DEFERRED_VALIDATION_PROVIDER);
|
||||
|
||||
class DeferredValidationProvider implements jsonSchemaValidator {
|
||||
getValidator<T>(): JsonSchemaValidator<T> {
|
||||
return (input: unknown) => ({
|
||||
valid: true,
|
||||
data: input as T,
|
||||
errorMessage: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const DEFERRED_VALIDATION_PROVIDER = new DeferredValidationProvider();
|
||||
|
||||
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
|
@ -10,29 +10,16 @@ import {
|
|||
import type { AnalysisOperationPort } from "../application/AnalysisProvider.js";
|
||||
import type { BinarySessionPort } from "../application/BinarySession.js";
|
||||
import { PRODUCT_IDENTITY } from "../identity.js";
|
||||
import { registerEnhancedTools } from "./registerEnhancedTools.js";
|
||||
import { registerOfficialTools } from "./registerOfficialTools.js";
|
||||
import { registerSessionTools } from "./registerSessionTools.js";
|
||||
import { registerNativeTools } from "./registerNativeTools.js";
|
||||
import { registerArtifactTools } from "./registerArtifactTools.js";
|
||||
import { registerManagedTools } from "./registerManagedTools.js";
|
||||
import { registerManagedWorkflowTools } from "./registerManagedWorkflowTools.js";
|
||||
import { silentLogger, type Logger } from "../logger.js";
|
||||
import type { ProcessExecutionPolicy } from "../domain/processCapture.js";
|
||||
import type { EvidenceFilePolicy } from "../domain/evidenceBundle.js";
|
||||
import { registerGuidedPrompts } from "./registerPrompts.js";
|
||||
import { registerEvidenceResources } from "./registerEvidenceResources.js";
|
||||
import { createServerIdentity } from "../serverIdentity.js";
|
||||
import type { PermissionAuthority } from "../application/PermissionAuthority.js";
|
||||
import type { BrowserObservationPort } from "../application/BrowserObservationPort.js";
|
||||
import type { BrowserScenarioCapturePort } from "../application/BrowserScenarioCapturePort.js";
|
||||
import { registerBrowserTools } from "./registerBrowserTools.js";
|
||||
import { registerBrowserScenarioTool } from "./registerBrowserScenarioTool.js";
|
||||
import type { ElectronObservationPort } from "../application/ElectronObservationPort.js";
|
||||
import { registerElectronTools } from "./registerElectronTools.js";
|
||||
import type { JavaScriptRuntimeObservationPort } from "../application/JavaScriptRuntimeObservationPort.js";
|
||||
import { registerJavaScriptRuntimeObservationTools } from "./registerJavaScriptRuntimeObservationTools.js";
|
||||
import { registerApplicationTools } from "./registerApplicationTools.js";
|
||||
import type { SessionAvailability } from "./sessionAvailabilityPolicy.js";
|
||||
import { sessionAvailabilityPolicy } from "./sessionAvailabilityPolicy.js";
|
||||
import {
|
||||
|
|
@ -52,12 +39,11 @@ import type {
|
|||
JavaScriptReplayRunner,
|
||||
} from "../application/JavaScriptReplayPlanning.js";
|
||||
import type { ManagedRuntimePolicy } from "../application/ManagedRuntimeCorrelationService.js";
|
||||
import { LinuxJavaScriptReplayRunner } from "../replay/LinuxJavaScriptReplayRunner.js";
|
||||
import { SystemJavaScriptReplayHost } from "../replay/SystemJavaScriptReplayHost.js";
|
||||
import {
|
||||
PROCESS_CAPTURE_ELICITATION_POLICY,
|
||||
type ProcessCaptureElicitationState,
|
||||
} from "./ProcessCaptureElicitation.js";
|
||||
import { LazyToolCatalog } from "./LazyToolCatalog.js";
|
||||
|
||||
export interface CreateServerOptions {
|
||||
readonly logger?: Logger;
|
||||
|
|
@ -76,6 +62,12 @@ export interface CreateServerOptions {
|
|||
readonly javascriptReplayRunner?: JavaScriptReplayRunner;
|
||||
readonly managedRuntimePolicy?: ManagedRuntimePolicy;
|
||||
readonly availabilityPolicy?: () => SessionAvailability;
|
||||
readonly loadOptionalProviders?: () => Promise<{
|
||||
readonly browserObservation: BrowserObservationPort;
|
||||
readonly browserScenarioCapture: BrowserScenarioCapturePort;
|
||||
readonly electronObservation: ElectronObservationPort;
|
||||
readonly javascriptRuntimeObservation: JavaScriptRuntimeObservationPort;
|
||||
}>;
|
||||
}
|
||||
|
||||
const installSessionToolAvailability = (
|
||||
|
|
@ -155,51 +147,88 @@ export const createServer = (
|
|||
const toolLogger = logger.child({ layer: "server" });
|
||||
const { activeTarget, recordEvidence, recordEvidenceWithUnknown } =
|
||||
createSessionRecorders(server, session);
|
||||
const toolContext = {
|
||||
server,
|
||||
analysis,
|
||||
session,
|
||||
options,
|
||||
logger: toolLogger,
|
||||
permissionAuthority,
|
||||
activeTarget,
|
||||
recordEvidence,
|
||||
recordEvidenceWithUnknown,
|
||||
const processCaptureElicitation = {
|
||||
stateCodec: processCaptureStateCodec,
|
||||
supported: (context: {
|
||||
readonly mcpReq: {
|
||||
readonly envelope?: Readonly<Record<string, unknown>>;
|
||||
};
|
||||
}) => {
|
||||
const envelope = context.mcpReq.envelope;
|
||||
const version = envelope?.[PROTOCOL_VERSION_META_KEY];
|
||||
const capabilities = envelope?.[CLIENT_CAPABILITIES_META_KEY];
|
||||
return (
|
||||
typeof version === "string" &&
|
||||
PROCESS_CAPTURE_ELICITATION_POLICY.protocolVersions.some(
|
||||
(supported) => supported === version,
|
||||
) &&
|
||||
isRecord(capabilities) &&
|
||||
isRecord(capabilities.elicitation) &&
|
||||
capabilities.elicitation.form !== undefined
|
||||
);
|
||||
},
|
||||
now: Date.now,
|
||||
consumedNonces: new Map<string, number>(),
|
||||
};
|
||||
registerBinaryAnalysisTools(toolContext);
|
||||
registerObservationTools(toolContext);
|
||||
let optionalProviders:
|
||||
| ReturnType<NonNullable<CreateServerOptions["loadOptionalProviders"]>>
|
||||
| undefined;
|
||||
const loadOptionalProviders = () => {
|
||||
optionalProviders ??= options
|
||||
.loadOptionalProviders?.()
|
||||
.catch((cause: unknown) => {
|
||||
optionalProviders = undefined;
|
||||
throw cause;
|
||||
});
|
||||
return optionalProviders;
|
||||
};
|
||||
const lazyTools = new LazyToolCatalog(
|
||||
server,
|
||||
async (kind) => {
|
||||
const needsObservationProviders =
|
||||
kind === "browser-provider" ||
|
||||
kind === "electron-provider" ||
|
||||
kind === "runtime-provider";
|
||||
const [{ hydrateServerToolFamily }, loadedOptionalProviders] =
|
||||
await Promise.all([
|
||||
import("./hydrateServerTools.js"),
|
||||
needsObservationProviders ? loadOptionalProviders() : undefined,
|
||||
]);
|
||||
const hydratedOptions =
|
||||
loadedOptionalProviders === undefined
|
||||
? options
|
||||
: { ...options, ...loadedOptionalProviders };
|
||||
await hydrateServerToolFamily({
|
||||
kind,
|
||||
server,
|
||||
analysis,
|
||||
session,
|
||||
options: hydratedOptions,
|
||||
context: {
|
||||
logger: toolLogger,
|
||||
permissionAuthority,
|
||||
activeTarget,
|
||||
recordEvidence,
|
||||
recordEvidenceWithUnknown,
|
||||
availabilityPolicy: dynamicTools?.policy,
|
||||
startedAt,
|
||||
processCaptureElicitation,
|
||||
},
|
||||
});
|
||||
},
|
||||
session !== undefined,
|
||||
);
|
||||
lazyTools.register();
|
||||
registerGuidedPrompts(server, analysis, session);
|
||||
if (session !== undefined) {
|
||||
registerEvidenceResources(server, session);
|
||||
registerSessionTools(server, session, toolLogger, {
|
||||
...options,
|
||||
...(dynamicTools === undefined
|
||||
? {}
|
||||
: { availabilityPolicy: dynamicTools.policy }),
|
||||
...(permissionAuthority === undefined ? {} : { permissionAuthority }),
|
||||
startedAt,
|
||||
processCaptureElicitation: {
|
||||
stateCodec: processCaptureStateCodec,
|
||||
supported: (context) => {
|
||||
const envelope = context.mcpReq.envelope;
|
||||
const version = envelope?.[PROTOCOL_VERSION_META_KEY];
|
||||
const capabilities = envelope?.[CLIENT_CAPABILITIES_META_KEY];
|
||||
return (
|
||||
typeof version === "string" &&
|
||||
PROCESS_CAPTURE_ELICITATION_POLICY.protocolVersions.some(
|
||||
(supported) => supported === version,
|
||||
) &&
|
||||
isRecord(capabilities) &&
|
||||
isRecord(capabilities.elicitation) &&
|
||||
capabilities.elicitation.form !== undefined
|
||||
);
|
||||
},
|
||||
now: Date.now,
|
||||
consumedNonces: new Map(),
|
||||
},
|
||||
});
|
||||
}
|
||||
dynamicTools?.controller.synchronize();
|
||||
const close = server.close.bind(server);
|
||||
server.close = async () => {
|
||||
await lazyTools.close();
|
||||
await close();
|
||||
};
|
||||
return server;
|
||||
};
|
||||
|
||||
|
|
@ -243,7 +272,8 @@ const registerServerIdentityResource = (
|
|||
description: "Live package, SDK, protocol, and catalog identity.",
|
||||
mimeType: "application/json",
|
||||
},
|
||||
(uri) => {
|
||||
async (uri) => {
|
||||
const { createServerIdentity } = await import("../serverIdentity.js");
|
||||
const client = server.server.getClientVersion();
|
||||
const protocolVersion = server.server.getNegotiatedProtocolVersion();
|
||||
return {
|
||||
|
|
@ -267,154 +297,5 @@ const registerServerIdentityResource = (
|
|||
);
|
||||
};
|
||||
|
||||
interface ServerToolContext extends ReturnType<typeof createSessionRecorders> {
|
||||
readonly server: McpServer;
|
||||
readonly analysis: AnalysisOperationPort;
|
||||
readonly session: BinarySessionPort | undefined;
|
||||
readonly options: CreateServerOptions;
|
||||
readonly logger: Logger;
|
||||
readonly permissionAuthority: PermissionAuthority | undefined;
|
||||
}
|
||||
|
||||
const registerBinaryAnalysisTools = ({
|
||||
server,
|
||||
analysis,
|
||||
session,
|
||||
options,
|
||||
logger,
|
||||
permissionAuthority,
|
||||
activeTarget,
|
||||
recordEvidence,
|
||||
recordEvidenceWithUnknown,
|
||||
}: ServerToolContext): void => {
|
||||
const recordUnknown =
|
||||
session === undefined
|
||||
? undefined
|
||||
: (input: Parameters<typeof session.recordUnknown>[0]) =>
|
||||
session.recordUnknown(input);
|
||||
registerOfficialTools(server, analysis, {
|
||||
logger,
|
||||
activeTarget,
|
||||
recordEvidence,
|
||||
recordUnknown,
|
||||
});
|
||||
registerEnhancedTools(server, analysis, {
|
||||
logger,
|
||||
activeTarget,
|
||||
analysisProfile:
|
||||
session === undefined ? undefined : () => session.analysisProfile(),
|
||||
recordEvidence,
|
||||
recordUnknown,
|
||||
});
|
||||
registerNativeTools(server, analysis, {
|
||||
logger,
|
||||
activeTarget,
|
||||
recordEvidence,
|
||||
});
|
||||
registerArtifactTools(server, analysis, {
|
||||
logger,
|
||||
activeTarget,
|
||||
recordEvidence,
|
||||
...(permissionAuthority === undefined ? {} : { permissionAuthority }),
|
||||
});
|
||||
registerManagedTools(server, analysis, {
|
||||
logger,
|
||||
activeTarget,
|
||||
recordEvidence,
|
||||
session,
|
||||
});
|
||||
if (session !== undefined)
|
||||
registerManagedWorkflowTools(server, {
|
||||
logger,
|
||||
recordEvidence,
|
||||
recordEvidenceWithUnknown,
|
||||
session,
|
||||
runtime: {
|
||||
policy: options.managedRuntimePolicy ?? {
|
||||
enabled: false,
|
||||
roots: [],
|
||||
executablePath: "/usr/bin/dotnet",
|
||||
},
|
||||
authority: permissionAuthority,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const registerObservationTools = ({
|
||||
server,
|
||||
session,
|
||||
options,
|
||||
logger,
|
||||
permissionAuthority,
|
||||
recordEvidence,
|
||||
recordEvidenceWithUnknown,
|
||||
}: ServerToolContext): void => {
|
||||
registerBrowserTools(server, {
|
||||
logger,
|
||||
browser: options.browserObservation,
|
||||
permissionAuthority,
|
||||
recordEvidence,
|
||||
});
|
||||
registerBrowserScenarioTool(server, {
|
||||
logger,
|
||||
provider: options.browserScenarioCapture,
|
||||
permissionAuthority,
|
||||
recordEvidence,
|
||||
});
|
||||
registerElectronTools(server, {
|
||||
logger,
|
||||
electron: options.electronObservation,
|
||||
permissionAuthority,
|
||||
recordEvidence,
|
||||
});
|
||||
registerJavaScriptRuntimeObservationTools(server, {
|
||||
logger,
|
||||
runtime: options.javascriptRuntimeObservation,
|
||||
permissionAuthority,
|
||||
recordEvidence,
|
||||
});
|
||||
registerApplicationTools(server, {
|
||||
logger,
|
||||
recordEvidence,
|
||||
recordEvidenceWithUnknown,
|
||||
evidenceLookup:
|
||||
session === undefined
|
||||
? undefined
|
||||
: (evidenceId) => session.evidenceById(evidenceId),
|
||||
evidenceFilePolicy: options.evidenceFilePolicy ?? {
|
||||
roots: [],
|
||||
maxBytes: 1,
|
||||
maxDepth: 1,
|
||||
maxStringLength: 1,
|
||||
maxNodes: 1,
|
||||
},
|
||||
permissionAuthority,
|
||||
retainCoverageWorkspace:
|
||||
session === undefined
|
||||
? undefined
|
||||
: (workspace) => {
|
||||
const retained =
|
||||
session.retainReconstructionCoverageWorkspace(workspace);
|
||||
if (retained === "added") server.sendResourceListChanged();
|
||||
return retained;
|
||||
},
|
||||
replay: {
|
||||
policy: options.javascriptReplayPolicy ?? {
|
||||
enabled: false,
|
||||
roots: [],
|
||||
nodePath: process.execPath,
|
||||
bubblewrapPath: "/usr/bin/bwrap",
|
||||
systemdRunPath: "/usr/bin/systemd-run",
|
||||
systemctlPath: "/usr/bin/systemctl",
|
||||
shellPath: "/usr/bin/bash",
|
||||
},
|
||||
host: options.javascriptReplayHost ?? new SystemJavaScriptReplayHost(),
|
||||
runner:
|
||||
options.javascriptReplayRunner ?? new LinuxJavaScriptReplayRunner(),
|
||||
authority: permissionAuthority,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,300 @@
|
|||
import type { McpServer } from "@modelcontextprotocol/server";
|
||||
|
||||
import type { AnalysisOperationPort } from "../application/AnalysisProvider.js";
|
||||
import type { BinarySessionPort } from "../application/BinarySession.js";
|
||||
import type { PermissionAuthority } from "../application/PermissionAuthority.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTarget.js";
|
||||
import type { Logger } from "../logger.js";
|
||||
import type { ToolKind } from "../contracts/toolContractTypes.js";
|
||||
import type { CreateServerOptions } from "./createServer.js";
|
||||
import type { ProcessCaptureElicitation } from "./ProcessCaptureElicitation.js";
|
||||
import type { SessionAvailability } from "./sessionAvailabilityPolicy.js";
|
||||
|
||||
interface HydratedToolContext {
|
||||
readonly logger: Logger;
|
||||
readonly permissionAuthority: PermissionAuthority | undefined;
|
||||
readonly activeTarget: (() => BinaryTarget | undefined) | undefined;
|
||||
readonly recordEvidence: BinarySessionPort["recordEvidence"] | undefined;
|
||||
readonly recordEvidenceWithUnknown:
|
||||
| BinarySessionPort["recordEvidenceWithUnknown"]
|
||||
| undefined;
|
||||
readonly availabilityPolicy: (() => SessionAvailability) | undefined;
|
||||
readonly startedAt: string;
|
||||
readonly processCaptureElicitation: ProcessCaptureElicitation;
|
||||
}
|
||||
|
||||
export interface HydrateServerToolFamilyInput {
|
||||
readonly kind: ToolKind;
|
||||
readonly server: McpServer;
|
||||
readonly analysis: AnalysisOperationPort;
|
||||
readonly session: BinarySessionPort | undefined;
|
||||
readonly options: CreateServerOptions;
|
||||
readonly context: HydratedToolContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install production callbacks on generated lazy tool registrations.
|
||||
*/
|
||||
export const hydrateServerToolFamily = async ({
|
||||
kind,
|
||||
server,
|
||||
analysis,
|
||||
session,
|
||||
options,
|
||||
context,
|
||||
}: HydrateServerToolFamilyInput): Promise<void> => {
|
||||
const toolContext = {
|
||||
server,
|
||||
analysis,
|
||||
session,
|
||||
options,
|
||||
logger: context.logger,
|
||||
permissionAuthority: context.permissionAuthority,
|
||||
activeTarget: context.activeTarget,
|
||||
recordEvidence: context.recordEvidence,
|
||||
recordEvidenceWithUnknown: context.recordEvidenceWithUnknown,
|
||||
};
|
||||
switch (kind) {
|
||||
case "official-proxy": {
|
||||
const { registerOfficialTools } = await import(
|
||||
"./registerOfficialTools.js"
|
||||
);
|
||||
registerOfficialTools(server, analysis, analysisOptions(toolContext));
|
||||
return;
|
||||
}
|
||||
case "enhanced": {
|
||||
const { registerEnhancedTools } = await import(
|
||||
"./registerEnhancedTools.js"
|
||||
);
|
||||
registerEnhancedTools(server, analysis, enhancedOptions(toolContext));
|
||||
return;
|
||||
}
|
||||
case "native-provider": {
|
||||
const { registerNativeTools } = await import("./registerNativeTools.js");
|
||||
registerNativeTools(server, analysis, evidenceOptions(toolContext));
|
||||
return;
|
||||
}
|
||||
case "artifact-provider": {
|
||||
const { registerArtifactTools } = await import(
|
||||
"./registerArtifactTools.js"
|
||||
);
|
||||
registerArtifactTools(server, analysis, {
|
||||
...evidenceOptions(toolContext),
|
||||
...(context.permissionAuthority === undefined
|
||||
? {}
|
||||
: { permissionAuthority: context.permissionAuthority }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
case "managed-provider": {
|
||||
const { registerManagedTools } = await import(
|
||||
"./registerManagedTools.js"
|
||||
);
|
||||
registerManagedTools(server, analysis, {
|
||||
...evidenceOptions(toolContext),
|
||||
session,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case "application":
|
||||
await registerApplicationFamily(toolContext);
|
||||
return;
|
||||
case "browser-provider":
|
||||
case "electron-provider":
|
||||
case "runtime-provider": {
|
||||
await hydrateObservationFamily(kind, toolContext);
|
||||
return;
|
||||
}
|
||||
case "session": {
|
||||
if (session === undefined)
|
||||
throw new TypeError("Session tools require a binary session");
|
||||
const { registerSessionTools } = await import(
|
||||
"./registerSessionTools.js"
|
||||
);
|
||||
registerSessionTools(server, session, context.logger, {
|
||||
...options,
|
||||
...(context.availabilityPolicy === undefined
|
||||
? {}
|
||||
: { availabilityPolicy: context.availabilityPolicy }),
|
||||
...(context.permissionAuthority === undefined
|
||||
? {}
|
||||
: { permissionAuthority: context.permissionAuthority }),
|
||||
startedAt: context.startedAt,
|
||||
processCaptureElicitation: context.processCaptureElicitation,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const hydrateObservationFamily = async (
|
||||
kind: "browser-provider" | "electron-provider" | "runtime-provider",
|
||||
context: ServerToolContext,
|
||||
): Promise<void> => {
|
||||
const common = {
|
||||
logger: context.logger,
|
||||
permissionAuthority: context.permissionAuthority,
|
||||
recordEvidence: context.recordEvidence,
|
||||
};
|
||||
if (kind === "browser-provider") {
|
||||
const [{ registerBrowserTools }, { registerBrowserScenarioTool }] =
|
||||
await Promise.all([
|
||||
import("./registerBrowserTools.js"),
|
||||
import("./registerBrowserScenarioTool.js"),
|
||||
]);
|
||||
registerBrowserTools(context.server, {
|
||||
...common,
|
||||
browser: context.options.browserObservation,
|
||||
});
|
||||
registerBrowserScenarioTool(context.server, {
|
||||
...common,
|
||||
provider: context.options.browserScenarioCapture,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (kind === "electron-provider") {
|
||||
const { registerElectronTools } = await import(
|
||||
"./registerElectronTools.js"
|
||||
);
|
||||
registerElectronTools(context.server, {
|
||||
...common,
|
||||
electron: context.options.electronObservation,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { registerJavaScriptRuntimeObservationTools } = await import(
|
||||
"./registerJavaScriptRuntimeObservationTools.js"
|
||||
);
|
||||
registerJavaScriptRuntimeObservationTools(context.server, {
|
||||
...common,
|
||||
runtime: context.options.javascriptRuntimeObservation,
|
||||
});
|
||||
};
|
||||
|
||||
interface ServerToolContext {
|
||||
readonly server: McpServer;
|
||||
readonly analysis: AnalysisOperationPort;
|
||||
readonly session: BinarySessionPort | undefined;
|
||||
readonly options: CreateServerOptions;
|
||||
readonly logger: Logger;
|
||||
readonly permissionAuthority: PermissionAuthority | undefined;
|
||||
readonly activeTarget: (() => BinaryTarget | undefined) | undefined;
|
||||
readonly recordEvidence: BinarySessionPort["recordEvidence"] | undefined;
|
||||
readonly recordEvidenceWithUnknown:
|
||||
| BinarySessionPort["recordEvidenceWithUnknown"]
|
||||
| undefined;
|
||||
}
|
||||
|
||||
const analysisOptions = ({
|
||||
logger,
|
||||
activeTarget,
|
||||
recordEvidence,
|
||||
session,
|
||||
}: ServerToolContext) => {
|
||||
const recordUnknown =
|
||||
session === undefined
|
||||
? undefined
|
||||
: (input: Parameters<typeof session.recordUnknown>[0]) =>
|
||||
session.recordUnknown(input);
|
||||
return {
|
||||
logger,
|
||||
activeTarget,
|
||||
recordEvidence,
|
||||
recordUnknown,
|
||||
};
|
||||
};
|
||||
|
||||
const enhancedOptions = (context: ServerToolContext) => ({
|
||||
...analysisOptions(context),
|
||||
analysisProfile:
|
||||
context.session === undefined
|
||||
? undefined
|
||||
: () => context.session?.analysisProfile(),
|
||||
});
|
||||
|
||||
const evidenceOptions = ({
|
||||
logger,
|
||||
activeTarget,
|
||||
recordEvidence,
|
||||
}: ServerToolContext) => ({
|
||||
logger,
|
||||
activeTarget,
|
||||
recordEvidence,
|
||||
});
|
||||
|
||||
const registerApplicationFamily = async ({
|
||||
server,
|
||||
session,
|
||||
options,
|
||||
logger,
|
||||
permissionAuthority,
|
||||
recordEvidence,
|
||||
recordEvidenceWithUnknown,
|
||||
}: ServerToolContext): Promise<void> => {
|
||||
const [
|
||||
{ registerApplicationTools },
|
||||
{ registerManagedWorkflowTools },
|
||||
{ LinuxJavaScriptReplayRunner },
|
||||
{ SystemJavaScriptReplayHost },
|
||||
] = await Promise.all([
|
||||
import("./registerApplicationTools.js"),
|
||||
import("./registerManagedWorkflowTools.js"),
|
||||
import("../replay/LinuxJavaScriptReplayRunner.js"),
|
||||
import("../replay/SystemJavaScriptReplayHost.js"),
|
||||
]);
|
||||
if (session !== undefined)
|
||||
registerManagedWorkflowTools(server, {
|
||||
logger,
|
||||
recordEvidence,
|
||||
recordEvidenceWithUnknown,
|
||||
session,
|
||||
runtime: {
|
||||
policy: options.managedRuntimePolicy ?? {
|
||||
enabled: false,
|
||||
roots: [],
|
||||
executablePath: "/usr/bin/dotnet",
|
||||
},
|
||||
authority: permissionAuthority,
|
||||
},
|
||||
});
|
||||
registerApplicationTools(server, {
|
||||
logger,
|
||||
recordEvidence,
|
||||
recordEvidenceWithUnknown,
|
||||
evidenceLookup:
|
||||
session === undefined
|
||||
? undefined
|
||||
: (evidenceId) => session.evidenceById(evidenceId),
|
||||
evidenceFilePolicy: options.evidenceFilePolicy ?? {
|
||||
roots: [],
|
||||
maxBytes: 1,
|
||||
maxDepth: 1,
|
||||
maxStringLength: 1,
|
||||
maxNodes: 1,
|
||||
},
|
||||
permissionAuthority,
|
||||
retainCoverageWorkspace:
|
||||
session === undefined
|
||||
? undefined
|
||||
: (workspace) => {
|
||||
const retained =
|
||||
session.retainReconstructionCoverageWorkspace(workspace);
|
||||
if (retained === "added") server.sendResourceListChanged();
|
||||
return retained;
|
||||
},
|
||||
replay: {
|
||||
policy: options.javascriptReplayPolicy ?? {
|
||||
enabled: false,
|
||||
roots: [],
|
||||
nodePath: process.execPath,
|
||||
bubblewrapPath: "/usr/bin/bwrap",
|
||||
systemdRunPath: "/usr/bin/systemd-run",
|
||||
systemctlPath: "/usr/bin/systemctl",
|
||||
shellPath: "/usr/bin/bash",
|
||||
},
|
||||
host: options.javascriptReplayHost ?? new SystemJavaScriptReplayHost(),
|
||||
runner:
|
||||
options.javascriptReplayRunner ?? new LinuxJavaScriptReplayRunner(),
|
||||
authority: permissionAuthority,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { MCP_STARTUP_POLICY } from "../src/mcpStartupPolicy.js";
|
||||
import {
|
||||
createEsmModuleProfileCollector,
|
||||
parseLinuxVmRssBytes,
|
||||
} from "../scripts/lib/mcp-startup-probe.mjs";
|
||||
|
||||
describe("MCP startup policy", () => {
|
||||
it("preserves headroom beneath the configured Codex timeout", () => {
|
||||
const codexTimeoutMs =
|
||||
MCP_STARTUP_POLICY.codexStartupTimeoutSeconds * 1_000;
|
||||
|
||||
expect(MCP_STARTUP_POLICY.initializeBudgetMs).toBeLessThan(codexTimeoutMs);
|
||||
expect(MCP_STARTUP_POLICY.firstCatalogBudgetMs).toBeLessThan(
|
||||
codexTimeoutMs,
|
||||
);
|
||||
expect(MCP_STARTUP_POLICY.firstCatalogBudgetMs).toBe(
|
||||
MCP_STARTUP_POLICY.initializeBudgetMs +
|
||||
MCP_STARTUP_POLICY.toolsListBudgetMs,
|
||||
);
|
||||
expect(MCP_STARTUP_POLICY.doctorDeadlineMs).toBeGreaterThanOrEqual(
|
||||
codexTimeoutMs,
|
||||
);
|
||||
});
|
||||
|
||||
it("parses Linux resident memory without accepting other fields", () => {
|
||||
expect(
|
||||
parseLinuxVmRssBytes("Name:\tnode\nVmRSS:\t 12345 kB\nThreads:\t7\n"),
|
||||
).toBe(12_641_280);
|
||||
expect(parseLinuxVmRssBytes("VmSize:\t12345 kB\n")).toBeNull();
|
||||
});
|
||||
|
||||
it("profiles unique file-backed ESM modules without retaining paths", () => {
|
||||
const collector = createEsmModuleProfileCollector();
|
||||
collector.append(
|
||||
"ESM 1: Storing file:///opt/rea/dist/main.js\n" +
|
||||
"ESM 1: Translating StandardModule file:///opt/rea/dist/main.js\n" +
|
||||
"ESM 1: Storing file:///opt/rea/node_modules/zod/index.js",
|
||||
);
|
||||
|
||||
expect(collector.result()).toEqual({
|
||||
supported: true,
|
||||
method: "node-debug-esm",
|
||||
unique_file_modules: 2,
|
||||
unique_package_modules: 0,
|
||||
unique_dependency_modules: 1,
|
||||
debug_lines: 3,
|
||||
truncated: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -105,7 +105,7 @@ describe("process-capture MCP elicitation", () => {
|
|||
await Promise.allSettled([client.close(), server.close()]);
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
}, 20_000);
|
||||
|
||||
it.each(["tampered", "expired"] as const)(
|
||||
"rejects %s signed state through the modern MCP client and server",
|
||||
|
|
|
|||
|
|
@ -87,7 +87,9 @@ describe("target-free MCP lifecycle", () => {
|
|||
expect(closed).toEqual([canonicalTargetPath]);
|
||||
expect(
|
||||
z
|
||||
.object({ result: z.object({ path: z.string(), sha256: z.string() }) })
|
||||
.object({
|
||||
result: z.object({ path: z.string(), sha256: z.string() }),
|
||||
})
|
||||
.parse(
|
||||
structured(
|
||||
await mcp.callTool({
|
||||
|
|
@ -100,7 +102,7 @@ describe("target-free MCP lifecycle", () => {
|
|||
path: canonicalTargetPath,
|
||||
sha256: z.object({ sha256: z.string() }).parse(second).sha256,
|
||||
});
|
||||
});
|
||||
}, 10_000);
|
||||
|
||||
it("reports no-target, opens, analyzes, switches, reports status, and closes", async () => {
|
||||
directory = await createTestTempDirectory("rea-mcp-session-");
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import { defineConfig } from "vitest/config";
|
|||
|
||||
export const TEST_FILES = ["tests/**/*.test.ts"];
|
||||
const IS_CONTINUOUS_INTEGRATION = process.env.CI === "true";
|
||||
// Subprocess-heavy parallel tests exceed their existing deadlines above two workers.
|
||||
// Lazy MCP integration tests are memory-bound while Vite transforms tool families.
|
||||
const MAX_TEST_WORKERS = 2;
|
||||
const MAX_UNIT_TEST_WORKERS = 4;
|
||||
const MAX_UNIT_TEST_WORKERS = 1;
|
||||
const PROCESS_CAPTURE_TEST = "tests/processCapture.test.ts";
|
||||
const CANONICAL_TEMPORARY_DIRECTORY = realpathSync(tmpdir());
|
||||
export const SERIAL_INTEGRATION_TESTS = [
|
||||
|
|
|
|||
Loading…
Reference in New Issue