# 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
