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>
139 lines
3.8 KiB
Go
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
|
|
}
|