mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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:
@@ -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-"));
|
||||
|
||||
@@ -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<void>();
|
||||
const globalEvents = [
|
||||
|
||||
@@ -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<void> {
|
||||
const response = await run();
|
||||
const error = response.error;
|
||||
const error = response.error ?? readOpenCodeMcpOperationError(response.data, name);
|
||||
if (!error) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user