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}