mcp-tools.go

  1package tools
  2
  3import (
  4	"context"
  5	"fmt"
  6
  7	"charm.land/fantasy"
  8	"github.com/charmbracelet/crush/internal/agent/tools/mcp"
  9	"github.com/charmbracelet/crush/internal/config"
 10	"github.com/charmbracelet/crush/internal/permission"
 11)
 12
 13// GetMCPTools gets all the currently available MCP tools.
 14func GetMCPTools(permissions permission.Service, cfg *config.Config, wd string) []*Tool {
 15	var result []*Tool
 16	for mcpName, tools := range mcp.Tools() {
 17		for _, tool := range tools {
 18			result = append(result, &Tool{
 19				mcpName:     mcpName,
 20				tool:        tool,
 21				permissions: permissions,
 22				workingDir:  wd,
 23				cfg:         cfg,
 24			})
 25		}
 26	}
 27	return result
 28}
 29
 30// Tool is a tool from a MCP.
 31type Tool struct {
 32	mcpName         string
 33	tool            *mcp.Tool
 34	cfg             *config.Config
 35	permissions     permission.Service
 36	workingDir      string
 37	providerOptions fantasy.ProviderOptions
 38}
 39
 40func (m *Tool) SetProviderOptions(opts fantasy.ProviderOptions) {
 41	m.providerOptions = opts
 42}
 43
 44func (m *Tool) ProviderOptions() fantasy.ProviderOptions {
 45	return m.providerOptions
 46}
 47
 48func (m *Tool) Name() string {
 49	return fmt.Sprintf("mcp_%s_%s", m.mcpName, m.tool.Name)
 50}
 51
 52func (m *Tool) MCP() string {
 53	return m.mcpName
 54}
 55
 56func (m *Tool) MCPToolName() string {
 57	return m.tool.Name
 58}
 59
 60func (m *Tool) Info() fantasy.ToolInfo {
 61	parameters := make(map[string]any)
 62	required := make([]string, 0)
 63
 64	if input, ok := m.tool.InputSchema.(map[string]any); ok {
 65		if props, ok := input["properties"].(map[string]any); ok {
 66			parameters = props
 67		}
 68		if req, ok := input["required"].([]any); ok {
 69			// Convert []any -> []string when elements are strings
 70			for _, v := range req {
 71				if s, ok := v.(string); ok {
 72					required = append(required, s)
 73				}
 74			}
 75		} else if reqStr, ok := input["required"].([]string); ok {
 76			// Handle case where it's already []string
 77			required = reqStr
 78		}
 79	}
 80
 81	return fantasy.ToolInfo{
 82		Name:        m.Name(),
 83		Description: m.tool.Description,
 84		Parameters:  parameters,
 85		Required:    required,
 86	}
 87}
 88
 89func (m *Tool) Run(ctx context.Context, params fantasy.ToolCall) (fantasy.ToolResponse, error) {
 90	sessionID := GetSessionFromContext(ctx)
 91	if sessionID == "" {
 92		return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
 93	}
 94	permissionDescription := fmt.Sprintf("execute %s with the following parameters:", m.Info().Name)
 95	p, err := m.permissions.Request(ctx,
 96		permission.CreatePermissionRequest{
 97			SessionID:   sessionID,
 98			ToolCallID:  params.ID,
 99			Path:        m.workingDir,
100			ToolName:    m.Info().Name,
101			Action:      "execute",
102			Description: permissionDescription,
103			Params:      params.Input,
104		},
105	)
106	if err != nil {
107		return fantasy.ToolResponse{}, err
108	}
109	if !p {
110		return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
111	}
112
113	result, err := mcp.RunTool(ctx, m.cfg, m.mcpName, m.tool.Name, params.Input)
114	if err != nil {
115		return fantasy.NewTextErrorResponse(err.Error()), nil
116	}
117
118	switch result.Type {
119	case "image", "media":
120		if !GetSupportsImagesFromContext(ctx) {
121			modelName := GetModelNameFromContext(ctx)
122			return fantasy.NewTextErrorResponse(fmt.Sprintf("This model (%s) does not support image data.", modelName)), nil
123		}
124
125		var response fantasy.ToolResponse
126		if result.Type == "image" {
127			response = fantasy.NewImageResponse(result.Data, result.MediaType)
128		} else {
129			response = fantasy.NewMediaResponse(result.Data, result.MediaType)
130		}
131		response.Content = result.Content
132		return response, nil
133	default:
134		return fantasy.NewTextResponse(result.Content), nil
135	}
136}