diff --git a/docs/providers.md b/docs/providers.md index f9e0d600f..fd6038856 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -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. --- 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 bd2c72afb..570646780 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.test.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.test.ts @@ -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(); 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 ec28c29c0..436fcb14e 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.ts @@ -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 { diff --git a/packages/server/src/server/agent/providers/opencode/test-utils/test-opencode-runtime.ts b/packages/server/src/server/agent/providers/opencode/test-utils/test-opencode-runtime.ts index 15e1c8ebd..2f0060b70 100644 --- a/packages/server/src/server/agent/providers/opencode/test-utils/test-opencode-runtime.ts +++ b/packages/server/src/server/agent/providers/opencode/test-utils/test-opencode-runtime.ts @@ -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; experimentalSessionListResponse: OpenCodeResponse = { data: [] }; + mcpAddResponse: OpenCodeResponse = {}; + mcpConnectResponse: OpenCodeResponse = {}; permissionReplyResponse: OpenCodeResponse = {}; providerListResponse: OpenCodeResponse = { data: { connected: [], all: [] } }; providerListImplementation: (() => Promise) | 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) => {