feat(mcp): resources support

Carlos Alexandro Becker created

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

Change summary

internal/llm/agent/mcp-tools.go    | 63 +++++++++++++++++++++++++------
internal/tui/components/mcp/mcp.go |  3 +
2 files changed, 54 insertions(+), 12 deletions(-)

Detailed changes

internal/llm/agent/mcp-tools.go 🔗

@@ -69,8 +69,9 @@ type MCPEvent struct {
 
 // MCPCounts number of available tools, prompts, etc.
 type MCPCounts struct {
-	Tools   int
-	Prompts int
+	Tools     int
+	Prompts   int
+	Resources int
 }
 
 // MCPClientInfo holds information about an MCP client's state
@@ -84,14 +85,16 @@ type MCPClientInfo struct {
 }
 
 var (
-	mcpToolsOnce      sync.Once
-	mcpTools          = csync.NewMap[string, tools.BaseTool]()
-	mcpClient2Tools   = csync.NewMap[string, []tools.BaseTool]()
-	mcpClients        = csync.NewMap[string, *mcp.ClientSession]()
-	mcpStates         = csync.NewMap[string, MCPClientInfo]()
-	mcpBroker         = pubsub.NewBroker[MCPEvent]()
-	mcpPrompts        = csync.NewMap[string, *mcp.Prompt]()
-	mcpClient2Prompts = csync.NewMap[string, []*mcp.Prompt]()
+	mcpToolsOnce        sync.Once
+	mcpTools            = csync.NewMap[string, tools.BaseTool]()
+	mcpClient2Tools     = csync.NewMap[string, []tools.BaseTool]()
+	mcpClients          = csync.NewMap[string, *mcp.ClientSession]()
+	mcpStates           = csync.NewMap[string, MCPClientInfo]()
+	mcpBroker           = pubsub.NewBroker[MCPEvent]()
+	mcpPrompts          = csync.NewMap[string, *mcp.Prompt]()
+	mcpClient2Prompts   = csync.NewMap[string, []*mcp.Prompt]()
+	mcpResources        = csync.NewMap[string, *mcp.Resource]()
+	mcpClient2Resources = csync.NewMap[string, []*mcp.Resource]()
 )
 
 type McpTool struct {
@@ -327,12 +330,22 @@ func doGetMCPTools(ctx context.Context, permissions permission.Service, cfg *con
 				return
 			}
 
+			resources, err := getResources(ctx, c)
+			if err != nil {
+				slog.Error("error listing resources", "error", err)
+				updateMCPState(name, MCPStateError, err, nil, MCPCounts{})
+				c.Close()
+				return
+			}
+
 			updateMcpTools(name, tools)
 			updateMcpPrompts(name, prompts)
+			updateMcpResources(name, resources)
 			mcpClients.Set(name, c)
 			counts := MCPCounts{
-				Tools:   len(tools),
-				Prompts: len(prompts),
+				Tools:     len(tools),
+				Prompts:   len(prompts),
+				Resources: len(resources),
 			}
 			updateMCPState(name, MCPStateConnected, nil, c, counts)
 		}(name, m)
@@ -496,6 +509,32 @@ func updateMcpPrompts(mcpName string, prompts []*mcp.Prompt) {
 	}
 }
 
+func getResources(ctx context.Context, c *mcp.ClientSession) ([]*mcp.Resource, error) {
+	if c.InitializeResult().Capabilities.Resources == nil {
+		return nil, nil
+	}
+	result, err := c.ListResources(ctx, &mcp.ListResourcesParams{})
+	if err != nil {
+		return nil, err
+	}
+	return result.Resources, nil
+}
+
+// updateMcpResources updates the global mcpResources and mcpClient2Resources maps.
+func updateMcpResources(mcpName string, resources []*mcp.Resource) {
+	if len(resources) == 0 {
+		mcpClient2Resources.Del(mcpName)
+	} else {
+		mcpClient2Resources.Set(mcpName, resources)
+	}
+	for clientName, resources := range mcpClient2Resources.Seq2() {
+		for _, p := range resources {
+			key := clientName + ":" + p.Name
+			mcpResources.Set(key, p)
+		}
+	}
+}
+
 // GetMCPPrompts returns all available MCP prompts.
 func GetMCPPrompts() map[string]*mcp.Prompt {
 	return maps.Collect(mcpPrompts.Seq2())

internal/tui/components/mcp/mcp.go 🔗

@@ -74,6 +74,9 @@ func RenderMCPList(opts RenderOptions) []string {
 				if count := state.Counts.Prompts; count > 0 {
 					extraContent = append(extraContent, t.S().Subtle.Render(fmt.Sprintf("%d prompts", count)))
 				}
+				if count := state.Counts.Resources; count > 0 {
+					extraContent = append(extraContent, t.S().Subtle.Render(fmt.Sprintf("%d resources", count)))
+				}
 			case agent.MCPStateError:
 				icon = t.ItemErrorIcon
 				if state.Error != nil {