1fccae1568
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>
242 lines
11 KiB
Markdown
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
|