1package tools
2
3import (
4 "cmp"
5 "context"
6 _ "embed"
7 "fmt"
8 "sort"
9 "strings"
10
11 "charm.land/fantasy"
12 "github.com/charmbracelet/crush/internal/agent/tools/mcp"
13 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/filepathext"
15 "github.com/charmbracelet/crush/internal/permission"
16)
17
18type ListMCPResourcesParams struct {
19 MCPName string `json:"mcp_name" description:"The MCP server name"`
20}
21
22type ListMCPResourcesPermissionsParams struct {
23 MCPName string `json:"mcp_name"`
24}
25
26const ListMCPResourcesToolName = "list_mcp_resources"
27
28//go:embed list_mcp_resources.md
29var listMCPResourcesDescription []byte
30
31func NewListMCPResourcesTool(cfg *config.ConfigStore, permissions permission.Service) fantasy.AgentTool {
32 return fantasy.NewParallelAgentTool(
33 ListMCPResourcesToolName,
34 string(listMCPResourcesDescription),
35 func(ctx context.Context, params ListMCPResourcesParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
36 params.MCPName = strings.TrimSpace(params.MCPName)
37 if params.MCPName == "" {
38 return fantasy.NewTextErrorResponse("mcp_name parameter is required"), nil
39 }
40
41 sessionID := GetSessionFromContext(ctx)
42 if sessionID == "" {
43 return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for listing MCP resources")
44 }
45
46 relPath := filepathext.SmartJoin(cfg.WorkingDir(), params.MCPName)
47 p, err := permissions.Request(ctx,
48 permission.CreatePermissionRequest{
49 SessionID: sessionID,
50 Path: relPath,
51 ToolCallID: call.ID,
52 ToolName: ListMCPResourcesToolName,
53 Action: "list",
54 Description: fmt.Sprintf("List MCP resources from %s", params.MCPName),
55 Params: ListMCPResourcesPermissionsParams(params),
56 },
57 )
58 if err != nil {
59 return fantasy.ToolResponse{}, err
60 }
61 if !p {
62 return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
63 }
64
65 resources, err := mcp.ListResources(ctx, cfg, params.MCPName)
66 if err != nil {
67 return fantasy.NewTextErrorResponse(err.Error()), nil
68 }
69 if len(resources) == 0 {
70 return fantasy.NewTextResponse("No resources found"), nil
71 }
72
73 lines := make([]string, 0, len(resources))
74 for _, resource := range resources {
75 if resource == nil {
76 continue
77 }
78 title := cmp.Or(resource.Title, resource.Name, resource.URI)
79 line := fmt.Sprintf("- %s", title)
80 if resource.URI != "" {
81 line = fmt.Sprintf("%s (%s)", line, resource.URI)
82 }
83 if resource.Description != "" {
84 line = fmt.Sprintf("%s: %s", line, resource.Description)
85 }
86 if resource.MIMEType != "" {
87 line = fmt.Sprintf("%s [mime: %s]", line, resource.MIMEType)
88 }
89 if resource.Size > 0 {
90 line = fmt.Sprintf("%s [size: %d]", line, resource.Size)
91 }
92 lines = append(lines, line)
93 }
94
95 sort.Strings(lines)
96 return fantasy.NewTextResponse(strings.Join(lines, "\n")), nil
97 },
98 )
99}