1package tools
2
3import (
4 "cmp"
5 "context"
6 _ "embed"
7 "fmt"
8 "log/slog"
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 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}