feat(repl): color .info mcp-server verdicts and complete only running servers
This commit is contained in:
@@ -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,19 +8718,21 @@ 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}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user