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/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}