fix(mcp): omit declared-but-empty prompt/resource capabilities from .info mcp-server

This commit is contained in:
2026-08-28 11:33:03 -06:00
parent c707aaf3ee
commit c170d08654
2 changed files with 50 additions and 23 deletions
+47 -22
View File
@@ -805,23 +805,23 @@ impl RequestContext {
if features.resources { if features.resources {
let resources = handle.list_all_resources().await; let resources = handle.list_all_resources().await;
let templates = handle.list_all_resource_templates().await; let templates = handle.list_all_resource_templates().await;
capabilities.push(if resources.is_err() && templates.is_err() { let any_failed = resources.is_err() || templates.is_err();
"resources (declared, list failed)".to_string() let count = resources.map(|r| r.len()).unwrap_or_default()
} else { + templates.map(|t| t.len()).unwrap_or_default();
let count = resources.map(|r| r.len()).unwrap_or_default() if count > 0 {
+ templates.map(|t| t.len()).unwrap_or_default(); capabilities.push(format!("resources ({count})"));
match count { } else if any_failed {
0 => "resources (declared, none served)".to_string(), capabilities.push("resources (declared, list failed)".to_string());
n => format!("resources ({n})"), }
}
});
} }
if features.prompts { if features.prompts {
capabilities.push(match handle.list_all_prompts().await { match handle.list_all_prompts().await {
Ok(prompts) if prompts.is_empty() => "prompts (declared, none served)".to_string(), Ok(prompts) if prompts.is_empty() => {}
Ok(prompts) => format!("prompts ({})", prompts.len()), Ok(prompts) => capabilities.push(format!("prompts ({})", prompts.len())),
Err(_) => "prompts (declared, list failed)".to_string(), Err(_) => {
}); capabilities.push("prompts (declared, list failed)".to_string());
}
}
} }
const INFO_LABEL_WIDTH: usize = 15; const INFO_LABEL_WIDTH: usize = 15;
@@ -8746,7 +8746,7 @@ mod tests {
} }
#[test] #[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 mut ctx = RequestContext::new(mcp_app_state(&["fixture"]), WorkingMode::Cmd);
let info = run_async(async { let info = run_async(async {
@@ -8762,12 +8762,11 @@ mod tests {
ctx.mcp_server_info("fixture").await.unwrap() ctx.mcp_server_info("fixture").await.unwrap()
}); });
assert!( let cap_line = info
info.contains( .lines()
"capabilities tools, resources (declared, none served), prompts (declared, none served)" .find(|line| line.starts_with("capabilities"))
), .unwrap();
"got:\n{info}" assert_eq!(cap_line, "capabilities tools", "got:\n{info}");
);
} }
#[test] #[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] #[test]
fn info_mcp_server_errors_when_unconfigured_or_not_running() { fn info_mcp_server_errors_when_unconfigured_or_not_running() {
let ctx = RequestContext::new(mcp_app_state(&["gh"]), WorkingMode::Cmd); let ctx = RequestContext::new(mcp_app_state(&["gh"]), WorkingMode::Cmd);
+3 -1
View File
@@ -645,6 +645,7 @@ pub(crate) mod test_fixtures {
pub(crate) prompts_capability: bool, pub(crate) prompts_capability: bool,
pub(crate) hostile_prompt: bool, pub(crate) hostile_prompt: bool,
pub(crate) fail_resource_listings: bool, pub(crate) fail_resource_listings: bool,
pub(crate) fail_template_listings: bool,
pub(crate) fail_prompt_listings: bool, pub(crate) fail_prompt_listings: bool,
pub(crate) empty_resource_listings: bool, pub(crate) empty_resource_listings: bool,
pub(crate) empty_prompt_listings: bool, pub(crate) empty_prompt_listings: bool,
@@ -666,6 +667,7 @@ pub(crate) mod test_fixtures {
prompts_capability: false, prompts_capability: false,
hostile_prompt: false, hostile_prompt: false,
fail_resource_listings: false, fail_resource_listings: false,
fail_template_listings: false,
fail_prompt_listings: false, fail_prompt_listings: false,
empty_resource_listings: false, empty_resource_listings: false,
empty_prompt_listings: false, empty_prompt_listings: false,
@@ -756,7 +758,7 @@ pub(crate) mod test_fixtures {
_request: Option<PaginatedRequestParams>, _request: Option<PaginatedRequestParams>,
_context: RequestContext<RoleServer>, _context: RequestContext<RoleServer>,
) -> Result<ListResourceTemplatesResult, ErrorData> { ) -> Result<ListResourceTemplatesResult, ErrorData> {
if self.fail_resource_listings { if self.fail_resource_listings || self.fail_template_listings {
return Err(ErrorData::internal_error("template listing exploded", None)); return Err(ErrorData::internal_error("template listing exploded", None));
} }
if self.empty_resource_listings { if self.empty_resource_listings {