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.
This commit is contained in:
Mohamed Boudra
2026-05-27 16:01:06 +07:00
parent 8307a0ca6f
commit d787aefa4c
4 changed files with 119 additions and 3 deletions

View File

@@ -285,6 +285,65 @@ describe("agent MCP end-to-end (offline)", () => {
} }
}, 30_000); }, 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 () => { 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 paseoHome = await mkdtemp(path.join(os.tmpdir(), "paseo-home-"));
const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-")); const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-"));

View File

@@ -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 () => { test("emits turn_started before live OpenCode timeline items", async () => {
const eventsGate = createTestDeferred<void>(); const eventsGate = createTestDeferred<void>();
const globalEvents = [ const globalEvents = [

View File

@@ -425,6 +425,15 @@ function isAlreadyPresentMcpError(error: unknown): boolean {
return MCP_ALREADY_PRESENT_ERROR_TOKENS.some((token) => normalized.includes(token)); 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( function resolvePartDedupeKey(
part: { id: string; messageID: string }, part: { id: string; messageID: string },
partType: "text" | "reasoning", partType: "text" | "reasoning",
@@ -3507,10 +3516,10 @@ class OpenCodeAgentSession implements AgentSession {
private async runMcpOperation( private async runMcpOperation(
operation: "add", operation: "add",
name: string, name: string,
run: () => Promise<{ error?: unknown }>, run: () => Promise<{ data?: unknown; error?: unknown }>,
): Promise<void> { ): Promise<void> {
const response = await run(); const response = await run();
const error = response.error; const error = response.error ?? readOpenCodeMcpOperationError(response.data, name);
if (!error) { if (!error) {
return; return;
} }

View File

@@ -152,13 +152,24 @@ function formatHostForHttpUrl(host: string): string {
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host; 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 { function createAgentMcpBaseUrl(listenTarget: ListenTarget | null): string | null {
if (!listenTarget || listenTarget.type !== "tcp") { if (!listenTarget || listenTarget.type !== "tcp") {
return null; return null;
} }
const host = resolveAgentMcpClientHost(listenTarget.host);
return new URL( return new URL(
"/mcp/agents", "/mcp/agents",
`http://${formatHostForHttpUrl(listenTarget.host)}:${listenTarget.port}`, `http://${formatHostForHttpUrl(host)}:${listenTarget.port}`,
).toString(); ).toString();
} }