resources.go

  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}