From d787aefa4cae6ceb2fa69bbcb069c114966b1d46 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 27 May 2026 16:01:06 +0700 Subject: [PATCH] Fix OpenCode MCP injection on wildcard binds Wildcard listen addresses are bind targets, not client endpoints. Inject loopback for agent MCP URLs and surface OpenCode MCP add failures returned in data payloads. --- .../src/server/agent/agent-mcp.e2e.test.ts | 59 +++++++++++++++++++ .../agent/providers/opencode-agent.test.ts | 37 ++++++++++++ .../server/agent/providers/opencode-agent.ts | 13 +++- packages/server/src/server/bootstrap.ts | 13 +++- 4 files changed, 119 insertions(+), 3 deletions(-) diff --git a/packages/server/src/server/agent/agent-mcp.e2e.test.ts b/packages/server/src/server/agent/agent-mcp.e2e.test.ts index d36412b6b..a1b639a8c 100644 --- a/packages/server/src/server/agent/agent-mcp.e2e.test.ts +++ b/packages/server/src/server/agent/agent-mcp.e2e.test.ts @@ -285,6 +285,65 @@ describe("agent MCP end-to-end (offline)", () => { } }, 30_000); + test("create_agent injects a loopback MCP URL when the daemon listens on all interfaces", async () => { + const paseoHome = await mkdtemp(path.join(os.tmpdir(), "paseo-home-")); + const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-")); + const agentCwd = await mkdtemp(path.join(os.tmpdir(), "paseo-agent-cwd-")); + const port = await getAvailablePort(); + + const daemonConfig: PaseoDaemonConfig = { + listen: `0.0.0.0:${port}`, + paseoHome, + corsAllowedOrigins: [], + hostnames: true, + mcpEnabled: true, + staticDir, + mcpDebug: false, + agentClients: createTestAgentClients(), + agentStoragePath: path.join(paseoHome, "agents"), + }; + + const daemon = await createPaseoDaemon(daemonConfig, pino({ level: "silent" })); + await daemon.start(); + + const client = await createMcpClient(`http://127.0.0.1:${port}/mcp/agents`); + + let agentId: string | null = null; + try { + const result = await client.callTool({ + name: "create_agent", + args: { + cwd: agentCwd, + title: "Wildcard MCP", + provider: "claude/claude-test-model", + mode: "bypassPermissions", + initialPrompt: "reply with done and stop", + background: true, + }, + }); + const payload = getStructuredContent(result); + agentId = typeof payload?.agentId === "string" ? payload.agentId : null; + expect(agentId).toBeTruthy(); + + const injectedAgent = daemon.agentManager.getAgent(agentId!); + expect(injectedAgent?.config.mcpServers).toMatchObject({ + paseo: { + type: "http", + url: `http://127.0.0.1:${port}/mcp/agents?callerAgentId=${agentId!}`, + }, + }); + } finally { + if (agentId) { + await client.callTool({ name: "kill_agent", args: { agentId } }); + } + await client.close(); + await daemon.stop(); + await rm(paseoHome, { recursive: true, force: true }); + await rm(staticDir, { recursive: true, force: true }); + await rm(agentCwd, { recursive: true, force: true }); + } + }, 30_000); + test("create_agent with background initialPrompt reflects running state once the first turn starts", async () => { const paseoHome = await mkdtemp(path.join(os.tmpdir(), "paseo-home-")); const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-")); diff --git a/packages/server/src/server/agent/providers/opencode-agent.test.ts b/packages/server/src/server/agent/providers/opencode-agent.test.ts index a5a7cc875..32813ef0a 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.test.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.test.ts @@ -753,6 +753,43 @@ describe("OpenCode adapter startTurn error handling", () => { } }); + test("fails the turn when OpenCode reports MCP add failure in data payload", async () => { + const runtime = new TestOpenCodeRuntime(); + const openCodeClient = new TestOpenCodeClient(); + openCodeClient.mcpAddResponse = { + data: { + paseo: { + status: "failed", + error: "SSE error: Non-200 status code (400)", + }, + }, + }; + runtime.enqueueClient(openCodeClient); + const cwd = tmpCwd(); + const client = new OpenCodeAgentClient(createTestLogger(), undefined, { runtime }); + + try { + const session = await client.createSession({ + provider: "opencode", + cwd, + mcpServers: { + paseo: { + type: "http", + url: "http://127.0.0.1:6767/mcp/agents?callerAgentId=test-agent", + }, + }, + }); + + await expect(collectTurnEvents(streamSession(session, "hello"))).rejects.toThrow( + /Failed to add OpenCode MCP server 'paseo': SSE error/, + ); + + await session.close(); + } finally { + rmSync(cwd, { recursive: true, force: true }); + } + }); + test("emits turn_started before live OpenCode timeline items", async () => { const eventsGate = createTestDeferred(); const globalEvents = [ diff --git a/packages/server/src/server/agent/providers/opencode-agent.ts b/packages/server/src/server/agent/providers/opencode-agent.ts index 51005a803..9192d943a 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.ts @@ -425,6 +425,15 @@ function isAlreadyPresentMcpError(error: unknown): boolean { return MCP_ALREADY_PRESENT_ERROR_TOKENS.some((token) => normalized.includes(token)); } +function readOpenCodeMcpOperationError(data: unknown, name: string): unknown { + const root = readOpenCodeRecord(data); + const entry = readOpenCodeRecord(root?.[name]); + if (!entry || entry.status !== "failed") { + return undefined; + } + return entry.error ?? `OpenCode reported MCP server '${name}' failed`; +} + function resolvePartDedupeKey( part: { id: string; messageID: string }, partType: "text" | "reasoning", @@ -3507,10 +3516,10 @@ class OpenCodeAgentSession implements AgentSession { private async runMcpOperation( operation: "add", name: string, - run: () => Promise<{ error?: unknown }>, + run: () => Promise<{ data?: unknown; error?: unknown }>, ): Promise { const response = await run(); - const error = response.error; + const error = response.error ?? readOpenCodeMcpOperationError(response.data, name); if (!error) { return; } diff --git a/packages/server/src/server/bootstrap.ts b/packages/server/src/server/bootstrap.ts index 95e0d4d00..76fa76e07 100644 --- a/packages/server/src/server/bootstrap.ts +++ b/packages/server/src/server/bootstrap.ts @@ -152,13 +152,24 @@ function formatHostForHttpUrl(host: string): string { return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host; } +function resolveAgentMcpClientHost(host: string): string { + if (host === "0.0.0.0") { + return "127.0.0.1"; + } + if (host === "::" || host === "[::]") { + return "::1"; + } + return host; +} + function createAgentMcpBaseUrl(listenTarget: ListenTarget | null): string | null { if (!listenTarget || listenTarget.type !== "tcp") { return null; } + const host = resolveAgentMcpClientHost(listenTarget.host); return new URL( "/mcp/agents", - `http://${formatHostForHttpUrl(listenTarget.host)}:${listenTarget.port}`, + `http://${formatHostForHttpUrl(host)}:${listenTarget.port}`, ).toString(); }