diff --git a/CHANGELOG.md b/CHANGELOG.md index 48cd431..01a64f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [0.6.3] - 2026-01-03 + +### Summary + +**CLI Enhancement: Config-Based Introspection** + +This patch release adds `--from-config` support to the `introspect` command, enabling users to load server configurations from `~/.claude/mcp.json` instead of specifying manual arguments. + +**Key Achievements**: +- New `--from-config` flag for `introspect` command +- Security improvements to error messages +- 556 tests passing (100% pass rate) +- Dependency updates (rmcp 0.12, tokio 1.49) + +### Added + +- **`--from-config` for introspect command**: Load server configuration from `~/.claude/mcp.json` by name + - `mcp-execution-cli introspect --from-config github` instead of manual docker/npx args + - Matches existing `--from-config` in `generate` command + - Configuration Modes section in help text + - 3 new integration tests for config loading + +### Changed + +- **Error messages**: Improved security by removing information disclosure + - Removed server list from "not found" errors (prevents enumeration) + - Use `~/.claude/mcp.json` instead of full filesystem path +- **Logging**: Changed config loading logs from `info!` to `debug!` level +- **Help text**: Added Configuration Modes section with recommended usage + +### Dependencies + +- `rmcp`: 0.10 → 0.12 +- `tokio`: 1.48 → 1.49 +- `handlebars`: 6.3 → 6.4 +- `schemars`: 1.1 → 1.2 +- `tempfile`: 3.23 → 3.24 + +--- + ## [0.6.2] - 2025-12-08 ### Summary @@ -943,5 +983,17 @@ Phase 6 (Optimization) is currently OPTIONAL and DEFERRED because: --- -**Last Updated**: 2025-12-08 -**Version**: 0.6.2 (Production Ready) +**Last Updated**: 2026-01-03 +**Version**: 0.6.3 (Production Ready) + +--- + +[Unreleased]: https://github.com/bug-ops/mcp-execution/compare/v0.6.3...HEAD +[0.6.3]: https://github.com/bug-ops/mcp-execution/compare/v0.6.2...v0.6.3 +[0.6.2]: https://github.com/bug-ops/mcp-execution/compare/v0.6.1...v0.6.2 +[0.6.1]: https://github.com/bug-ops/mcp-execution/compare/v0.6.0...v0.6.1 +[0.6.0]: https://github.com/bug-ops/mcp-execution/compare/v0.5.0...v0.6.0 +[0.5.0]: https://github.com/bug-ops/mcp-execution/compare/v0.4.0...v0.5.0 +[0.4.0]: https://github.com/bug-ops/mcp-execution/compare/v0.3.0...v0.4.0 +[0.3.0]: https://github.com/bug-ops/mcp-execution/compare/v0.2.0...v0.3.0 +[0.2.0]: https://github.com/bug-ops/mcp-execution/releases/tag/v0.2.0 diff --git a/Cargo.lock b/Cargo.lock index 7edfe84..c1a08a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -161,9 +161,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytes" @@ -179,9 +179,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.49" +version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", "shlex", @@ -242,9 +242,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.53" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", "clap_derive", @@ -252,9 +252,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstream", "anstyle", @@ -264,9 +264,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.62" +version = "4.5.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "004eef6b14ce34759aa7de4aea3217e368f463f46a3ed3764ca4b5a4404003b4" +checksum = "4c0da80818b2d95eca9aa614a30783e42f62bf5fdfee24e68cfb960b071ba8d1" dependencies = [ "clap", ] @@ -306,9 +306,9 @@ dependencies = [ [[package]] name = "console" -version = "0.16.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4" +checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" dependencies = [ "encode_unicode", "libc", @@ -616,9 +616,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] name = "fnv" @@ -767,9 +767,9 @@ dependencies = [ [[package]] name = "handlebars" -version = "6.3.2" +version = "6.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098" +checksum = "9b3f9296c208515b87bd915a2f5d1163d4b3f863ba83337d7713cf478055948e" dependencies = [ "derive_builder", "log", @@ -850,9 +850,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" @@ -872,15 +872,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.179" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags", "libc", @@ -918,7 +918,7 @@ dependencies = [ [[package]] name = "mcp-codegen" -version = "0.6.2" +version = "0.6.3" dependencies = [ "criterion", "handlebars", @@ -932,7 +932,7 @@ dependencies = [ [[package]] name = "mcp-core" -version = "0.6.2" +version = "0.6.3" dependencies = [ "async-trait", "chrono", @@ -948,7 +948,7 @@ dependencies = [ [[package]] name = "mcp-execution-cli" -version = "0.6.2" +version = "0.6.3" dependencies = [ "anyhow", "clap", @@ -974,7 +974,7 @@ dependencies = [ [[package]] name = "mcp-files" -version = "0.6.2" +version = "0.6.3" dependencies = [ "criterion", "dhat", @@ -987,7 +987,7 @@ dependencies = [ [[package]] name = "mcp-introspector" -version = "0.6.2" +version = "0.6.3" dependencies = [ "criterion", "mcp-core", @@ -1000,7 +1000,7 @@ dependencies = [ [[package]] name = "mcp-server" -version = "0.6.2" +version = "0.6.3" dependencies = [ "anyhow", "chrono", @@ -1168,15 +1168,15 @@ dependencies = [ [[package]] name = "pastey" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d6c094ee800037dff99e02cab0eaf3142826586742a270ab3d7a62656bd27a" +checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" [[package]] name = "pest" -version = "2.8.4" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" dependencies = [ "memchr", "ucd-trie", @@ -1184,9 +1184,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.4" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" dependencies = [ "pest", "pest_generator", @@ -1194,9 +1194,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.4" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" dependencies = [ "pest", "pest_meta", @@ -1207,9 +1207,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.4" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" dependencies = [ "pest", "sha2", @@ -1266,9 +1266,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" dependencies = [ "unicode-ident", ] @@ -1615,9 +1615,9 @@ dependencies = [ [[package]] name = "shell-words" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" [[package]] name = "shlex" @@ -1627,10 +1627,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -1654,9 +1655,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.111" +version = "2.0.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4" dependencies = [ "proc-macro2", "quote", @@ -1723,9 +1724,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", "libc", @@ -2337,6 +2338,6 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zmij" -version = "1.0.2" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4a4e8e9dc5c62d159f04fcdbe07f4c3fb710415aab4754bf11505501e3251d" +checksum = "4ee2a72b10d087f75fb2e1c2c7343e308fe6970527c22a41caf8372e165ff5c1" diff --git a/Cargo.toml b/Cargo.toml index b231985..ea0ba36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ default-members = [ "crates/*" ] resolver = "2" [workspace.package] -version = "0.6.2" +version = "0.6.3" edition = "2024" rust-version = "1.89" authors = ["Andrei G "] @@ -23,7 +23,7 @@ criterion = "0.8" dhat = "0.3" dialoguer = "0.12" dirs = "6.0" -handlebars = "6.3" +handlebars = "6.4" mcp-codegen = { path = "crates/mcp-codegen" } mcp-core = { path = "crates/mcp-core" } mcp-files = { path = "crates/mcp-files" } @@ -32,13 +32,13 @@ mcp-server = { path = "crates/mcp-server" } rayon = "1.11" regex = "1.12" rmcp = "0.12" -schemars = "1.2.0" +schemars = "1.2" serde = "1.0" serde_json = "1.0" static_assertions = "1.1" tempfile = "3.24" thiserror = "2.0" -tokio = "1.48" +tokio = "1.49" toml = "0.9" tracing = "0.1" tracing-subscriber = "0.3" diff --git a/README.md b/README.md index 6d9e8e6..e743a23 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ cargo install --path crates/mcp-cli ### Generate TypeScript Tools ```bash -# 1. Configure MCP server in ~/.config/claude/mcp.json +# 1. Configure MCP server in ~/.claude/mcp.json # 2. Generate tools mcp-execution-cli generate --from-config github @@ -88,6 +88,7 @@ node ~/.claude/servers/github/createIssue.ts --repo="owner/repo" --title="Bug" | **Type-Safe** | Full TypeScript interfaces from MCP JSON schemas | | **Lightning Fast** | 526x faster than target (0.19ms for 10 tools) | | **100% MCP Compatible** | Works with all MCP servers via [rmcp SDK](https://docs.rs/rmcp) | +| **Thoroughly Tested** | 556 tests with 100% pass rate | ## Workspace Crates @@ -115,19 +116,25 @@ mcp-files → mcp-core See [mcp-cli README](crates/mcp-cli) for full reference. ```bash -# Generate TypeScript tools +# Generate TypeScript tools from config (recommended) mcp-execution-cli generate --from-config github -# Introspect MCP server +# Introspect MCP server from config (v0.6.3+) mcp-execution-cli introspect --from-config github -# View cache statistics -mcp-execution-cli stats +# Introspect with detailed schemas +mcp-execution-cli introspect --from-config github --detailed + +# Manual configuration (alternative) +mcp-execution-cli introspect docker --arg=run --arg=-i --env=TOKEN=xxx # Shell completions mcp-execution-cli completions bash ``` +> [!TIP] +> Use `--from-config` to load server settings from `~/.claude/mcp.json` instead of manual arguments. + ## Performance | Metric | Target | Achieved | diff --git a/crates/mcp-cli/README.md b/crates/mcp-cli/README.md index c16d311..4476d07 100644 --- a/crates/mcp-cli/README.md +++ b/crates/mcp-cli/README.md @@ -54,7 +54,7 @@ mcp-execution-cli generate github-mcp-server --env GITHUB_TOKEN=ghp_xxx ``` > [!TIP] -> Use `--from-config` to load server configuration from `~/.config/claude/mcp.json`. +> Use `--from-config` to load server configuration from `~/.claude/mcp.json`. ### Discover Available Tools @@ -118,7 +118,46 @@ mcp-execution-cli generate docker \ Analyze MCP servers and discover capabilities: ```bash -mcp-execution-cli introspect --from-config github --output json +mcp-execution-cli introspect [OPTIONS] +``` + +**Configuration Modes**: + +1. Load from `~/.claude/mcp.json` (recommended): + ```bash + mcp-execution-cli introspect --from-config github + ``` + +2. Manual configuration: + ```bash + mcp-execution-cli introspect github-mcp-server --arg=stdio + ``` + +**Options**: + +- `--from-config `: Load config from mcp.json +- `--arg `: Server command argument (repeatable) +- `--env `: Environment variable (repeatable) +- `--detailed`: Show full input/output schemas +- `--format `: Output format (json, text, pretty) +- `--http `: Use HTTP transport +- `--sse `: Use SSE transport + +**Examples**: + +```bash +# From config with detailed schemas +mcp-execution-cli introspect --from-config github --detailed + +# Manual with Docker +mcp-execution-cli introspect docker \ + --arg=run --arg=-i --arg=--rm \ + --arg=ghcr.io/github/github-mcp-server \ + --env=GITHUB_TOKEN=ghp_xxx + +# HTTP transport +mcp-execution-cli introspect --http https://api.example.com/mcp \ + --header "Authorization=Bearer token" ``` ### `stats` diff --git a/crates/mcp-cli/src/commands/common.rs b/crates/mcp-cli/src/commands/common.rs index 46f9bc3..1048e03 100644 --- a/crates/mcp-cli/src/commands/common.rs +++ b/crates/mcp-cli/src/commands/common.rs @@ -38,7 +38,7 @@ fn load_mcp_config() -> Result { let config_path = home.join(".claude").join("mcp.json"); let content = std::fs::read_to_string(&config_path) - .with_context(|| format!("failed to read MCP config from {}", config_path.display()))?; + .with_context(|| "failed to read MCP config from ~/.claude/mcp.json")?; let config: McpConfig = serde_json::from_str(&content).context("failed to parse MCP config JSON")?; @@ -74,8 +74,10 @@ pub fn load_server_from_config(name: &str) -> Result<(ServerId, ServerConfig)> { let config = load_mcp_config()?; let server_config = config.mcp_servers.get(name).with_context(|| { - let available: Vec<_> = config.mcp_servers.keys().collect(); - format!("server '{name}' not found in MCP config\nAvailable servers: {available:?}") + format!( + "server '{name}' not found in MCP config at ~/.claude/mcp.json\n\ + Hint: Use 'mcp-execution-cli server list' to see available servers" + ) })?; let id = ServerId::new(name); diff --git a/crates/mcp-cli/src/commands/generate.rs b/crates/mcp-cli/src/commands/generate.rs index 83dadd8..65878e1 100644 --- a/crates/mcp-cli/src/commands/generate.rs +++ b/crates/mcp-cli/src/commands/generate.rs @@ -14,7 +14,7 @@ use mcp_files::FilesBuilder; use mcp_introspector::Introspector; use serde::Serialize; use std::path::PathBuf; -use tracing::{info, warn}; +use tracing::{debug, info, warn}; /// Result of progressive loading code generation. #[derive(Debug, Serialize)] @@ -78,7 +78,7 @@ pub async fn run( ) -> Result { // Build server config: either from mcp.json or from CLI arguments let (server_id, server_config) = if let Some(config_name) = from_config { - info!( + debug!( "Loading server configuration from ~/.claude/mcp.json: {}", config_name ); diff --git a/crates/mcp-cli/src/commands/introspect.rs b/crates/mcp-cli/src/commands/introspect.rs index e2298f6..a2eb3fd 100644 --- a/crates/mcp-cli/src/commands/introspect.rs +++ b/crates/mcp-cli/src/commands/introspect.rs @@ -2,12 +2,12 @@ //! //! Connects to an MCP server and displays its capabilities, tools, and metadata. -use super::common::build_server_config; +use super::common::{build_server_config, load_server_from_config}; use anyhow::{Context, Result}; use mcp_core::cli::{ExitCode, OutputFormat}; use mcp_introspector::{Introspector, ServerInfo, ToolInfo}; use serde::Serialize; -use tracing::info; +use tracing::{debug, info}; /// Result of server introspection. /// @@ -86,7 +86,7 @@ pub struct ToolMetadata { /// /// # Process /// -/// 1. Builds `ServerConfig` from CLI arguments +/// 1. Builds `ServerConfig` from CLI arguments or loads from ~/.claude/mcp.json /// 2. Creates an introspector and connects to the server /// 3. Discovers server capabilities and tools /// 4. Formats the output according to the specified format @@ -94,6 +94,7 @@ pub struct ToolMetadata { /// /// # Arguments /// +/// * `from_config` - Load server config from ~/.claude/mcp.json by name /// * `server` - Server command (binary name or path), None for HTTP/SSE /// * `args` - Arguments to pass to the server command /// * `env` - Environment variables in KEY=VALUE format @@ -121,6 +122,7 @@ pub struct ToolMetadata { /// # async fn example() -> anyhow::Result<()> { /// // Simple server /// let exit_code = introspect::run( +/// None, /// Some("github-mcp-server".to_string()), /// vec!["stdio".to_string()], /// vec![], @@ -135,6 +137,7 @@ pub struct ToolMetadata { /// // HTTP transport /// let exit_code = introspect::run( /// None, +/// None, /// vec![], /// vec![], /// None, @@ -149,6 +152,7 @@ pub struct ToolMetadata { /// ``` #[allow(clippy::too_many_arguments)] pub async fn run( + from_config: Option, server: Option, args: Vec, env: Vec, @@ -159,8 +163,16 @@ pub async fn run( detailed: bool, output_format: OutputFormat, ) -> Result { - // Build ServerConfig from CLI arguments - let (server_id, config) = build_server_config(server, args, env, cwd, http, sse, headers)?; + // Build server config: either from mcp.json or from CLI arguments + let (server_id, config) = if let Some(config_name) = from_config { + debug!( + "Loading server configuration from ~/.claude/mcp.json: {}", + config_name + ); + load_server_from_config(&config_name)? + } else { + build_server_config(server, args, env, cwd, http, sse, headers)? + }; info!("Introspecting server: {}", server_id); info!("Transport: {:?}", config.transport()); @@ -500,6 +512,7 @@ mod tests { #[tokio::test] async fn test_run_server_connection_failure() { let result = run( + None, Some("nonexistent-server-xyz".to_string()), vec![], vec![], @@ -608,6 +621,7 @@ mod tests { async fn test_run_with_text_format() { // Test that Text format output works correctly (compact JSON) let result = run( + None, Some("nonexistent-server".to_string()), vec![], vec![], @@ -628,6 +642,7 @@ mod tests { async fn test_run_with_pretty_format() { // Test that Pretty format output works correctly (colorized) let result = run( + None, Some("nonexistent-server".to_string()), vec![], vec![], @@ -648,6 +663,7 @@ mod tests { async fn test_run_with_detailed_mode() { // Test that detailed mode doesn't cause crashes even with connection failure let result = run( + None, Some("nonexistent-server".to_string()), vec![], vec![], @@ -667,6 +683,7 @@ mod tests { async fn test_run_http_transport() { // Test HTTP transport with invalid URL let result = run( + None, None, vec![], vec![], @@ -688,6 +705,7 @@ mod tests { async fn test_run_sse_transport() { // Test SSE transport with invalid URL let result = run( + None, None, vec![], vec![], @@ -710,6 +728,7 @@ mod tests { // Test all output formats don't cause panics for format in [OutputFormat::Json, OutputFormat::Text, OutputFormat::Pretty] { let result = run( + None, Some("nonexistent".to_string()), vec![], vec![], @@ -731,6 +750,7 @@ mod tests { // Test detailed mode with all output formats for format in [OutputFormat::Json, OutputFormat::Text, OutputFormat::Pretty] { let result = run( + None, Some("nonexistent".to_string()), vec![], vec![], @@ -940,4 +960,83 @@ mod tests { assert!(result.server.supports_resources); assert!(!result.server.supports_prompts); } + + #[tokio::test] + async fn test_run_from_config_not_found() { + let result = run( + Some("nonexistent-server-xyz".to_string()), + None, + vec![], + vec![], + None, + None, + None, + vec![], + false, + OutputFormat::Json, + ) + .await; + + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("not found in MCP config") + || err_msg.contains("failed to read MCP config"), + "Expected config-related error, got: {err_msg}" + ); + } + + #[tokio::test] + async fn test_run_from_config_takes_priority() { + // When from_config is Some, it should be used for config loading + // (server arg should be None due to clap conflicts, but we test the logic) + let result = run( + Some("test-server".to_string()), + None, // server is None when from_config is used + vec![], + vec![], + None, + None, + None, + vec![], + false, + OutputFormat::Json, + ) + .await; + + // Should fail because config doesn't exist, not because of server + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + // Should try to load from config, not use manual server + assert!( + err_msg.contains("MCP config") || err_msg.contains("test-server"), + "Should attempt config loading: {err_msg}" + ); + } + + #[tokio::test] + async fn test_run_manual_mode_backward_compatible() { + // Existing behavior: from_config = None, use server arg + let result = run( + None, // from_config + Some("test-server-direct".to_string()), + vec![], + vec![], + None, + None, + None, + vec![], + false, + OutputFormat::Json, + ) + .await; + + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + // Should fail with connection error, not config error + assert!( + err_msg.contains("failed to connect") || err_msg.contains("test-server-direct"), + "Should try direct connection: {err_msg}" + ); + } } diff --git a/crates/mcp-cli/src/main.rs b/crates/mcp-cli/src/main.rs index 68f086a..7ae1997 100644 --- a/crates/mcp-cli/src/main.rs +++ b/crates/mcp-cli/src/main.rs @@ -75,16 +75,34 @@ pub enum Commands { /// Connects to an MCP server, discovers its tools, and displays /// detailed information about available capabilities. /// + /// # Configuration Modes + /// + /// 1. Load from ~/.claude/mcp.json (recommended): + /// ```bash + /// mcp-cli introspect --from-config github + /// ``` + /// + /// 2. Manual configuration: + /// ```bash + /// mcp-cli introspect github-mcp-server --arg=stdio + /// ``` + /// /// # Examples /// /// ```bash - /// # Simple binary + /// # Load GitHub server config from mcp.json + /// mcp-cli introspect --from-config github + /// + /// # Load with detailed schemas + /// mcp-cli introspect --from-config github --detailed + /// + /// # Manual: Simple binary /// mcp-cli introspect github-mcp-server /// - /// # With arguments + /// # Manual: With arguments /// mcp-cli introspect github-mcp-server --arg=stdio /// - /// # Docker container + /// # Manual: Docker container /// mcp-cli introspect docker --arg=run --arg=-i --arg=--rm \ /// --arg=ghcr.io/github/github-mcp-server \ /// --env=GITHUB_PERSONAL_ACCESS_TOKEN=ghp_xxx @@ -94,11 +112,31 @@ pub enum Commands { /// --header "Authorization=Bearer ghp_xxx" /// ``` Introspect { + /// Load server configuration from ~/.claude/mcp.json by name + /// + /// When specified, all other server configuration options are ignored. + /// The server must be defined in ~/.claude/mcp.json with matching name. + /// + /// Example mcp.json: + /// ```json + /// { + /// "mcpServers": { + /// "github": { + /// "command": "docker", + /// "args": ["run", "-i", "--rm", "..."], + /// "env": {"GITHUB_PERSONAL_ACCESS_TOKEN": "..."} + /// } + /// } + /// } + /// ``` + #[arg(long = "from-config", conflicts_with_all = ["server", "args", "env", "cwd", "http", "sse"])] + from_config: Option, + /// Server command (binary name or path) /// /// For stdio transport: command to execute (e.g., "docker", "npx", "github-mcp-server") /// Not required when using --http or --sse - #[arg(required_unless_present_any = ["http", "sse"])] + #[arg(required_unless_present_any = ["from_config", "http", "sse"])] server: Option, /// Arguments to pass to the server command @@ -318,6 +356,7 @@ fn init_logging(verbose: bool) -> Result<()> { async fn execute_command(command: Commands, output_format: OutputFormat) -> Result { match command { Commands::Introspect { + from_config, server, args, env, @@ -328,6 +367,7 @@ async fn execute_command(command: Commands, output_format: OutputFormat) -> Resu detailed, } => { commands::introspect::run( + from_config, server, args, env,