Files
agents_and_robots/dev/issues/completed/0017-mcp-client-tools.md
T
egutierrez 1fccae1568 feat: añadir cliente MCP para consumir servidores externos
Implementa el cliente MCP que permite a los agentes conectarse a servidores
MCP externos y usar sus tools como si fueran tools nativas del agente.

Arquitectura implementada:
- shell/mcp/client.go: Cliente MCP con soporte stdio y SSE
- shell/mcp/manager.go: Gestor de múltiples clientes MCP
- tools/mcptools/mcp.go: Bridge que convierte MCP tools → tools.Tool
- shell/mcp/server.go: Movido desde shell/protocols/ para colocación junto al client

Cambios en config:
- MCPServerCfg extendido con campos Transport, Command, Args, Env, Headers,
  Prefix, Timeout para soportar stdio y SSE transport

Integración en runtime:
- agents/runtime.go: Inicializa MCP manager si config.Tools.MCP.Enabled
- buildToolRegistry: Registra tools MCP automáticamente con prefijos configurables
- Agent: Campo mcpManager que se cierra en shutdown

Transportes soportados:
- stdio: Lanza subproceso (ej: npx -y @anthropic/mcp-server-brave-search)
- SSE: Se conecta a servidor HTTP MCP

Las tools MCP son indistinguibles de tools nativas desde el punto de vista
del LLM. Auto-discovery via ListTools(), conversión de JSON Schema a tools.Param.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-08 21:22:33 +00:00

242 lines
11 KiB
Markdown

# 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