返回 CodeWhale
mcp_routing.rs
根目录 / crates / tui / src / tui / mcp_routing.rs
1 //! MCP manager formatting and UI action helpers.
2
3 use crate::mcp::{McpManagerSnapshot, McpServerSnapshot, format_mcp_tool_description};
4 use crate::tui::app::App;
5 use crate::tui::history::HistoryCell;
6 use crate::tui::pager::PagerView;
7
8 pub(super) fn format_mcp_manager(snapshot: &McpManagerSnapshot) -> String {
9 let mut lines = vec![
10 format!("MCP config: {}", snapshot.config_path.display()),
11 format!("Config exists: {}", snapshot.config_exists),
12 ];
13 if snapshot.reload_required {
14 lines.push(
15 "Reload required: MCP config changed; run /mcp reload to rebuild the live model-visible tool pool."
16 .to_string(),
17 );
18 } else {
19 lines.push("Reload required: no pending config change.".to_string());
20 }
21 lines.push(String::new());
22
23 if snapshot.servers.is_empty() {
24 lines.push("No MCP servers configured.".to_string());
25 } else {
26 lines.push(format!("Servers ({})", snapshot.servers.len()));
27 lines.push("----------------------------------------".to_string());
28 for server in &snapshot.servers {
29 push_server(lines.as_mut(), server);
30 }
31 }
32
33 lines.push(String::new());
34 lines.push(
35 "Actions: /mcp init, /mcp add stdio <name> <command> [args...], /mcp add http <name> <url>, /mcp enable <name>, /mcp disable <name>, /mcp remove <name>, /mcp validate, /mcp reload."
36 .to_string(),
37 );
38 lines.join("\n")
39 }
40
41 fn push_server(lines: &mut Vec<String>, server: &McpServerSnapshot) {
42 let state = if server.enabled {
43 if server.connected {
44 "connected"
45 } else if server.error.is_some() {
46 "failed"
47 } else {
48 "enabled"
49 }
50 } else {
51 "disabled"
52 };
53 let required = if server.required { " required" } else { "" };
54 lines.push(format!(
55 "- {} [{}{}] {} {}",
56 server.name, state, required, server.transport, server.command_or_url
57 ));
58 lines.push(format!(
59 " timeouts: connect={}s execute={}s read={}s",
60 server.connect_timeout, server.execute_timeout, server.read_timeout
61 ));
62 if let Some(error) = server.error.as_ref() {
63 lines.push(format!(" error: {error}"));
64 }
65 lines.push(format!(
66 " discovered: {} tools, {} resources, {} prompts",
67 server.tools.len(),
68 server.resources.len(),
69 server.prompts.len()
70 ));
71 for tool in &server.tools {
72 lines.push(format!(
73 " tool {}{}",
74 tool.model_name,
75 format_mcp_tool_description(tool.description.as_deref())
76 ));
77 }
78 for resource in &server.resources {
79 lines.push(format!(" resource {}", resource.name));
80 }
81 for prompt in &server.prompts {
82 lines.push(format!(" prompt {}", prompt.model_name));
83 }
84 }
85
86 pub(super) fn open_mcp_manager_pager(app: &mut App, snapshot: &McpManagerSnapshot) {
87 let width = app
88 .viewport
89 .last_transcript_area
90 .map(|area| area.width)
91 .unwrap_or(100)
92 .saturating_sub(4);
93 app.view_stack.push(PagerView::from_text(
94 "MCP Manager".to_string(),
95 &format_mcp_manager(snapshot),
96 width.max(60),
97 ));
98 }
99
100 pub(super) fn add_mcp_message(app: &mut App, content: String) {
101 app.add_message(HistoryCell::System { content });
102 }
103
104 #[cfg(test)]
105 mod tests {
106 use super::*;
107 use crate::mcp::McpDiscoveredItem;
108 use std::path::PathBuf;
109
110 #[test]
111 fn manager_text_shows_failed_disabled_and_runtime_names() {
112 let snapshot = McpManagerSnapshot {
113 config_path: PathBuf::from("/tmp/mcp.json"),
114 config_exists: true,
115 reload_required: true,
116 servers: vec![
117 McpServerSnapshot {
118 name: "fs".to_string(),
119 enabled: true,
120 required: false,
121 transport: "stdio".to_string(),
122 command_or_url: "node server.js".to_string(),
123 connect_timeout: 10,
124 execute_timeout: 60,
125 read_timeout: 120,
126 connected: true,
127 error: None,
128 tools: vec![McpDiscoveredItem {
129 name: "read".to_string(),
130 model_name: "mcp_fs_read".to_string(),
131 description: Some("Read a file".to_string()),
132 }],
133 resources: Vec::new(),
134 prompts: Vec::new(),
135 },
136 McpServerSnapshot {
137 name: "bad".to_string(),
138 enabled: true,
139 required: false,
140 transport: "http/sse".to_string(),
141 command_or_url: "https://example.invalid/mcp".to_string(),
142 connect_timeout: 10,
143 execute_timeout: 60,
144 read_timeout: 120,
145 connected: false,
146 error: Some("boom".to_string()),
147 tools: Vec::new(),
148 resources: Vec::new(),
149 prompts: Vec::new(),
150 },
151 ],
152 };
153 let text = format_mcp_manager(&snapshot);
154 assert!(text.contains("Reload required"));
155 assert!(text.contains("/mcp reload"));
156 assert!(text.contains("mcp_fs_read"));
157 assert!(text.contains("[failed]"));
158 assert!(text.contains("boom"));
159 }
160 }
161
161 lines RUST