diff --git a/rust/crates/commands/src/lib.rs b/rust/crates/commands/src/lib.rs index 6ef5983..0ea493a 100644 --- a/rust/crates/commands/src/lib.rs +++ b/rust/crates/commands/src/lib.rs @@ -5,7 +5,10 @@ use std::fs; use std::path::{Path, PathBuf}; use plugins::{PluginError, PluginManager, PluginSummary}; -use runtime::{compact_session, CompactionConfig, Session}; +use runtime::{ + compact_session, CompactionConfig, ConfigLoader, ConfigSource, McpOAuthConfig, McpServerConfig, + ScopedMcpServerConfig, Session, +}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct CommandManifestEntry { @@ -117,6 +120,13 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[ argument_hint: Some("[env|hooks|model|plugins]"), resume_supported: true, }, + SlashCommandSpec { + name: "mcp", + aliases: &[], + summary: "Inspect configured MCP servers", + argument_hint: Some("[list|show |help]"), + resume_supported: true, + }, SlashCommandSpec { name: "memory", aliases: &[], @@ -272,6 +282,10 @@ pub enum SlashCommand { Config { section: Option, }, + Mcp { + action: Option, + target: Option, + }, Memory, Init, Diff, @@ -393,6 +407,7 @@ pub fn validate_slash_command_input( "config" => SlashCommand::Config { section: parse_config_section(&args)?, }, + "mcp" => parse_mcp_command(&args)?, "memory" => { validate_no_args(command, &args)?; SlashCommand::Memory @@ -551,6 +566,39 @@ fn parse_session_command(args: &[&str]) -> Result Result { + match args { + [] => Ok(SlashCommand::Mcp { + action: None, + target: None, + }), + ["list"] => Ok(SlashCommand::Mcp { + action: Some("list".to_string()), + target: None, + }), + ["list", ..] => Err(usage_error("mcp list", "")), + ["show"] => Err(usage_error("mcp show", "")), + ["show", target] => Ok(SlashCommand::Mcp { + action: Some("show".to_string()), + target: Some((*target).to_string()), + }), + ["show", ..] => Err(command_error( + "Unexpected arguments for /mcp show.", + "mcp", + "/mcp show ", + )), + ["help"] | ["-h"] | ["--help"] => Ok(SlashCommand::Mcp { + action: Some("help".to_string()), + target: None, + }), + [action, ..] => Err(command_error( + &format!("Unknown /mcp action '{action}'. Use list, show , or help."), + "mcp", + "/mcp [list|show |help]", + )), + } +} + fn parse_plugin_command(args: &[&str]) -> Result { match args { [] => Ok(SlashCommand::Plugins { @@ -728,7 +776,7 @@ fn slash_command_category(name: &str) -> &'static str { | "version" => "Session & visibility", "compact" | "clear" | "config" | "memory" | "init" | "diff" | "commit" | "pr" | "issue" | "export" | "plugin" => "Workspace & git", - "agents" | "skills" | "teleport" | "debug-tool-call" => "Discovery & debugging", + "agents" | "skills" | "teleport" | "debug-tool-call" | "mcp" => "Discovery & debugging", "bughunter" | "ultraplan" => "Analysis & automation", _ => "Other", } @@ -1066,6 +1114,14 @@ pub fn handle_agents_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R } } +pub fn handle_mcp_slash_command( + args: Option<&str>, + cwd: &Path, +) -> Result { + let loader = ConfigLoader::default_for(cwd); + render_mcp_report_for(&loader, cwd, args) +} + pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::Result { match normalize_optional_args(args) { None | Some("list") => { @@ -1078,6 +1134,41 @@ pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R } } +fn render_mcp_report_for( + loader: &ConfigLoader, + cwd: &Path, + args: Option<&str>, +) -> Result { + match normalize_optional_args(args) { + None | Some("list") => { + let runtime_config = loader.load()?; + Ok(render_mcp_summary_report( + cwd, + runtime_config.mcp().servers(), + )) + } + Some("-h" | "--help" | "help") => Ok(render_mcp_usage(None)), + Some("show") => Ok(render_mcp_usage(Some("show"))), + Some(args) if args.split_whitespace().next() == Some("show") => { + let mut parts = args.split_whitespace(); + let _ = parts.next(); + let Some(server_name) = parts.next() else { + return Ok(render_mcp_usage(Some("show"))); + }; + if parts.next().is_some() { + return Ok(render_mcp_usage(Some(args))); + } + let runtime_config = loader.load()?; + Ok(render_mcp_server_report( + cwd, + server_name, + runtime_config.mcp().get(server_name), + )) + } + Some(args) => Ok(render_mcp_usage(Some(args))), + } +} + #[must_use] pub fn render_plugins_report(plugins: &[PluginSummary]) -> String { let mut lines = vec!["Plugins".to_string()]; @@ -1571,6 +1662,112 @@ fn render_skills_report(skills: &[SkillSummary]) -> String { lines.join("\n").trim_end().to_string() } +fn render_mcp_summary_report( + cwd: &Path, + servers: &BTreeMap, +) -> String { + let mut lines = vec![ + "MCP".to_string(), + format!(" Working directory {}", cwd.display()), + format!(" Configured servers {}", servers.len()), + ]; + if servers.is_empty() { + lines.push(" No MCP servers configured.".to_string()); + return lines.join("\n"); + } + + lines.push(String::new()); + for (name, server) in servers { + lines.push(format!( + " {name:<16} {transport:<13} {scope:<7} {summary}", + transport = mcp_transport_label(&server.config), + scope = config_source_label(server.scope), + summary = mcp_server_summary(&server.config) + )); + } + + lines.join("\n") +} + +fn render_mcp_server_report( + cwd: &Path, + server_name: &str, + server: Option<&ScopedMcpServerConfig>, +) -> String { + let Some(server) = server else { + return format!( + "MCP\n Working directory {}\n Result server `{server_name}` is not configured", + cwd.display() + ); + }; + + let mut lines = vec![ + "MCP".to_string(), + format!(" Working directory {}", cwd.display()), + format!(" Name {server_name}"), + format!(" Scope {}", config_source_label(server.scope)), + format!( + " Transport {}", + mcp_transport_label(&server.config) + ), + ]; + + match &server.config { + McpServerConfig::Stdio(config) => { + lines.push(format!(" Command {}", config.command)); + lines.push(format!( + " Args {}", + format_optional_list(&config.args) + )); + lines.push(format!( + " Env keys {}", + format_optional_keys(config.env.keys().cloned().collect()) + )); + lines.push(format!( + " Tool timeout {}", + config + .tool_call_timeout_ms + .map_or_else(|| "".to_string(), |value| format!("{value} ms")) + )); + } + McpServerConfig::Sse(config) | McpServerConfig::Http(config) => { + lines.push(format!(" URL {}", config.url)); + lines.push(format!( + " Header keys {}", + format_optional_keys(config.headers.keys().cloned().collect()) + )); + lines.push(format!( + " Header helper {}", + config.headers_helper.as_deref().unwrap_or("") + )); + lines.push(format!( + " OAuth {}", + format_mcp_oauth(config.oauth.as_ref()) + )); + } + McpServerConfig::Ws(config) => { + lines.push(format!(" URL {}", config.url)); + lines.push(format!( + " Header keys {}", + format_optional_keys(config.headers.keys().cloned().collect()) + )); + lines.push(format!( + " Header helper {}", + config.headers_helper.as_deref().unwrap_or("") + )); + } + McpServerConfig::Sdk(config) => { + lines.push(format!(" SDK name {}", config.name)); + } + McpServerConfig::ManagedProxy(config) => { + lines.push(format!(" URL {}", config.url)); + lines.push(format!(" Proxy id {}", config.id)); + } + } + + lines.join("\n") +} + fn normalize_optional_args(args: Option<&str>) -> Option<&str> { args.map(str::trim).filter(|value| !value.is_empty()) } @@ -1601,6 +1798,95 @@ fn render_skills_usage(unexpected: Option<&str>) -> String { lines.join("\n") } +fn render_mcp_usage(unexpected: Option<&str>) -> String { + let mut lines = vec![ + "MCP".to_string(), + " Usage /mcp [list|show |help]".to_string(), + " Direct CLI claw mcp [list|show |help]".to_string(), + " Sources .claw/settings.json, .claw/settings.local.json".to_string(), + ]; + if let Some(args) = unexpected { + lines.push(format!(" Unexpected {args}")); + } + lines.join("\n") +} + +fn config_source_label(source: ConfigSource) -> &'static str { + match source { + ConfigSource::User => "user", + ConfigSource::Project => "project", + ConfigSource::Local => "local", + } +} + +fn mcp_transport_label(config: &McpServerConfig) -> &'static str { + match config { + McpServerConfig::Stdio(_) => "stdio", + McpServerConfig::Sse(_) => "sse", + McpServerConfig::Http(_) => "http", + McpServerConfig::Ws(_) => "ws", + McpServerConfig::Sdk(_) => "sdk", + McpServerConfig::ManagedProxy(_) => "managed-proxy", + } +} + +fn mcp_server_summary(config: &McpServerConfig) -> String { + match config { + McpServerConfig::Stdio(config) => { + if config.args.is_empty() { + config.command.clone() + } else { + format!("{} {}", config.command, config.args.join(" ")) + } + } + McpServerConfig::Sse(config) | McpServerConfig::Http(config) => config.url.clone(), + McpServerConfig::Ws(config) => config.url.clone(), + McpServerConfig::Sdk(config) => config.name.clone(), + McpServerConfig::ManagedProxy(config) => format!("{} ({})", config.id, config.url), + } +} + +fn format_optional_list(values: &[String]) -> String { + if values.is_empty() { + "".to_string() + } else { + values.join(" ") + } +} + +fn format_optional_keys(mut keys: Vec) -> String { + if keys.is_empty() { + return "".to_string(); + } + keys.sort(); + keys.join(", ") +} + +fn format_mcp_oauth(oauth: Option<&McpOAuthConfig>) -> String { + let Some(oauth) = oauth else { + return "".to_string(); + }; + + let mut parts = Vec::new(); + if let Some(client_id) = &oauth.client_id { + parts.push(format!("client_id={client_id}")); + } + if let Some(port) = oauth.callback_port { + parts.push(format!("callback_port={port}")); + } + if let Some(url) = &oauth.auth_server_metadata_url { + parts.push(format!("metadata_url={url}")); + } + if let Some(xaa) = oauth.xaa { + parts.push(format!("xaa={xaa}")); + } + if parts.is_empty() { + "enabled".to_string() + } else { + parts.join(", ") + } +} + #[must_use] pub fn handle_slash_command( input: &str, @@ -1653,6 +1939,7 @@ pub fn handle_slash_command( | SlashCommand::Cost | SlashCommand::Resume { .. } | SlashCommand::Config { .. } + | SlashCommand::Mcp { .. } | SlashCommand::Memory | SlashCommand::Init | SlashCommand::Diff @@ -1676,7 +1963,9 @@ mod tests { validate_slash_command_input, DefinitionSource, SkillOrigin, SkillRoot, SlashCommand, }; use plugins::{PluginKind, PluginManager, PluginManagerConfig, PluginMetadata, PluginSummary}; - use runtime::{CompactionConfig, ContentBlock, ConversationMessage, MessageRole, Session}; + use runtime::{ + CompactionConfig, ConfigLoader, ContentBlock, ConversationMessage, MessageRole, Session, + }; use std::fs; use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; @@ -1877,6 +2166,20 @@ mod tests { section: Some("env".to_string()) })) ); + assert_eq!( + SlashCommand::parse("/mcp"), + Ok(Some(SlashCommand::Mcp { + action: None, + target: None + })) + ); + assert_eq!( + SlashCommand::parse("/mcp show remote"), + Ok(Some(SlashCommand::Mcp { + action: Some("show".to_string()), + target: Some("remote".to_string()) + })) + ); assert_eq!( SlashCommand::parse("/memory"), Ok(Some(SlashCommand::Memory)) @@ -2019,6 +2322,18 @@ mod tests { assert!(skills_error.contains(" Usage /skills [list|help]")); } + #[test] + fn rejects_invalid_mcp_arguments() { + let show_error = parse_error_message("/mcp show alpha beta"); + assert!(show_error.contains("Unexpected arguments for /mcp show.")); + assert!(show_error.contains(" Usage /mcp show ")); + + let action_error = parse_error_message("/mcp inspect alpha"); + assert!(action_error + .contains("Unknown /mcp action 'inspect'. Use list, show , or help.")); + assert!(action_error.contains(" Usage /mcp [list|show |help]")); + } + #[test] fn renders_help_from_shared_specs() { let help = render_slash_command_help(); @@ -2045,6 +2360,7 @@ mod tests { assert!(help.contains("/cost")); assert!(help.contains("/resume ")); assert!(help.contains("/config [env|hooks|model|plugins]")); + assert!(help.contains("/mcp [list|show |help]")); assert!(help.contains("/memory")); assert!(help.contains("/init")); assert!(help.contains("/diff")); @@ -2058,8 +2374,8 @@ mod tests { assert!(help.contains("aliases: /plugins, /marketplace")); assert!(help.contains("/agents [list|help]")); assert!(help.contains("/skills [list|help]")); - assert_eq!(slash_command_specs().len(), 26); - assert_eq!(resume_supported_slash_commands().len(), 14); + assert_eq!(slash_command_specs().len(), 27); + assert_eq!(resume_supported_slash_commands().len(), 15); } #[test] @@ -2077,6 +2393,15 @@ mod tests { assert!(help.contains("Category Workspace & git")); } + #[test] + fn renders_per_command_help_detail_for_mcp() { + let help = render_slash_command_help_detail("mcp").expect("detail help should exist"); + assert!(help.contains("/mcp")); + assert!(help.contains("Summary Inspect configured MCP servers")); + assert!(help.contains("Category Discovery & debugging")); + assert!(help.contains("Resume Supported with --resume SESSION.jsonl")); + } + #[test] fn validate_slash_command_input_rejects_extra_single_value_arguments() { // given @@ -2211,6 +2536,7 @@ mod tests { assert!( handle_slash_command("/config env", &session, CompactionConfig::default()).is_none() ); + assert!(handle_slash_command("/mcp list", &session, CompactionConfig::default()).is_none()); assert!(handle_slash_command("/diff", &session, CompactionConfig::default()).is_none()); assert!(handle_slash_command("/version", &session, CompactionConfig::default()).is_none()); assert!( @@ -2384,6 +2710,98 @@ mod tests { let _ = fs::remove_dir_all(cwd); } + #[test] + fn mcp_usage_supports_help_and_unexpected_args() { + let cwd = temp_dir("mcp-usage"); + + let help = super::handle_mcp_slash_command(Some("help"), &cwd).expect("mcp help"); + assert!(help.contains("Usage /mcp [list|show |help]")); + assert!(help.contains("Direct CLI claw mcp [list|show |help]")); + + let unexpected = + super::handle_mcp_slash_command(Some("show alpha beta"), &cwd).expect("mcp usage"); + assert!(unexpected.contains("Unexpected show alpha beta")); + + let _ = fs::remove_dir_all(cwd); + } + + #[test] + fn renders_mcp_reports_from_loaded_config() { + let workspace = temp_dir("mcp-config-workspace"); + let config_home = temp_dir("mcp-config-home"); + fs::create_dir_all(workspace.join(".claw")).expect("workspace config dir"); + fs::create_dir_all(&config_home).expect("config home"); + fs::write( + workspace.join(".claw").join("settings.json"), + r#"{ + "mcpServers": { + "alpha": { + "command": "uvx", + "args": ["alpha-server"], + "env": {"ALPHA_TOKEN": "secret"}, + "toolCallTimeoutMs": 1200 + }, + "remote": { + "type": "http", + "url": "https://remote.example/mcp", + "headers": {"Authorization": "Bearer secret"}, + "headersHelper": "./bin/headers", + "oauth": { + "clientId": "remote-client", + "callbackPort": 7878 + } + } + } + }"#, + ) + .expect("write settings"); + fs::write( + workspace.join(".claw").join("settings.local.json"), + r#"{ + "mcpServers": { + "remote": { + "type": "ws", + "url": "wss://remote.example/mcp" + } + } + }"#, + ) + .expect("write local settings"); + + let loader = ConfigLoader::new(&workspace, &config_home); + let list = super::render_mcp_report_for(&loader, &workspace, None) + .expect("mcp list report should render"); + assert!(list.contains("Configured servers 2")); + assert!(list.contains("alpha")); + assert!(list.contains("stdio")); + assert!(list.contains("project")); + assert!(list.contains("uvx alpha-server")); + assert!(list.contains("remote")); + assert!(list.contains("ws")); + assert!(list.contains("local")); + assert!(list.contains("wss://remote.example/mcp")); + + let show = super::render_mcp_report_for(&loader, &workspace, Some("show alpha")) + .expect("mcp show report should render"); + assert!(show.contains("Name alpha")); + assert!(show.contains("Command uvx")); + assert!(show.contains("Args alpha-server")); + assert!(show.contains("Env keys ALPHA_TOKEN")); + assert!(show.contains("Tool timeout 1200 ms")); + + let remote = super::render_mcp_report_for(&loader, &workspace, Some("show remote")) + .expect("mcp show remote report should render"); + assert!(remote.contains("Transport ws")); + assert!(remote.contains("URL wss://remote.example/mcp")); + + let missing = super::render_mcp_report_for(&loader, &workspace, Some("show missing")) + .expect("missing report should render"); + assert!(missing.contains("server `missing` is not configured")); + + let _ = fs::remove_dir_all(workspace); + let _ = fs::remove_dir_all(config_home); + } + #[test] fn parses_quoted_skill_frontmatter_values() { let contents = "---\nname: \"hud\"\ndescription: 'Quoted description'\n---\n"; diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index f23fa60..d048e2a 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -1,4 +1,11 @@ -#![allow(dead_code, unused_imports, unused_variables, clippy::unneeded_struct_pattern, clippy::unnecessary_wraps, clippy::unused_self)] +#![allow( + dead_code, + unused_imports, + unused_variables, + clippy::unneeded_struct_pattern, + clippy::unnecessary_wraps, + clippy::unused_self +)] mod init; mod input; mod render; @@ -22,9 +29,9 @@ use api::{ }; use commands::{ - handle_agents_slash_command, handle_plugins_slash_command, handle_skills_slash_command, - render_slash_command_help, resume_supported_slash_commands, slash_command_specs, - validate_slash_command_input, SlashCommand, + handle_agents_slash_command, handle_mcp_slash_command, handle_plugins_slash_command, + handle_skills_slash_command, render_slash_command_help, resume_supported_slash_commands, + slash_command_specs, validate_slash_command_input, SlashCommand, }; use compat_harness::{extract_manifest, UpstreamPaths}; use init::initialize_repo; @@ -99,6 +106,7 @@ fn run() -> Result<(), Box> { CliAction::DumpManifests => dump_manifests(), CliAction::BootstrapPlan => print_bootstrap_plan(), CliAction::Agents { args } => LiveCli::print_agents(args.as_deref())?, + CliAction::Mcp { args } => LiveCli::print_mcp(args.as_deref())?, CliAction::Skills { args } => LiveCli::print_skills(args.as_deref())?, CliAction::PrintSystemPrompt { cwd, date } => print_system_prompt(cwd, date), CliAction::Version => print_version(), @@ -139,6 +147,9 @@ enum CliAction { Agents { args: Option, }, + Mcp { + args: Option, + }, Skills { args: Option, }, @@ -334,6 +345,9 @@ fn parse_args(args: &[String]) -> Result { "agents" => Ok(CliAction::Agents { args: join_optional_args(&rest[1..]), }), + "mcp" => Ok(CliAction::Mcp { + args: join_optional_args(&rest[1..]), + }), "skills" => Ok(CliAction::Skills { args: join_optional_args(&rest[1..]), }), @@ -392,6 +406,7 @@ fn bare_slash_command_guidance(command_name: &str) -> Option { "dump-manifests" | "bootstrap-plan" | "agents" + | "mcp" | "skills" | "system-prompt" | "login" @@ -427,6 +442,14 @@ fn parse_direct_slash_cli_action(rest: &[String]) -> Result { match SlashCommand::parse(&raw) { Ok(Some(SlashCommand::Help)) => Ok(CliAction::Help), Ok(Some(SlashCommand::Agents { args })) => Ok(CliAction::Agents { args }), + Ok(Some(SlashCommand::Mcp { action, target })) => Ok(CliAction::Mcp { + args: match (action, target) { + (None, None) => None, + (Some(action), None) => Some(action), + (Some(action), Some(target)) => Some(format!("{action} {target}")), + (None, Some(target)) => Some(target), + }, + }), Ok(Some(SlashCommand::Skills { args })) => Ok(CliAction::Skills { args }), Ok(Some(SlashCommand::Unknown(name))) => Err(format_unknown_direct_slash_command(&name)), Ok(Some(command)) => Err({ @@ -1345,6 +1368,19 @@ fn run_resume_command( session: session.clone(), message: Some(render_config_report(section.as_deref())?), }), + SlashCommand::Mcp { action, target } => { + let cwd = env::current_dir()?; + let args = match (action.as_deref(), target.as_deref()) { + (None, None) => None, + (Some(action), None) => Some(action.to_string()), + (Some(action), Some(target)) => Some(format!("{action} {target}")), + (None, Some(target)) => Some(target.to_string()), + }; + Ok(ResumeCommandOutcome { + session: session.clone(), + message: Some(handle_mcp_slash_command(args.as_deref(), &cwd)?), + }) + } SlashCommand::Memory => Ok(ResumeCommandOutcome { session: session.clone(), message: Some(render_memory_report()?), @@ -1795,6 +1831,16 @@ impl LiveCli { Self::print_config(section.as_deref())?; false } + SlashCommand::Mcp { action, target } => { + let args = match (action.as_deref(), target.as_deref()) { + (None, None) => None, + (Some(action), None) => Some(action.to_string()), + (Some(action), Some(target)) => Some(format!("{action} {target}")), + (None, Some(target)) => Some(target.to_string()), + }; + Self::print_mcp(args.as_deref())?; + false + } SlashCommand::Memory => { Self::print_memory()?; false @@ -2056,6 +2102,12 @@ impl LiveCli { Ok(()) } + fn print_mcp(args: Option<&str>) -> Result<(), Box> { + let cwd = env::current_dir()?; + println!("{}", handle_mcp_slash_command(args, &cwd)?); + Ok(()) + } + fn print_skills(args: Option<&str>) -> Result<(), Box> { let cwd = env::current_dir()?; println!("{}", handle_skills_slash_command(args, &cwd)?); @@ -4048,6 +4100,9 @@ fn slash_command_completion_candidates_with_sessions( "/config hooks", "/config model", "/config plugins", + "/mcp ", + "/mcp list", + "/mcp show ", "/export ", "/issue ", "/model ", @@ -4073,6 +4128,7 @@ fn slash_command_completion_candidates_with_sessions( "/teleport ", "/ultraplan ", "/agents help", + "/mcp help", "/skills help", ] { completions.insert(candidate.to_string()); @@ -4763,6 +4819,7 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> { writeln!(out, " claw dump-manifests")?; writeln!(out, " claw bootstrap-plan")?; writeln!(out, " claw agents")?; + writeln!(out, " claw mcp")?; writeln!(out, " claw skills")?; writeln!(out, " claw system-prompt [--cwd PATH] [--date YYYY-MM-DD]")?; writeln!(out, " claw login")?; @@ -4834,6 +4891,7 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> { " claw --resume {LATEST_SESSION_REFERENCE} /status /diff /export notes.txt" )?; writeln!(out, " claw agents")?; + writeln!(out, " claw mcp show my-server")?; writeln!(out, " claw /skills")?; writeln!(out, " claw login")?; writeln!(out, " claw init")?; @@ -5106,6 +5164,10 @@ mod tests { parse_args(&["agents".to_string()]).expect("agents should parse"), CliAction::Agents { args: None } ); + assert_eq!( + parse_args(&["mcp".to_string()]).expect("mcp should parse"), + CliAction::Mcp { args: None } + ); assert_eq!( parse_args(&["skills".to_string()]).expect("skills should parse"), CliAction::Skills { args: None } @@ -5165,11 +5227,18 @@ mod tests { } #[test] - fn parses_direct_agents_and_skills_slash_commands() { + fn parses_direct_agents_mcp_and_skills_slash_commands() { assert_eq!( parse_args(&["/agents".to_string()]).expect("/agents should parse"), CliAction::Agents { args: None } ); + assert_eq!( + parse_args(&["/mcp".to_string(), "show".to_string(), "demo".to_string()]) + .expect("/mcp show demo should parse"), + CliAction::Mcp { + args: Some("show demo".to_string()) + } + ); assert_eq!( parse_args(&["/skills".to_string()]).expect("/skills should parse"), CliAction::Skills { args: None } @@ -5376,6 +5445,7 @@ mod tests { assert!(help.contains("/cost")); assert!(help.contains("/resume ")); assert!(help.contains("/config [env|hooks|model|plugins]")); + assert!(help.contains("/mcp [list|show |help]")); assert!(help.contains("/memory")); assert!(help.contains("/init")); assert!(help.contains("/diff")); @@ -5406,6 +5476,7 @@ mod tests { assert!(completions.contains(&"/session list".to_string())); assert!(completions.contains(&"/session switch session-current".to_string())); assert!(completions.contains(&"/resume session-old".to_string())); + assert!(completions.contains(&"/mcp list".to_string())); assert!(completions.contains(&"/ultraplan ".to_string())); } @@ -5442,7 +5513,7 @@ mod tests { assert_eq!( names, vec![ - "help", "status", "sandbox", "compact", "clear", "cost", "config", "memory", + "help", "status", "sandbox", "compact", "clear", "cost", "config", "mcp", "memory", "init", "diff", "version", "export", "agents", "skills", ] ); @@ -5515,6 +5586,7 @@ mod tests { assert!(help.contains("claw sandbox")); assert!(help.contains("claw init")); assert!(help.contains("claw agents")); + assert!(help.contains("claw mcp")); assert!(help.contains("claw skills")); assert!(help.contains("claw /skills")); }