From 808e981f56ae230a09e2c713b6ff797cdcc0ce4d Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Sat, 9 May 2026 00:11:51 -0500 Subject: [PATCH] fix(mcp): paginate tools/resources/prompts discovery via nextCursor (#1250, #1256) MCP servers are allowed by spec to paginate list responses. The old implementation made a single request and stopped, silently dropping subsequent pages. Servers that paginate at fewer items than their total tool count (e.g. gbrain at 5 per page) would appear to expose only those first few tools. All four discovery methods now follow nextCursor until the server signals no more pages, accumulating results across all pages: - discover_tools - discover_resources - discover_resource_templates - discover_prompts Thanks to Liu-Vince for the original diagnosis and fix (PR #1256). --- crates/tui/src/mcp.rs | 174 +++++++++++++++++++++++++++++------------- 1 file changed, 120 insertions(+), 54 deletions(-) diff --git a/crates/tui/src/mcp.rs b/crates/tui/src/mcp.rs index 8a3d36ab..57807ae2 100644 --- a/crates/tui/src/mcp.rs +++ b/crates/tui/src/mcp.rs @@ -774,94 +774,160 @@ impl McpConnection { /// Discover available tools from the MCP server async fn discover_tools(&mut self) -> Result<()> { - let list_id = self.next_id(); - self.send(serde_json::json!({ - "jsonrpc": "2.0", - "id": list_id, - "method": "tools/list", - "params": {} - })) - .await?; + let mut cursor: Option = None; + loop { + let list_id = self.next_id(); + let params = match &cursor { + Some(c) => serde_json::json!({ "cursor": c }), + None => serde_json::json!({}), + }; + self.send(serde_json::json!({ + "jsonrpc": "2.0", + "id": list_id, + "method": "tools/list", + "params": params + })) + .await?; - let response = self.recv(list_id).await?; + let response = self.recv(list_id).await?; + let Some(result) = response.get("result") else { + break; + }; - if let Some(result) = response.get("result") - && let Some(tools) = result.get("tools") - { - self.tools = serde_json::from_value(tools.clone()).unwrap_or_default(); + if let Some(tools) = result.get("tools") { + let page: Vec = serde_json::from_value(tools.clone()).unwrap_or_default(); + self.tools.extend(page); + } + + cursor = result + .get("nextCursor") + .and_then(|v| v.as_str()) + .map(str::to_owned); + if cursor.is_none() { + break; + } } - Ok(()) } /// Discover available resources from the MCP server async fn discover_resources(&mut self) -> Result<()> { - let list_id = self.next_id(); - self.send(serde_json::json!({ - "jsonrpc": "2.0", - "id": list_id, - "method": "resources/list", - "params": {} - })) - .await?; + let mut cursor: Option = None; + loop { + let list_id = self.next_id(); + let params = match &cursor { + Some(c) => serde_json::json!({ "cursor": c }), + None => serde_json::json!({}), + }; + self.send(serde_json::json!({ + "jsonrpc": "2.0", + "id": list_id, + "method": "resources/list", + "params": params + })) + .await?; - let response = self.recv(list_id).await?; + let response = self.recv(list_id).await?; + let Some(result) = response.get("result") else { + break; + }; - if let Some(result) = response.get("result") - && let Some(resources) = result.get("resources") - { - self.resources = serde_json::from_value(resources.clone()).unwrap_or_default(); + if let Some(resources) = result.get("resources") { + let page: Vec = + serde_json::from_value(resources.clone()).unwrap_or_default(); + self.resources.extend(page); + } + + cursor = result + .get("nextCursor") + .and_then(|v| v.as_str()) + .map(str::to_owned); + if cursor.is_none() { + break; + } } - Ok(()) } /// Discover available resource templates from the MCP server async fn discover_resource_templates(&mut self) -> Result<()> { - let list_id = self.next_id(); - self.send(serde_json::json!({ - "jsonrpc": "2.0", - "id": list_id, - "method": "resources/templates/list", - "params": {} - })) - .await?; + let mut cursor: Option = None; + loop { + let list_id = self.next_id(); + let params = match &cursor { + Some(c) => serde_json::json!({ "cursor": c }), + None => serde_json::json!({}), + }; + self.send(serde_json::json!({ + "jsonrpc": "2.0", + "id": list_id, + "method": "resources/templates/list", + "params": params + })) + .await?; - let response = self.recv(list_id).await?; + let response = self.recv(list_id).await?; + let Some(result) = response.get("result") else { + break; + }; - if let Some(result) = response.get("result") { let templates = result .get("resourceTemplates") .or_else(|| result.get("templates")) .or_else(|| result.get("resource_templates")); if let Some(templates) = templates { - self.resource_templates = + let page: Vec = serde_json::from_value(templates.clone()).unwrap_or_default(); + self.resource_templates.extend(page); + } + + cursor = result + .get("nextCursor") + .and_then(|v| v.as_str()) + .map(str::to_owned); + if cursor.is_none() { + break; } } - Ok(()) } /// Discover available prompts from the MCP server async fn discover_prompts(&mut self) -> Result<()> { - let list_id = self.next_id(); - self.send(serde_json::json!({ - "jsonrpc": "2.0", - "id": list_id, - "method": "prompts/list", - "params": {} - })) - .await?; + let mut cursor: Option = None; + loop { + let list_id = self.next_id(); + let params = match &cursor { + Some(c) => serde_json::json!({ "cursor": c }), + None => serde_json::json!({}), + }; + self.send(serde_json::json!({ + "jsonrpc": "2.0", + "id": list_id, + "method": "prompts/list", + "params": params + })) + .await?; - let response = self.recv(list_id).await?; + let response = self.recv(list_id).await?; + let Some(result) = response.get("result") else { + break; + }; - if let Some(result) = response.get("result") - && let Some(prompts) = result.get("prompts") - { - self.prompts = serde_json::from_value(prompts.clone()).unwrap_or_default(); + if let Some(prompts) = result.get("prompts") { + let page: Vec = + serde_json::from_value(prompts.clone()).unwrap_or_default(); + self.prompts.extend(page); + } + + cursor = result + .get("nextCursor") + .and_then(|v| v.as_str()) + .map(str::to_owned); + if cursor.is_none() { + break; + } } - Ok(()) }