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

11 KiB

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:

    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:

    // 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:

    // 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.Tooltools.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:

    // 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():

    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):

    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