read_mcp_resource.go

  1package tools
  2
  3import (
  4	"cmp"
  5	"context"
  6	_ "embed"
  7	"fmt"
  8	"log/slog"
  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 ReadMCPResourceParams struct {
 19	MCPName string `json:"mcp_name" description:"The MCP server name"`
 20	URI     string `json:"uri" description:"The resource URI to read"`
 21}
 22
 23type ReadMCPResourcePermissionsParams struct {
 24	MCPName string `json:"mcp_name"`
 25	URI     string `json:"uri"`
 26}
 27
 28const ReadMCPResourceToolName = "read_mcp_resource"
 29
 30//go:embed read_mcp_resource.md
 31var readMCPResourceDescription string
 32
 33func NewReadMCPResourceTool(cfg *config.ConfigStore, permissions permission.Service) fantasy.AgentTool {
 34	return fantasy.NewParallelAgentTool(
 35		ReadMCPResourceToolName,
 36		readMCPResourceDescription,
 37		func(ctx context.Context, params ReadMCPResourceParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
 38			params.MCPName = strings.TrimSpace(params.MCPName)
 39			params.URI = strings.TrimSpace(params.URI)
 40			if params.MCPName == "" {
 41				return fantasy.NewTextErrorResponse("mcp_name parameter is required"), nil
 42			}
 43			if params.URI == "" {
 44				return fantasy.NewTextErrorResponse("uri parameter is required"), nil
 45			}
 46
 47			sessionID := GetSessionFromContext(ctx)
 48			if sessionID == "" {
 49				return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for reading MCP resources")
 50			}
 51
 52			relPath := filepathext.SmartJoin(cfg.WorkingDir(), cmp.Or(params.URI, "mcp-resource"))
 53			p, err := permissions.Request(
 54				ctx,
 55				permission.CreatePermissionRequest{
 56					SessionID:   sessionID,
 57					Path:        relPath,
 58					ToolCallID:  call.ID,
 59					ToolName:    ReadMCPResourceToolName,
 60					Action:      "read",
 61					Description: fmt.Sprintf("Read MCP resource from %s", params.MCPName),
 62					Params:      ReadMCPResourcePermissionsParams(params),
 63				},
 64			)
 65			if err != nil {
 66				return fantasy.ToolResponse{}, err
 67			}
 68			if !p {
 69				return NewPermissionDeniedResponse(), nil
 70			}
 71
 72			contents, err := mcp.ReadResource(ctx, cfg, params.MCPName, params.URI)
 73			if err != nil {
 74				return fantasy.NewTextErrorResponse(err.Error()), nil
 75			}
 76			if len(contents) == 0 {
 77				return fantasy.NewTextResponse(""), nil
 78			}
 79
 80			var textParts []string
 81			for _, content := range contents {
 82				if content == nil {
 83					continue
 84				}
 85				if content.Text != "" {
 86					textParts = append(textParts, content.Text)
 87					continue
 88				}
 89				if len(content.Blob) > 0 {
 90					textParts = append(textParts, string(content.Blob))
 91					continue
 92				}
 93				slog.Debug("MCP resource content missing text/blob", "uri", content.URI)
 94			}
 95
 96			if len(textParts) == 0 {
 97				return fantasy.NewTextResponse(""), nil
 98			}
 99
100			return fantasy.NewTextResponse(strings.Join(textParts, "\n")), nil
101		},
102	)
103}