mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor: consolidate agent MCP onto a single persistent engine (#26599)
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
This commit is contained in:
@@ -104,7 +104,6 @@ type AgentConn interface {
|
||||
DialContext(ctx context.Context, network string, addr string) (net.Conn, error)
|
||||
GetPeerDiagnostics() tailnet.PeerDiagnostics
|
||||
ListContainers(ctx context.Context) (codersdk.WorkspaceAgentListContainersResponse, error)
|
||||
ListMCPTools(ctx context.Context) (ListMCPToolsResponse, error)
|
||||
ListProcesses(ctx context.Context) (ListProcessesResponse, error)
|
||||
ListeningPorts(ctx context.Context) (codersdk.WorkspaceAgentListeningPortsResponse, error)
|
||||
Netcheck(ctx context.Context) (healthsdk.AgentNetcheckReport, error)
|
||||
@@ -1132,12 +1131,6 @@ type FileEditResult struct {
|
||||
Diff string `json:"diff"`
|
||||
}
|
||||
|
||||
// ListMCPToolsResponse is the response from the agent's
|
||||
// MCP tool discovery endpoint.
|
||||
type ListMCPToolsResponse struct {
|
||||
Tools []MCPToolInfo `json:"tools"`
|
||||
}
|
||||
|
||||
// MCPToolInfo describes a single tool discovered from an MCP
|
||||
// server configured in the workspace's .mcp.json file.
|
||||
type MCPToolInfo struct {
|
||||
@@ -1215,23 +1208,6 @@ func (c *agentConn) ListProcesses(ctx context.Context) (ListProcessesResponse, e
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
||||
// ListMCPTools returns tools discovered from MCP servers configured
|
||||
// in the workspace.
|
||||
func (c *agentConn) ListMCPTools(ctx context.Context) (ListMCPToolsResponse, error) {
|
||||
ctx, span := tracing.StartSpan(ctx)
|
||||
defer span.End()
|
||||
res, err := c.apiRequest(ctx, http.MethodGet, "/api/v0/mcp/tools", nil)
|
||||
if err != nil {
|
||||
return ListMCPToolsResponse{}, xerrors.Errorf("do request: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return ListMCPToolsResponse{}, codersdk.ReadBodyAsError(res)
|
||||
}
|
||||
var resp ListMCPToolsResponse
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
||||
// ContextConfig returns the resolved context configuration from
|
||||
// the workspace agent.
|
||||
func (c *agentConn) ContextConfig(ctx context.Context) (ContextConfigResponse, error) {
|
||||
|
||||
@@ -282,21 +282,6 @@ func (mr *MockAgentConnMockRecorder) ListContainers(ctx any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListContainers", reflect.TypeOf((*MockAgentConn)(nil).ListContainers), ctx)
|
||||
}
|
||||
|
||||
// ListMCPTools mocks base method.
|
||||
func (m *MockAgentConn) ListMCPTools(ctx context.Context) (workspacesdk.ListMCPToolsResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ListMCPTools", ctx)
|
||||
ret0, _ := ret[0].(workspacesdk.ListMCPToolsResponse)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ListMCPTools indicates an expected call of ListMCPTools.
|
||||
func (mr *MockAgentConnMockRecorder) ListMCPTools(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListMCPTools", reflect.TypeOf((*MockAgentConn)(nil).ListMCPTools), ctx)
|
||||
}
|
||||
|
||||
// ListProcesses mocks base method.
|
||||
func (m *MockAgentConn) ListProcesses(ctx context.Context) (workspacesdk.ListProcessesResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
Reference in New Issue
Block a user