From 6da3839b572219ea4cefa62fbecc5cd626908b15 Mon Sep 17 00:00:00 2001 From: Adolanium <94890352+Adolanium@users.noreply.github.com> Date: Thu, 22 Oct 2026 21:22:03 +0300 Subject: [PATCH] fix(process): capture MCP process scenarios with the server environment (#6173) --- src/server/registerSessionTools.ts | 7 ++- .../process/processSessionMcp.test.ts | 54 +++++++++++++++- .../processCaptureServerEnvironment.test.ts | 61 +++++++++++++++++++ 3 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 tests/process-global/processCaptureServerEnvironment.test.ts diff --git a/src/server/registerSessionTools.ts b/src/server/registerSessionTools.ts index 4bc6ac6bb8..504a9f0d1d 100644 --- a/src/server/registerSessionTools.ts +++ b/src/server/registerSessionTools.ts @@ -88,6 +88,7 @@ interface ProcessToolRegistration { typeof toolContract<"capture_process_scenario"> >; readonly resourceScope: ProcessCaptureResourceScope; + readonly environment: Readonly; } 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 { readonly artifactResources: ArtifactResourceScope; + /** Server environment snapshot that process capture resolves and inherits. */ + readonly environment: Readonly; 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 () => { diff --git a/tests/boundary/process/processSessionMcp.test.ts b/tests/boundary/process/processSessionMcp.test.ts index 06035e9214..565f16a744 100644 --- a/tests/boundary/process/processSessionMcp.test.ts +++ b/tests/boundary/process/processSessionMcp.test.ts @@ -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", + ); + }, +); diff --git a/tests/process-global/processCaptureServerEnvironment.test.ts b/tests/process-global/processCaptureServerEnvironment.test.ts new file mode 100644 index 0000000000..d80ccae67d --- /dev/null +++ b/tests/process-global/processCaptureServerEnvironment.test.ts @@ -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 }> = []; +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" }); +});