list_mcp_resources.go

 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}