返回 DeepSeek-TUI-2026
mcp.rs
根目录 / crates / tui / src / commands / mcp.rs
1 //! In-TUI MCP manager command parser.
2
3 use crate::tui::app::{App, AppAction, McpUiAction};
4
5 use super::CommandResult;
6
7 pub fn mcp(_app: &mut App, args: Option<&str>) -> CommandResult {
8 let raw = args.unwrap_or("").trim();
9 if raw.is_empty() || raw.eq_ignore_ascii_case("status") || raw.eq_ignore_ascii_case("list") {
10 return CommandResult::action(AppAction::Mcp(McpUiAction::Show));
11 }
12
13 let mut parts = raw.split_whitespace();
14 let action = parts.next().unwrap_or("").to_ascii_lowercase();
15 match action.as_str() {
16 "init" => CommandResult::action(AppAction::Mcp(McpUiAction::Init {
17 force: parts.any(|part| part == "--force" || part == "-f"),
18 })),
19 "add" => parse_add(parts.collect()),
20 "enable" => match parse_name(parts.next(), "Usage: /mcp enable <name>") {
21 Ok(name) => CommandResult::action(AppAction::Mcp(McpUiAction::Enable { name })),
22 Err(msg) => CommandResult::error(msg),
23 },
24 "disable" => match parse_name(parts.next(), "Usage: /mcp disable <name>") {
25 Ok(name) => CommandResult::action(AppAction::Mcp(McpUiAction::Disable { name })),
26 Err(msg) => CommandResult::error(msg),
27 },
28 "remove" | "rm" => match parse_name(parts.next(), "Usage: /mcp remove <name>") {
29 Ok(name) => CommandResult::action(AppAction::Mcp(McpUiAction::Remove { name })),
30 Err(msg) => CommandResult::error(msg),
31 },
32 "validate" => CommandResult::action(AppAction::Mcp(McpUiAction::Validate)),
33 "reload" | "reconnect" => CommandResult::action(AppAction::Mcp(McpUiAction::Reload)),
34 _ => CommandResult::error(
35 "Usage: /mcp [init|add stdio <name> <command> [args...]|add http <name> <url>|enable <name>|disable <name>|remove <name>|validate|reload]",
36 ),
37 }
38 }
39
40 fn parse_name(name: Option<&str>, usage: &str) -> Result<String, String> {
41 match name {
42 Some(name) if !name.trim().is_empty() => Ok(name.to_string()),
43 _ => Err(usage.to_string()),
44 }
45 }
46
47 fn parse_add(parts: Vec<&str>) -> CommandResult {
48 if parts.len() < 3 {
49 return CommandResult::error(
50 "Usage: /mcp add stdio <name> <command> [args...] OR /mcp add http <name> <url>",
51 );
52 }
53 match parts[0].to_ascii_lowercase().as_str() {
54 "stdio" => CommandResult::action(AppAction::Mcp(McpUiAction::AddStdio {
55 name: parts[1].to_string(),
56 command: parts[2].to_string(),
57 args: parts[3..].iter().map(|s| (*s).to_string()).collect(),
58 })),
59 "http" | "sse" => CommandResult::action(AppAction::Mcp(McpUiAction::AddHttp {
60 name: parts[1].to_string(),
61 url: parts[2].to_string(),
62 })),
63 _ => CommandResult::error(
64 "Usage: /mcp add stdio <name> <command> [args...] OR /mcp add http <name> <url>",
65 ),
66 }
67 }
68
69 #[cfg(test)]
70 mod tests {
71 use super::*;
72 use crate::config::Config;
73 use crate::tui::app::TuiOptions;
74 use std::path::PathBuf;
75
76 fn app() -> App {
77 App::new(
78 TuiOptions {
79 model: "deepseek-v4-pro".to_string(),
80 workspace: PathBuf::from("."),
81 config_path: None,
82 config_profile: None,
83 allow_shell: false,
84 use_alt_screen: false,
85 use_mouse_capture: false,
86 use_bracketed_paste: true,
87 max_subagents: 2,
88 skills_dir: PathBuf::from("."),
89 memory_path: PathBuf::from("memory.md"),
90 notes_path: PathBuf::from("notes.txt"),
91 mcp_config_path: PathBuf::from("mcp.json"),
92 use_memory: false,
93 start_in_agent_mode: false,
94 skip_onboarding: true,
95 yolo: false,
96 resume_session_id: None,
97 initial_input: None,
98 },
99 &Config::default(),
100 )
101 }
102
103 #[test]
104 fn parses_add_and_validate() {
105 let mut app = app();
106 let add = mcp(&mut app, Some("add stdio local node server.js"));
107 assert!(matches!(
108 add.action,
109 Some(AppAction::Mcp(McpUiAction::AddStdio { name, command, args }))
110 if name == "local" && command == "node" && args == vec!["server.js".to_string()]
111 ));
112
113 let validate = mcp(&mut app, Some("validate"));
114 assert!(matches!(
115 validate.action,
116 Some(AppAction::Mcp(McpUiAction::Validate))
117 ));
118 }
119 }
120
120 lines RUST