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	"git.secluded.site/crush/internal/agent/tools/mcp"
 13	"git.secluded.site/crush/internal/config"
 14	"git.secluded.site/crush/internal/filepathext"
 15	"git.secluded.site/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 string
 30
 31func NewListMCPResourcesTool(cfg *config.ConfigStore, permissions permission.Service) fantasy.AgentTool {
 32	return fantasy.NewParallelAgentTool(
 33		ListMCPResourcesToolName,
 34		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(
 48				ctx,
 49				permission.CreatePermissionRequest{
 50					SessionID:   sessionID,
 51					Path:        relPath,
 52					ToolCallID:  call.ID,
 53					ToolName:    ListMCPResourcesToolName,
 54					Action:      "list",
 55					Description: fmt.Sprintf("List MCP resources from %s", params.MCPName),
 56					Params:      ListMCPResourcesPermissionsParams(params),
 57				},
 58			)
 59			if err != nil {
 60				return fantasy.ToolResponse{}, err
 61			}
 62			if !p {
 63				return NewPermissionDeniedResponse(), nil
 64			}
 65
 66			resources, err := mcp.ListResources(ctx, cfg, params.MCPName)
 67			if err != nil {
 68				return fantasy.NewTextErrorResponse(err.Error()), nil
 69			}
 70			if len(resources) == 0 {
 71				return fantasy.NewTextResponse("No resources found"), nil
 72			}
 73
 74			lines := make([]string, 0, len(resources))
 75			for _, resource := range resources {
 76				if resource == nil {
 77					continue
 78				}
 79				title := cmp.Or(resource.Title, resource.Name, resource.URI)
 80				line := fmt.Sprintf("- %s", title)
 81				if resource.URI != "" {
 82					line = fmt.Sprintf("%s (%s)", line, resource.URI)
 83				}
 84				if resource.Description != "" {
 85					line = fmt.Sprintf("%s: %s", line, resource.Description)
 86				}
 87				if resource.MIMEType != "" {
 88					line = fmt.Sprintf("%s [mime: %s]", line, resource.MIMEType)
 89				}
 90				if resource.Size > 0 {
 91					line = fmt.Sprintf("%s [size: %d]", line, resource.Size)
 92				}
 93				lines = append(lines, line)
 94			}
 95
 96			sort.Strings(lines)
 97			return fantasy.NewTextResponse(strings.Join(lines, "\n")), nil
 98		},
 99	)
100}