From e9a7f1add96fd40189d0c94fc79b2bfbb9843e1a Mon Sep 17 00:00:00 2001 From: ian_p Date: Mon, 1 Jun 2026 00:03:42 +0800 Subject: [PATCH] fix(coding-agent/acp): MCP tools from Zed not activated in ACP sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Zed provisions MCP servers through `session/new.mcpServers`, the ACP agent correctly connects to them and registers their tools via `refreshMCPTools`, but the tools were never activated. `refreshMCPTools` builds the next active tool set from `getSelectedMCPToolNames()`, which — with discovery disabled — returns only MCP tools already in the active set. Since ACP sessions start with no MCP tools, this created a circular deadlock: tools could only become active if they were already active. Added an optional `{ activateAll?: boolean }` parameter to `refreshMCPTools`. When true, every newly registered tool is force-activated regardless of prior selection. `AcpAgent#configureMcpServers` passes `activateAll: true` on both call sites so client-provisioned tools are immediately usable. --- .../coding-agent/src/modes/acp/acp-agent.ts | 4 +- .../coding-agent/src/session/agent-session.ts | 19 +++++++- .../test/agent-session-mcp-discovery.test.ts | 45 +++++++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/src/modes/acp/acp-agent.ts b/packages/coding-agent/src/modes/acp/acp-agent.ts index d558b65c2..c70375e1b 100644 --- a/packages/coding-agent/src/modes/acp/acp-agent.ts +++ b/packages/coding-agent/src/modes/acp/acp-agent.ts @@ -1981,7 +1981,7 @@ export class AcpAgent implements Agent { } if (servers.length === 0) { record.mcpManager = undefined; - await record.session.refreshMCPTools([]); + await record.session.refreshMCPTools([], { activateAll: true }); return; } @@ -2008,7 +2008,7 @@ export class AcpAgent implements Agent { } record.mcpManager = manager; - await record.session.refreshMCPTools(result.tools); + await record.session.refreshMCPTools(result.tools, { activateAll: true }); } #toMcpConfig(server: McpServer): MCPServerConfig { diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 5a587d0d9..250de2c5a 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -3617,8 +3617,13 @@ export class AgentSession { /** * Replace MCP tools in the registry and recompute the visible MCP tool set immediately. * This allows /mcp add/remove/reauth to take effect without restarting the session. + * + * @param mcpTools The new MCP tools to register. + * @param options.activateAll When true, force-activates every newly registered MCP tool + * regardless of prior selection state. Used when an ACP client provisions MCP servers + * for a session where MCP discovery is disabled. */ - async refreshMCPTools(mcpTools: CustomTool[]): Promise { + async refreshMCPTools(mcpTools: CustomTool[], options?: { activateAll?: boolean }): Promise { const previousSelectedMCPToolNames = this.getSelectedMCPToolNames(); const existingNames = Array.from(this.#toolRegistry.keys()); for (const name of existingNames) { @@ -3659,6 +3664,18 @@ export class AgentSession { this.#getConfiguredDefaultSelectedMCPToolNames(), ); + if (options?.activateAll) { + // Force-activate every newly registered MCP tool. This path is used + // when an ACP client provisions MCP servers for a session where MCP + // discovery is disabled — without it, getSelectedMCPToolNames() + // returns only already-active tools (circular deadlock: tools can + // only become active if they're already active). + const newMcpNames = mcpTools.map(t => t.name); + const nextActive = [...new Set([...this.#getActiveNonMCPToolNames(), ...newMcpNames])]; + await this.#applyActiveToolsByName(nextActive, { previousSelectedMCPToolNames }); + return; + } + const nextActive = [...this.#getActiveNonMCPToolNames(), ...this.getSelectedMCPToolNames()]; await this.#applyActiveToolsByName(nextActive, { previousSelectedMCPToolNames }); } diff --git a/packages/coding-agent/test/agent-session-mcp-discovery.test.ts b/packages/coding-agent/test/agent-session-mcp-discovery.test.ts index 8854829d7..4d3eed103 100644 --- a/packages/coding-agent/test/agent-session-mcp-discovery.test.ts +++ b/packages/coding-agent/test/agent-session-mcp-discovery.test.ts @@ -228,6 +228,51 @@ describe("AgentSession MCP discovery", () => { expect(session.systemPrompt).toEqual(["tools:read"]); }); + it("activates all new MCP tools when activateAll is true, even with discovery off", async () => { + const readTool = createBasicTool("read", "Read"); + const toolRegistry = new Map([[readTool.name, readTool]]); + const agent = new Agent({ + initialState: { + model: createModel(), + systemPrompt: ["initial"], + tools: [readTool], + messages: [], + }, + }); + const session = new AgentSession({ + agent, + sessionManager: SessionManager.inMemory(), + settings: Settings.isolated({ "mcp.discoveryMode": false }), + modelRegistry: {} as never, + toolRegistry, + mcpDiscoveryEnabled: false, + rebuildSystemPrompt: async toolNames => ({ + systemPrompt: [`tools:${toolNames.join(",")}`], + }), + }); + sessions.push(session); + + // Start with only non-MCP tools active — no MCP tools in registry. + expect(session.getActiveToolNames()).toEqual(["read"]); + expect(session.getSelectedMCPToolNames()).toEqual([]); + + // Load MCP tools via activateAll path (simulating ACP client provisioning). + await session.refreshMCPTools( + [ + createMcpCustomTool("mcp__docs_search", "docs", "search", "Search internal docs", ["query"]), + createMcpCustomTool("mcp__slack_send_message", "slack", "send_message", "Send a Slack message", [ + "channel", + "text", + ]), + ], + { activateAll: true }, + ); + + expect(session.getSelectedMCPToolNames()).toEqual(["mcp__docs_search", "mcp__slack_send_message"]); + expect(session.getActiveToolNames()).toEqual(["read", "mcp__docs_search", "mcp__slack_send_message"]); + expect(session.systemPrompt).toEqual(["tools:read,mcp__docs_search,mcp__slack_send_message"]); + }); + it("preserves directly activated MCP tools across refreshes in discovery mode", async () => { const readTool = createBasicTool("read", "Read"); const docsSearchTool = createMcpTool("mcp__docs_search", "docs", "search", "Search internal docs", ["query"]);