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}