mcp-tools.go

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