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}