mirror of
https://github.com/coder/coder.git
synced 2026-06-04 21:48:22 +00:00
ca6450cf94
The first `/mcp/tools` request could race workspace startup and return an empty tool list before startup scripts had a chance to write `.mcp.json`. Chatd may only discover tools once for a turn, so that empty response could hide workspace MCP tools even though the agent loaded them later. Make the manager wait for startup to settle before treating missing MCP config files as a real empty state. Tool listing now goes through one manager-owned path that starts reload work independently of caller cancellation; caller contexts only bound that caller's wait. After the first reload body settles, transient reload errors return cached tools with the error so the HTTP handler can degrade to the last known tool set instead of returning `[]`. The handler is intentionally thin: it asks the manager for tools, logs any degraded path, and still returns the tool response shape callers already expect. Tests cover startup gating, caller-canceled waits, manager close, reload timeout via quartz, and cached-tool fallback after a later reload error.
92 lines
2.5 KiB
Go
92 lines
2.5 KiB
Go
package agentmcp
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"cdr.dev/slog/v3"
|
|
"github.com/coder/coder/v2/agent/agentchat"
|
|
"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
|
|
mcpConfigFiles func() []string
|
|
}
|
|
|
|
// NewAPI creates a new MCP API handler. mcpConfigFiles returns
|
|
// the resolved .mcp.json paths and is called on every tool-list
|
|
// request to detect config changes.
|
|
func NewAPI(logger slog.Logger, m *Manager, mcpConfigFiles func() []string) *API {
|
|
return &API{logger: logger, manager: m, mcpConfigFiles: mcpConfigFiles}
|
|
}
|
|
|
|
// 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 current MCP tool cache after the
|
|
// manager performs startup-safe config synchronization.
|
|
func (api *API) handleListTools(rw http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
logger := api.logger.With(agentchat.Fields(ctx)...)
|
|
|
|
tools, err := api.manager.Tools(ctx, api.mcpConfigFiles())
|
|
if err != nil {
|
|
switch {
|
|
case errors.Is(err, context.Canceled):
|
|
logger.Warn(ctx, "mcp tool list canceled by caller", slog.Error(err))
|
|
case errors.Is(err, context.DeadlineExceeded):
|
|
logger.Warn(ctx, "mcp tool list timed out", slog.Error(err))
|
|
default:
|
|
logger.Warn(ctx, "mcp tool list failed", slog.Error(err))
|
|
}
|
|
}
|
|
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)
|
|
}
|