Skip to content

refactor: consolidate agent MCP onto a single persistent engine#26599

Merged
kylecarbs merged 3 commits into
mainfrom
refactor/agent-mcp-single-engine
Jun 23, 2026
Merged

refactor: consolidate agent MCP onto a single persistent engine#26599
kylecarbs merged 3 commits into
mainfrom
refactor/agent-mcp-single-engine

Conversation

@kylecarbs

Copy link
Copy Markdown
Member

Two MCP code paths both spawned the servers declared in a workspace's .mcp.json: the persistent engine in agent/x/agentmcp (which owns tool-call execution via CallTool) and an ephemeral one-shot runner in agent/agentcontext (mcprunner.go) that connected, listed tools, and immediately closed each server purely for discovery. Every declared server was launched twice, and the discovery path duplicated the engine's .mcp.json parse, transport-build, env-resolve, and connect logic.

This makes agent/x/agentmcp the single persistent MCP engine. The agentcontext manager now reads that engine's per-server catalog in-process through an injected MCPCatalog option and surfaces each server as a KindMCPServer resource. The engine wires SetOnReload to the manager's Trigger, so a reload (startup connect or .mcp.json edit) re-resolves and re-pushes the pinned resources. Tool-call execution is unchanged: it still flows through the engine's CallTool over POST /api/v0/mcp/call-tool.

The now-dead HTTP discovery surface is removed: the agent GET /api/v0/mcp/tools route with agentmcp.API.handleListTools, and workspacesdk.AgentConn.ListMCPTools with ListMCPToolsResponse (mock regenerated). The change nets roughly -1370 lines, mostly the deleted duplicate runner and its tests.

Decision log

The merge of #26585 made pinned chat_context_resources the sole source of workspace context, which surfaced the duplicate spawning. Two options were considered:

  • Option A + dependency injection (chosen): keep agent/x/agentmcp as the single persistent engine; agentcontext consumes its catalog in-process and stays the orchestrator/owner at the API boundary (it still pushes KindMCPServer resources). This is low-risk because agentcontext already exposed the resolver.MCPResources seam, so the change just rebinds it from the ephemeral runner to the shared engine.
  • Option B (rejected): reimplement persistent pooling, reconnect, singleflight, and race handling inside agentcontext and delete agentmcp. Too broad, and it discards the engine's tested lifecycle for no behavioral gain.

agentcontext's discovery was never what kept servers alive; its runner closed each server immediately after listing tools. The component holding persistent connections was always agentmcp, which is why execution already lived there. Consolidating onto it removes the duplicated stack rather than a whole package: both packages survive with distinct roles (agentmcp is the engine, agentcontext is the orchestrator/owner).

Coder Agents generated on behalf of @kylecarbs

Two MCP paths both spawned the servers declared in .mcp.json: the
persistent engine in agent/x/agentmcp, which owns tool-call execution,
and an ephemeral one-shot runner in agent/agentcontext that spawned,
listed tools, and immediately closed servers just for discovery. Every
declared server was therefore launched twice.

Make agent/x/agentmcp the single persistent MCP engine. The agentcontext
manager now reads that engine's per-server catalog in-process through an
injected MCPCatalog option and surfaces it as KindMCPServer resources.
The engine fires SetOnReload into Manager.Trigger, so a reload
re-resolves and re-pushes. Tool-call execution still flows through the
engine's CallTool over /api/v0/mcp/call-tool.

- agentmcp.Manager: replace the flat prefixed tool cache with a
  per-server catalog (Catalog/ServerStatus/ToolInfo) plus a change hook
  (SetOnReload/fireOnChange). refreshCatalog lists tools per connected
  server and surfaces failed servers as unreadable entries.
- agentcontext: drop the ephemeral mcprunner, a duplicate
  parser/transport/env/connect stack, and the MCPExecer/MCPUpdateEnv
  options; consume the injected catalog instead.
- Remove the now-dead discovery surface: the agent GET
  /api/v0/mcp/tools route with agentmcp.API.handleListTools, and
  workspacesdk.AgentConn.ListMCPTools with ListMCPToolsResponse (mock
  regenerated).

Coder Agents generated on behalf of @kylecarbs
@datadog-coder

This comment has been minimized.

…atch make gen

go generate alone leaves the generated mock's imports ungrouped. The
make target additionally runs scripts/format_go_file.sh, which applies
the repo's gci local-prefix grouping (cdr.dev and github.com/coder in a
trailing group). Run that step so the committed mock matches CI's
make gen output.

Coder Agents generated on behalf of @kylecarbs
…ol names

The flatTools test helper re-implemented chatd's server__tool prefixing
to flatten the manager's catalog. Joining per-server tools into a single
namespace is the control plane's concern; the agent exposes raw
per-server tool names. Replace flatTools with connectedTools, which
returns (server, tool) pairs from Catalog(), and assert on those fields.
The one execution-path test that feeds CallTool still builds a prefixed
name via ToolNameSep, because that mirrors what coderd sends to the
agent's tool-call endpoint.

Coder Agents generated on behalf of @kylecarbs
@kylecarbs kylecarbs marked this pull request as ready for review June 23, 2026 03:49
@coder-tasks

coder-tasks Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Documentation Check

Updates Needed

  • docs/ai-coder/agents/extending-agents.md (lines 146-151) - The "How discovery works" section describes the old per-chat-turn ephemeral discovery behavior: "The agent reads .mcp.json via the workspace agent connection on each chat turn. Discovery uses a 5-second timeout. ... Empty results are not cached because the MCP servers may still be starting." This PR replaces that with a single persistent MCP engine that connects at startup and on .mcp.json file changes, pushing its catalog in-process. The per-turn re-read and re-connect path is removed. This section should be updated to reflect the new persistent-engine discovery model (startup + file-watch driven, not per-turn).

Automated review via Coder Agents

@kylecarbs kylecarbs merged commit 27ecd17 into main Jun 23, 2026
34 of 35 checks passed
@kylecarbs kylecarbs deleted the refactor/agent-mcp-single-engine branch June 23, 2026 04:22
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 23, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants