fix(server): inject OpenCode MCP servers once

This commit is contained in:
Mohamed Boudra
2026-05-24 12:58:30 +07:00
parent d43d30eeeb
commit 68c893f643
4 changed files with 55 additions and 9 deletions
+2
View File
@@ -26,6 +26,8 @@ Pi import discovery reads Pi's persisted JSONL session files because Pi RPC does
Pi RPC extension UI dialog requests (`select`, `input`, `editor`, `confirm`) are bridged into Paseo question permissions and answered with `extension_ui_response`. Fire-and-forget extension UI requests such as notifications are intentionally ignored by the provider adapter unless Paseo grows first-class UI for them.
OpenCode MCP injection is dynamic and session-scoped. Call OpenCode's `mcp.add` endpoint with the MCP server config and do not follow it with `mcp.connect`; `connect` only toggles MCP servers already present in OpenCode's own config. New OpenCode versions return `McpServerNotFoundError`/404 for `connect` after a dynamic add because the server is not config-backed, while older versions silently swallowed the same missing-config path.
Draft metadata lookups should avoid creating provider sessions when the upstream provider has top-level APIs for that metadata. Prefer `AgentClient.listModels`, `listModes`, `listCommands`, or `listFeatures` over creating a scratch `AgentSession`; scratch sessions can show up as empty native sessions in provider import/history UIs.
---
@@ -585,6 +585,46 @@ describe("OpenCode adapter context-window normalization", () => {
});
describe("OpenCode adapter startTurn error handling", () => {
test("dynamically adds injected MCP servers without config-backed connect", async () => {
const runtime = new TestOpenCodeRuntime();
const openCodeClient = new TestOpenCodeClient();
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 collectTurnEvents(streamSession(session, "hello"));
expect(openCodeClient.calls.mcpAdd).toEqual([
{
directory: cwd,
name: "paseo",
config: {
type: "remote",
url: "http://127.0.0.1:6767/mcp/agents?callerAgentId=test-agent",
enabled: true,
},
},
]);
expect(openCodeClient.calls.mcpConnect).toEqual([]);
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 = [
@@ -3170,16 +3170,10 @@ class OpenCodeAgentSession implements AgentSession {
config,
}),
);
await this.runMcpOperation("connect", name, () =>
this.client.mcp.connect({
directory: this.config.cwd,
name,
}),
);
}
private async runMcpOperation(
operation: "add" | "connect",
operation: "add",
name: string,
run: () => Promise<{ error?: unknown }>,
): Promise<void> {
@@ -56,6 +56,8 @@ export class TestOpenCodeClient {
eventSubscribe: [] as unknown[],
experimentalSessionList: [] as unknown[],
globalEvent: [] as unknown[],
mcpAdd: [] as unknown[],
mcpConnect: [] as unknown[],
permissionReply: [] as unknown[],
providerList: [] as unknown[],
questionReject: [] as unknown[],
@@ -74,6 +76,8 @@ export class TestOpenCodeClient {
commandListResponse: OpenCodeResponse = { data: [] };
eventStream: AsyncIterable<unknown>;
experimentalSessionListResponse: OpenCodeResponse = { data: [] };
mcpAddResponse: OpenCodeResponse = {};
mcpConnectResponse: OpenCodeResponse = {};
permissionReplyResponse: OpenCodeResponse = {};
providerListResponse: OpenCodeResponse = { data: { connected: [], all: [] } };
providerListImplementation: (() => Promise<OpenCodeResponse>) | null = null;
@@ -135,8 +139,14 @@ export class TestOpenCodeClient {
},
},
mcp: {
add: async () => ({}),
connect: async () => ({}),
add: async (parameters: unknown) => {
this.calls.mcpAdd.push(parameters);
return this.mcpAddResponse;
},
connect: async (parameters: unknown) => {
this.calls.mcpConnect.push(parameters);
return this.mcpConnectResponse;
},
},
permission: {
reply: async (parameters: unknown) => {