🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

feat: push MCP server context and tools from agentcontext - #26533

Merged
kylecarbs merged 1 commit into
mainfrom
kylecarbs/agentcontext-mcp-push
Jun 21, 2026
Merged

feat: push MCP server context and tools from agentcontext#26533
kylecarbs merged 1 commit into
mainfrom
kylecarbs/agentcontext-mcp-push

Conversation

@kylecarbs

@kylecarbs kylecarbs commented Jun 18, 2026

Copy link
Copy Markdown
Member

What

Live MCP servers and their tools now flow into the agentcontext snapshot and are pushed to coderd via PushContextState, stored alongside instruction files and skills. Previously the resolver's MCP seam was unimplemented, so live MCP tool lists never reached the pushed snapshot.

agentcontext is now fully self-contained for MCP: it connects to the MCP servers declared in the .mcp.json files its own watcher already discovers, lists their tools, and emits KindMCPServer resources. It does not depend on or modify agent/x/agentmcp — that package is left pristine and keeps serving the agent's MCP HTTP API. The two MCP paths run independently, which means the legacy package can be deleted later without touching this code.

How

  • Self-contained runner (agentcontext/mcprunner.go): a one-shot MCP client (connect → initialize → list tools → close) with its own .mcp.json parser. A Manager goroutine (runMCPSync) reloads it whenever the discovered KindMCPConfig path:contenthash set changes, then re-resolves so the new tools are published. Per-server connects run in parallel (bounded) with a per-server timeout; a server that fails to connect is recorded as a failure rather than aborting the batch. Each connect also force-kills its subprocess on close, because mcp-go's stdio Close() closes stdin and then blocks on cmd.Wait() with no kill — a server that ignores stdin-close would otherwise stall the whole reload loop.
  • Resource production (agentcontext/mcp.go): buildMCPServerResources turns the runner's non-blocking per-server snapshot into KindMCPServer resources. Connected servers carry their sorted tools (StatusOK); failed servers surface as StatusUnreadable issues instead of vanishing; connected-but-no-tools-yet are skipped until a later reload. The content hash is tool-set sensitive. The resolver consumes this through a plain MCPResources func() []Resource field (no MCPProvider interface).
  • Tool names: emitted exactly as the server reports them. Flattening into a single namespace (e.g. server__tool) is left to the control plane in the next step, since each resource already carries the server name.
  • Drift: MCP resources are excluded from the snapshot aggregate/drift hash (driftResources). MCP servers connect asynchronously after boot; without this, a server finishing its connect would dirty every hydrated chat even though nothing the user pinned changed.
  • Wiring (agent.go): the manager is given ManagerOptions.MCPExecer/MCPUpdateEnv; agent/x/agentmcp is untouched.
  • Config validation: a structurally broken .mcp.json surfaces as StatusInvalid rather than silently dropping all its servers.

coderd already persists mcp_server/mcp_config resource bodies (including tools), so no coderd or proto changes were required.

Testing

  • Unit: buildMCPServerResources (grouping/sort/skip/failed/hash sensitivity), MCP resources applied via the resolver seam, MCP exclusion from the aggregate hash, .mcp.json parsing (transport inference, env expansion), toolInputSchema, and mcpConfigSet change detection.
  • Proto serialization (TestDRPCPusher_HappyPathSerializesAllFields): a KindMCPServer resource (tools + input schema) round-trips through PushContextState into the MCPServerBody wire form, asserting the server name, tool name/description, and the decoded input_schema.
  • Manager-level, real subprocess (TestManager_MCPServerToolsInSnapshot): a .mcp.json points at a re-exec'd fake stdio MCP server; the runner connects it and its echo tool surfaces as a KindMCPServer resource in the Manager snapshot — the same snapshot pushed to coderd — exercising runMCPSync and the resolver wiring end to end.
  • Regression (TestManager_MCPServerHangingCloseDoesNotStall): the fake server ignores stdin-close; the test asserts its tool still surfaces, proving the runner force-kills the subprocess instead of stalling the reload. Verified to fail without the fix.
  • All pass under -race; go build ./..., go vet, and golangci-lint are clean on the touched packages.

Scope / follow-ups

