mirror of
https://github.com/coder/coder.git
synced 2026-11-09 03:43:54 +08:00
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.
116 lines
3.1 KiB
Go
116 lines
3.1 KiB
Go
package agentmcp
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
|
|
"golang.org/x/xerrors"
|
|
)
|
|
|
|
// ServerConfig describes a single MCP server parsed from a .mcp.json file.
|
|
type ServerConfig struct {
|
|
Name string `json:"name"`
|
|
Transport string `json:"type"`
|
|
Command string `json:"command"`
|
|
Args []string `json:"args"`
|
|
Env map[string]string `json:"env"`
|
|
URL string `json:"url"`
|
|
Headers map[string]string `json:"headers"`
|
|
}
|
|
|
|
// mcpConfigFile mirrors the on-disk .mcp.json schema.
|
|
type mcpConfigFile struct {
|
|
MCPServers map[string]json.RawMessage `json:"mcpServers"`
|
|
}
|
|
|
|
// mcpServerEntry is a single server block inside mcpServers.
|
|
type mcpServerEntry struct {
|
|
Command string `json:"command"`
|
|
Args []string `json:"args"`
|
|
Env map[string]string `json:"env"`
|
|
Type string `json:"type"`
|
|
URL string `json:"url"`
|
|
Headers map[string]string `json:"headers"`
|
|
}
|
|
|
|
// ParseConfig reads a .mcp.json file at path and returns the declared
|
|
// MCP servers sorted by name. It returns an empty slice when the
|
|
// mcpServers key is missing or empty.
|
|
func ParseConfig(path string) ([]ServerConfig, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("read mcp config %q: %w", path, err)
|
|
}
|
|
|
|
var cfg mcpConfigFile
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
return nil, xerrors.Errorf("parse mcp config %q: %w", path, err)
|
|
}
|
|
|
|
if len(cfg.MCPServers) == 0 {
|
|
return []ServerConfig{}, nil
|
|
}
|
|
|
|
servers := make([]ServerConfig, 0, len(cfg.MCPServers))
|
|
for name, raw := range cfg.MCPServers {
|
|
var entry mcpServerEntry
|
|
if err := json.Unmarshal(raw, &entry); err != nil {
|
|
return nil, xerrors.Errorf("parse server %q in %q: %w", name, path, err)
|
|
}
|
|
|
|
if strings.Contains(name, ToolNameSep) || strings.HasPrefix(name, "_") || strings.HasSuffix(name, "_") {
|
|
return nil, xerrors.Errorf("server name %q in %q contains reserved separator %q or leading/trailing underscore", name, path, ToolNameSep)
|
|
}
|
|
|
|
transport := inferTransport(entry)
|
|
|
|
if transport == "" {
|
|
return nil, xerrors.Errorf("server %q in %q has no command or url", name, path)
|
|
}
|
|
|
|
resolveEnvVars(entry.Env)
|
|
|
|
servers = append(servers, ServerConfig{
|
|
Name: name,
|
|
Transport: transport,
|
|
Command: entry.Command,
|
|
Args: entry.Args,
|
|
Env: entry.Env,
|
|
URL: entry.URL,
|
|
Headers: entry.Headers,
|
|
})
|
|
}
|
|
|
|
slices.SortFunc(servers, func(a, b ServerConfig) int {
|
|
return strings.Compare(a.Name, b.Name)
|
|
})
|
|
|
|
return servers, nil
|
|
}
|
|
|
|
// inferTransport determines the transport type for a server entry.
|
|
// An explicit "type" field takes priority; otherwise the presence
|
|
// of "command" implies stdio and "url" implies http.
|
|
func inferTransport(e mcpServerEntry) string {
|
|
if e.Type != "" {
|
|
return e.Type
|
|
}
|
|
if e.Command != "" {
|
|
return "stdio"
|
|
}
|
|
if e.URL != "" {
|
|
return "http"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// resolveEnvVars expands ${VAR} references in env map values
|
|
// using the current process environment.
|
|
func resolveEnvVars(env map[string]string) {
|
|
for k, v := range env {
|
|
env[k] = os.Expand(v, os.Getenv)
|
|
}
|
|
}
|