mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
0f86c4237e
Coder's chat (chatd) can now discover and use MCP servers configured in a workspace's `.mcp.json` file. This brings project-specific tooling (GitHub, databases, docs servers, etc.) into the chat without any manual configuration. ## How it works The workspace agent reads `.mcp.json` from the workspace directory (same format Claude Code uses), connects to the declared MCP servers — spawning child processes for stdio servers and connecting over the network for HTTP/SSE — and caches their tool lists. Two new agent HTTP endpoints expose this: - `GET /api/v0/mcp/tools` returns the cached tool list (supports `?refresh=true`) - `POST /api/v0/mcp/call-tool` proxies calls to the correct server On each chat turn, chatd calls `ListMCPTools` through the existing `AgentConn` tailnet connection, wraps each tool as a `fantasy.AgentTool`, and adds them to the LLM's tool set alongside built-in and admin-configured MCP tools. Tool names are prefixed with the server name (`github__create_issue`) to avoid collisions. Failed server connections are logged and skipped — they never block the agent or break the chat. Child stdio processes are terminated on agent shutdown.
89 lines
2.2 KiB
Go
89 lines
2.2 KiB
Go
package agentmcp
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"cdr.dev/slog/v3"
|
|
"github.com/coder/coder/v2/coderd/httpapi"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
|
)
|
|
|
|
// API exposes MCP tool discovery and call proxying through the
|
|
// agent.
|
|
type API struct {
|
|
logger slog.Logger
|
|
manager *Manager
|
|
}
|
|
|
|
// NewAPI creates a new MCP API handler backed by the given
|
|
// manager.
|
|
func NewAPI(logger slog.Logger, manager *Manager) *API {
|
|
return &API{
|
|
logger: logger,
|
|
manager: manager,
|
|
}
|
|
}
|
|
|
|
// Routes returns the HTTP handler for MCP-related routes.
|
|
func (api *API) Routes() http.Handler {
|
|
r := chi.NewRouter()
|
|
r.Get("/tools", api.handleListTools)
|
|
r.Post("/call-tool", api.handleCallTool)
|
|
return r
|
|
}
|
|
|
|
// handleListTools returns the cached MCP tool definitions,
|
|
// optionally refreshing them first if ?refresh=true is set.
|
|
func (api *API) handleListTools(rw http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
// Allow callers to force a tool re-scan before listing.
|
|
if r.URL.Query().Get("refresh") == "true" {
|
|
if err := api.manager.RefreshTools(ctx); err != nil {
|
|
api.logger.Warn(ctx, "failed to refresh MCP tools", slog.Error(err))
|
|
}
|
|
}
|
|
|
|
tools := api.manager.Tools()
|
|
// Ensure non-nil so JSON serialization returns [] not null.
|
|
if tools == nil {
|
|
tools = []workspacesdk.MCPToolInfo{}
|
|
}
|
|
|
|
httpapi.Write(ctx, rw, http.StatusOK, workspacesdk.ListMCPToolsResponse{
|
|
Tools: tools,
|
|
})
|
|
}
|
|
|
|
// 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)
|
|
}
|