返回 DeepSeek-TUI-2026
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};
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.restart_required {
14 lines.push(
15 "Restart required: MCP config changed; the current model-visible MCP tool pool is not hot-reloaded."
16 .to_string(),
17 );
18 } else {
19 lines.push("Restart required: no pending in-TUI 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 tool.description
76 .as_ref()
77 .map_or(String::new(), |desc| format!(" - {desc}"))
78 ));
79 }
80 for resource in &server.resources {
81 lines.push(format!(" resource {}", resource.name));
82 }
83 for prompt in &server.prompts {
84 lines.push(format!(" prompt {}", prompt.model_name));
85 }
86 }
87
88 pub(super) fn open_mcp_manager_pager(app: &mut App, snapshot: &McpManagerSnapshot) {
89 let width = app
90 .viewport
91 .last_transcript_area
92 .map(|area| area.width)
93 .unwrap_or(100)
94 .saturating_sub(4);
95 app.view_stack.push(PagerView::from_text(
96 "MCP Manager".to_string(),
97 &format_mcp_manager(snapshot),
98 width.max(60),
99 ));
100 }
101
102 pub(super) fn add_mcp_message(app: &mut App, content: String) {
103 app.add_message(HistoryCell::System { content });
104 }
105
106 #[cfg(test)]
107 mod tests {
108 use super::*;
109 use crate::mcp::McpDiscoveredItem;
110 use std::path::PathBuf;
111
112 #[test]
113 fn manager_text_shows_failed_disabled_and_runtime_names() {
114 let snapshot = McpManagerSnapshot {
115 config_path: PathBuf::from("/tmp/mcp.json"),
116 config_exists: true,
117 restart_required: true,
118 servers: vec![
119 McpServerSnapshot {
120 name: "fs".to_string(),
121 enabled: true,
122 required: false,
123 transport: "stdio".to_string(),
124 command_or_url: "node server.js".to_string(),
125 connect_timeout: 10,
126 execute_timeout: 60,
127 read_timeout: 120,
128 connected: true,
129 error: None,
130 tools: vec![McpDiscoveredItem {
131 name: "read".to_string(),
132 model_name: "mcp_fs_read".to_string(),
133 description: Some("Read a file".to_string()),
134 }],
135 resources: Vec::new(),
136 prompts: Vec::new(),
137 },
138 McpServerSnapshot {
139 name: "bad".to_string(),
140 enabled: true,
141 required: false,
142 transport: "http/sse".to_string(),
143 command_or_url: "https://example.invalid/mcp".to_string(),
144 connect_timeout: 10,
145 execute_timeout: 60,
146 read_timeout: 120,
147 connected: false,
148 error: Some("boom".to_string()),
149 tools: Vec::new(),
150 resources: Vec::new(),
151 prompts: Vec::new(),
152 },
153 ],
154 };
155 let text = format_mcp_manager(&snapshot);
156 assert!(text.contains("Restart required"));
157 assert!(text.contains("mcp_fs_read"));
158 assert!(text.contains("[failed]"));
159 assert!(text.contains("boom"));
160 }
161 }
162
162 lines RUST