mirror of
https://github.com/coder/coder.git
synced 2026-09-23 22:20:22 +08:00
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. <details> <summary>Decision log</summary> 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). </details> Coder Agents generated on behalf of @kylecarbs
61 lines
1.5 KiB
Go
61 lines
1.5 KiB
Go
package agentmcp
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"github.com/coder/coder/v2/coderd/httpapi"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
|
)
|
|
|
|
// API exposes MCP tool-call proxying through the agent. Tool discovery
|
|
// is handled in-process by the agentcontext manager, which reads the
|
|
// shared Manager's catalog and pushes it to coderd as pinned context
|
|
// resources; this API serves only execution.
|
|
type API struct {
|
|
manager *Manager
|
|
}
|
|
|
|
// NewAPI creates a new MCP API handler.
|
|
func NewAPI(m *Manager) *API {
|
|
return &API{manager: m}
|
|
}
|
|
|
|
// Routes returns the HTTP handler for MCP-related routes.
|
|
func (api *API) Routes() http.Handler {
|
|
r := chi.NewRouter()
|
|
r.Post("/call-tool", api.handleCallTool)
|
|
return r
|
|
}
|
|
|
|
// handleCallTool proxies a tool invocation to the appropriate
|
|
// MCP server based on the tool name prefix.
|
|
func (api *API) handleCallTool(rw http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
var req workspacesdk.CallMCPToolRequest
|
|
if !httpapi.Read(ctx, rw, r, &req) {
|
|
return
|
|
}
|
|
|
|
resp, err := api.manager.CallTool(ctx, req)
|
|
if err != nil {
|
|
status := http.StatusBadGateway
|
|
if errors.Is(err, ErrInvalidToolName) {
|
|
status = http.StatusBadRequest
|
|
} else if errors.Is(err, ErrUnknownServer) {
|
|
status = http.StatusNotFound
|
|
}
|
|
httpapi.Write(ctx, rw, status, codersdk.Response{
|
|
Message: "MCP tool call failed.",
|
|
Detail: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
httpapi.Write(ctx, rw, http.StatusOK, resp)
|
|
}
|