feat(repl): color .info mcp-server verdicts and complete only running servers

This commit is contained in:
2026-08-28 12:32:45 -06:00
parent 544a6bbbbc
commit f323c919f5
+48 -14
View File
@@ -885,23 +885,28 @@ impl RequestContext {
.map(|tool| tool.chars().count()) .map(|tool| tool.chars().count())
.max() .max()
.unwrap_or_default(); .unwrap_or_default();
let allowed_marker = "".green().bold().to_string();
let hidden_marker = "".red().bold().to_string();
for tool in &names { for tool in &names {
let explained = filter.map(|f| f.allows_explain(tool)); let explained = filter.map(|f| f.allows_explain(tool));
match explained { match explained {
None => out.push_str(&format!(" {tool}\n")), None => out.push_str(&format!(" {allowed_marker} {tool}\n")),
Some(Ok(matches)) => { Some(Ok(matches)) => {
let chain: Vec<String> = matches let chain: Vec<String> = matches
.iter() .iter()
.map(|(source, pattern)| format!("{pattern} ({})", source.short_label())) .map(|(source, pattern)| format!("{pattern} ({})", source.short_label()))
.collect(); .collect();
if chain.is_empty() { if chain.is_empty() {
out.push_str(&format!(" {tool}\n")); out.push_str(&format!(" {allowed_marker} {tool}\n"));
} else { } else {
out.push_str(&format!("{tool:<name_width$} {}\n", chain.join(""))); out.push_str(&format!(
" {allowed_marker} {tool:<name_width$} {}\n",
chain.join("")
));
} }
} }
Some(Err(source)) => out.push_str(&format!( Some(Err(source)) => out.push_str(&format!(
" {tool:<name_width$} hidden by {} layer\n", " {hidden_marker} {tool:<name_width$} hidden by {} layer\n",
source.short_label() source.short_label()
)), )),
} }
@@ -3831,10 +3836,15 @@ impl RequestContext {
); );
} }
} else if cmd == ".info" && args.first() == Some(&"mcp-server") && args.len() == 2 { } else if cmd == ".info" && args.first() == Some(&"mcp-server") && args.len() == 2 {
if let Some(mcp_config) = &self.app.mcp_config { let mut names: Vec<String> = self
values = .tool_scope
super::map_completion_values(mcp_config.mcp_servers.keys().cloned().collect()); .mcp_runtime
} .servers
.keys()
.cloned()
.collect();
names.sort_unstable();
values = super::map_completion_values(names);
} else if cmd == ".mcp" } else if cmd == ".mcp"
&& (args.first() == Some(&"enable") || args.first() == Some(&"disable")) && (args.first() == Some(&"enable") || args.first() == Some(&"disable"))
&& args.len() == 2 && args.len() == 2
@@ -8708,18 +8718,20 @@ mod tests {
assert!(info.contains("get_* | bogus_zzz*"), "got:\n{info}"); assert!(info.contains("get_* | bogus_zzz*"), "got:\n{info}");
assert!(info.contains("tools (1 allowed / 3 total)"), "got:\n{info}"); assert!(info.contains("tools (1 allowed / 3 total)"), "got:\n{info}");
assert!( assert!(
info.lines().any(|line| line.contains("✓ get_issue") info.lines().any(|line| line.contains('✓')
&& line.contains("get_issue")
&& line.contains("get_* (global) ∧ get_* (role)")), && line.contains("get_* (global) ∧ get_* (role)")),
"got:\n{info}" "got:\n{info}"
); );
assert!( assert!(
info.lines() info.lines().any(|line| line.contains('✗')
.any(|line| line.contains("✗ list_prs") && line.contains("hidden by role layer")), && line.contains("list_prs")
&& line.contains("hidden by role layer")),
"got:\n{info}" "got:\n{info}"
); );
assert!( assert!(
info.lines() info.lines().any(|line| line.contains('✗')
.any(|line| line.contains("✗ delete_repo") && line.contains("delete_repo")
&& line.contains("hidden by global layer")), && line.contains("hidden by global layer")),
"got:\n{info}" "got:\n{info}"
); );
@@ -8848,10 +8860,32 @@ mod tests {
assert!(info.contains("(none — all tools allowed)"), "got:\n{info}"); assert!(info.contains("(none — all tools allowed)"), "got:\n{info}");
assert!(info.contains("tools (1 allowed / 1 total)"), "got:\n{info}"); assert!(info.contains("tools (1 allowed / 1 total)"), "got:\n{info}");
assert!(info.contains("✓ dup"), "got:\n{info}"); assert!(
info.lines()
.any(|line| line.contains('✓') && line.contains("dup")),
"got:\n{info}"
);
assert!(!info.contains('∧'), "got:\n{info}"); assert!(!info.contains('∧'), "got:\n{info}");
} }
#[test]
fn repl_complete_info_mcp_server_offers_only_running_servers() {
let mut ctx = RequestContext::new(mcp_app_state(&["gh"]), WorkingMode::Cmd);
let (runtime, _server) = run_async(fixture_runtime(FixtureServer::default()));
ctx.tool_scope.mcp_runtime = runtime;
let values = ctx.repl_complete(".info", &["mcp-server", ""], "");
assert!(
values.iter().any(|(name, _)| name == "fixture"),
"running server must be offered, got: {values:?}"
);
assert!(
!values.iter().any(|(name, _)| name == "gh"),
"configured-but-not-running server must not be offered, got: {values:?}"
);
}
#[test] #[test]
fn mcp_servers_listing_tags_filtered_servers() { fn mcp_servers_listing_tags_filtered_servers() {
let mut ctx = RequestContext::new(mcp_app_state(&["gh", "jira"]), WorkingMode::Cmd); let mut ctx = RequestContext::new(mcp_app_state(&["gh", "jira"]), WorkingMode::Cmd);