This is the agent-side production+push half. The chatd consumer (reading the pinned MCP resources for prompt/tool injection, including any server-prefix flattening of tool names) and removing the legacy workspaceMCPToolsCache pull path remain follow-ups, per the RFC rollout. While both agent/x/agentmcp and agentcontext exist, stdio MCP servers are spawned by both; this is intentional and temporary until agentmcp is removed.

Implementation plan and decisions

Goal: produce live MCP server resources (with tools) from agentcontext and push them to coderd.

Starting state (main): proto (PushContextState, MCPServerBody, MCPTool), the drpc adapter, coderd storage (workspace_agent_context_resources, body kind mcp_server), and the resolver's MCP seam already existed; nothing implemented the seam or fed live tools into the snapshot.

Decision (agentcontext fully separate from agentmcp): agentcontext starts and lists its own MCP servers using only the connect-and-list half of an mcp-go client, driven by the .mcp.json files its existing watcher discovers. It shares no state with agent/x/agentmcp and does not import it. Two earlier revisions of this branch were discarded: (1) relocating agentmcp into agentcontext (rejected — it duplicates config parsing and file watching agentcontext already does); (2) reading agentmcp's cached server snapshot via new accessors (rejected — unnecessary coupling between two packages that should simply run independently while one is being retired). The temporary double-spawn of stdio servers is the accepted cost of keeping the two paths cleanly separated until agentmcp is removed.

Decision (no tool-name prefixing, no MCPProvider interface): the agent pushes raw, unflattened data — server name plus verbatim tool names — and lets the control plane own any server__tool flattening. With a single self-contained producer, the MCPProvider interface was collapsed into a func() []Resource field on the resolver.

Invariants held: no secrets (env/headers) in pushed resources, only server/tool metadata; MCP excluded from the drift hash; the seam is non-blocking so the resolver never stalls on MCP I/O.


This PR was created by Coder Agents on behalf of @kylecarbs.

@kylecarbs
kylecarbs force-pushed the kylecarbs/agentcontext-mcp-push branch 4 times, most recently from 426ed9f to 70b4e21 Compare June 21, 2026 19:39
agentcontext now starts its own MCP servers from the .mcp.json files its
watcher discovers, lists their tools, and surfaces them as KindMCPServer
resources pushed to coderd alongside instruction files and skills. This
runs independently of agent/x/agentmcp (which serves the agent's MCP HTTP
API); the two MCP paths share no state during the rollout, so removing
the old package later touches nothing here.

- agentcontext/mcprunner.go: self-contained one-shot MCP runner (connect,
  initialize, list tools, close) with its own .mcp.json parser. A Manager
  goroutine (runMCPSync) reloads it when the discovered KindMCPConfig
  path:contenthash set changes, then re-resolves to publish the tools.
  Each per-server connect force-kills its subprocess on close so a server
  that ignores stdin-close cannot stall the reload (mcp-go's stdio Close
  blocks on cmd.Wait with no kill).
- agentcontext/mcp.go: buildMCPServerResources turns the runner's
  non-blocking per-server snapshot into KindMCPServer resources
  (connected->OK with tools, failed->unreadable issue, no-tools-yet
  skipped). Tool names are emitted exactly as the server reports them;
  flattening into a single namespace (e.g. server__tool) is left to the
  control plane, since the resource already carries the server name.
- agentcontext/resolve.go: the resolver takes an MCPResources func()
  []Resource seam (no MCPProvider interface); MCP resources are excluded
  from the drift/aggregate hash so a server connecting does not dirty
  hydrated chats; structurally broken .mcp.json is flagged StatusInvalid.
- agent.go: wires the runner via ManagerOptions.MCPExecer/MCPUpdateEnv;
  agent/x/agentmcp is left untouched.
- agentcontext/mcpexec_test.go: the runner's end-to-end coverage lives
  with the package, re-exec'ing the test binary as a fake stdio MCP
  server to assert tools reach the snapshot and that a server ignoring
  stdin-close is force-killed instead of stalling the reload.
@kylecarbs
kylecarbs force-pushed the kylecarbs/agentcontext-mcp-push branch from 70b4e21 to e4d9ac9 Compare June 21, 2026 22:01
@kylecarbs
kylecarbs merged commit 2f8bba7 into main Jun 21, 2026
29 checks passed
@kylecarbs
kylecarbs deleted the kylecarbs/agentcontext-mcp-push branch June 21, 2026 22:31
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 21, 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