Files
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

139 lines
3.8 KiB
Go

// Package mcptools provides bridges to convert MCP server tools into native agent tools.
package mcptools
import (
"context"
"fmt"
"log/slog"
"time"
"github.com/mark3labs/mcp-go/mcp"
shellmcp "github.com/enmanuel/agents/shell/mcp"
"github.com/enmanuel/agents/tools"
)
// FromMCPServer converts tools from an MCP client into native agent tools.
// prefix is prepended to tool names to avoid collisions (e.g., "brave_" → "brave_web_search").
// filter limits which tools to expose (empty = all tools).
// timeout is the default timeout for tool calls (0 = no timeout).
func FromMCPServer(mcpClient *shellmcp.Client, prefix string, filter []string, timeout time.Duration, logger *slog.Logger) []tools.Tool {
if timeout == 0 {
timeout = 30 * time.Second // default timeout
}
mcpTools := mcpClient.Tools()
filterSet := make(map[string]bool)
for _, name := range filter {
filterSet[name] = true
}
var result []tools.Tool
for _, mcpTool := range mcpTools {
// Apply filter if specified
if len(filterSet) > 0 && !filterSet[mcpTool.Name] {
continue
}
// Convert MCP tool to native tool
toolName := prefix + mcpTool.Name
tool := convertMCPTool(mcpClient, mcpTool, toolName, timeout, logger)
result = append(result, tool)
}
logger.Info("converted MCP tools", "server", mcpClient.Name(), "count", len(result))
return result
}
// convertMCPTool converts a single mcp.Tool to a tools.Tool.
func convertMCPTool(mcpClient *shellmcp.Client, mcpTool mcp.Tool, prefixedName string, timeout time.Duration, logger *slog.Logger) tools.Tool {
return tools.Tool{
Def: tools.Def{
Name: prefixedName,
Description: mcpTool.Description,
Parameters: convertSchema(mcpTool.InputSchema),
},
Exec: func(ctx context.Context, args map[string]any) tools.Result {
// Call the MCP tool (using original name without prefix)
result, err := mcpClient.CallTool(ctx, mcpTool.Name, args, timeout)
if err != nil {
logger.Error("MCP tool call failed", "tool", mcpTool.Name, "error", err)
return tools.Result{Err: err}
}
// Extract text from result
output := extractTextFromResult(result)
return tools.Result{Output: output}
},
}
}
// convertSchema converts an MCP InputSchema to agent tool Parameters.
func convertSchema(schema mcp.ToolInputSchema) []tools.Param {
var params []tools.Param
// MCP schemas are JSON Schema objects with type: "object" and properties
if schema.Type != "object" || schema.Properties == nil {
return params
}
requiredSet := make(map[string]bool)
for _, name := range schema.Required {
requiredSet[name] = true
}
for propName, propVal := range schema.Properties {
param := tools.Param{
Name: propName,
Required: requiredSet[propName],
}
// Extract type and description from property schema
if propMap, ok := propVal.(map[string]any); ok {
if typeStr, ok := propMap["type"].(string); ok {
param.Type = typeStr
}
if desc, ok := propMap["description"].(string); ok {
param.Description = desc
}
}
// Default to string if type not found
if param.Type == "" {
param.Type = "string"
}
params = append(params, param)
}
return params
}
// extractTextFromResult extracts text content from an MCP CallToolResult.
func extractTextFromResult(result *mcp.CallToolResult) string {
if result == nil {
return ""
}
var output string
for _, content := range result.Content {
// Handle different content types
switch c := content.(type) {
case mcp.TextContent:
output += c.Text
case *mcp.TextContent:
output += c.Text
default:
// For other content types (image, audio, resources), just indicate presence
output += fmt.Sprintf("[non-text content: %T]\n", content)
}
}
// If result has IsError flag set, prepend error indicator
if result.IsError {
output = "[ERROR] " + output
}
return output
}