From c170d08654cb80fdfe8d983c3fee8168da179910 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 28 Aug 2026 11:33:03 -0600 Subject: [PATCH] fix(mcp): omit declared-but-empty prompt/resource capabilities from .info mcp-server --- src/config/request_context.rs | 69 ++++++++++++++++++++++++----------- src/config/tool_scope.rs | 4 +- 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/src/config/request_context.rs b/src/config/request_context.rs index af710ef..611190e 100644 --- a/src/config/request_context.rs +++ b/src/config/request_context.rs @@ -805,23 +805,23 @@ impl RequestContext { if features.resources { let resources = handle.list_all_resources().await; let templates = handle.list_all_resource_templates().await; - capabilities.push(if resources.is_err() && templates.is_err() { - "resources (declared, list failed)".to_string() - } else { - let count = resources.map(|r| r.len()).unwrap_or_default() - + templates.map(|t| t.len()).unwrap_or_default(); - match count { - 0 => "resources (declared, none served)".to_string(), - n => format!("resources ({n})"), - } - }); + let any_failed = resources.is_err() || templates.is_err(); + let count = resources.map(|r| r.len()).unwrap_or_default() + + templates.map(|t| t.len()).unwrap_or_default(); + if count > 0 { + capabilities.push(format!("resources ({count})")); + } else if any_failed { + capabilities.push("resources (declared, list failed)".to_string()); + } } if features.prompts { - capabilities.push(match handle.list_all_prompts().await { - Ok(prompts) if prompts.is_empty() => "prompts (declared, none served)".to_string(), - Ok(prompts) => format!("prompts ({})", prompts.len()), - Err(_) => "prompts (declared, list failed)".to_string(), - }); + match handle.list_all_prompts().await { + Ok(prompts) if prompts.is_empty() => {} + Ok(prompts) => capabilities.push(format!("prompts ({})", prompts.len())), + Err(_) => { + capabilities.push("prompts (declared, list failed)".to_string()); + } + } } const INFO_LABEL_WIDTH: usize = 15; @@ -8746,7 +8746,7 @@ mod tests { } #[test] - fn info_mcp_server_annotates_declared_but_empty_capabilities() { + fn info_mcp_server_omits_declared_but_empty_capabilities() { let mut ctx = RequestContext::new(mcp_app_state(&["fixture"]), WorkingMode::Cmd); let info = run_async(async { @@ -8762,12 +8762,11 @@ mod tests { ctx.mcp_server_info("fixture").await.unwrap() }); - assert!( - info.contains( - "capabilities tools, resources (declared, none served), prompts (declared, none served)" - ), - "got:\n{info}" - ); + let cap_line = info + .lines() + .find(|line| line.starts_with("capabilities")) + .unwrap(); + assert_eq!(cap_line, "capabilities tools", "got:\n{info}"); } #[test] @@ -8795,6 +8794,32 @@ mod tests { ); } + #[test] + fn info_mcp_server_annotates_partial_resource_listing_failure() { + let mut ctx = RequestContext::new(mcp_app_state(&["fixture"]), WorkingMode::Cmd); + + let info = run_async(async { + let (runtime, _server) = fixture_runtime(FixtureServer { + resources_capability: true, + empty_resource_listings: true, + fail_template_listings: true, + ..Default::default() + }) + .await; + ctx.tool_scope.mcp_runtime = runtime; + ctx.mcp_server_info("fixture").await.unwrap() + }); + + let cap_line = info + .lines() + .find(|line| line.starts_with("capabilities")) + .unwrap(); + assert_eq!( + cap_line, "capabilities tools, resources (declared, list failed)", + "got:\n{info}" + ); + } + #[test] fn info_mcp_server_errors_when_unconfigured_or_not_running() { let ctx = RequestContext::new(mcp_app_state(&["gh"]), WorkingMode::Cmd); diff --git a/src/config/tool_scope.rs b/src/config/tool_scope.rs index b6230f2..dc00add 100644 --- a/src/config/tool_scope.rs +++ b/src/config/tool_scope.rs @@ -645,6 +645,7 @@ pub(crate) mod test_fixtures { pub(crate) prompts_capability: bool, pub(crate) hostile_prompt: bool, pub(crate) fail_resource_listings: bool, + pub(crate) fail_template_listings: bool, pub(crate) fail_prompt_listings: bool, pub(crate) empty_resource_listings: bool, pub(crate) empty_prompt_listings: bool, @@ -666,6 +667,7 @@ pub(crate) mod test_fixtures { prompts_capability: false, hostile_prompt: false, fail_resource_listings: false, + fail_template_listings: false, fail_prompt_listings: false, empty_resource_listings: false, empty_prompt_listings: false, @@ -756,7 +758,7 @@ pub(crate) mod test_fixtures { _request: Option, _context: RequestContext, ) -> Result { - if self.fail_resource_listings { + if self.fail_resource_listings || self.fail_template_listings { return Err(ErrorData::internal_error("template listing exploded", None)); } if self.empty_resource_listings {