fix(process): capture MCP process scenarios with the server environment (#6173)

This commit is contained in:
Adolanium
2026-10-22 21:22:03 +03:00
committed by GitHub
parent 54ea293e7c
commit 6da3839b57
3 changed files with 120 additions and 2 deletions
+6 -1
View File
@@ -88,6 +88,7 @@ interface ProcessToolRegistration {
typeof toolContract<"capture_process_scenario">
>;
readonly resourceScope: ProcessCaptureResourceScope;
readonly environment: Readonly<NodeJS.ProcessEnv>;
}
const registerProcessTools = ({
@@ -96,6 +97,7 @@ const registerProcessTools = ({
logger,
captureContract,
resourceScope,
environment,
}: ProcessToolRegistration): void => {
const admission = withAdmittedAnalysis({ kind: "session", session });
server.registerTool(
@@ -117,7 +119,7 @@ const registerProcessTools = ({
input,
context.mcpReq.signal,
process.platform,
process.env,
environment,
undefined,
undefined,
progress,
@@ -223,6 +225,8 @@ const registerOpenLifecycleTool = (
/** Register MCP-only target lifecycle operations on a long-lived session. */
export interface SessionToolOptions extends Partial<SessionAvailabilityProbes> {
readonly artifactResources: ArtifactResourceScope;
/** Server environment snapshot that process capture resolves and inherits. */
readonly environment: Readonly<NodeJS.ProcessEnv>;
readonly startedAt?: string;
readonly availabilityPolicy?: () => AvailabilityPolicy;
}
@@ -381,6 +385,7 @@ export const registerSessionTools = (
logger,
captureContract,
resourceScope: processResources,
environment: options.environment,
});
const closeServer = server.close.bind(server);
server.close = async () => {
@@ -1,5 +1,5 @@
import { fileURLToPath } from "node:url";
import { access } from "node:fs/promises";
import { access, chmod, mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
@@ -233,3 +233,55 @@ itWithCaptureCapability(
);
},
);
itWithCaptureCapability(
"resolves and inherits the server's selected environment through MCP",
async () => {
const root = await createTestTempDirectory("rea-process-mcp-env-");
const bin = join(root, "bin");
const executable = join(bin, "rea-injected-process-probe");
await mkdir(bin);
await writeFile(
executable,
[
"#!/bin/sh",
'printf "%s|%s\\n" "$REA_CAPTURE_INJECTED_MARKER" "$REA_CAPTURE_SCENARIO_OVERRIDE"',
].join("\n"),
);
await chmod(executable, 0o755);
const session = createTestBinarySession(createCacheProvider([]));
const server = createServer(
{ kind: "session", session },
{
logger: silentLogger,
environment: {
PATH: bin,
REA_CAPTURE_INJECTED_MARKER: "injected-value",
REA_CAPTURE_SCENARIO_OVERRIDE: "host-value",
},
},
);
const client = new Client({ name: "process-mcp-env", version: "1" });
resources.push(client, server, session);
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
const result = await client.callTool({
name: "capture_process_scenario",
arguments: {
executable: "rea-injected-process-probe",
working_directory: root,
environment: { REA_CAPTURE_SCENARIO_OVERRIDE: "scenario-value" },
},
});
expect(result.isError, JSON.stringify(result.content)).not.toBe(true);
const capture = toolContract("capture_process_scenario").outputSchema.parse(
result.structuredContent,
).normalized_result;
expect(capture.frames.map(({ data }) => data).join("")).toContain(
"injected-value|scenario-value",
);
},
);
@@ -0,0 +1,61 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { afterEach, expect, it, vi } from "vitest";
import { AnalysisCapabilityUnavailableError } from "../../src/domain/analysisErrorCore.js";
import { err } from "../../src/domain/result.js";
import { captureProcessScenario } from "../../src/process/capture/ProcessHarness.js";
import { createServer } from "../../src/server/createServer.js";
import { createTestBinarySession } from "../fixtures/binarySession.js";
// Windows rejects capture before it reads the environment, so record the
// environment the MCP adapter hands to the capture owner on every host.
vi.mock(
"../../src/process/capture/ProcessHarness.js",
async (importOriginal) => ({
...(await importOriginal<
typeof import("../../src/process/capture/ProcessHarness.js")
>()),
captureProcessScenario: vi.fn(async () =>
err(
new AnalysisCapabilityUnavailableError(
"rea-process",
"capture_process_scenario",
"recorded by test",
),
),
),
}),
);
const resources: Array<{ close(): Promise<unknown> }> = [];
afterEach(async () => {
await Promise.all(resources.splice(0).map((resource) => resource.close()));
vi.unstubAllEnvs();
});
it("captures MCP process scenarios with the server's selected environment", async () => {
vi.stubEnv("REA_CAPTURE_SELECTION_MARKER", "ambient");
vi.stubEnv("REA_CAPTURE_AMBIENT_ONLY", "ambient");
const session = createTestBinarySession(() => {
throw new Error("Process capture must not launch a binary provider");
});
const server = createServer(
{ kind: "session", session },
{ environment: { REA_CAPTURE_SELECTION_MARKER: "selected" } },
);
const client = new Client({ name: "capture-environment", version: "1" });
resources.push(client, server, session);
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
const result = await client.callTool({
name: "capture_process_scenario",
arguments: { executable: process.execPath },
});
expect(result.isError).toBe(true);
const environment = vi.mocked(captureProcessScenario).mock.lastCall?.[3];
expect(environment).toEqual({ REA_CAPTURE_SELECTION_MARKER: "selected" });
});