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/permission"
11)
12
13var whitelistCrushDockerTools = []string{
14 "mcp_crush_docker_mcp-find",
15 "mcp_crush_docker_mcp-add",
16 "mcp_crush_docker_mcp-remove",
17}
18
19// GetMCPTools gets all the currently available MCP tools.
20func GetMCPTools(permissions permission.Service, wd string) []*Tool {
21 var result []*Tool
22 for mcpName, tools := range mcp.Tools() {
23 for _, tool := range tools {
24 result = append(result, &Tool{
25 mcpName: mcpName,
26 tool: tool,
27 permissions: permissions,
28 workingDir: wd,
29 })
30 }
31 }
32 return result
33}
34
35// Tool is a tool from a MCP.
36type Tool struct {
37 mcpName string
38 tool *mcp.Tool
39 permissions permission.Service
40 workingDir string
41 providerOptions fantasy.ProviderOptions
42}
43
44func (m *Tool) SetProviderOptions(opts fantasy.ProviderOptions) {
45 m.providerOptions = opts
46}
47
48func (m *Tool) ProviderOptions() fantasy.ProviderOptions {
49 return m.providerOptions
50}
51
52func (m *Tool) Name() string {
53 return fmt.Sprintf("mcp_%s_%s", m.mcpName, m.tool.Name)
54}
55
56func (m *Tool) MCP() string {
57 return m.mcpName
58}
59
60func (m *Tool) MCPToolName() string {
61 return m.tool.Name
62}
63
64func (m *Tool) Info() fantasy.ToolInfo {
65 parameters := make(map[string]any)
66 required := make([]string, 0)
67
68 if input, ok := m.tool.InputSchema.(map[string]any); ok {
69 if props, ok := input["properties"].(map[string]any); ok {
70 parameters = props
71 }
72 if req, ok := input["required"].([]any); ok {
73 // Convert []any -> []string when elements are strings
74 for _, v := range req {
75 if s, ok := v.(string); ok {
76 required = append(required, s)
77 }
78 }
79 } else if reqStr, ok := input["required"].([]string); ok {
80 // Handle case where it's already []string
81 required = reqStr
82 }
83 }
84
85 return fantasy.ToolInfo{
86 Name: m.Name(),
87 Description: m.tool.Description,
88 Parameters: parameters,
89 Required: required,
90 }
91}
92
93func (m *Tool) Run(ctx context.Context, params fantasy.ToolCall) (fantasy.ToolResponse, error) {
94 sessionID := GetSessionFromContext(ctx)
95 if sessionID == "" {
96 return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
97 }
98 if !slices.Contains(whitelistCrushDockerTools, params.Name) {
99 permissionDescription := fmt.Sprintf("execute %s with the following parameters:", m.Info().Name)
100 p := m.permissions.Request(
101 permission.CreatePermissionRequest{
102 SessionID: sessionID,
103 ToolCallID: params.ID,
104 Path: m.workingDir,
105 ToolName: m.Info().Name,
106 Action: "execute",
107 Description: permissionDescription,
108 Params: params.Input,
109 },
110 )
111 if !p {
112 return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
113 }
114 }
115
116 content, err := mcp.RunTool(ctx, m.mcpName, m.tool.Name, params.Input)
117 if err != nil {
118 return fantasy.NewTextErrorResponse(err.Error()), nil
119 }
120 return fantasy.NewTextResponse(content), nil
121}