use crate::tui::app::{App, AppAction, McpUiAction};
use super::CommandResult;
pub fn mcp(_app: &mut App, args: Option<&str>) -> CommandResult {
let raw = args.unwrap_or("").trim();
if raw.is_empty() || raw.eq_ignore_ascii_case("status") || raw.eq_ignore_ascii_case("list") {
return CommandResult::action(AppAction::Mcp(McpUiAction::Show));
}
let mut parts = raw.split_whitespace();
let action = parts.next().unwrap_or("").to_ascii_lowercase();
match action.as_str() {
"init" => CommandResult::action(AppAction::Mcp(McpUiAction::Init {
force: parts.any(|part| part == "--force" || part == "-f"),
})),
"add" => parse_add(parts.collect()),
"enable" => match parse_name(parts.next(), "Usage: /mcp enable <name>") {
Ok(name) => CommandResult::action(AppAction::Mcp(McpUiAction::Enable { name })),
Err(msg) => CommandResult::error(msg),
},
"disable" => match parse_name(parts.next(), "Usage: /mcp disable <name>") {
Ok(name) => CommandResult::action(AppAction::Mcp(McpUiAction::Disable { name })),
Err(msg) => CommandResult::error(msg),
},
"remove" | "rm" => match parse_name(parts.next(), "Usage: /mcp remove <name>") {
Ok(name) => CommandResult::action(AppAction::Mcp(McpUiAction::Remove { name })),
Err(msg) => CommandResult::error(msg),
},
"login" => match parse_name(parts.next(), "Usage: /mcp login <name> [--scope scope]") {
Ok(name) => CommandResult::action(AppAction::Mcp(McpUiAction::Login {
name,
scopes: parse_scopes(parts.collect()),
})),
Err(msg) => CommandResult::error(msg),
},
"logout" => match parse_name(parts.next(), "Usage: /mcp logout <name>") {
Ok(name) => CommandResult::action(AppAction::Mcp(McpUiAction::Logout { name })),
Err(msg) => CommandResult::error(msg),
},
"validate" => CommandResult::action(AppAction::Mcp(McpUiAction::Validate)),
"reload" | "reconnect" => CommandResult::action(AppAction::Mcp(McpUiAction::Reload)),
_ => CommandResult::error(
"Usage: /mcp [init|add stdio <name> <command> [args...]|add http <name> <url>|enable <name>|disable <name>|remove <name>|login <name>|logout <name>|validate|reload]",
),
}
}
fn parse_name(name: Option<&str>, usage: &str) -> Result<String, String> {
match name {
Some(name) if !name.trim().is_empty() => Ok(name.to_string()),
_ => Err(usage.to_string()),
}
}
fn parse_add(parts: Vec<&str>) -> CommandResult {
if parts.len() < 3 {
return CommandResult::error(
"Usage: /mcp add stdio <name> <command> [args...] OR /mcp add http <name> <url>",
);
}
match parts[0].to_ascii_lowercase().as_str() {
"stdio" => CommandResult::action(AppAction::Mcp(McpUiAction::AddStdio {
name: parts[1].to_string(),
command: parts[2].to_string(),
args: parts[3..].iter().map(|s| (*s).to_string()).collect(),
})),
"http" => CommandResult::action(AppAction::Mcp(McpUiAction::AddHttp {
name: parts[1].to_string(),
url: parts[2].to_string(),
transport: None,
})),
"sse" => CommandResult::action(AppAction::Mcp(McpUiAction::AddHttp {
name: parts[1].to_string(),
url: parts[2].to_string(),
transport: Some("sse".to_string()),
})),
_ => CommandResult::error(
"Usage: /mcp add stdio <name> <command> [args...] OR /mcp add http <name> <url>",
),
}
}
fn parse_scopes(parts: Vec<&str>) -> Vec<String> {
let mut scopes = Vec::new();
let mut iter = parts.into_iter();
while let Some(part) = iter.next() {
if part == "--scope" {
let Some(value) = iter.next() else {
continue;
};
for scope in value.split(',') {
let scope = scope.trim();
if !scope.is_empty() {
scopes.push(scope.to_string());
}
}
continue;
}
let value = part.strip_prefix("--scope=");
let Some(value) = value else {
for scope in part.split(',') {
let scope = scope.trim();
if !scope.is_empty() {
scopes.push(scope.to_string());
}
}
continue;
};
for scope in value.split(',') {
let scope = scope.trim();
if !scope.is_empty() {
scopes.push(scope.to_string());
}
}
}
scopes
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use crate::tui::app::TuiOptions;
use std::path::PathBuf;
fn app() -> App {
App::new(
TuiOptions {
model: "deepseek-v4-pro".to_string(),
workspace: PathBuf::from("."),
config_path: None,
config_profile: None,
allow_shell: false,
use_alt_screen: false,
use_mouse_capture: false,
use_bracketed_paste: true,
max_subagents: 2,
skills_dir: PathBuf::from("."),
memory_path: PathBuf::from("memory.md"),
notes_path: PathBuf::from("notes.txt"),
mcp_config_path: PathBuf::from("mcp.json"),
use_memory: false,
start_in_agent_mode: false,
skip_onboarding: true,
yolo: false,
resume_session_id: None,
initial_input: None,
},
&Config::default(),
)
}
#[test]
fn parses_add_and_validate() {
let mut app = app();
let add = mcp(&mut app, Some("add stdio local node server.js"));
assert!(matches!(
add.action,
Some(AppAction::Mcp(McpUiAction::AddStdio { name, command, args }))
if name == "local" && command == "node" && args == vec!["server.js".to_string()]
));
let validate = mcp(&mut app, Some("validate"));
assert!(matches!(
validate.action,
Some(AppAction::Mcp(McpUiAction::Validate))
));
let login = mcp(
&mut app,
Some("login remote --scope tools/read,tools/write"),
);
assert!(matches!(
login.action,
Some(AppAction::Mcp(McpUiAction::Login { name, scopes }))
if name == "remote"
&& scopes == vec!["tools/read".to_string(), "tools/write".to_string()]
));
}
}