# 017 — MCP Client: consumir servidores MCP como tools del agente ## Objetivo Permitir que los agentes se conecten a servidores MCP externos y expongan las tools de esos servidores como tools normales en su registry. Desde el punto de vista del LLM, una tool MCP es indistinguible de una tool nativa (ssh_command, http_get, etc.) — aparece en el function calling con su nombre, descripcion y parametros. ## Contexto - Ya existe `shell/protocols/mcp.go` que **expone** tools del agente como MCP server (server-side). Falta el **cliente** que consume tools de servidores MCP externos. - La dependencia `github.com/mark3labs/mcp-go v0.44.1` ya esta en go.mod. Incluye paquetes `client` y `mcp` con soporte para stdio y SSE/HTTP. - El config ya tiene `MCPToolCfg` con `Servers []MCPServerCfg` en `internal/config/schema.go`, pero solo soporta `url` — hay que extender para soportar transporte stdio (command + args). - El tool registry (`tools/Registry`) ya soporta registrar cualquier `tools.Tool` (Def + Exec). - El runtime (`agents/runtime.go:buildToolRegistry`) ya tiene el patron para registrar tools condicionalmente. ## Prerequisitos - Ninguno estricto. La infraestructura de tools y config ya existe. --- ## Arquitectura ``` config.yaml (tools.mcp.servers) ↓ shell/mcp/client.go ← conecta a servidores MCP, descubre tools ↓ tools/mcptools/mcp.go ← wrappea cada tool MCP como tools.Tool ↓ agents/runtime.go ← registra en el Registry como cualquier otra tool ↓ LLM ve las tools MCP en function calling, las invoca normalmente ``` ### Patron pure core / impure shell ``` pkg/ (nada nuevo) → no se necesitan tipos puros nuevos; tools.Def ya cubre shell/mcp/ → IMPURE: cliente MCP real (I/O, subprocesos, red) tools/mcptools/ → bridge: convierte MCP tool → tools.Tool ``` ## Transportes MCP soportados | Transporte | Config | Descripcion | |-----------|--------|-------------| | **stdio** | `command` + `args` | Lanza un subproceso y se comunica via stdin/stdout. El mas comun (Claude Desktop, npx servers). | | **SSE/HTTP** | `url` | Se conecta a un servidor MCP remoto via HTTP con Server-Sent Events. | ## Tareas ### Fase 1: Extender config para stdio transport - [ ] **1.1** Modificar `MCPServerCfg` en `internal/config/schema.go` para soportar ambos transportes: ```go type MCPServerCfg struct { Name string `yaml:"name"` // nombre logico del servidor Transport string `yaml:"transport"` // "stdio" | "sse" (default: auto-detect) Command string `yaml:"command"` // stdio: comando a ejecutar Args []string `yaml:"args"` // stdio: argumentos del comando Env map[string]string `yaml:"env"` // stdio: variables de entorno extra URL string `yaml:"url"` // sse: URL del servidor Headers map[string]string `yaml:"headers"` // sse: headers HTTP extra (auth, etc.) Tools []string `yaml:"tools"` // filtro: solo exponer estas tools (vacio = todas) Prefix string `yaml:"prefix"` // prefijo para nombres de tools (evitar colisiones) Timeout time.Duration `yaml:"timeout"` // timeout por llamada (default: 30s) } ``` - [ ] **1.2** Validar que `Command` o `URL` este presente (al menos uno). ### Fase 2: MCP Client en `shell/mcp/` - [ ] **2.1** Crear `shell/mcp/client.go` — wrapper sobre `mcp-go/client`: ```go // Client conecta a un servidor MCP y descubre sus tools. type Client struct { name string mcpClient *client.StdioMCPClient // o SSEMCPClient tools []mcp.Tool // tools descubiertas logger *slog.Logger } func NewStdioClient(name, command string, args []string, env map[string]string, logger *slog.Logger) (*Client, error) func NewSSEClient(name, url string, headers map[string]string, logger *slog.Logger) (*Client, error) func (c *Client) Tools() []mcp.Tool // tools descubiertas func (c *Client) CallTool(ctx context.Context, name string, args map[string]any) (*mcp.CallToolResult, error) func (c *Client) Close() error ``` - [ ] **2.2** Implementar `NewStdioClient`: - Crear `client.NewStdioMCPClient(command, env, args...)` (ver API de mcp-go) - Llamar `Initialize()` con info del agente - Llamar `ListTools()` para descubrir tools disponibles - Guardar la lista de tools - [ ] **2.3** Implementar `NewSSEClient`: - Crear `client.NewSSEMCPClient(url, options...)` - Initialize + ListTools igual que stdio - [ ] **2.4** Implementar `CallTool`: - Delegar a `mcpClient.CallTool(ctx, mcp.CallToolRequest{...})` - Extraer texto del resultado (manejar text y error results) - [ ] **2.5** Implementar `Close`: - Cerrar el cliente MCP (mata el subproceso en stdio, cierra conexion en SSE) ### Fase 3: Bridge MCP → tools.Tool en `tools/mcptools/` - [ ] **3.1** Crear `tools/mcptools/mcp.go` — convierte tools de un MCP server en `[]tools.Tool`: ```go // FromMCPServer toma un shell/mcp.Client y genera tools.Tool para cada tool MCP. // prefix se antepone al nombre de la tool (ej: "brave_" → "brave_web_search"). // filter limita que tools exponer (vacio = todas). func FromMCPServer(mcpClient *shellmcp.Client, prefix string, filter []string, timeout time.Duration) []tools.Tool ``` - [ ] **3.2** Implementar conversion de `mcp.Tool` → `tools.Def`: - `Name` = prefix + tool.Name - `Description` = tool.Description - `Parameters` = convertir `tool.InputSchema` (JSON Schema) → `[]tools.Param` - JSON Schema properties → Param con name, type, description - JSON Schema required → Param.Required = true - [ ] **3.3** Implementar el `ToolFunc` wrapper: - Recibe `args map[string]any` - Llama a `mcpClient.CallTool(ctx, originalName, args)` (sin prefix) - Convierte el resultado MCP a `tools.Result` ### Fase 4: Integracion en runtime - [ ] **4.1** Crear `shell/mcp/manager.go` — gestiona multiples clientes MCP: ```go // Manager inicializa y gestiona conexiones a multiples servidores MCP. type Manager struct { clients map[string]*Client // name → client logger *slog.Logger } func NewManager(servers []config.MCPServerCfg, logger *slog.Logger) (*Manager, error) func (m *Manager) AllTools(reg *tools.Registry) // registra todas las tools en el registry func (m *Manager) Close() error // cierra todos los clientes ``` - [ ] **4.2** Integrar en `agents/runtime.go`: - En `New()`: si `cfg.Tools.MCP.Enabled && len(cfg.Tools.MCP.Servers) > 0`, crear `mcp.NewManager(...)` - Llamar `manager.AllTools(toolReg)` para registrar las tools MCP en el registry - Guardar manager en `Agent` struct para cerrar en `Run()` defer - Las tools MCP aparecen automaticamente en el function calling del LLM - [ ] **4.3** Anadir campo `mcpManager` al struct `Agent` y cerrar en `Run()`: ```go type Agent struct { // ...existing fields... mcpManager *shellmcp.Manager // nil when MCP client is disabled } ``` ### Fase 5: Ejemplo de configuracion - [ ] **5.1** Documentar ejemplo con servidor MCP stdio (ej: brave-search, filesystem): ```yaml tools: mcp: enabled: true servers: - name: brave-search command: npx args: ["-y", "@anthropic/mcp-server-brave-search"] env: BRAVE_API_KEY: "${BRAVE_API_KEY}" prefix: "brave_" - name: filesystem command: npx args: ["-y", "@anthropic/mcp-server-filesystem", "/home/data"] prefix: "fs_" - name: remote-tools url: "http://localhost:8080/mcp" tools: ["search", "summarize"] # solo estas tools prefix: "remote_" ``` - [ ] **5.2** Probar con al menos un servidor MCP real (brave-search o filesystem) en un agente de prueba. ### Fase 6: Tests - [ ] **6.1** Unit tests para `tools/mcptools/mcp.go` — verificar conversion de schema MCP → tools.Def - [ ] **6.2** Unit tests para `shell/mcp/client.go` — mock del protocolo MCP (o test con echo server) - [ ] **6.3** Integration test: un agente con MCP habilitado lista tools MCP en su registry ### Fase 7: Cleanup y docs - [ ] **7.1** Actualizar `CLAUDE.md` — anadir `shell/mcp/`, `tools/mcptools/` a la estructura - [ ] **7.2** Actualizar `.claude/rules/create_tool.md` si es necesario — mencionar que tools MCP se auto-registran - [ ] **7.3** Mover o refactorizar `shell/protocols/mcp.go` (MCP server) a `shell/mcp/server.go` para colocarlo junto al client --- ## Ejemplo de flujo completo ``` 1. Agente arranca, config tiene tools.mcp.servers con brave-search (stdio) 2. runtime.go → mcp.NewManager() → lanza `npx -y @anthropic/mcp-server-brave-search` → Initialize → ListTools → descubre: web_search, local_search 3. mcptools.FromMCPServer() convierte: - mcp.Tool{name: "web_search", ...} → tools.Tool{Def: {Name: "brave_web_search", ...}, Exec: wrapper} - mcp.Tool{name: "local_search", ...} → tools.Tool{Def: {Name: "brave_local_search", ...}, Exec: wrapper} 4. Se registran en el toolReg → aparecen en ToLLMSpecs() 5. Usuario pregunta: "busca noticias sobre Go 1.23" → LLM ve brave_web_search en sus tools → genera tool_call → runtime ejecuta → wrapper llama mcpClient.CallTool("web_search", args) → resultado vuelve al LLM → genera respuesta final ``` ## Decisiones de diseno - **Prefix por servidor**: evita colisiones de nombres entre servidores MCP que tengan tools con el mismo nombre. Configurable por servidor. - **Filter de tools**: permite exponer solo un subset de tools de un servidor MCP (seguridad + reducir contexto del LLM). - **Manager pattern**: centraliza lifecycle de multiples clientes MCP. Similar a como el bus manager gestiona multiples agentes. - **Stdio como transporte principal**: es el estandar de facto en MCP. Los servidores mas populares (brave, filesystem, github, etc.) usan stdio. - **Auto-discovery**: las tools se descubren automaticamente via `ListTools()`. No hace falta declararlas manualmente. - **Sin tipos puros nuevos**: `tools.Def` y `tools.Param` ya cubren la especificacion de una tool. No se necesita nada en `pkg/`. ## Riesgos - **Subprocesos zombie**: si el agente crashea, los procesos MCP stdio pueden quedar huerfanos. Mitigar con process groups y cleanup en `Close()`. - **Latencia de inicio**: `npx -y` descarga paquetes la primera vez. Puede tardar. Considerar cache o pre-instalacion. - **Schema complejo**: algunos MCP servers tienen input schemas con nested objects/arrays. La conversion a `tools.Param` debe manejar esto (al menos `object` y `array` como tipos). - **Seguridad**: un servidor MCP malicioso podria exponer tools daninas. El filtro de tools y el prefix ayudan, pero la confianza es del operador. - **Timeout**: llamadas a MCP servers externos pueden ser lentas. Timeout configurable por servidor. ## Dependencias - `github.com/mark3labs/mcp-go v0.44.1` — ya en go.mod, incluye `client` package - No se necesitan dependencias nuevas