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:
@@ -0,0 +1,138 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user