1package mcp
2
3import (
4 "context"
5 "errors"
6 "iter"
7 "log/slog"
8
9 "github.com/charmbracelet/crush/internal/config"
10 "github.com/charmbracelet/crush/internal/csync"
11 "github.com/modelcontextprotocol/go-sdk/jsonrpc"
12 "github.com/modelcontextprotocol/go-sdk/mcp"
13)
14
15type Resource = mcp.Resource
16
17type ResourceContents = mcp.ResourceContents
18
19var allResources = csync.NewMap[string, []*Resource]()
20
21// Resources returns all available MCP resources.
22func Resources() iter.Seq2[string, []*Resource] {
23 return allResources.Seq2()
24}
25
26// ListResources returns the current resources for an MCP server.
27func ListResources(ctx context.Context, cfg *config.ConfigStore, name string) ([]*Resource, error) {
28 session, err := getOrRenewClient(ctx, cfg, name)
29 if err != nil {
30 return nil, err
31 }
32
33 resources, err := getResources(ctx, session)
34 if err != nil {
35 return nil, err
36 }
37
38 resourceCount := updateResources(name, resources)
39 prev, _ := states.Get(name)
40 prev.Counts.Resources = resourceCount
41 updateState(name, StateConnected, nil, session, prev.Counts)
42 return resources, nil
43}
44
45// ReadResource reads the contents of a resource from an MCP server.
46func ReadResource(ctx context.Context, cfg *config.ConfigStore, name, uri string) ([]*ResourceContents, error) {
47 session, err := getOrRenewClient(ctx, cfg, name)
48 if err != nil {
49 return nil, err
50 }
51 result, err := session.ReadResource(ctx, &mcp.ReadResourceParams{URI: uri})
52 if err != nil {
53 return nil, err
54 }
55 return result.Contents, nil
56}
57
58// RefreshResources gets the updated list of resources from the MCP and updates the
59// global state.
60func RefreshResources(ctx context.Context, name string) {
61 session, ok := sessions.Get(name)
62 if !ok {
63 slog.Warn("Refresh resources: no session", "name", name)
64 return
65 }
66
67 resources, err := getResources(ctx, session)
68 if err != nil {
69 updateState(name, StateError, err, nil, Counts{})
70 return
71 }
72
73 resourceCount := updateResources(name, resources)
74
75 prev, _ := states.Get(name)
76 prev.Counts.Resources = resourceCount
77 updateState(name, StateConnected, nil, session, prev.Counts)
78}
79
80func getResources(ctx context.Context, c *ClientSession) ([]*Resource, error) {
81 if c.InitializeResult().Capabilities.Resources == nil {
82 return nil, nil
83 }
84 result, err := c.ListResources(ctx, &mcp.ListResourcesParams{})
85 if err != nil {
86 // Handle "Method not found" errors from MCP servers that don't support resources/list.
87 if isMethodNotFoundError(err) {
88 slog.Warn("MCP server does not support resources/list", "error", err)
89 return nil, nil
90 }
91 return nil, err
92 }
93 return result.Resources, nil
94}
95
96// isMethodNotFoundError checks if the error is a JSON-RPC "Method not found" error.
97func isMethodNotFoundError(err error) bool {
98 var rpcErr *jsonrpc.Error
99 return errors.As(err, &rpcErr) && rpcErr != nil && rpcErr.Code == jsonrpc.CodeMethodNotFound
100}
101
102func updateResources(name string, resources []*Resource) int {
103 if len(resources) == 0 {
104 allResources.Del(name)
105 return 0
106 }
107 allResources.Set(name, resources)
108 return len(resources)
109}