Эх сурвалжийг харах

Expose configured MCP servers from the CLI

PARITY.md called out missing MCP management in the Rust CLI, so this adds a focused read-only /mcp path instead of expanding the broader config surface first.

The new command works in the REPL, with --resume, and as a direct 7⠋ 🦀 Thinking...8✘ ❌ Request failed
 entrypoint. It lists merged MCP server definitions, supports detailed inspection for one server, and adds targeted tests for parsing, help text, completion hints, and config-backed rendering.

Constraint: Keep the enhancement inside the existing Rust slash-command architecture
Rejected: Extend /config with a raw mcp dump only | less discoverable than a dedicated MCP workflow
Confidence: high
Scope-risk: narrow
Directive: Keep /mcp read-only unless MCP lifecycle commands gain shared runtime orchestration
Tested: cargo test -p commands parses_supported_slash_commands
Tested: cargo test -p commands rejects_invalid_mcp_arguments
Tested: cargo test -p commands renders_help_from_shared_specs
Tested: cargo test -p commands renders_per_command_help_detail_for_mcp
Tested: cargo test -p commands ignores_unknown_or_runtime_bound_slash_commands
Tested: cargo test -p commands mcp_usage_supports_help_and_unexpected_args
Tested: cargo test -p commands renders_mcp_reports_from_loaded_config
Tested: cargo test -p rusty-claude-cli parses_login_and_logout_subcommands
Tested: cargo test -p rusty-claude-cli parses_direct_agents_mcp_and_skills_slash_commands
Tested: cargo test -p rusty-claude-cli repl_help_includes_shared_commands_and_exit
Tested: cargo test -p rusty-claude-cli completion_candidates_include_workflow_shortcuts_and_dynamic_sessions
Tested: cargo test -p rusty-claude-cli resume_supported_command_list_matches_expected_surface
Tested: cargo test -p rusty-claude-cli init_help_mentions_direct_subcommand
Tested: cargo run -p rusty-claude-cli -- mcp help
Not-tested: Live MCP server connectivity against a real remote or stdio backend
Yeachan-Heo 2 сар өмнө
parent
commit
9c67607670

+ 423 - 5
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 <server>|help]"),
+        resume_supported: true,
+    },
     SlashCommandSpec {
         name: "memory",
         aliases: &[],
@@ -272,6 +282,10 @@ pub enum SlashCommand {
     Config {
         section: Option<String>,
     },
+    Mcp {
+        action: Option<String>,
+        target: Option<String>,
+    },
     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<SlashCommand, SlashCommandPars
     }
 }
 
+fn parse_mcp_command(args: &[&str]) -> Result<SlashCommand, SlashCommandParseError> {
+    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", "<server>")),
+        ["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 <server>",
+        )),
+        ["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 <server>, or help."),
+            "mcp",
+            "/mcp [list|show <server>|help]",
+        )),
+    }
+}
+
 fn parse_plugin_command(args: &[&str]) -> Result<SlashCommand, SlashCommandParseError> {
     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<String, runtime::ConfigError> {
+    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<String> {
     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<String, runtime::ConfigError> {
+    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, ScopedMcpServerConfig>,
+) -> 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(|| "<default>".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("<none>")
+            ));
+            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("<none>")
+            ));
+        }
+        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 <server>|help]".to_string(),
+        "  Direct CLI       claw mcp [list|show <server>|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() {
+        "<none>".to_string()
+    } else {
+        values.join(" ")
+    }
+}
+
+fn format_optional_keys(mut keys: Vec<String>) -> String {
+    if keys.is_empty() {
+        return "<none>".to_string();
+    }
+    keys.sort();
+    keys.join(", ")
+}
+
+fn format_mcp_oauth(oauth: Option<&McpOAuthConfig>) -> String {
+    let Some(oauth) = oauth else {
+        return "<none>".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 <server>"));
+
+        let action_error = parse_error_message("/mcp inspect alpha");
+        assert!(action_error
+            .contains("Unknown /mcp action 'inspect'. Use list, show <server>, or help."));
+        assert!(action_error.contains("  Usage            /mcp [list|show <server>|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 <session-path>"));
         assert!(help.contains("/config [env|hooks|model|plugins]"));
+        assert!(help.contains("/mcp [list|show <server>|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 <server>|help]"));
+        assert!(help.contains("Direct CLI       claw mcp [list|show <server>|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";

+ 78 - 6
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<dyn std::error::Error>> {
         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<String>,
     },
+    Mcp {
+        args: Option<String>,
+    },
     Skills {
         args: Option<String>,
     },
@@ -334,6 +345,9 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
         "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<String> {
         "dump-manifests"
             | "bootstrap-plan"
             | "agents"
+            | "mcp"
             | "skills"
             | "system-prompt"
             | "login"
@@ -427,6 +442,14 @@ fn parse_direct_slash_cli_action(rest: &[String]) -> Result<CliAction, String> {
     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<dyn std::error::Error>> {
+        let cwd = env::current_dir()?;
+        println!("{}", handle_mcp_slash_command(args, &cwd)?);
+        Ok(())
+    }
+
     fn print_skills(args: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
         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 <session-path>"));
         assert!(help.contains("/config [env|hooks|model|plugins]"));
+        assert!(help.contains("/mcp [list|show <server>|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"));
     }