// 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 }