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; + } + }); +});