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