From 0e7a2d2c95de1b0384bac65955b5a2dd103a1ca5 Mon Sep 17 00:00:00 2001 From: Olivier BERTHET Date: Thu, 20 Aug 2026 15:19:07 +0200 Subject: [PATCH] fix: resolve ACP stdio MCP proxy without import.meta in CJS bundle The CLI ships as CJS, so import.meta.url is empty and URL.pathname breaks Windows paths. Locate mcpProxy.mjs like the PTY worker and copy it to dist/workers during bundle. --- agentchatbus-ts/scripts/build-bundle.mjs | 1 + .../src/core/services/acpAdapter.ts | 3 +- .../src/core/services/acpMcpProxyPath.ts | 35 +++++++++++++++++++ .../unit/test_acp_mcp_proxy_path.test.ts | 30 ++++++++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 agentchatbus-ts/src/core/services/acpMcpProxyPath.ts create mode 100644 agentchatbus-ts/tests/unit/test_acp_mcp_proxy_path.test.ts diff --git a/agentchatbus-ts/scripts/build-bundle.mjs b/agentchatbus-ts/scripts/build-bundle.mjs index 0198f23e..341cade1 100644 --- a/agentchatbus-ts/scripts/build-bundle.mjs +++ b/agentchatbus-ts/scripts/build-bundle.mjs @@ -22,6 +22,7 @@ async function prepareDistDir() { async function copyWorkerAssets() { const workers = [ ['src/core/services/adapters/workers/interactivePtyWorker.mjs', 'dist/workers/interactivePtyWorker.mjs'], + ['src/transports/stdio/mcpProxy.mjs', 'dist/workers/mcpProxy.mjs'], ]; for (const [source, target] of workers) { await copyFile(path.join(projectRoot, source), path.join(projectRoot, target)); diff --git a/agentchatbus-ts/src/core/services/acpAdapter.ts b/agentchatbus-ts/src/core/services/acpAdapter.ts index 0da77c24..05ee949f 100644 --- a/agentchatbus-ts/src/core/services/acpAdapter.ts +++ b/agentchatbus-ts/src/core/services/acpAdapter.ts @@ -26,6 +26,7 @@ import { getAcpAgent, type AcpAgentConfig, } from "./acpRegistry.js"; +import { resolveAcpMcpProxyScript } from "./acpMcpProxyPath.js"; import type { CliAdapterActivityEvent, CliAdapterNativeRuntimeEvent, @@ -406,7 +407,7 @@ export class AcpAdapter implements CliSessionAdapter { // Try stdio MCP proxy as a fallback for agents that don't have // pre-configured MCP servers. Agents with pre-configured MCP will // use their own config. - const proxyScript = new URL("../../transports/stdio/mcpProxy.mjs", import.meta.url).pathname; + const proxyScript = resolveAcpMcpProxyScript(); const envVars: acp.EnvVariable[] = [ { name: "AGENTCHATBUS_BASE_URL", value: baseUrl }, ]; diff --git a/agentchatbus-ts/src/core/services/acpMcpProxyPath.ts b/agentchatbus-ts/src/core/services/acpMcpProxyPath.ts new file mode 100644 index 00000000..0277beaf --- /dev/null +++ b/agentchatbus-ts/src/core/services/acpMcpProxyPath.ts @@ -0,0 +1,35 @@ +import { existsSync } from "node:fs"; +import path from "node:path"; + +const PROXY_FILE_NAME = "mcpProxy.mjs"; + +/** + * Resolve the standalone ACP stdio MCP proxy script. + * + * The CLI is bundled as CJS (`dist/cli/index.js`), where `import.meta.url` is + * empty. The proxy stays an ESM `.mjs` asset spawned as a child process, copied + * next to the bundle as `dist/workers/mcpProxy.mjs` (same pattern as + * `resolveWorkerPath` in interactivePtyChildBridge). + */ +export function resolveAcpMcpProxyScript(): string { + const entryFile = path.resolve(process.argv[1] || process.cwd()); + const entryDir = path.dirname(entryFile); + const candidates = [ + path.resolve(entryDir, "../workers", PROXY_FILE_NAME), + path.resolve(entryDir, "../../transports/stdio", PROXY_FILE_NAME), + path.resolve(entryDir, "../../src/transports/stdio", PROXY_FILE_NAME), + path.resolve(process.cwd(), "dist/workers", PROXY_FILE_NAME), + path.resolve(process.cwd(), "src/transports/stdio", PROXY_FILE_NAME), + ]; + + for (const candidate of candidates) { + if (existsSync(candidate)) { + return candidate; + } + } + + throw new Error( + `Unable to locate ACP stdio MCP proxy '${PROXY_FILE_NAME}'. ` + + `Checked: ${candidates.join(", ")}`, + ); +} diff --git a/agentchatbus-ts/tests/unit/test_acp_mcp_proxy_path.test.ts b/agentchatbus-ts/tests/unit/test_acp_mcp_proxy_path.test.ts new file mode 100644 index 00000000..6c366191 --- /dev/null +++ b/agentchatbus-ts/tests/unit/test_acp_mcp_proxy_path.test.ts @@ -0,0 +1,30 @@ +import { existsSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { resolveAcpMcpProxyScript } from "../../src/core/services/acpMcpProxyPath.js"; + +const SOURCE_PROXY = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../../src/transports/stdio/mcpProxy.mjs", +); + +describe("ACP stdio MCP proxy path", () => { + it("resolves an existing mcpProxy.mjs", () => { + const resolved = resolveAcpMcpProxyScript(); + expect(existsSync(resolved)).toBe(true); + expect(path.basename(resolved)).toBe("mcpProxy.mjs"); + expect(resolved).not.toMatch(/^\/[A-Za-z]:/); + }); + + it("finds the source script when the bundled worker copy is absent", () => { + const originalArgv1 = process.argv[1]; + process.argv[1] = path.join(path.dirname(SOURCE_PROXY), "not-the-cli.js"); + try { + const resolved = resolveAcpMcpProxyScript(); + expect(path.normalize(resolved)).toBe(path.normalize(SOURCE_PROXY)); + } finally { + process.argv[1] = originalArgv1; + } + }); +});