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>
This commit is contained in:
2026-03-08 21:22:33 +00:00
parent 5003201dd8
commit 1fccae1568
9 changed files with 489 additions and 8 deletions
+47 -2
View File
@@ -29,6 +29,7 @@ import (
shellknowledge "github.com/enmanuel/agents/shell/knowledge"
shelllm "github.com/enmanuel/agents/shell/llm"
"github.com/enmanuel/agents/shell/matrix"
shellmcp "github.com/enmanuel/agents/shell/mcp"
shellmem "github.com/enmanuel/agents/shell/memory"
"github.com/enmanuel/agents/shell/ssh"
"github.com/enmanuel/agents/tools"
@@ -37,6 +38,7 @@ import (
toolhttp "github.com/enmanuel/agents/tools/http"
toolknowledge "github.com/enmanuel/agents/tools/knowledgetools"
toolmatrix "github.com/enmanuel/agents/tools/matrix"
toolmcp "github.com/enmanuel/agents/tools/mcptools"
toolmemory "github.com/enmanuel/agents/tools/memorytools"
toolssh "github.com/enmanuel/agents/tools/ssh"
toolweather "github.com/enmanuel/agents/tools/weather"
@@ -62,6 +64,7 @@ type Agent struct {
toolReg *tools.Registry
logger *slog.Logger
cryptoStore io.Closer // non-nil when E2EE is enabled; closed on shutdown
mcpManager *shellmcp.Manager // nil when MCP client is disabled
// Lifecycle — cancel stops this agent individually; done is closed when Run returns.
cancel context.CancelFunc
@@ -236,8 +239,20 @@ func New(cfg *config.AgentConfig, rules []decision.Rule, agentACL acl.ACL, logge
logger.Info("acl enabled (centralized security policy)")
}
// MCP client manager — connects to external MCP servers
var mcpManager *shellmcp.Manager
if cfg.Tools.MCP.Enabled && len(cfg.Tools.MCP.Servers) > 0 {
var mcpErr error
mcpManager, mcpErr = shellmcp.NewManager(context.Background(), cfg.Tools.MCP.Servers, logger)
if mcpErr != nil {
logger.Error("mcp_manager_init_failed", "err", mcpErr)
} else {
logger.Info("mcp manager initialized", "servers", len(cfg.Tools.MCP.Servers))
}
}
// Tool registry — register tools enabled in config
toolReg := buildToolRegistry(cfg, sshExec, matrixClient, memStore, kStore, roomCtx, logger)
toolReg := buildToolRegistry(cfg, sshExec, matrixClient, memStore, kStore, mcpManager, roomCtx, logger)
// Rate limiting for tools
if cfg.Security.ToolRateLimit.Enabled {
@@ -272,7 +287,8 @@ func New(cfg *config.AgentConfig, rules []decision.Rule, agentACL acl.ACL, logge
toolReg: toolReg,
logger: logger,
cryptoStore: cryptoStore,
done: make(chan struct{}),
mcpManager: mcpManager,
done: make(chan struct{}),
commands: make(map[string]CommandHandler),
cmdAliases: command.BuiltinNames(),
startTime: time.Now(),
@@ -401,6 +417,9 @@ func (a *Agent) Run(ctx context.Context) error {
if a.knowledgeStore != nil {
defer a.knowledgeStore.Close()
}
if a.mcpManager != nil {
defer a.mcpManager.Close()
}
a.logger.Info("agent starting",
"id", a.cfg.Agent.ID,
"name", a.cfg.Agent.Name,
@@ -980,6 +999,7 @@ func buildToolRegistry(
matrixClient *matrix.Client,
memStore memory.Store,
kStore *shellknowledge.FileStore,
mcpManager *shellmcp.Manager,
roomCtx *toolmemory.RoomContext,
logger *slog.Logger,
) *tools.Registry {
@@ -1031,5 +1051,30 @@ func buildToolRegistry(
logger.Debug("registered knowledge tools")
}
// MCP tools — register tools from all connected MCP servers
if mcpManager != nil {
for serverName, mcpClient := range mcpManager.AllClients() {
// Find the config for this server to get prefix, filter, timeout
var serverCfg *config.MCPServerCfg
for i := range cfg.Tools.MCP.Servers {
if cfg.Tools.MCP.Servers[i].Name == serverName {
serverCfg = &cfg.Tools.MCP.Servers[i]
break
}
}
if serverCfg == nil {
logger.Warn("no config found for MCP server", "name", serverName)
continue
}
// Convert and register MCP tools
mcpTools := toolmcp.FromMCPServer(mcpClient, serverCfg.Prefix, serverCfg.Tools, serverCfg.Timeout, logger)
for _, tool := range mcpTools {
reg.Register(tool)
}
logger.Debug("registered MCP tools", "server", serverName, "count", len(mcpTools))
}
}
return reg
}