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