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 []byte
 32
 33func NewReadMCPResourceTool(cfg *config.ConfigStore, permissions permission.Service) fantasy.AgentTool {
 34	return fantasy.NewParallelAgentTool(
 35		ReadMCPResourceToolName,
 36		string(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(ctx,
 54				permission.CreatePermissionRequest{
 55					SessionID:   sessionID,
 56					Path:        relPath,
 57					ToolCallID:  call.ID,
 58					ToolName:    ReadMCPResourceToolName,
 59					Action:      "read",
 60					Description: fmt.Sprintf("Read MCP resource from %s", params.MCPName),
 61					Params:      ReadMCPResourcePermissionsParams(params),
 62				},
 63			)
 64			if err != nil {
 65				return fantasy.ToolResponse{}, err
 66			}
 67			if !p {
 68				return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
 69			}
 70
 71			contents, err := mcp.ReadResource(ctx, cfg, params.MCPName, params.URI)
 72			if err != nil {
 73				return fantasy.NewTextErrorResponse(err.Error()), nil
 74			}
 75			if len(contents) == 0 {
 76				return fantasy.NewTextResponse(""), nil
 77			}
 78
 79			var textParts []string
 80			for _, content := range contents {
 81				if content == nil {
 82					continue
 83				}
 84				if content.Text != "" {
 85					textParts = append(textParts, content.Text)
 86					continue
 87				}
 88				if len(content.Blob) > 0 {
 89					textParts = append(textParts, string(content.Blob))
 90					continue
 91				}
 92				slog.Debug("MCP resource content missing text/blob", "uri", content.URI)
 93			}
 94
 95			if len(textParts) == 0 {
 96				return fantasy.NewTextResponse(""), nil
 97			}
 98
 99			return fantasy.NewTextResponse(strings.Join(textParts, "\n")), nil
100		},
101	)
102}