Skip to main content

clap_mcp/
lib.rs

1//! # clap-mcp
2//!
3//! Expose your [clap](https://docs.rs/clap) CLI as an MCP (Model Context Protocol) server over stdio.
4//!
5//! ## Quick start
6//!
7//! Prefer a single `run` function with `#[clap_mcp_output_from = "run"]` so CLI and MCP
8//! share one implementation (no duplicated logic).
9//!
10//! ```rust,ignore
11//! use clap::Parser;
12//! use clap_mcp::ClapMcp;
13//!
14//! #[derive(Parser, ClapMcp)]
15//! #[clap_mcp(reinvocation_safe, parallel_safe = false)]
16//! #[clap_mcp_output_from = "run"]
17//! enum Cli {
18//!     Greet { #[arg(long)] name: Option<String> },
19//! }
20//!
21//! fn run(cmd: Cli) -> String {
22//!     match cmd {
23//!         Cli::Greet { name } => format!("Hello, {}!", name.as_deref().unwrap_or("world")),
24//!     }
25//! }
26//!
27//! fn main() {
28//!     let cli = Cli::parse_or_serve_mcp();
29//!     println!("{}", run(cli));
30//! }
31//! ```
32//!
33//! Run with `--mcp` to start the MCP server instead of executing the CLI.
34
35use clap::{Arg, ArgAction, Command};
36use rmcp::model::{Meta, TaskSupport, Tool, ToolExecution};
37use serde::{Deserialize, Serialize};
38use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
39
40mod server;
41
42#[cfg(feature = "http")]
43mod http;
44
45mod serve;
46
47pub use rmcp::model::ErrorData as ClapMcpErrorData;
48
49pub mod logging;
50
51/// Custom MCP resources and prompts, and skill export.
52pub mod content;
53
54#[cfg(feature = "derive")]
55pub use clap_mcp_macros::ClapMcp;
56pub use serve::{ServeMcp, ServeMcpBuilder};
57
58/// Convenience macro for struct root + subcommand CLIs: parse root then run.
59///
60/// Expands to: parse the root with [`ParseOrServeMcp::parse_or_serve_mcp`], then evaluate the given
61/// expression (which can use `args` for the parsed root). Use in `main` so the pattern
62/// is one line and hard to forget.
63///
64/// # Example
65///
66/// ```rust,ignore
67/// fn main() {
68///     clap_mcp_main!(Cli, |args| match args.command {
69///         None => println!("No subcommand"),
70///         Some(cmd) => println!("{}", run(cmd)),
71///     });
72/// }
73/// ```
74///
75/// For `Result`-returning run logic, use `?` in main or call [`run_or_serve_mcp`].
76#[macro_export]
77macro_rules! clap_mcp_main {
78    ($root:ty, |$args:ident| $run_expr:expr) => {{
79        let $args = <$root as $crate::ParseOrServeMcp>::parse_or_serve_mcp();
80        $run_expr
81    }};
82    ($root:ty, $run_expr:expr) => {{
83        macro_rules! __clap_mcp_with_args {
84            ($args:ident, $expr:expr) => {{
85                let $args = <$root as $crate::ParseOrServeMcp>::parse_or_serve_mcp();
86                $expr
87            }};
88        }
89        __clap_mcp_with_args!(args, $run_expr)
90    }};
91}
92
93/// Long flag that triggers MCP server mode. Add to your CLI via [`command_with_mcp_flag`].
94pub const MCP_FLAG_LONG: &str = "mcp";
95
96/// Stable clap arg id for the stdio MCP flag (internal; [`ClapMcpBuiltinFlags::stdio_long`] is user-facing).
97pub const CLAP_MCP_STDIO_FLAG_ID: &str = "clap_mcp_stdio";
98
99/// Stable clap arg id for the HTTP MCP flag (`http` feature).
100#[cfg(feature = "http")]
101pub const CLAP_MCP_HTTP_FLAG_ID: &str = "clap_mcp_http";
102
103/// Stable clap arg id for the export-skills flag.
104pub const CLAP_MCP_EXPORT_SKILLS_FLAG_ID: &str = "clap_mcp_export_skills";
105
106/// Legacy arg id used before stable ids; still recognized in [`matches_stdio_flag`].
107pub(crate) const CLAP_MCP_STDIO_FLAG_ID_LEGACY: &str = "mcp";
108
109/// Long flag for Streamable HTTP MCP server (`http` feature).
110#[cfg(feature = "http")]
111pub const MCP_HTTP_FLAG_LONG: &str = "mcp-http";
112
113/// Environment variable for HTTP bind host when [`MCP_HTTP_LISTEN_ENV`] is unset.
114#[cfg(feature = "http")]
115pub const MCP_HTTP_BIND_ENV: &str = "CLAP_MCP_HTTP_BIND";
116
117/// Environment variable for HTTP listen address (`host:port`).
118#[cfg(feature = "http")]
119pub const MCP_HTTP_LISTEN_ENV: &str = "CLAP_MCP_HTTP_LISTEN";
120
121/// Environment variable for HTTP port when [`MCP_HTTP_LISTEN_ENV`] is unset (requires [`MCP_HTTP_BIND_ENV`]).
122#[cfg(feature = "http")]
123pub const MCP_HTTP_PORT_ENV: &str = "CLAP_MCP_HTTP_PORT";
124
125/// Long flag that triggers [Agent Skills](https://agentskills.io/specification) export (generates SKILL.md). Add via [`command_with_export_skills_flag`].
126pub const EXPORT_SKILLS_FLAG_LONG: &str = "export-skills";
127
128/// User-facing long names for clap-mcp builtin global flags (stdio, HTTP, export-skills).
129///
130/// Override via `#[clap_mcp(mcp_flag = "...")]` on the derive or [`ClapMcpConfig::builtin_flags`]
131/// when your CLI already uses `--mcp` for something else. Values must be `'static` str literals
132/// (clap stores long names with static lifetime).
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub struct ClapMcpBuiltinFlags {
135    /// Long name for stdio MCP (default [`MCP_FLAG_LONG`]).
136    pub stdio_long: &'static str,
137    /// Long name for HTTP MCP (default [`MCP_HTTP_FLAG_LONG`], `http` feature).
138    #[cfg(feature = "http")]
139    pub http_long: &'static str,
140    /// Long name for export-skills (default [`EXPORT_SKILLS_FLAG_LONG`]).
141    pub export_skills_long: &'static str,
142}
143
144impl Default for ClapMcpBuiltinFlags {
145    fn default() -> Self {
146        Self {
147            stdio_long: MCP_FLAG_LONG,
148            #[cfg(feature = "http")]
149            http_long: MCP_HTTP_FLAG_LONG,
150            export_skills_long: EXPORT_SKILLS_FLAG_LONG,
151        }
152    }
153}
154
155impl ClapMcpBuiltinFlags {
156    /// Override the stdio MCP flag long name (without `--`).
157    pub const fn with_stdio_long(mut self, long: &'static str) -> Self {
158        self.stdio_long = long;
159        self
160    }
161
162    /// Override the export-skills flag long name (without `--`).
163    pub const fn with_export_skills_long(mut self, long: &'static str) -> Self {
164        self.export_skills_long = long;
165        self
166    }
167
168    /// Override the HTTP MCP flag long name (`http` feature).
169    #[cfg(feature = "http")]
170    pub const fn with_http_long(mut self, long: &'static str) -> Self {
171        self.http_long = long;
172        self
173    }
174}
175
176/// URI for the clap schema resource exposed by the MCP server.
177pub const MCP_RESOURCE_URI_SCHEMA: &str = "clap://schema";
178
179/// Provides MCP execution safety configuration from `#[clap_mcp(...)]` attributes.
180/// Implemented by the `#[derive(ClapMcp)]` macro.
181///
182/// # Example
183///
184/// ```rust
185/// use clap::Parser;
186/// use clap_mcp::ClapMcpConfigProvider;
187/// use clap_mcp::ClapMcp;
188///
189/// #[derive(Debug, Parser, ClapMcp)]
190/// #[clap_mcp(reinvocation_safe, parallel_safe = false)]
191/// #[clap_mcp_output_from = "run"]
192/// enum MyCli { Foo }
193///
194/// fn run(cmd: MyCli) -> String {
195///     match cmd { MyCli::Foo => "ok".to_string() }
196/// }
197///
198/// let config = MyCli::clap_mcp_config();
199/// assert!(config.reinvocation_safe);
200/// assert!(!config.parallel_safe);
201/// ```
202pub trait ClapMcpConfigProvider {
203    fn clap_mcp_config() -> ClapMcpConfig;
204}
205
206/// Provides MCP schema metadata (skip, requires, task tools, serialize) from `#[clap_mcp(skip)]`,
207/// `#[clap_mcp(requires = "arg_name")]`, optional `#[clap_mcp(task)]`, and `#[clap_mcp(serialized)]`
208/// on variants.
209///
210/// Implemented by the `#[derive(ClapMcp)]` macro. For custom types, implement
211/// with `fn clap_mcp_schema_metadata() -> ClapMcpSchemaMetadata { ClapMcpSchemaMetadata::default() }`.
212pub trait ClapMcpSchemaMetadataProvider {
213    fn clap_mcp_schema_metadata() -> ClapMcpSchemaMetadata;
214}
215
216/// Produces the output string for a parsed CLI value.
217/// Used for in-process MCP tool execution when `reinvocation_safe` is true.
218/// Implemented by the `#[derive(ClapMcp)]` macro via the blanket impl for `ClapMcpToolExecutor`.
219pub trait ClapMcpRunnable {
220    fn run(self) -> String;
221}
222
223/// Error produced when a tool's `run` function returns `Err(e)` (e.g. `Result<O, E>`).
224///
225/// When your `run` returns `Result<O, E>`, `Err(e)` is converted via [`IntoClapMcpToolError`]
226/// into this type. Implement that trait for your error type to get structured JSON in the
227/// response when `E: Serialize`.
228#[derive(Debug, Clone)]
229pub struct ClapMcpToolError {
230    /// Human-readable error message for MCP content.
231    pub message: String,
232    /// Optional structured JSON when `E: Serialize` and [`IntoClapMcpToolError`] provides it.
233    pub structured: Option<serde_json::Value>,
234}
235
236impl ClapMcpToolError {
237    /// Create a plain text error.
238    pub fn text(message: impl Into<String>) -> Self {
239        Self {
240            message: message.into(),
241            structured: None,
242        }
243    }
244
245    /// Create an error with structured serialization.
246    pub fn structured(message: impl Into<String>, value: serde_json::Value) -> Self {
247        Self {
248            message: message.into(),
249            structured: Some(value),
250        }
251    }
252}
253
254impl From<String> for ClapMcpToolError {
255    fn from(s: String) -> Self {
256        Self::text(s)
257    }
258}
259
260impl From<&str> for ClapMcpToolError {
261    fn from(s: &str) -> Self {
262        Self::text(s)
263    }
264}
265
266/// Converts the return value of a `run` function (used with `#[clap_mcp_output_from]`) into
267/// MCP tool output or error.
268///
269/// Implemented for:
270/// - `String` / `&str` → text output
271/// - [`AsStructured`]`<T>` where `T: Serialize` → structured JSON output
272/// - `Option<O>` → `None` → empty text; `Some(o)` → `o.into_tool_result()`
273/// - `Result<O, E>` → `Ok(o)` → output; `Err(e)` → `ClapMcpToolError`
274///
275/// `Result<AsStructured<T>, E>` is fully supported as a `run` return type; use it when you want
276/// structured success payloads and a separate error type.
277pub trait IntoClapMcpResult {
278    fn into_tool_result(self) -> std::result::Result<ClapMcpToolOutput, ClapMcpToolError>;
279}
280
281impl IntoClapMcpResult for String {
282    fn into_tool_result(self) -> std::result::Result<ClapMcpToolOutput, ClapMcpToolError> {
283        Ok(ClapMcpToolOutput::Text(self))
284    }
285}
286
287impl IntoClapMcpResult for &str {
288    fn into_tool_result(self) -> std::result::Result<ClapMcpToolOutput, ClapMcpToolError> {
289        Ok(ClapMcpToolOutput::Text(self.to_string()))
290    }
291}
292
293/// Wrapper for structured (JSON) output when using `#[clap_mcp_output_from]`.
294/// Use when your `run` function returns a type that implements `Serialize` but is not `String`/`&str`.
295///
296/// Fully supported when used as the `Ok` type in `Result<AsStructured<T>, E>`; there are no known
297/// limitations for mixed success/error types. [`IntoClapMcpResult`] is implemented for
298/// `AsStructured<T>` where `T: Serialize`.
299///
300/// # Example
301///
302/// ```rust,ignore
303/// fn run(cmd: Cli) -> Result<clap_mcp::AsStructured<SubcommandResult>, Error> {
304///     match cmd { ... }
305/// }
306/// ```
307#[derive(Debug, Clone)]
308pub struct AsStructured<T>(pub T);
309
310impl<T: Serialize> IntoClapMcpResult for AsStructured<T> {
311    fn into_tool_result(self) -> std::result::Result<ClapMcpToolOutput, ClapMcpToolError> {
312        serde_json::to_value(&self.0)
313            .map(ClapMcpToolOutput::Structured)
314            .map_err(|e| ClapMcpToolError::text(e.to_string()))
315    }
316}
317
318impl<O: IntoClapMcpResult> IntoClapMcpResult for Option<O> {
319    fn into_tool_result(self) -> std::result::Result<ClapMcpToolOutput, ClapMcpToolError> {
320        match self {
321            None => Ok(ClapMcpToolOutput::Text(String::new())),
322            Some(o) => o.into_tool_result(),
323        }
324    }
325}
326
327/// Converts an error type from a `run` function into [`ClapMcpToolError`].
328/// Used when `run` returns `Result<O, E>` and the `Err` branch is taken.
329///
330/// Implement this for your error type when you need custom formatting or structured errors.
331/// For plain string errors, you can use `String` or `&str`, which have built-in impls.
332pub trait IntoClapMcpToolError {
333    fn into_tool_error(self) -> ClapMcpToolError;
334}
335
336impl IntoClapMcpToolError for String {
337    fn into_tool_error(self) -> ClapMcpToolError {
338        ClapMcpToolError::text(self)
339    }
340}
341
342impl IntoClapMcpToolError for &str {
343    fn into_tool_error(self) -> ClapMcpToolError {
344        ClapMcpToolError::text(self.to_string())
345    }
346}
347
348impl<O: IntoClapMcpResult, E: IntoClapMcpToolError> IntoClapMcpResult for Result<O, E> {
349    fn into_tool_result(self) -> std::result::Result<ClapMcpToolOutput, ClapMcpToolError> {
350        match self {
351            Ok(o) => o.into_tool_result(),
352            Err(e) => Err(e.into_tool_error()),
353        }
354    }
355}
356
357/// Runs a closure with stdout captured. Returns `(result, captured_stdout)`.
358/// Unix-only; on Windows returns empty captured string.
359#[cfg(unix)]
360fn run_with_stdout_capture<R, F>(f: F) -> (R, String)
361where
362    F: FnOnce() -> R,
363{
364    use std::io::{Read, Write};
365    use std::os::unix::io::FromRawFd;
366
367    // SAFETY: We use a pipe and dup2 to temporarily redirect stdout. All fds are either
368    // created by pipe()/dup() or are well-known (STDOUT_FILENO). We close or restore every
369    // fd on every path (success or error); from_raw_fd(read_fd) takes ownership of read_fd
370    // so it is not double-closed. No fd is used after being closed.
371    let mut fds: [libc::c_int; 2] = [0, 0];
372    if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
373        return (f(), String::new());
374    }
375    let (read_fd, write_fd) = (fds[0], fds[1]);
376
377    let stdout_fd = libc::STDOUT_FILENO;
378    let saved_stdout = unsafe { libc::dup(stdout_fd) };
379    if saved_stdout < 0 {
380        unsafe {
381            libc::close(read_fd);
382            libc::close(write_fd);
383        }
384        return (f(), String::new());
385    }
386
387    if unsafe { libc::dup2(write_fd, stdout_fd) } < 0 {
388        unsafe {
389            libc::close(saved_stdout);
390            libc::close(read_fd);
391            libc::close(write_fd);
392        }
393        return (f(), String::new());
394    }
395
396    let result = f();
397
398    let _ = std::io::stdout().flush();
399    unsafe {
400        libc::dup2(saved_stdout, stdout_fd);
401        libc::close(saved_stdout);
402        libc::close(write_fd);
403    }
404
405    let mut reader = unsafe { std::fs::File::from_raw_fd(read_fd) };
406    let mut captured = String::new();
407    let _ = reader.read_to_string(&mut captured);
408
409    (result, captured)
410}
411
412#[cfg(not(unix))]
413fn run_with_stdout_capture<R, F>(f: F) -> (R, String)
414where
415    F: FnOnce() -> R,
416{
417    (f(), String::new())
418}
419
420/// Output produced by a CLI command for MCP tool results.
421///
422/// Use `Text` for plain string output; use `Structured` for serializable JSON
423/// (e.g. when using `#[clap_mcp_output_from = "run"]` with `AsStructured<T>`, or
424/// (e.g. when using `#[clap_mcp_output_from = "run"]` with `AsStructured<T>`).
425///
426/// # Example
427///
428/// ```
429/// use clap_mcp::ClapMcpToolOutput;
430///
431/// let text = ClapMcpToolOutput::Text("hello".into());
432/// assert_eq!(text.into_string(), "hello");
433///
434/// let structured = ClapMcpToolOutput::Structured(serde_json::json!({"x": 1}));
435/// assert!(structured.as_structured().unwrap().get("x").is_some());
436/// ```
437#[derive(Debug, Clone)]
438pub enum ClapMcpToolOutput {
439    /// Plain text output (stdout-style).
440    Text(String),
441    /// Structured JSON output for machine consumption.
442    Structured(serde_json::Value),
443}
444
445impl ClapMcpToolOutput {
446    /// Returns the text content if this is `Text`, or the JSON string if `Structured`.
447    ///
448    /// # Example
449    ///
450    /// ```
451    /// use clap_mcp::ClapMcpToolOutput;
452    ///
453    /// assert_eq!(ClapMcpToolOutput::Text("hi".into()).into_string(), "hi");
454    /// assert!(ClapMcpToolOutput::Structured(serde_json::json!({"a":1})).into_string().contains("a"));
455    /// ```
456    pub fn into_string(self) -> String {
457        match self {
458            ClapMcpToolOutput::Text(s) => s,
459            ClapMcpToolOutput::Structured(v) => {
460                serde_json::to_string(&v).unwrap_or_else(|_| v.to_string())
461            }
462        }
463    }
464
465    /// Returns `Some(&str)` for `Text`, `None` for `Structured`.
466    ///
467    /// # Example
468    ///
469    /// ```
470    /// use clap_mcp::ClapMcpToolOutput;
471    ///
472    /// assert_eq!(ClapMcpToolOutput::Text("hi".into()).as_text(), Some("hi"));
473    /// assert!(ClapMcpToolOutput::Structured(serde_json::json!(1)).as_text().is_none());
474    /// ```
475    pub fn as_text(&self) -> Option<&str> {
476        match self {
477            ClapMcpToolOutput::Text(s) => Some(s),
478            ClapMcpToolOutput::Structured(_) => None,
479        }
480    }
481
482    /// Returns `Some(&Value)` for `Structured`, `None` for `Text`.
483    ///
484    /// # Example
485    ///
486    /// ```
487    /// use clap_mcp::ClapMcpToolOutput;
488    ///
489    /// let v = serde_json::json!({"sum": 10});
490    /// assert_eq!(ClapMcpToolOutput::Structured(v.clone()).as_structured(), Some(&v));
491    /// assert!(ClapMcpToolOutput::Text("x".into()).as_structured().is_none());
492    /// ```
493    pub fn as_structured(&self) -> Option<&serde_json::Value> {
494        match self {
495            ClapMcpToolOutput::Text(_) => None,
496            ClapMcpToolOutput::Structured(v) => Some(v),
497        }
498    }
499}
500
501/// Produces MCP tool output (text or structured) for a parsed CLI value.
502///
503/// Implemented by the `#[derive(ClapMcp)]` macro. Used for in-process execution.
504///
505/// When using **`#[clap_mcp_output_from = "run"]`** on the enum (required), the macro
506/// implements this trait by calling `run(self)` and converting the result via [`IntoClapMcpResult`].
507/// CLI and MCP share a single implementation.
508pub trait ClapMcpToolExecutor {
509    fn execute_for_mcp(self) -> std::result::Result<ClapMcpToolOutput, ClapMcpToolError>;
510}
511
512/// In-process MCP tool execution with session state shared across `tools/call` invocations.
513///
514/// Implemented by `#[derive(ClapMcp)]` when using `#[clap_mcp_output_from_with_state = "run"]`.
515/// The MCP server stores state in an [`Arc`] for its lifetime and passes **`&Self::State`** on
516/// each tool call (not `&Arc<…>` — the shared pointer is an implementation detail).
517///
518/// Session state is shared for the **MCP server process lifetime**, not per MCP client or OS user.
519/// Stateful MCP is intended for localhost or a single trusted operator. Do not use it when
520/// multiple or untrusted callers can reach the server (for example Streamable HTTP beyond
521/// loopback). See [Security](https://github.com/canardleteer/clap-mcp/blob/main/docs/security.md)
522/// and [Stateful MCP tools](https://github.com/canardleteer/clap-mcp/blob/main/docs/stateful-tools.md).
523///
524/// # Setup
525///
526/// * Set [`ClapMcpConfig::reinvocation_safe`](ClapMcpConfig::reinvocation_safe) — subprocess mode
527///   cannot share in-process state.
528/// * On the **leaf** command enum, set `#[clap_mcp_output_from_with_state = "run"]` and
529///   `#[clap_mcp_state_type = "…"]`. The state type must match the second parameter of your
530///   `run` function (e.g. `run(cmd, state: &Mutex<CounterState>)` →
531///   `#[clap_mcp_state_type = "Mutex<CounterState>"]`).
532/// * On struct roots or intermediate subcommand enums, add `#[clap_mcp(stateful)]` and delegate;
533///   `State` is inferred from the subcommand field (no duplicate `state_type`).
534///
535/// # Example
536///
537/// ```rust,ignore
538/// use clap::{Parser, Subcommand};
539/// use clap_mcp::{ClapMcp, ParseOrServeMcpWithState};
540/// use std::sync::{Arc, Mutex};
541///
542/// #[derive(Default)]
543/// struct CounterState { count: u64 }
544///
545/// #[derive(Parser, ClapMcp)]
546/// #[clap_mcp(reinvocation_safe = true, stateful)]
547/// struct App {
548///     #[command(subcommand)]
549///     command: Command,
550/// }
551///
552/// #[derive(Subcommand, ClapMcp)]
553/// #[clap_mcp(reinvocation_safe = true)]
554/// #[clap_mcp_output_from_with_state = "run"]
555/// #[clap_mcp_state_type = "Mutex<CounterState>"]
556/// enum Command { Increment, Read }
557///
558/// fn run(cmd: Command, state: &Mutex<CounterState>) -> String { /* ... */ }
559///
560/// fn main() {
561///     let state = Arc::new(Mutex::new(CounterState::default()));
562///     let _ = App::parse_or_serve_mcp_with_state(state);
563/// }
564/// ```
565///
566/// See the `stateful_counter` example and
567/// [PR #11](https://github.com/canardleteer/clap-mcp/pull/11) (Eddy Stefes / fneddy) for the
568/// original motivation: counters, open handles, and in-memory caches across MCP tool calls
569/// without globals or manual handler wiring.
570pub trait ClapMcpToolExecutorWithState {
571    /// Session state type; must match the second parameter of your stateful `run` function.
572    type State: Send + Sync + 'static;
573
574    /// Run this parsed CLI value as an MCP tool, reading/updating shared `state`.
575    fn execute_for_mcp_with_state(
576        self,
577        state: &Self::State,
578    ) -> std::result::Result<ClapMcpToolOutput, ClapMcpToolError>;
579}
580
581impl<T: ClapMcpToolExecutor> ClapMcpRunnable for T {
582    fn run(self) -> String {
583        self.execute_for_mcp()
584            .unwrap_or_else(|e| ClapMcpToolOutput::Text(e.message))
585            .into_string()
586    }
587}
588
589/// Errors that can occur when running the MCP server.
590#[derive(Debug, thiserror::Error)]
591pub enum ClapMcpError {
592    #[error("invalid MCP configuration: {0}")]
593    InvalidConfig(String),
594    /// Returned when [`ServeMcpBuilder::serve`] or [`serve_mcp`] runs on a
595    /// `current_thread` runtime but [`ClapMcpConfig::needs_multi_thread_runtime`] is true.
596    #[error("multi-thread tokio runtime required: {reason}")]
597    RequiresMultiThreadRuntime { reason: String },
598    #[error("failed to serialize clap schema to JSON: {0}")]
599    SchemaJson(#[from] serde_json::Error),
600    #[error("MCP service error: {0}")]
601    Mcp(#[from] Box<rmcp::RmcpError>),
602    #[error("MCP service runtime error: {0}")]
603    Service(#[from] rmcp::ServiceError),
604    #[error("MCP join error: {0}")]
605    Join(#[from] tokio::task::JoinError),
606    #[error("I/O error during skill export: {0}")]
607    Io(#[from] std::io::Error),
608    #[error("tokio runtime context: {0}")]
609    RuntimeContext(String),
610    #[error("async tool thread panicked or failed: {0}")]
611    ToolThread(String),
612}
613
614/// Configuration for execution safety when exposing a CLI over MCP.
615///
616/// Use this to declare whether your CLI tool can be safely invoked multiple times,
617/// whether it can run in parallel with other tool calls, and how async tools run.
618///
619/// # Crash and panic behavior
620///
621/// - **Subprocess (`reinvocation_safe` = false):** If the tool process exits with a non-zero
622///   status, the server returns an MCP tool result with `is_error: true` and a message
623///   that includes the exit code (and stderr when non-empty).
624/// - **In-process (`reinvocation_safe` = true), `catch_in_process_panics` = false:** Any panic
625///   in tool code (including from [`run_async_tool`]) crashes the server.
626/// - **In-process, `catch_in_process_panics` = true:** Panics are caught and returned as an
627///   MCP error; the server stays up. After a caught panic, the process may no longer be
628///   reinvocation_safe (global state may be corrupted); consider restarting the server.
629///
630/// # Example
631///
632/// ```
633/// use clap_mcp::ClapMcpConfig;
634///
635/// // Default: subprocess per call, serialized
636/// let config = ClapMcpConfig::default();
637///
638/// // In-process, parallel-safe
639/// let config = ClapMcpConfig {
640///     reinvocation_safe: true,
641///     parallel_safe: true,
642///     ..Default::default()
643/// };
644/// ```
645#[derive(Debug, Clone)]
646pub struct ClapMcpConfig {
647    /// If true, the CLI can be invoked multiple times without tearing down the process.
648    /// When false (default), each tool call spawns a fresh subprocess.
649    /// When true, uses in-process execution (no subprocess).
650    pub reinvocation_safe: bool,
651
652    /// If true, tool calls may run concurrently. When false, calls are serialized.
653    /// Default is false (serialize by default) for safety.
654    pub parallel_safe: bool,
655
656    /// When `reinvocation_safe` is true, controls how async tool execution runs.
657    /// Only applies to in-process execution; ignored when `reinvocation_safe` is false.
658    ///
659    /// | Value | Behavior | When to use |
660    /// |-------|----------|-------------|
661    /// | `false` (default) | Dedicated thread with its own tokio runtime per tool call. No nesting, no special setup. | **Recommended.** Use unless you need deep integration. |
662    /// | `true` | Shares the MCP server's tokio runtime. Uses a multi-thread runtime so `block_on` can run async work. | Advanced: share runtime state, spawn long-lived tasks, or integrate with other async code. |
663    ///
664    /// Use with [`run_async_tool`] in `#[clap_mcp_output]` for async subcommands.
665    pub share_runtime: bool,
666
667    /// When true and `reinvocation_safe` is true, panics in tool code are caught and returned
668    /// as an MCP error (`is_error: true`) instead of crashing the server. Default is `false` (opt-in).
669    ///
670    /// **Warning:** After a caught panic, the process may no longer be reinvocation_safe: global
671    /// state (e.g. static or process-wide resources) could be left in an inconsistent state.
672    /// For reliability, restart the MCP server after a caught panic when using in-process execution.
673    pub catch_in_process_panics: bool,
674
675    /// When true (default), `myapp --mcp` (or `--mcp-http`) may start the MCP server without a
676    /// subcommand on the argv, by inspecting argv **before** clap runs. This preserves CLIs that
677    /// use `subcommand_required = true` — you do not need `Option<Commands>` for MCP.
678    ///
679    /// When false, `--mcp` alone goes through normal clap parsing; use `subcommand_required =
680    /// false` (typically with `Option<Commands>`) if clap must accept `--mcp` without a subcommand
681    /// token.
682    pub allow_mcp_without_subcommand: bool,
683
684    /// Long names for clap-mcp builtin global flags (`--mcp`, `--mcp-http`, `--export-skills`).
685    pub builtin_flags: ClapMcpBuiltinFlags,
686}
687
688impl Default for ClapMcpConfig {
689    fn default() -> Self {
690        Self {
691            reinvocation_safe: false,
692            parallel_safe: false,
693            share_runtime: false,
694            catch_in_process_panics: false,
695            allow_mcp_without_subcommand: true,
696            builtin_flags: ClapMcpBuiltinFlags::default(),
697        }
698    }
699}
700
701impl ClapMcpConfig {
702    /// Whether in-process MCP needs a multi-thread tokio runtime
703    /// (`share_runtime` or `parallel_safe` with `reinvocation_safe`).
704    ///
705    /// When true, async [`ServeMcpBuilder::serve`] on an existing runtime requires
706    /// `#[tokio::main(flavor = "multi_thread")]`. [`ServeMcpBuilder::serve_blocking`]
707    /// and [`serve_mcp_blocking`] create a suitable runtime internally.
708    pub fn needs_multi_thread_runtime(&self) -> bool {
709        self.reinvocation_safe && (self.share_runtime || self.parallel_safe)
710    }
711}
712
713pub(crate) fn build_mcp_blocking_runtime(
714    config: &ClapMcpConfig,
715) -> Result<tokio::runtime::Runtime, ClapMcpError> {
716    let rt = if config.needs_multi_thread_runtime() {
717        tokio::runtime::Builder::new_multi_thread()
718    } else {
719        tokio::runtime::Builder::new_current_thread()
720    }
721    .enable_all()
722    .build()?;
723    Ok(rt)
724}
725
726/// Derive-path and imperative MCP run options (execution config + serve behavior).
727#[derive(Debug)]
728pub struct ClapMcpRunOptions {
729    pub config: ClapMcpConfig,
730    pub serve: ClapMcpServeOptions,
731}
732
733impl ClapMcpRunOptions {
734    /// Build options with default [`ClapMcpServeOptions`].
735    pub fn from_config(config: ClapMcpConfig) -> Self {
736        Self {
737            config,
738            serve: ClapMcpServeOptions::default(),
739        }
740    }
741}
742
743impl From<ClapMcpConfig> for ClapMcpRunOptions {
744    fn from(config: ClapMcpConfig) -> Self {
745        Self::from_config(config)
746    }
747}
748
749/// MCP transport listen target for low-level embedders.
750///
751/// Use with [`ServeMcpBuilder`] (recommended), [`serve_mcp`], or [`serve_mcp_blocking`].
752#[derive(Debug, Clone, Copy)]
753pub enum McpListen {
754    /// Stdio MCP (default `--mcp` mode).
755    Stdio,
756    /// Streamable HTTP at the given socket address (`http` feature).
757    #[cfg(feature = "http")]
758    Http(std::net::SocketAddr),
759}
760
761/// Optional configuration for MCP serve behavior (logging, etc.).
762///
763/// Pass to [`parse_or_serve_mcp_with`], [`ServeMcpBuilder::serve_options`],
764/// or the lower-level [`serve_mcp`] / [`serve_mcp_blocking`] functions.
765/// When `log_rx` is set, enables the logging capability and forwards messages to the MCP client.
766///
767/// # Example
768///
769/// ```rust,ignore
770/// use clap_mcp::{ClapMcpServeOptions, logging::log_channel};
771///
772/// let (log_tx, log_rx) = log_channel(32);
773/// let mut opts = ClapMcpServeOptions::default();
774/// opts.log_rx = Some(log_rx);
775/// // Pass opts to parse_or_serve_mcp_with or ServeMcpBuilder::serve_options
776/// ```
777#[derive(Debug, Default)]
778pub struct ClapMcpServeOptions {
779    /// When set, log messages received on this channel are forwarded to the MCP client
780    /// via `notifications/message`. Enables the logging capability and instructions.
781    pub log_rx: Option<tokio::sync::mpsc::Receiver<logging::LoggingMessageNotificationParams>>,
782
783    /// When true and running in-process, capture stdout written during tool execution
784    /// and merge it with Text output. Only has effect when `reinvocation_safe` is true.
785    /// Unix only; **not available on Windows** (this field does not exist there; code
786    /// setting it will fail to compile on Windows).
787    #[cfg(unix)]
788    pub capture_stdout: bool,
789
790    /// Custom MCP resources (static or async dynamic). Merged with the built-in `clap://schema` resource.
791    pub custom_resources: Vec<content::CustomResource>,
792
793    /// Custom MCP prompts (static or async dynamic). Merged with the built-in logging guide when logging is enabled.
794    pub custom_prompts: Vec<content::CustomPrompt>,
795}
796
797/// Log interpretation hint for MCP clients (included in `instructions` when logging is enabled).
798///
799/// When changing logging behavior (logger names in `logging`, subprocess stderr handling below),
800/// update this and [`LOGGING_GUIDE_CONTENT`].
801pub const LOG_INTERPRETATION_INSTRUCTIONS: &str = r#"When this server emits log messages (notifications/message), the `logger` field indicates the source:
802- "stderr": Subprocess stderr (CLI tools run as subprocesses)
803- "app": In-process application logs
804- Other: Application-defined logger names"#;
805
806/// Name of the logging guide prompt.
807pub const PROMPT_LOGGING_GUIDE: &str = "clap-mcp-logging-guide";
808
809/// Full content for the logging guide prompt (returned when clients request `PROMPT_LOGGING_GUIDE`).
810///
811/// When changing logging behavior (logger names in `logging`, subprocess stderr handling below),
812/// update this and [`LOG_INTERPRETATION_INSTRUCTIONS`].
813pub const LOGGING_GUIDE_CONTENT: &str = r#"# clap-mcp Logging Guide
814
815When this server emits log messages (notifications/message), use the `logger` field to interpret the source:
816
817- **"stderr"**: Output from subprocess stderr (CLI tools run as subprocesses). The `meta` field may include `tool` for the command name.
818- **"app"**: In-process application logs.
819- **Other**: Application-defined logger names.
820
821The `level` field uses RFC 5424 syslog severity: debug, info, notice, warning, error, critical, alert, emergency.
822The `data` field contains the message (string or JSON object)."#;
823
824/// Metadata for filtering and adjusting the MCP schema.
825///
826/// Use with [`schema_from_command_with_metadata`] to exclude commands/args from MCP
827/// or to make optional args required in the MCP tool schema.
828///
829/// # Example (imperative)
830///
831/// ```rust
832/// use clap::Command;
833/// use clap_mcp::{schema_from_command_with_metadata, ClapMcpSchemaMetadata};
834///
835/// let mut metadata = ClapMcpSchemaMetadata::default();
836/// metadata.skip_commands.push("internal".into());
837/// metadata.skip_args.insert("mycmd".into(), vec!["verbose".into()]);
838/// metadata.requires_args.insert("mycmd".into(), vec!["path".into()]);
839///
840/// let cmd = Command::new("myapp").subcommand(Command::new("mycmd").arg(clap::Arg::new("path")));
841/// let schema = schema_from_command_with_metadata(&cmd, &metadata);
842/// ```
843#[derive(Debug, Clone, Default)]
844pub struct ClapMcpSchemaMetadata {
845    /// Command names to exclude from MCP exposure.
846    pub skip_commands: Vec<String>,
847    /// Per-command arg ids to exclude (command_name -> arg_ids).
848    pub skip_args: std::collections::HashMap<String, Vec<String>>,
849    /// Per-command arg ids to treat as required in MCP (command_name -> arg_ids).
850    pub requires_args: std::collections::HashMap<String, Vec<String>>,
851    /// When `true` and the root command has subcommands, the root is excluded from the
852    /// MCP tool list (only subcommands become tools). Use when the meaningful tools are
853    /// the leaf subcommands (e.g. explain, compare, sort) and the root is rarely invoked.
854    pub skip_root_command_when_subcommands: bool,
855    /// Subcommand tool names that may be invoked with MCP task-augmented `tools/call` when
856    /// [`ClapMcpSchemaMetadata::task_augmented_tools`] is enabled. Populated by `#[clap_mcp(task)]` on
857    /// enum variants. When **empty**, every tool is eligible for task augmentation (when enabled
858    /// in metadata). When **non-empty**, only listed tool names are eligible.
859    pub task_tool_names: Vec<String>,
860    /// When true, advertise MCP task support and handle task-augmented `tools/call`.
861    /// Set by `#[clap_mcp(task_augmented_tools)]` on the derive (requires `reinvocation_safe`).
862    pub task_augmented_tools: bool,
863    /// Optional JSON schema for tool output. When set (e.g. via `#[clap_mcp_output_type]` or
864    /// `#[clap_mcp_output_one_of]` with the `output-schema` feature), this schema is attached
865    /// to each tool's `output_schema` field.
866    pub output_schema: Option<serde_json::Value>,
867    /// Per-tool topical serialization when [`ClapMcpConfig::parallel_safe`] is true.
868    /// Populated by `#[clap_mcp(serialized)]` or `#[clap_mcp(serialized = "arg1, arg2")]` on
869    /// enum variants.
870    pub serialize_tools: std::collections::HashMap<String, ClapMcpSerializeScope>,
871    /// Optional per-arg topic key functions for arg-scoped serialization (tool name → arg id → fn).
872    /// Populated when a field has `#[clap_mcp(serialize_topic)]` and the variant uses arg-scoped
873    /// `#[clap_mcp(serialized = "...")]`. Requires in-process / derive wiring; subprocess-only
874    /// servers set this imperatively when needed.
875    pub serialize_topic_args: std::collections::HashMap<
876        String,
877        std::collections::HashMap<String, SerializeTopicSegmentFn>,
878    >,
879}
880
881impl ClapMcpSchemaMetadata {
882    /// Deep-merges `other` into `self`. Lists and per-command maps are extended; map
883    /// entries from `other` overwrite same keys in `serialize_tools` and
884    /// `serialize_topic_args`. Use when folding nested subcommand metadata into a parent
885    /// or when combining derive output with imperative overrides.
886    pub fn merge_from(&mut self, other: Self) {
887        self.skip_commands.extend(other.skip_commands);
888        for (k, v) in other.skip_args {
889            self.skip_args.entry(k).or_default().extend(v);
890        }
891        for (k, v) in other.requires_args {
892            self.requires_args.entry(k).or_default().extend(v);
893        }
894        self.task_tool_names.extend(other.task_tool_names);
895        self.task_augmented_tools = self.task_augmented_tools || other.task_augmented_tools;
896        self.skip_root_command_when_subcommands |= other.skip_root_command_when_subcommands;
897        for (k, v) in other.serialize_tools {
898            self.serialize_tools.insert(k, v);
899        }
900        for (tool, args) in other.serialize_topic_args {
901            let entry = self.serialize_topic_args.entry(tool).or_default();
902            for (arg, f) in args {
903                entry.insert(arg, f);
904            }
905        }
906        if other.output_schema.is_some() {
907            self.output_schema = other.output_schema;
908        }
909    }
910}
911
912/// Whether a flattened field contributes clap arg ids or subcommand names to MCP skip metadata.
913#[doc(hidden)]
914#[derive(Debug, Clone, Copy, PartialEq, Eq)]
915pub enum FlattenSkipKind {
916    /// `#[command(flatten)]` on a `clap::Args` type.
917    Args,
918    /// `#[command(flatten)]` on a `clap::Subcommand` type.
919    Subcommand,
920}
921
922fn collect_flatten_subcommand_names(cmd: &clap::Command, out: &mut Vec<String>) {
923    for sub in cmd.get_subcommands() {
924        out.push(sub.get_name().to_string());
925        collect_flatten_subcommand_names(sub, out);
926    }
927}
928
929/// Applies `#[clap_mcp(skip)]` on a flattened `clap::Args` field.
930///
931/// Used by `#[derive(ClapMcp)]`; prefer `#[clap_mcp(skip)]` on the field instead of calling directly.
932#[doc(hidden)]
933pub fn apply_flatten_args_field_skip<T: clap::Args>(
934    skip_commands: &mut Vec<String>,
935    skip_args: &mut std::collections::HashMap<String, Vec<String>>,
936    root_command: &str,
937    explicit: Option<&[String]>,
938    run_bare_probe: bool,
939) {
940    let _ = skip_commands;
941    if run_bare_probe {
942        let probe = T::augment_args(clap::Command::new("_clap_mcp_skip_probe"));
943        let collected: Vec<String> = probe
944            .get_arguments()
945            .map(|a| a.get_id().as_str().to_string())
946            .collect();
947        skip_args
948            .entry(root_command.to_string())
949            .or_default()
950            .extend(collected);
951    }
952    if let Some(ids) = explicit {
953        skip_args
954            .entry(root_command.to_string())
955            .or_default()
956            .extend(ids.iter().cloned());
957    }
958}
959
960/// Applies `#[clap_mcp(skip)]` on a flattened `clap::Subcommand` field.
961///
962/// Used by `#[derive(ClapMcp)]`; prefer `#[clap_mcp(skip)]` on the field instead of calling directly.
963#[doc(hidden)]
964pub fn apply_flatten_subcommand_field_skip<T: clap::Subcommand>(
965    skip_commands: &mut Vec<String>,
966    skip_args: &mut std::collections::HashMap<String, Vec<String>>,
967    root_command: &str,
968    explicit: Option<&[String]>,
969    run_bare_probe: bool,
970) {
971    let _ = (skip_args, root_command);
972    if run_bare_probe {
973        let probe = T::augment_subcommands(clap::Command::new("_clap_mcp_skip_probe"));
974        let mut names = Vec::new();
975        collect_flatten_subcommand_names(&probe, &mut names);
976        skip_commands.extend(names);
977    }
978    if let Some(ids) = explicit {
979        skip_commands.extend(ids.iter().cloned());
980    }
981}
982
983/// Serialize-topic bindings contributed by a flattened `clap::Args` helper type.
984///
985/// Implement via `#[derive(ClapMcp)]` with `#[clap_mcp(args_metadata)]` on the shared `Args` struct.
986pub trait ClapMcpFlattenArgsTopics {
987    /// Clap arg ids for args in this flattened group (from derive metadata collection).
988    const FIELD_IDS: &'static [&'static str];
989
990    /// Clap arg ids from nested flattened `Args` groups (see [`Self::FIELD_IDS`]).
991    const NESTED_FIELD_IDS: &'static [&'static str] = &[];
992
993    /// Registers `#[clap_mcp(serialize_topic)]` fields for `tool_name` on the parent variant.
994    fn merge_serialize_topics(
995        tool_name: &str,
996        target: &mut std::collections::HashMap<
997            String,
998            std::collections::HashMap<String, SerializeTopicSegmentFn>,
999        >,
1000    );
1001}
1002
1003const fn str_eq_const(a: &str, b: &str) -> bool {
1004    let a = a.as_bytes();
1005    let b = b.as_bytes();
1006    if a.len() != b.len() {
1007        return false;
1008    }
1009    let mut i = 0usize;
1010    while i < a.len() {
1011        if a[i] != b[i] {
1012            return false;
1013        }
1014        i += 1;
1015    }
1016    true
1017}
1018
1019/// Returns true when `arg` matches a field ident on flattened `Args` type `T`.
1020const fn ids_contain(ids: &[&str], arg: &str) -> bool {
1021    let mut i = 0usize;
1022    while i < ids.len() {
1023        if str_eq_const(ids[i], arg) {
1024            return true;
1025        }
1026        i += 1;
1027    }
1028    false
1029}
1030
1031/// Returns true when `arg` matches a clap arg id on flattened `Args` type `T` (including one nested flatten).
1032///
1033/// Used by `#[derive(ClapMcp)]`; prefer attributes over calling directly.
1034#[doc(hidden)]
1035pub const fn flatten_args_contains_field<T: ClapMcpFlattenArgsTopics>(arg: &str) -> bool {
1036    ids_contain(T::FIELD_IDS, arg) || ids_contain(T::NESTED_FIELD_IDS, arg)
1037}
1038
1039/// Compile-time check that `arg` appears on at least one flattened `Args` type in `checks`.
1040///
1041/// Used by `#[derive(ClapMcp)]`; prefer attributes over calling directly.
1042#[doc(hidden)]
1043pub const fn assert_serialized_in_any_flatten_args(arg: &str, checks: &[bool]) {
1044    let mut i = 0usize;
1045    while i < checks.len() {
1046        if checks[i] {
1047            return;
1048        }
1049        i += 1;
1050    }
1051    let _ = arg;
1052    panic!("serialized arg is not a field on any flattened Args type for this variant");
1053}
1054
1055/// Computes one arg's contribution to a topical lock key from MCP JSON.
1056pub type SerializeTopicSegmentFn = fn(value: &serde_json::Value) -> Option<String>;
1057
1058/// Optional typed topical lock segments for arg-scoped serialization.
1059///
1060/// Default arg-scoped serialization uses canonical MCP JSON (no `Hash` or `Eq` on your Rust types).
1061/// Implement this trait (or use [`impl_serialize_topic_hash_eq`] / [`impl_serialize_topic_serde_eq`])
1062/// and mark the field with `#[clap_mcp(serialize_topic)]` when parsed-type identity should drive
1063/// the lock topic.
1064///
1065/// Topical locks do not isolate session state ([`ClapMcpToolExecutorWithState`]). Derive metadata
1066/// uses the Rust **field ident** as the MCP arg id for `serialize_topic` and `serialized = "..."`
1067/// validation, not `#[arg(id = "...")]`. Match the field name to the clap id or set
1068/// [`ClapMcpSchemaMetadata::serialize_topic_args`] imperatively. See
1069/// [execution-safety](https://github.com/canardleteer/clap-mcp/blob/main/docs/execution-safety.md).
1070pub trait ClapMcpSerializeTopic {
1071    /// Returns a stable lock-key segment for this arg value, or `None` to fall back to canonical JSON
1072    /// for that arg (and then to the tool-wide topic if the arg is absent).
1073    fn serialize_topic_segment(value: &serde_json::Value) -> Option<String>;
1074}
1075
1076/// Uses [`Hash`] of the deserialized value for the topic segment (after JSON parse succeeds).
1077#[macro_export]
1078macro_rules! impl_serialize_topic_hash_eq {
1079    ($ty:ty) => {
1080        impl $crate::ClapMcpSerializeTopic for $ty {
1081            fn serialize_topic_segment(value: &serde_json::Value) -> Option<String> {
1082                use std::hash::{Hash, Hasher};
1083                let parsed: $ty = serde_json::from_value(value.clone()).ok()?;
1084                let mut hasher = std::collections::hash_map::DefaultHasher::new();
1085                parsed.hash(&mut hasher);
1086                Some(format!("h:{}", hasher.finish()))
1087            }
1088        }
1089    };
1090}
1091
1092/// Uses [`serde`] JSON of the deserialized value for the topic segment (semantic equality when
1093/// `Eq` matches serde's encoding).
1094#[macro_export]
1095macro_rules! impl_serialize_topic_serde_eq {
1096    ($ty:ty) => {
1097        impl $crate::ClapMcpSerializeTopic for $ty {
1098            fn serialize_topic_segment(value: &serde_json::Value) -> Option<String> {
1099                let parsed: $ty = serde_json::from_value(value.clone()).ok()?;
1100                serde_json::to_string(&parsed).ok()
1101            }
1102        }
1103    };
1104}
1105
1106impl_serialize_topic_serde_eq!(String);
1107impl_serialize_topic_serde_eq!(bool);
1108impl_serialize_topic_serde_eq!(i32);
1109impl_serialize_topic_serde_eq!(i64);
1110impl_serialize_topic_serde_eq!(u32);
1111impl_serialize_topic_serde_eq!(u64);
1112
1113impl<T> ClapMcpSerializeTopic for Option<T>
1114where
1115    T: ClapMcpSerializeTopic,
1116{
1117    fn serialize_topic_segment(value: &serde_json::Value) -> Option<String> {
1118        if value.is_null() {
1119            return None;
1120        }
1121        T::serialize_topic_segment(value)
1122    }
1123}
1124
1125#[derive(Debug, Clone, PartialEq, Eq)]
1126pub enum ClapMcpSerializeScope {
1127    /// All invocations of the tool share one lock topic.
1128    Tool,
1129    /// Lock topic includes canonical MCP JSON values for the listed arg ids.
1130    Args(Vec<String>),
1131}
1132
1133pub(crate) fn tool_task_eligible(tool_name: &str, metadata: &ClapMcpSchemaMetadata) -> bool {
1134    if !metadata.task_augmented_tools {
1135        return false;
1136    }
1137    if metadata.task_tool_names.is_empty() {
1138        true
1139    } else {
1140        metadata.task_tool_names.iter().any(|n| n == tool_name)
1141    }
1142}
1143
1144/// Builds a JSON schema for a single type. Used by the derive macro when `#[clap_mcp_output_type = "T"]` is set.
1145/// When the `output-schema` feature is enabled and `T: schemars::JsonSchema`, returns the schema; otherwise returns `None`.
1146#[cfg(feature = "output-schema")]
1147pub fn output_schema_for_type<T: schemars::JsonSchema>() -> Option<serde_json::Value> {
1148    serde_json::to_value(schemars::schema_for!(T)).ok()
1149}
1150
1151#[cfg(not(feature = "output-schema"))]
1152pub fn output_schema_for_type<T>() -> Option<serde_json::Value> {
1153    let _ = std::marker::PhantomData::<T>;
1154    None
1155}
1156
1157/// Builds a JSON schema with `oneOf` for the given types. Used by the derive macro when
1158/// `#[clap_mcp_output_one_of = "T1, T2, T3"]` is set. Requires the `output-schema` feature
1159/// and each type must implement `schemars::JsonSchema`.
1160#[macro_export]
1161macro_rules! output_schema_one_of {
1162    ($($T:ty),+ $(,)?) => {{
1163        #[cfg(feature = "output-schema")]
1164        {
1165            let mut one_of = vec![];
1166            $( one_of.push(serde_json::to_value(&schemars::schema_for!($T)).unwrap()); )+
1167            Some(serde_json::json!({ "oneOf": one_of }))
1168        }
1169        #[cfg(not(feature = "output-schema"))]
1170        {
1171            None::<serde_json::Value>
1172        }
1173    }};
1174}
1175
1176/// Serializable schema extracted from a clap `Command`.
1177/// Used to build MCP tools and invoke the CLI.
1178#[derive(Debug, Clone, Serialize, Deserialize)]
1179pub struct ClapSchema {
1180    pub root: ClapCommand,
1181}
1182
1183/// One clap [`ArgGroup`](clap::ArgGroup) visible on a single command node at schema extraction.
1184///
1185/// Populated from `Command::get_groups()` and emitted on MCP tools as `meta.clapMcp.argGroups`
1186/// (plus an optional parse-time sentence on the tool `description`). Hints are **advisory**:
1187/// clap argv parse remains authoritative and invalid combinations still fail at parse time.
1188///
1189/// # Limitations
1190///
1191/// * **Not JSON Schema** — does not add `oneOf` / `anyOf` to `inputSchema`; do not treat
1192///   `argGroups` as machine-enforced constraints.
1193/// * **Per command node** — `args` lists MCP-visible arg ids on this command node only.
1194///   Parent or sibling subcommand groups are not merged into leaf tools.
1195/// * **Visibility** — `args` uses the same MCP visibility filter as schema/`inputSchema`
1196///   (builtins and `skip_args`; hidden args follow whatever that filter does today).
1197/// * **Sub-two-member groups** — groups with fewer than two visible members are omitted.
1198///
1199/// # Semantics
1200///
1201/// `required` and `multiple` mirror clap `ArgGroup` flags at schema extraction time.
1202/// For `required: true`, agents should supply one member; for optional groups, at most one
1203/// unless `multiple` is true.
1204#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1205pub struct ClapArgGroup {
1206    /// clap ArgGroup id (`ArgGroup::get_id()`).
1207    pub id: String,
1208    /// MCP-visible arg ids on this command node (after skip/builtin filtering).
1209    pub args: Vec<String>,
1210    /// Whether the group is required at parse time (`ArgGroup::is_required_set()`).
1211    pub required: bool,
1212    /// Whether multiple members may be set (`ArgGroup::is_multiple()`).
1213    pub multiple: bool,
1214}
1215
1216/// A command or subcommand in the schema.
1217#[derive(Debug, Clone, Serialize, Deserialize)]
1218pub struct ClapCommand {
1219    pub name: String,
1220    pub about: Option<String>,
1221    pub long_about: Option<String>,
1222    pub version: Option<String>,
1223    pub args: Vec<ClapArg>,
1224    /// clap ArgGroups on this command node (omitted from JSON when empty).
1225    ///
1226    /// Each nested MCP tool carries only groups attached to its own command node.
1227    /// Skipped commands (`ClapMcpSchemaMetadata::skip_commands`) never produce tools,
1228    /// so their groups are not exported.
1229    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1230    pub arg_groups: Vec<ClapArgGroup>,
1231    pub subcommands: Vec<ClapCommand>,
1232}
1233
1234impl ClapCommand {
1235    /// Returns this command and all subcommands in depth-first order.
1236    pub fn all_commands(&self) -> Vec<&ClapCommand> {
1237        let mut out = Vec::new();
1238        fn walk<'a>(cmd: &'a ClapCommand, acc: &mut Vec<&'a ClapCommand>) {
1239            acc.push(cmd);
1240            for sub in &cmd.subcommands {
1241                walk(sub, acc);
1242            }
1243        }
1244        walk(self, &mut out);
1245        out
1246    }
1247}
1248
1249/// Arg prefix before the first standalone `--` (clap end-of-options). Passthrough tokens after `--` are excluded.
1250pub fn argv_before_end_of_opts(args: &[String]) -> &[String] {
1251    match args.iter().position(|a| a == "--") {
1252        Some(i) => &args[..i],
1253        None => args,
1254    }
1255}
1256
1257/// Returns true when argv (before `--`) contains a clap-mcp builtin entry flag:
1258/// stdio MCP (`--mcp`), export-skills, or HTTP MCP when the `http` feature is enabled.
1259///
1260/// Used by [`parse_or_serve_mcp_preserve_cli_with`] and
1261/// [`get_matches_preserve_cli_or_serve_mcp_with_config_and_metadata`] to decide
1262/// whether to use clap-mcp's augmented parse path or the application's native
1263/// clap parse path.
1264pub fn argv_contains_clap_mcp_flags(args: &[String], flags: &ClapMcpBuiltinFlags) -> bool {
1265    let prefix = argv_before_end_of_opts(args);
1266    if argv_has_long_flag(prefix, flags.stdio_long) {
1267        return true;
1268    }
1269    if argv_export_skills_dir_from_args(args, flags).is_some() {
1270        return true;
1271    }
1272    #[cfg(feature = "http")]
1273    if argv_has_long_flag(prefix, flags.http_long) {
1274        return true;
1275    }
1276    false
1277}
1278
1279fn argv_has_long_flag(prefix: &[String], long: &str) -> bool {
1280    let flag = format!("--{long}");
1281    let flag_eq_prefix = format!("--{long}=");
1282    prefix
1283        .iter()
1284        .any(|a| a.as_str() == flag || a.starts_with(&flag_eq_prefix))
1285}
1286
1287fn command_has_arg_id_or_long(cmd: &Command, id: &str, long: &str) -> bool {
1288    cmd.get_arguments().any(|a| {
1289        a.get_id() == id
1290            || a.get_id() == CLAP_MCP_STDIO_FLAG_ID_LEGACY && long == MCP_FLAG_LONG
1291            || a.get_long().is_some_and(|l| l == long)
1292    })
1293}
1294
1295pub(crate) fn matches_stdio_flag(matches: &clap::ArgMatches, _flags: &ClapMcpBuiltinFlags) -> bool {
1296    matches.get_flag(CLAP_MCP_STDIO_FLAG_ID)
1297}
1298
1299#[cfg(feature = "http")]
1300pub(crate) fn matches_http_flag(matches: &clap::ArgMatches, flags: &ClapMcpBuiltinFlags) -> bool {
1301    matches.contains_id(CLAP_MCP_HTTP_FLAG_ID)
1302        || (flags.http_long == MCP_HTTP_FLAG_LONG && matches.contains_id(MCP_HTTP_FLAG_LONG))
1303}
1304
1305/// Arg IDs omitted from MCP tool arguments (built-in / clap-mcp global flags).
1306pub(crate) fn is_builtin_arg(id: &str) -> bool {
1307    is_clap_mcp_builtin_arg_id(id)
1308}
1309
1310pub(crate) fn is_clap_mcp_builtin_arg_id(id: &str) -> bool {
1311    matches!(
1312        id,
1313        "help"
1314            | "version"
1315            | CLAP_MCP_STDIO_FLAG_ID
1316            | CLAP_MCP_EXPORT_SKILLS_FLAG_ID
1317            | EXPORT_SKILLS_FLAG_LONG
1318    ) || {
1319        #[cfg(feature = "http")]
1320        {
1321            id == CLAP_MCP_HTTP_FLAG_ID || id == MCP_HTTP_FLAG_LONG
1322        }
1323        #[cfg(not(feature = "http"))]
1324        {
1325            let _ = id;
1326            false
1327        }
1328    }
1329}
1330
1331fn is_omitted_schema_clap_arg(arg: &clap::Arg) -> bool {
1332    let id = arg.get_id().as_str();
1333    is_clap_mcp_builtin_arg_id(id)
1334}
1335
1336/// MCP-visible arg ids on one clap command node (schema extraction and ArgGroup membership).
1337///
1338/// Single source of truth for which arg ids appear in both `ClapCommand::args` and
1339/// `ClapArgGroup::args` on this node, and therefore in leaf `inputSchema` for args
1340/// defined on that node (globals from ancestors are separate).
1341fn mcp_visible_arg_ids_on_command(
1342    cmd: &Command,
1343    metadata: &ClapMcpSchemaMetadata,
1344) -> std::collections::HashSet<String> {
1345    let cmd_name = cmd.get_name().to_string();
1346    let skip_args: std::collections::HashSet<_> = metadata
1347        .skip_args
1348        .get(&cmd_name)
1349        .map(|v| v.iter().cloned().collect())
1350        .unwrap_or_default();
1351
1352    cmd.get_arguments()
1353        .filter(|a| !is_omitted_schema_clap_arg(a))
1354        .map(|a| a.get_id().to_string())
1355        .filter(|id| !skip_args.contains(id))
1356        .collect()
1357}
1358
1359fn extract_arg_groups(cmd: &Command, metadata: &ClapMcpSchemaMetadata) -> Vec<ClapArgGroup> {
1360    let visible = mcp_visible_arg_ids_on_command(cmd, metadata);
1361    let mut built = cmd.clone();
1362    built.build();
1363    let mut groups = Vec::new();
1364    for group in built.get_groups() {
1365        let mut args: Vec<String> = group
1366            .get_args()
1367            .map(|id| id.to_string())
1368            .filter(|id| visible.contains(id))
1369            .collect();
1370        args.sort();
1371        if args.len() < 2 {
1372            continue;
1373        }
1374        let required = group.is_required_set();
1375        let multiple = {
1376            let mut g = group.clone();
1377            g.is_multiple()
1378        };
1379        groups.push(ClapArgGroup {
1380            id: group.get_id().to_string(),
1381            args,
1382            required,
1383            multiple,
1384        });
1385    }
1386    groups.sort_by(|a, b| a.id.cmp(&b.id));
1387    groups
1388}
1389
1390fn format_arg_groups_description_suffix(groups: &[ClapArgGroup]) -> Option<String> {
1391    if groups.is_empty() {
1392        return None;
1393    }
1394    let parts: Vec<String> = groups
1395        .iter()
1396        .map(|g| {
1397            let args_list = g
1398                .args
1399                .iter()
1400                .map(|a| format!("`{a}`"))
1401                .collect::<Vec<_>>()
1402                .join(", ");
1403            let constraint = if g.required {
1404                "requires one of"
1405            } else {
1406                "at most one of"
1407            };
1408            let mut s = format!("`{}` {constraint}: {args_list}", g.id);
1409            if g.multiple {
1410                s.push_str(" (multiple allowed)");
1411            }
1412            s
1413        })
1414        .collect();
1415    Some(format!("Arg groups (parse-time): {}.", parts.join("; ")))
1416}
1417
1418/// Builds MCP tools from a clap schema with execution config and metadata.
1419///
1420/// One tool per command (root + every subcommand). Tools include `meta.clapMcp` with
1421/// `reinvocationSafe`, `parallelSafe`, optional `taskAugmented`, optional topical
1422/// serialization hints, and optional `argGroups` when clap ArgGroups are present on
1423/// the tool's command node. Tool `description` may include a parse-time ArgGroup suffix
1424/// when groups exist.
1425pub fn tools_from_schema_with_metadata(
1426    schema: &ClapSchema,
1427    config: &ClapMcpConfig,
1428    metadata: &ClapMcpSchemaMetadata,
1429) -> Vec<Tool> {
1430    let commands: Vec<&ClapCommand> =
1431        if metadata.skip_root_command_when_subcommands && !schema.root.subcommands.is_empty() {
1432            schema
1433                .root
1434                .subcommands
1435                .iter()
1436                .flat_map(|c| c.all_commands())
1437                .collect()
1438        } else {
1439            schema.root.all_commands()
1440        };
1441    commands
1442        .into_iter()
1443        .map(|cmd| {
1444            command_to_tool_with_config(
1445                schema,
1446                cmd,
1447                config,
1448                metadata,
1449                metadata.output_schema.as_ref(),
1450            )
1451        })
1452        .collect()
1453}
1454
1455/// Args exposed for an MCP tool: leaf command args plus ancestor `#[arg(global)]` args.
1456fn effective_args_for_tool(schema: &ClapSchema, command_name: &str) -> Vec<ClapArg> {
1457    let Some(path) = command_path(schema, command_name) else {
1458        return Vec::new();
1459    };
1460    let mut by_id: BTreeMap<String, ClapArg> = BTreeMap::new();
1461    for depth in 0..path.len() {
1462        let subpath = &path[..=depth];
1463        let Some(cmd) = command_at_path(&schema.root, subpath) else {
1464            continue;
1465        };
1466        let is_leaf = depth + 1 == path.len();
1467        for arg in &cmd.args {
1468            if is_builtin_arg(arg.id.as_str()) {
1469                continue;
1470            }
1471            if is_leaf || arg.global {
1472                by_id.insert(arg.id.clone(), arg.clone());
1473            }
1474        }
1475    }
1476    by_id.into_values().collect()
1477}
1478
1479fn command_at_path<'a>(root: &'a ClapCommand, path: &[String]) -> Option<&'a ClapCommand> {
1480    if path.is_empty() || root.name != path[0] {
1481        return None;
1482    }
1483    let mut current = root;
1484    for segment in path.iter().skip(1) {
1485        current = current.subcommands.iter().find(|c| c.name == *segment)?;
1486    }
1487    Some(current)
1488}
1489
1490fn command_to_tool_with_config(
1491    schema: &ClapSchema,
1492    cmd: &ClapCommand,
1493    config: &ClapMcpConfig,
1494    metadata: &ClapMcpSchemaMetadata,
1495    output_schema: Option<&serde_json::Value>,
1496) -> Tool {
1497    let effective_args = effective_args_for_tool(schema, &cmd.name);
1498
1499    let mut properties: BTreeMap<String, serde_json::Map<String, serde_json::Value>> =
1500        BTreeMap::new();
1501    for arg in &effective_args {
1502        let mut prop = serde_json::Map::new();
1503        let (json_type, items) = mcp_type_for_arg(arg);
1504        prop.insert("type".to_string(), json_type);
1505        if let Some(items) = items {
1506            prop.insert("items".to_string(), items);
1507        }
1508        let desc = arg
1509            .long_help
1510            .as_deref()
1511            .or(arg.help.as_deref())
1512            .map(String::from);
1513        let mut desc = desc.unwrap_or_default();
1514        if let Some(hint) = mcp_action_description_hint(arg) {
1515            desc.push_str(&hint);
1516        }
1517        if !desc.is_empty() {
1518            prop.insert("description".to_string(), serde_json::Value::String(desc));
1519        }
1520        properties.insert(arg.id.clone(), prop);
1521    }
1522
1523    let required: Vec<String> = effective_args
1524        .iter()
1525        .filter(|a| a.required)
1526        .map(|a| a.id.clone())
1527        .collect();
1528
1529    let mut input_schema = serde_json::Map::new();
1530    input_schema.insert("type".into(), serde_json::json!("object"));
1531    input_schema.insert(
1532        "properties".into(),
1533        serde_json::Value::Object(
1534            properties
1535                .into_iter()
1536                .map(|(k, v)| (k, serde_json::Value::Object(v)))
1537                .collect(),
1538        ),
1539    );
1540    if !required.is_empty() {
1541        input_schema.insert(
1542            "required".into(),
1543            serde_json::Value::Array(
1544                required
1545                    .into_iter()
1546                    .map(serde_json::Value::String)
1547                    .collect(),
1548            ),
1549        );
1550    }
1551
1552    let mut description = cmd
1553        .long_about
1554        .as_deref()
1555        .or(cmd.about.as_deref())
1556        .map(String::from);
1557    if let Some(suffix) = format_arg_groups_description_suffix(&cmd.arg_groups) {
1558        description = Some(match description {
1559            Some(mut d) => {
1560                if !d.is_empty() {
1561                    d.push(' ');
1562                }
1563                d.push_str(&suffix);
1564                d
1565            }
1566            None => suffix,
1567        });
1568    }
1569    let title = cmd.about.as_ref().map(String::from);
1570
1571    let meta = {
1572        let mut clap_mcp = serde_json::Map::new();
1573        clap_mcp.insert(
1574            "reinvocationSafe".into(),
1575            serde_json::Value::Bool(config.reinvocation_safe),
1576        );
1577        clap_mcp.insert(
1578            "parallelSafe".into(),
1579            serde_json::Value::Bool(config.parallel_safe),
1580        );
1581        clap_mcp.insert(
1582            "shareRuntime".into(),
1583            serde_json::Value::Bool(config.share_runtime),
1584        );
1585        if tool_task_eligible(&cmd.name, metadata) {
1586            clap_mcp.insert("taskAugmented".into(), serde_json::Value::Bool(true));
1587        }
1588        if let Some(scope) = metadata.serialize_tools.get(&cmd.name) {
1589            clap_mcp.insert("serialized".into(), serde_json::Value::Bool(true));
1590            match scope {
1591                ClapMcpSerializeScope::Tool => {
1592                    clap_mcp.insert(
1593                        "serializeScope".into(),
1594                        serde_json::Value::String("tool".into()),
1595                    );
1596                }
1597                ClapMcpSerializeScope::Args(arg_ids) => {
1598                    clap_mcp.insert(
1599                        "serializeScope".into(),
1600                        serde_json::Value::String("args".into()),
1601                    );
1602                    clap_mcp.insert(
1603                        "serializeArgs".into(),
1604                        serde_json::Value::Array(
1605                            arg_ids
1606                                .iter()
1607                                .cloned()
1608                                .map(serde_json::Value::String)
1609                                .collect(),
1610                        ),
1611                    );
1612                    if let Some(topic_args) = metadata.serialize_topic_args.get(&cmd.name) {
1613                        let ids: Vec<_> = topic_args.keys().cloned().collect();
1614                        if !ids.is_empty() {
1615                            clap_mcp.insert(
1616                                "serializeTopicArgs".into(),
1617                                serde_json::Value::Array(
1618                                    ids.into_iter().map(serde_json::Value::String).collect(),
1619                                ),
1620                            );
1621                        }
1622                    }
1623                }
1624            }
1625        }
1626        if !cmd.arg_groups.is_empty()
1627            && let Ok(value) = serde_json::to_value(&cmd.arg_groups)
1628        {
1629            clap_mcp.insert("argGroups".into(), value);
1630        }
1631        let mut m = Meta::default();
1632        m.0.insert("clapMcp".into(), serde_json::Value::Object(clap_mcp));
1633        Some(m)
1634    };
1635
1636    let execution = if tool_task_eligible(&cmd.name, metadata) {
1637        Some(ToolExecution::from_raw(Some(TaskSupport::Optional)))
1638    } else {
1639        None
1640    };
1641
1642    let mut tool = Tool::new_with_raw(
1643        cmd.name.clone(),
1644        description.map(|d| d.into()),
1645        Arc::new(input_schema),
1646    );
1647    if let Some(title) = title {
1648        tool = tool.with_title(title);
1649    }
1650    if let Some(meta) = meta {
1651        tool = tool.with_meta(meta);
1652    }
1653    if let Some(execution) = execution {
1654        tool = tool.with_execution(execution);
1655    }
1656    if let Some(output_schema) = output_schema.cloned().and_then(|v| v.as_object().cloned()) {
1657        tool = tool.with_raw_output_schema(Arc::new(output_schema));
1658    }
1659    tool
1660}
1661
1662/// Serializable representation of a clap argument.
1663#[derive(Debug, Clone, Serialize, Deserialize)]
1664pub struct ClapArg {
1665    pub id: String,
1666    pub long: Option<String>,
1667    pub short: Option<char>,
1668    pub help: Option<String>,
1669    pub long_help: Option<String>,
1670    pub required: bool,
1671    pub global: bool,
1672    pub index: Option<usize>,
1673    pub action: Option<String>,
1674    pub value_names: Vec<String>,
1675    pub num_args: Option<String>,
1676}
1677
1678/// Returns the MCP input schema type for an argument based on its action (and num_args).
1679/// - SetTrue / SetFalse: boolean
1680/// - Count: integer
1681/// - Append (or multi-value num_args): array of strings
1682/// - Set / default: string
1683///
1684/// When the arg has a single value_name (e.g. VERSION), the array items schema gets a description
1685/// so clients know what each element represents.
1686fn mcp_type_for_arg(arg: &ClapArg) -> (serde_json::Value, Option<serde_json::Value>) {
1687    let action = arg.action.as_deref().unwrap_or("Set");
1688    let is_multi = matches!(action, "Append")
1689        || arg
1690            .num_args
1691            .as_deref()
1692            .is_some_and(|n| n.contains("..") && !n.contains("=1"));
1693    let (json_type, items) = if matches!(action, "SetTrue" | "SetFalse") {
1694        (serde_json::json!("boolean"), None)
1695    } else if action == "Count" {
1696        (serde_json::json!("integer"), None)
1697    } else if is_multi {
1698        let item_desc = arg
1699            .value_names
1700            .first()
1701            .map(|name| format!("A {} value", name));
1702        let items_schema = match item_desc {
1703            Some(desc) => serde_json::json!({ "type": "string", "description": desc }),
1704            None => serde_json::json!({ "type": "string" }),
1705        };
1706        (serde_json::json!("array"), Some(items_schema))
1707    } else {
1708        (serde_json::json!("string"), None)
1709    };
1710    (json_type, items)
1711}
1712
1713/// Optional description suffix so MCP clients know what to pass for flags/count/list.
1714fn mcp_action_description_hint(arg: &ClapArg) -> Option<String> {
1715    let action = arg.action.as_deref()?;
1716    let hint: String = match action {
1717        "SetTrue" => " Boolean flag: set to true to pass this flag.".into(),
1718        "SetFalse" => " Boolean flag: set to false to pass this flag (e.g. --no-xxx).".into(),
1719        "Count" => " Number of times the flag is passed (e.g. -vvv).".into(),
1720        "Append" => {
1721            if let Some(name) = arg.value_names.first() {
1722                format!(
1723                    " List of {} values; pass a JSON array (e.g. [\"a\", \"b\"]).",
1724                    name
1725                )
1726            } else {
1727                " List of values; pass a JSON array (e.g. [\"a\", \"b\"]).".into()
1728            }
1729        }
1730        _ => return None,
1731    };
1732    Some(hint)
1733}
1734
1735/// Adds a root-level `--mcp` flag to a `clap::Command` (imperative clap usage).
1736///
1737/// When present, the CLI should start an MCP server instead of normal execution.
1738/// If an arg with `--mcp` already exists, this is a no-op.
1739///
1740/// # Example
1741///
1742/// ```rust
1743/// use clap::Command;
1744/// use clap_mcp::command_with_mcp_flag;
1745///
1746/// let cmd = Command::new("myapp");
1747/// let cmd = command_with_mcp_flag(cmd);
1748/// assert!(cmd.get_arguments().any(|a| a.get_long() == Some("mcp")));
1749/// ```
1750pub fn command_with_mcp_flag(cmd: Command) -> Command {
1751    command_with_mcp_flag_with_flags(cmd, &ClapMcpBuiltinFlags::default())
1752}
1753
1754/// Like [`command_with_mcp_flag`] but uses `flags.stdio_long` for the user-facing long name.
1755pub fn command_with_mcp_flag_with_flags(mut cmd: Command, flags: &ClapMcpBuiltinFlags) -> Command {
1756    if command_has_arg_id_or_long(&cmd, CLAP_MCP_STDIO_FLAG_ID, flags.stdio_long) {
1757        return cmd;
1758    }
1759
1760    cmd = cmd.arg(
1761        Arg::new(CLAP_MCP_STDIO_FLAG_ID)
1762            .long(flags.stdio_long)
1763            .help("Run an MCP server over stdio that exposes this CLI's clap schema")
1764            .action(ArgAction::SetTrue)
1765            .global(true),
1766    );
1767
1768    cmd
1769}
1770
1771/// Adds a root-level `--export-skills` flag (optional value for output directory) to a `clap::Command`.
1772///
1773/// When present, the CLI should generate [Agent Skills](https://agentskills.io/specification)
1774/// (SKILL.md) and exit. If an arg with `--export-skills` already exists, this is a no-op.
1775///
1776/// # Example
1777///
1778/// ```rust
1779/// use clap::Command;
1780/// use clap_mcp::command_with_export_skills_flag;
1781///
1782/// let cmd = Command::new("myapp");
1783/// let cmd = command_with_export_skills_flag(cmd);
1784/// ```
1785pub fn command_with_export_skills_flag(cmd: Command) -> Command {
1786    command_with_export_skills_flag_with_flags(cmd, &ClapMcpBuiltinFlags::default())
1787}
1788
1789/// Like [`command_with_export_skills_flag`] but uses `flags.export_skills_long`.
1790pub fn command_with_export_skills_flag_with_flags(
1791    mut cmd: Command,
1792    flags: &ClapMcpBuiltinFlags,
1793) -> Command {
1794    if command_has_arg_id_or_long(
1795        &cmd,
1796        CLAP_MCP_EXPORT_SKILLS_FLAG_ID,
1797        flags.export_skills_long,
1798    ) {
1799        return cmd;
1800    }
1801
1802    cmd = cmd.arg(
1803        Arg::new(CLAP_MCP_EXPORT_SKILLS_FLAG_ID)
1804            .long(flags.export_skills_long)
1805            .value_name("DIR")
1806            .help("Generate Agent Skills (SKILL.md) from tools, resources, and prompts, then exit")
1807            .action(ArgAction::Set)
1808            .required(false)
1809            .global(true),
1810    );
1811
1812    cmd
1813}
1814
1815/// Adds both `--mcp` and `--export-skills` flags to the command.
1816/// Use this so schema extraction omits both; check for export-skills before mcp in the parse flow.
1817pub fn command_with_mcp_and_export_skills_flags(cmd: Command) -> Command {
1818    command_with_mcp_and_export_skills_flags_with_flags(cmd, &ClapMcpBuiltinFlags::default())
1819}
1820
1821/// Like [`command_with_mcp_and_export_skills_flags`] with custom builtin long names.
1822pub fn command_with_mcp_and_export_skills_flags_with_flags(
1823    mut cmd: Command,
1824    flags: &ClapMcpBuiltinFlags,
1825) -> Command {
1826    cmd = command_with_mcp_flag_with_flags(cmd, flags);
1827    #[cfg(feature = "http")]
1828    {
1829        cmd = command_with_mcp_http_flag_with_flags(cmd, flags);
1830    }
1831    command_with_export_skills_flag_with_flags(cmd, flags)
1832}
1833
1834#[cfg(feature = "http")]
1835pub fn command_with_mcp_http_flag(cmd: Command) -> Command {
1836    command_with_mcp_http_flag_with_flags(cmd, &ClapMcpBuiltinFlags::default())
1837}
1838
1839#[cfg(feature = "http")]
1840pub fn command_with_mcp_http_flag_with_flags(
1841    mut cmd: Command,
1842    flags: &ClapMcpBuiltinFlags,
1843) -> Command {
1844    if command_has_arg_id_or_long(&cmd, CLAP_MCP_HTTP_FLAG_ID, flags.http_long) {
1845        return cmd;
1846    }
1847
1848    cmd = cmd.arg(
1849        Arg::new(CLAP_MCP_HTTP_FLAG_ID)
1850            .long(flags.http_long)
1851            .value_name("ADDR")
1852            .help("Run an MCP server over Streamable HTTP at ADDR (e.g. 127.0.0.1:8080)")
1853            .global(true),
1854    );
1855    cmd
1856}
1857
1858#[cfg(feature = "http")]
1859pub(crate) fn mcp_http_listen_from_env() -> Option<String> {
1860    if let Ok(listen) = std::env::var(MCP_HTTP_LISTEN_ENV)
1861        && !listen.is_empty()
1862    {
1863        return Some(listen);
1864    }
1865    match (
1866        std::env::var(MCP_HTTP_BIND_ENV).ok(),
1867        std::env::var(MCP_HTTP_PORT_ENV).ok(),
1868    ) {
1869        (Some(bind), Some(port)) if !bind.is_empty() && !port.is_empty() => {
1870            Some(format!("{bind}:{port}"))
1871        }
1872        _ => None,
1873    }
1874}
1875
1876#[cfg(feature = "http")]
1877pub(crate) fn argv_mcp_http_listen_from_args(
1878    args: &[String],
1879    flags: &ClapMcpBuiltinFlags,
1880) -> Option<String> {
1881    let prefix = argv_before_end_of_opts(args);
1882    let http_flag = format!("--{}", flags.http_long);
1883    let http_prefix = format!("--{}=", flags.http_long);
1884    for (i, arg) in prefix.iter().enumerate() {
1885        if arg == &http_flag {
1886            if let Some(val) = prefix.get(i + 1).filter(|s| !s.starts_with('-')) {
1887                return Some(val.clone());
1888            }
1889            return mcp_http_listen_from_env();
1890        }
1891        if let Some(addr) = arg.strip_prefix(&http_prefix) {
1892            if addr.is_empty() {
1893                return mcp_http_listen_from_env();
1894            }
1895            return Some(addr.to_string());
1896        }
1897    }
1898    mcp_http_listen_from_env()
1899}
1900
1901#[cfg(feature = "http")]
1902fn parse_mcp_http_listen(raw: &str) -> Result<std::net::SocketAddr, ClapMcpError> {
1903    raw.parse().map_err(|_| {
1904        ClapMcpError::InvalidConfig(format!(
1905            "invalid MCP HTTP listen address `{raw}` (expected host:port)"
1906        ))
1907    })
1908}
1909
1910#[cfg(feature = "http")]
1911fn mcp_http_listen_error_message(flags: &ClapMcpBuiltinFlags) -> String {
1912    format!(
1913        "`--{}` requires HOST:PORT, or set {MCP_HTTP_LISTEN_ENV}, or {MCP_HTTP_BIND_ENV} + {MCP_HTTP_PORT_ENV}",
1914        flags.http_long
1915    )
1916}
1917
1918#[cfg(feature = "http")]
1919fn resolve_mcp_http_listen_from_args(
1920    args: &[String],
1921    flags: &ClapMcpBuiltinFlags,
1922) -> Result<Option<std::net::SocketAddr>, ClapMcpError> {
1923    let prefix = argv_before_end_of_opts(args);
1924    let http_flag = format!("--{}", flags.http_long);
1925    let http_prefix = format!("--{}=", flags.http_long);
1926    let wants_http = prefix
1927        .iter()
1928        .any(|a| a == &http_flag || a.starts_with(&http_prefix));
1929    if !wants_http {
1930        return Ok(None);
1931    }
1932    match argv_mcp_http_listen_from_args(args, flags) {
1933        Some(raw) if !raw.is_empty() => parse_mcp_http_listen(&raw).map(Some),
1934        _ => Err(ClapMcpError::InvalidConfig(mcp_http_listen_error_message(
1935            flags,
1936        ))),
1937    }
1938}
1939
1940/// Async MCP server for embedders: stdio or Streamable HTTP (`http` feature).
1941///
1942/// Runs on the **caller's tokio runtime**. Prefer [`ServeMcpBuilder`] from
1943/// `#[tokio::main]`; this function is the lower-level equivalent.
1944/// Use [`serve_mcp_blocking`] when `main` is synchronous.
1945///
1946/// When [`ClapMcpConfig::needs_multi_thread_runtime`] is true, the caller must use a
1947/// multi-thread runtime (e.g. `#[tokio::main(flavor = "multi_thread")]`).
1948///
1949/// # Example
1950///
1951/// ```rust,ignore
1952/// use clap_mcp::{ServeMcpBuilder, McpListen, ClapMcpServeOptions};
1953///
1954/// #[tokio::main(flavor = "multi_thread")]
1955/// async fn main() -> Result<(), clap_mcp::ClapMcpError> {
1956///     ServeMcpBuilder::for_cli::<Cli>(McpListen::Stdio)
1957///         .serve_options(ClapMcpServeOptions::default())
1958///         .serve()
1959///         .await
1960/// }
1961/// ```
1962pub async fn serve_mcp(
1963    listen: McpListen,
1964    schema_json: String,
1965    executable_path: Option<PathBuf>,
1966    config: ClapMcpConfig,
1967    in_process_handler: Option<InProcessToolHandler>,
1968    serve_options: ClapMcpServeOptions,
1969    metadata: &ClapMcpSchemaMetadata,
1970) -> Result<(), ClapMcpError> {
1971    ServeMcpBuilder::new()
1972        .listen(listen)
1973        .schema_json(schema_json)
1974        .config(config)
1975        .metadata(metadata.clone())
1976        .serve_options(serve_options)
1977        .executable_path(executable_path)
1978        .in_process_handler(in_process_handler)
1979        .serve()
1980        .await
1981}
1982
1983/// Blocking MCP server for embedders: stdio or Streamable HTTP (`http` feature).
1984///
1985/// Creates a tokio runtime internally. Prefer [`ServeMcpBuilder::serve_blocking`]
1986/// from sync `main`; this function is the lower-level equivalent.
1987/// For `#[tokio::main]`, prefer [`serve_mcp`] or [`ServeMcpBuilder::serve`].
1988///
1989/// # Example
1990///
1991/// ```rust,ignore
1992/// use clap_mcp::{ServeMcpBuilder, McpListen};
1993///
1994/// ServeMcpBuilder::new()
1995///     .listen(McpListen::Stdio)
1996///     .schema_json(schema_json)
1997///     .config(ClapMcpConfig::default())
1998///     .metadata(ClapMcpSchemaMetadata::default())
1999///     .serve_blocking()?;
2000/// ```
2001pub fn serve_mcp_blocking(
2002    listen: McpListen,
2003    schema_json: String,
2004    executable_path: Option<PathBuf>,
2005    config: ClapMcpConfig,
2006    in_process_handler: Option<InProcessToolHandler>,
2007    serve_options: ClapMcpServeOptions,
2008    metadata: &ClapMcpSchemaMetadata,
2009) -> Result<(), ClapMcpError> {
2010    ServeMcpBuilder::new()
2011        .listen(listen)
2012        .schema_json(schema_json)
2013        .config(config)
2014        .metadata(metadata.clone())
2015        .serve_options(serve_options)
2016        .executable_path(executable_path)
2017        .in_process_handler(in_process_handler)
2018        .serve_blocking()
2019}
2020
2021#[cfg(feature = "http")]
2022fn serve_prepared_mcp_blocking(
2023    http_listen: Option<std::net::SocketAddr>,
2024    schema_json: String,
2025    executable_path: Option<PathBuf>,
2026    config: ClapMcpConfig,
2027    in_process_handler: Option<InProcessToolHandler>,
2028    serve_options: ClapMcpServeOptions,
2029    metadata: &ClapMcpSchemaMetadata,
2030) -> Result<(), ClapMcpError> {
2031    let listen = match http_listen {
2032        Some(addr) => McpListen::Http(addr),
2033        None => McpListen::Stdio,
2034    };
2035    ServeMcpBuilder::new()
2036        .listen(listen)
2037        .schema_json(schema_json)
2038        .config(config)
2039        .metadata(metadata.clone())
2040        .serve_options(serve_options)
2041        .executable_path(executable_path)
2042        .in_process_handler(in_process_handler)
2043        .serve_blocking()
2044}
2045
2046#[cfg(not(feature = "http"))]
2047fn serve_prepared_mcp_blocking(
2048    _http_listen: Option<std::net::SocketAddr>,
2049    schema_json: String,
2050    executable_path: Option<PathBuf>,
2051    config: ClapMcpConfig,
2052    in_process_handler: Option<InProcessToolHandler>,
2053    serve_options: ClapMcpServeOptions,
2054    metadata: &ClapMcpSchemaMetadata,
2055) -> Result<(), ClapMcpError> {
2056    ServeMcpBuilder::new()
2057        .listen(McpListen::Stdio)
2058        .schema_json(schema_json)
2059        .config(config)
2060        .metadata(metadata.clone())
2061        .serve_options(serve_options)
2062        .executable_path(executable_path)
2063        .in_process_handler(in_process_handler)
2064        .serve_blocking()
2065}
2066
2067#[cfg(feature = "http")]
2068pub(crate) fn argv_requests_mcp_http_without_subcommand_from_args(
2069    args: &[String],
2070    cmd: &Command,
2071    flags: &ClapMcpBuiltinFlags,
2072) -> bool {
2073    let prefix = argv_before_end_of_opts(args);
2074    let subcommand_names: std::collections::HashSet<String> = cmd
2075        .get_subcommands()
2076        .map(|s| s.get_name().to_string())
2077        .collect();
2078    let http_flag = format!("--{}", flags.http_long);
2079    let http_prefix = format!("--{}=", flags.http_long);
2080    let has_http = prefix
2081        .iter()
2082        .any(|a| a == &http_flag || a.starts_with(&http_prefix));
2083    let has_subcommand = prefix.iter().any(|a| subcommand_names.contains(a.as_str()));
2084    has_http && !has_subcommand
2085}
2086
2087/// Returns true if argv contains the stdio MCP flag and no token before `--` is a subcommand name.
2088fn argv_requests_mcp_without_subcommand(cmd: &Command, flags: &ClapMcpBuiltinFlags) -> bool {
2089    let args: Vec<String> = std::env::args().skip(1).collect();
2090    argv_requests_mcp_without_subcommand_from_args(&args, cmd, flags)
2091}
2092
2093/// Pure helper for argv_requests_mcp_without_subcommand; testable with arbitrary args.
2094pub(crate) fn argv_requests_mcp_without_subcommand_from_args(
2095    args: &[String],
2096    cmd: &Command,
2097    flags: &ClapMcpBuiltinFlags,
2098) -> bool {
2099    let prefix = argv_before_end_of_opts(args);
2100    let subcommand_names: std::collections::HashSet<String> = cmd
2101        .get_subcommands()
2102        .map(|s| s.get_name().to_string())
2103        .collect();
2104    let has_mcp = argv_has_long_flag(prefix, flags.stdio_long);
2105    let has_subcommand = prefix.iter().any(|a| subcommand_names.contains(a.as_str()));
2106    has_mcp && !has_subcommand
2107}
2108
2109fn argv_export_skills_dir(flags: &ClapMcpBuiltinFlags) -> Option<Option<std::path::PathBuf>> {
2110    let args: Vec<String> = std::env::args().skip(1).collect();
2111    argv_export_skills_dir_from_args(&args, flags)
2112}
2113
2114/// Pure helper for argv_export_skills_dir; testable with arbitrary args.
2115pub(crate) fn argv_export_skills_dir_from_args(
2116    args: &[String],
2117    flags: &ClapMcpBuiltinFlags,
2118) -> Option<Option<std::path::PathBuf>> {
2119    let prefix = argv_before_end_of_opts(args);
2120    let export_flag = format!("--{}", flags.export_skills_long);
2121    let export_prefix = format!("--{}=", flags.export_skills_long);
2122    for (i, arg) in prefix.iter().enumerate() {
2123        if arg == &export_flag {
2124            return Some(
2125                prefix
2126                    .get(i + 1)
2127                    .filter(|s| !s.starts_with('-'))
2128                    .map(std::path::PathBuf::from),
2129            );
2130        }
2131        if let Some(dir) = arg.strip_prefix(&export_prefix) {
2132            return Some(Some(std::path::PathBuf::from(dir)));
2133        }
2134    }
2135    None
2136}
2137
2138/// Extracts a serializable schema from a `clap::Command` (imperative clap usage).
2139///
2140/// The schema reflects the CLI as defined by the application. Any `--mcp` flag
2141/// added via [`command_with_mcp_flag`] is intentionally omitted.
2142///
2143/// # Example
2144///
2145/// ```rust
2146/// use clap::{CommandFactory, Parser};
2147/// use clap_mcp::schema_from_command;
2148///
2149/// #[derive(Parser)]
2150/// #[command(name = "mycli")]
2151/// enum Cli { Foo }
2152///
2153/// let schema = schema_from_command(&Cli::command());
2154/// assert_eq!(schema.root.name, "mycli");
2155/// ```
2156pub fn schema_from_command(cmd: &Command) -> ClapSchema {
2157    schema_from_command_with_metadata(cmd, &ClapMcpSchemaMetadata::default())
2158}
2159
2160/// Extracts a schema from a `clap::Command` with MCP metadata applied.
2161///
2162/// Use [`ClapMcpSchemaMetadata`] to skip commands/args or make optional args required in MCP.
2163pub fn schema_from_command_with_metadata(
2164    cmd: &Command,
2165    metadata: &ClapMcpSchemaMetadata,
2166) -> ClapSchema {
2167    let skip_commands: std::collections::HashSet<_> =
2168        metadata.skip_commands.iter().cloned().collect();
2169    ClapSchema {
2170        root: command_to_schema_with_metadata(cmd, metadata, &skip_commands),
2171    }
2172}
2173
2174fn command_to_schema_with_metadata(
2175    cmd: &Command,
2176    metadata: &ClapMcpSchemaMetadata,
2177    skip_commands: &std::collections::HashSet<String>,
2178) -> ClapCommand {
2179    let visible = mcp_visible_arg_ids_on_command(cmd, metadata);
2180    let mut args: Vec<ClapArg> = cmd
2181        .get_arguments()
2182        .filter(|a| visible.contains(a.get_id().as_str()))
2183        .map(arg_to_schema)
2184        .collect();
2185
2186    let cmd_name = cmd.get_name().to_string();
2187    let requires_args: std::collections::HashSet<_> = metadata
2188        .requires_args
2189        .get(&cmd_name)
2190        .map(|v| v.iter().cloned().collect())
2191        .unwrap_or_default();
2192
2193    for arg in &mut args {
2194        if requires_args.contains(&arg.id) {
2195            arg.required = true;
2196        }
2197    }
2198    args.sort_by(|a, b| a.id.cmp(&b.id));
2199
2200    let arg_groups = extract_arg_groups(cmd, metadata);
2201
2202    let subcommands: Vec<ClapCommand> = cmd
2203        .get_subcommands()
2204        .filter(|s| !skip_commands.contains(&s.get_name().to_string()))
2205        .map(|s| command_to_schema_with_metadata(s, metadata, skip_commands))
2206        .collect();
2207
2208    ClapCommand {
2209        name: cmd.get_name().to_string(),
2210        about: cmd.get_about().map(|s| s.to_string()),
2211        long_about: cmd.get_long_about().map(|s| s.to_string()),
2212        version: cmd.get_version().map(|s| s.to_string()),
2213        args,
2214        arg_groups,
2215        subcommands,
2216    }
2217}
2218
2219/// Imperative clap entrypoint.
2220///
2221/// - Adds `--mcp` to the command (if not already present)
2222/// - If `--mcp` is present, starts an MCP stdio server and exits the process
2223/// - Otherwise, returns `ArgMatches` for normal app execution
2224///
2225/// # Example
2226///
2227/// ```rust,ignore
2228/// use clap::Command;
2229/// use clap_mcp::{command_with_mcp_flag, get_matches_or_serve_mcp};
2230///
2231/// let cmd = command_with_mcp_flag(Command::new("myapp"));
2232/// let matches = get_matches_or_serve_mcp(cmd);
2233/// // If we get here, --mcp was not passed
2234/// ```
2235pub fn get_matches_or_serve_mcp(cmd: Command) -> clap::ArgMatches {
2236    get_matches_or_serve_mcp_with_config(cmd, ClapMcpConfig::default())
2237}
2238
2239/// Imperative clap entrypoint with execution safety configuration.
2240///
2241/// See [`get_matches_or_serve_mcp`] for behavior. Use `config` to declare
2242/// reinvocation and parallel execution safety for tool execution.
2243pub fn get_matches_or_serve_mcp_with_config(
2244    cmd: Command,
2245    config: ClapMcpConfig,
2246) -> clap::ArgMatches {
2247    get_matches_or_serve_mcp_with_config_and_metadata(
2248        cmd,
2249        config,
2250        &ClapMcpSchemaMetadata::default(),
2251    )
2252}
2253
2254/// Imperative clap entrypoint with execution safety configuration and schema metadata.
2255///
2256/// Use `metadata` for `#[clap_mcp(skip)]` and `#[clap_mcp(requires = "arg_name")]` behavior.
2257pub fn get_matches_or_serve_mcp_with_config_and_metadata(
2258    cmd: Command,
2259    config: ClapMcpConfig,
2260    metadata: &ClapMcpSchemaMetadata,
2261) -> clap::ArgMatches {
2262    let schema = schema_from_command_with_metadata(&cmd, metadata);
2263    let flags = config.builtin_flags;
2264    let cmd = command_with_mcp_and_export_skills_flags_with_flags(cmd, &flags);
2265
2266    if let Some(maybe_dir) = argv_export_skills_dir(&flags) {
2267        let tools = tools_from_schema_with_metadata(&schema, &config, metadata);
2268        let output_dir = maybe_dir.unwrap_or_else(|| PathBuf::from(".agents").join("skills"));
2269        let app_name = schema.root.name.as_str();
2270        let serve_options = ClapMcpServeOptions::default();
2271        if let Err(e) = content::export_skills(
2272            &schema,
2273            metadata,
2274            &tools,
2275            &serve_options.custom_resources,
2276            &serve_options.custom_prompts,
2277            &output_dir,
2278            app_name,
2279        ) {
2280            eprintln!("export-skills failed: {}", e);
2281            std::process::exit(1);
2282        }
2283        std::process::exit(0);
2284    }
2285
2286    if config.allow_mcp_without_subcommand
2287        && (argv_requests_mcp_without_subcommand(&cmd, &flags) || {
2288            #[cfg(feature = "http")]
2289            {
2290                let args: Vec<String> = std::env::args().skip(1).collect();
2291                argv_requests_mcp_http_without_subcommand_from_args(&args, &cmd, &flags)
2292            }
2293            #[cfg(not(feature = "http"))]
2294            {
2295                false
2296            }
2297        })
2298    {
2299        let schema_json = match serde_json::to_string_pretty(&schema) {
2300            Ok(s) => s,
2301            Err(e) => {
2302                eprintln!("Failed to serialize CLI schema: {}", e);
2303                std::process::exit(1);
2304            }
2305        };
2306        #[cfg(feature = "http")]
2307        let http_listen = {
2308            let args: Vec<String> = std::env::args().skip(1).collect();
2309            resolve_mcp_http_listen_from_args(&args, &flags).unwrap_or_else(|e| {
2310                eprintln!("{e}");
2311                std::process::exit(2);
2312            })
2313        };
2314        #[cfg(not(feature = "http"))]
2315        let http_listen: Option<std::net::SocketAddr> = None;
2316
2317        if let Err(e) = serve_prepared_mcp_blocking(
2318            http_listen,
2319            schema_json,
2320            None,
2321            config,
2322            None,
2323            ClapMcpServeOptions::default(),
2324            metadata,
2325        ) {
2326            eprintln!("MCP server error: {}", e);
2327            std::process::exit(1);
2328        }
2329        std::process::exit(0);
2330    }
2331
2332    let matches = cmd.get_matches();
2333    let mcp_requested = matches_stdio_flag(&matches, &flags);
2334    #[cfg(feature = "http")]
2335    let http_listen = if matches_http_flag(&matches, &flags) {
2336        matches
2337            .get_one::<String>(CLAP_MCP_HTTP_FLAG_ID)
2338            .or_else(|| {
2339                if flags.http_long == MCP_HTTP_FLAG_LONG {
2340                    matches.get_one::<String>(MCP_HTTP_FLAG_LONG)
2341                } else {
2342                    None
2343                }
2344            })
2345            .map(|s| parse_mcp_http_listen(s))
2346            .transpose()
2347            .unwrap_or_else(|e| {
2348                eprintln!("{e}");
2349                std::process::exit(2);
2350            })
2351    } else {
2352        None
2353    };
2354    #[cfg(not(feature = "http"))]
2355    let http_listen: Option<std::net::SocketAddr> = None;
2356
2357    if mcp_requested && http_listen.is_some() {
2358        #[cfg(feature = "http")]
2359        eprintln!(
2360            "--{} and --{} are mutually exclusive",
2361            flags.stdio_long, flags.http_long
2362        );
2363        #[cfg(not(feature = "http"))]
2364        eprintln!("stdio and HTTP MCP flags are mutually exclusive");
2365        std::process::exit(2);
2366    }
2367
2368    if mcp_requested || http_listen.is_some() {
2369        let schema_json = match serde_json::to_string_pretty(&schema) {
2370            Ok(s) => s,
2371            Err(e) => {
2372                eprintln!("Failed to serialize CLI schema: {}", e);
2373                std::process::exit(1);
2374            }
2375        };
2376        if let Err(e) = serve_prepared_mcp_blocking(
2377            http_listen,
2378            schema_json,
2379            None,
2380            config,
2381            None,
2382            ClapMcpServeOptions::default(),
2383            metadata,
2384        ) {
2385            eprintln!("MCP server error: {}", e);
2386            std::process::exit(1);
2387        }
2388        std::process::exit(0);
2389    }
2390
2391    matches
2392}
2393
2394/// Imperative entrypoint like [`get_matches_or_serve_mcp_with_config_and_metadata`], but uses
2395/// un-augmented `cmd.get_matches()` when argv does not request clap-mcp entry.
2396pub fn get_matches_preserve_cli_or_serve_mcp_with_config_and_metadata(
2397    cmd: Command,
2398    config: ClapMcpConfig,
2399    metadata: &ClapMcpSchemaMetadata,
2400) -> clap::ArgMatches {
2401    let args: Vec<String> = std::env::args().skip(1).collect();
2402    if argv_contains_clap_mcp_flags(&args, &config.builtin_flags) {
2403        get_matches_or_serve_mcp_with_config_and_metadata(cmd, config, metadata)
2404    } else {
2405        cmd.get_matches()
2406    }
2407}
2408
2409/// Like [`get_matches_or_serve_mcp_with_config`] with native CLI parse when argv has no clap-mcp flags.
2410pub fn get_matches_preserve_cli_or_serve_mcp_with_config(
2411    cmd: Command,
2412    config: ClapMcpConfig,
2413) -> clap::ArgMatches {
2414    get_matches_preserve_cli_or_serve_mcp_with_config_and_metadata(
2415        cmd,
2416        config,
2417        &ClapMcpSchemaMetadata::default(),
2418    )
2419}
2420
2421/// Like [`get_matches_or_serve_mcp`] with native CLI parse when argv has no clap-mcp flags.
2422pub fn get_matches_preserve_cli_or_serve_mcp(cmd: Command) -> clap::ArgMatches {
2423    get_matches_preserve_cli_or_serve_mcp_with_config(cmd, ClapMcpConfig::default())
2424}
2425
2426/// Canonical entrypoint for derive-based CLIs: parse (or serve if `--mcp`) and return self.
2427///
2428/// With the trait in scope, use `Args::parse_or_serve_mcp()`.
2429///
2430/// # Example
2431///
2432/// ```rust,ignore
2433/// use clap::Parser;
2434/// use clap_mcp::{ClapMcp, ParseOrServeMcp};
2435///
2436/// #[derive(Parser, ClapMcp)]
2437/// #[clap_mcp(reinvocation_safe, parallel_safe = false)]
2438/// enum Cli { Foo }
2439///
2440/// fn main() {
2441///     let cli = Cli::parse_or_serve_mcp();
2442///     // ...
2443/// }
2444/// ```
2445pub trait ParseOrServeMcp {
2446    fn parse_or_serve_mcp() -> Self;
2447
2448    /// Like [`parse_or_serve_mcp`](Self::parse_or_serve_mcp) but uses [`clap::Parser::parse`]
2449    /// when argv does not request clap-mcp entry, preserving native clap error formatting
2450    /// for normal shell invocations.
2451    fn parse_or_serve_mcp_preserve_cli() -> Self;
2452}
2453
2454impl<T> ParseOrServeMcp for T
2455where
2456    T: ClapMcpConfigProvider
2457        + ClapMcpSchemaMetadataProvider
2458        + ClapMcpToolExecutor
2459        + clap::Parser
2460        + clap::CommandFactory
2461        + clap::FromArgMatches
2462        + 'static,
2463{
2464    fn parse_or_serve_mcp() -> Self {
2465        parse_or_serve_mcp_with(ClapMcpRunOptions {
2466            config: T::clap_mcp_config(),
2467            serve: ClapMcpServeOptions::default(),
2468        })
2469    }
2470
2471    fn parse_or_serve_mcp_preserve_cli() -> Self {
2472        parse_or_serve_mcp_preserve_cli_with(ClapMcpRunOptions {
2473            config: T::clap_mcp_config(),
2474            serve: ClapMcpServeOptions::default(),
2475        })
2476    }
2477}
2478
2479/// Run parsed CLI through a closure, or serve MCP if `--mcp` / `--mcp-http` is present.
2480pub fn run_or_serve_mcp<A, F, R, E>(f: F) -> Result<R, E>
2481where
2482    A: ClapMcpConfigProvider
2483        + ClapMcpSchemaMetadataProvider
2484        + ClapMcpToolExecutor
2485        + clap::Parser
2486        + clap::CommandFactory
2487        + clap::FromArgMatches
2488        + 'static,
2489    F: FnOnce(A) -> Result<R, E>,
2490{
2491    let args = A::parse_or_serve_mcp();
2492    f(args)
2493}
2494
2495struct PreparedDeriveMcpServe {
2496    schema_json: String,
2497    in_process_handler: Option<InProcessToolHandler>,
2498    executable_path: Option<PathBuf>,
2499    metadata: ClapMcpSchemaMetadata,
2500}
2501
2502fn capture_stdout_for_serve(serve_options: &ClapMcpServeOptions) -> bool {
2503    #[cfg(unix)]
2504    {
2505        serve_options.capture_stdout
2506    }
2507    #[cfg(not(unix))]
2508    {
2509        let _ = serve_options;
2510        false
2511    }
2512}
2513
2514fn finish_prepared_derive_mcp_serve(
2515    config: &ClapMcpConfig,
2516    metadata: ClapMcpSchemaMetadata,
2517    schema_json: String,
2518    in_process_handler: Option<InProcessToolHandler>,
2519) -> PreparedDeriveMcpServe {
2520    let executable_path = if config.reinvocation_safe {
2521        None
2522    } else {
2523        std::env::current_exe().ok()
2524    };
2525    PreparedDeriveMcpServe {
2526        schema_json,
2527        in_process_handler,
2528        executable_path,
2529        metadata,
2530    }
2531}
2532
2533/// Builds schema JSON, handler, and metadata for derive-based MCP serve (stateless tools).
2534pub(crate) fn prepare_derive_mcp_serve<T>(
2535    config: &ClapMcpConfig,
2536    serve_options: &ClapMcpServeOptions,
2537) -> PreparedDeriveMcpServe
2538where
2539    T: ClapMcpToolExecutor
2540        + ClapMcpSchemaMetadataProvider
2541        + clap::CommandFactory
2542        + clap::FromArgMatches
2543        + 'static,
2544{
2545    let metadata = T::clap_mcp_schema_metadata();
2546    let schema = schema_from_command_with_metadata(&T::command(), &metadata);
2547    let schema_json = serde_json::to_string_pretty(&schema).expect("schema should serialize");
2548    let capture_stdout = capture_stdout_for_serve(serve_options);
2549    let in_process_handler = if config.reinvocation_safe {
2550        Some(make_in_process_handler::<T>(schema, capture_stdout))
2551    } else {
2552        None
2553    };
2554    finish_prepared_derive_mcp_serve(config, metadata, schema_json, in_process_handler)
2555}
2556
2557/// Like [`prepare_derive_mcp_serve`], but captures shared session state in the in-process handler.
2558pub(crate) fn prepare_derive_mcp_serve_with_state<T>(
2559    config: &ClapMcpConfig,
2560    serve_options: &ClapMcpServeOptions,
2561    state: Arc<T::State>,
2562) -> PreparedDeriveMcpServe
2563where
2564    T: ClapMcpToolExecutorWithState
2565        + ClapMcpSchemaMetadataProvider
2566        + clap::CommandFactory
2567        + clap::FromArgMatches
2568        + 'static,
2569{
2570    let metadata = T::clap_mcp_schema_metadata();
2571    let schema = schema_from_command_with_metadata(&T::command(), &metadata);
2572    let schema_json = serde_json::to_string_pretty(&schema).expect("schema should serialize");
2573    let capture_stdout = capture_stdout_for_serve(serve_options);
2574    let in_process_handler = if config.reinvocation_safe {
2575        Some(make_in_process_handler_with_state::<T>(
2576            schema,
2577            state,
2578            capture_stdout,
2579        ))
2580    } else {
2581        None
2582    };
2583    finish_prepared_derive_mcp_serve(config, metadata, schema_json, in_process_handler)
2584}
2585
2586fn run_prepared_derive_mcp_serve(
2587    prepared: PreparedDeriveMcpServe,
2588    http_listen: Option<std::net::SocketAddr>,
2589    config: ClapMcpConfig,
2590    serve_options: ClapMcpServeOptions,
2591) -> Result<(), ClapMcpError> {
2592    serve_prepared_mcp_blocking(
2593        http_listen,
2594        prepared.schema_json,
2595        prepared.executable_path,
2596        config,
2597        prepared.in_process_handler,
2598        serve_options,
2599        &prepared.metadata,
2600    )
2601}
2602
2603fn exit_on_mcp_serve_error(result: Result<(), ClapMcpError>) -> ! {
2604    if let Err(e) = result {
2605        eprintln!("MCP server error: {}", e);
2606        std::process::exit(1);
2607    }
2608    std::process::exit(0);
2609}
2610
2611#[cfg(feature = "http")]
2612fn resolve_http_listen_from_env_or_exit(
2613    flags: &ClapMcpBuiltinFlags,
2614) -> Option<std::net::SocketAddr> {
2615    let args: Vec<String> = std::env::args().skip(1).collect();
2616    resolve_mcp_http_listen_from_args(&args, flags).unwrap_or_else(|e| {
2617        eprintln!("{e}");
2618        std::process::exit(2);
2619    })
2620}
2621
2622fn parse_or_serve_mcp_common<T>(
2623    options: ClapMcpRunOptions,
2624    prepare: impl FnOnce(&ClapMcpConfig, &ClapMcpServeOptions) -> PreparedDeriveMcpServe,
2625) -> T
2626where
2627    T: ClapMcpSchemaMetadataProvider + clap::Parser + clap::CommandFactory + clap::FromArgMatches,
2628{
2629    let ClapMcpRunOptions {
2630        config,
2631        serve: serve_options,
2632    } = options;
2633    let flags = config.builtin_flags;
2634    let mut cmd = T::command();
2635    cmd = command_with_mcp_and_export_skills_flags_with_flags(cmd, &flags);
2636
2637    if let Some(maybe_dir) = argv_export_skills_dir(&flags) {
2638        let base_cmd = T::command();
2639        let metadata = T::clap_mcp_schema_metadata();
2640        let schema = schema_from_command_with_metadata(&base_cmd, &metadata);
2641        let tools = tools_from_schema_with_metadata(&schema, &config, &metadata);
2642        let output_dir = maybe_dir.unwrap_or_else(|| PathBuf::from(".agents").join("skills"));
2643        let app_name = schema.root.name.as_str();
2644        if let Err(e) = content::export_skills(
2645            &schema,
2646            &metadata,
2647            &tools,
2648            &serve_options.custom_resources,
2649            &serve_options.custom_prompts,
2650            &output_dir,
2651            app_name,
2652        ) {
2653            eprintln!("export-skills failed: {}", e);
2654            std::process::exit(1);
2655        }
2656        std::process::exit(0);
2657    }
2658
2659    if config.allow_mcp_without_subcommand
2660        && (argv_requests_mcp_without_subcommand(&cmd, &flags) || {
2661            #[cfg(feature = "http")]
2662            {
2663                let args: Vec<String> = std::env::args().skip(1).collect();
2664                argv_requests_mcp_http_without_subcommand_from_args(&args, &cmd, &flags)
2665            }
2666            #[cfg(not(feature = "http"))]
2667            {
2668                false
2669            }
2670        })
2671    {
2672        #[cfg(feature = "http")]
2673        let http_listen = resolve_http_listen_from_env_or_exit(&flags);
2674        #[cfg(not(feature = "http"))]
2675        let http_listen: Option<std::net::SocketAddr> = None;
2676        let prepared = prepare(&config, &serve_options);
2677        exit_on_mcp_serve_error(run_prepared_derive_mcp_serve(
2678            prepared,
2679            http_listen,
2680            config,
2681            serve_options,
2682        ));
2683    }
2684
2685    let matches = cmd.get_matches();
2686    let mcp_requested = matches_stdio_flag(&matches, &flags);
2687    #[cfg(feature = "http")]
2688    let http_listen = if matches_http_flag(&matches, &flags) {
2689        match matches
2690            .get_one::<String>(CLAP_MCP_HTTP_FLAG_ID)
2691            .or_else(|| {
2692                if flags.http_long == MCP_HTTP_FLAG_LONG {
2693                    matches.get_one::<String>(MCP_HTTP_FLAG_LONG)
2694                } else {
2695                    None
2696                }
2697            })
2698            .map(|s| parse_mcp_http_listen(s))
2699            .transpose()
2700        {
2701            Ok(v) => v,
2702            Err(e) => {
2703                eprintln!("{e}");
2704                std::process::exit(2);
2705            }
2706        }
2707    } else {
2708        None
2709    };
2710    #[cfg(not(feature = "http"))]
2711    let http_listen: Option<std::net::SocketAddr> = None;
2712
2713    if mcp_requested && http_listen.is_some() {
2714        #[cfg(feature = "http")]
2715        eprintln!(
2716            "--{} and --{} are mutually exclusive",
2717            flags.stdio_long, flags.http_long
2718        );
2719        #[cfg(not(feature = "http"))]
2720        eprintln!("stdio and HTTP MCP flags are mutually exclusive");
2721        std::process::exit(2);
2722    }
2723
2724    if mcp_requested || http_listen.is_some() {
2725        let prepared = prepare(&config, &serve_options);
2726        exit_on_mcp_serve_error(run_prepared_derive_mcp_serve(
2727            prepared,
2728            http_listen,
2729            config,
2730            serve_options,
2731        ));
2732    }
2733
2734    T::from_arg_matches(&matches).unwrap_or_else(|e| e.exit())
2735}
2736
2737/// Derive-based entrypoint: parse CLI or start MCP server (stdio or HTTP) and exit.
2738///
2739/// Config comes from `T::clap_mcp_config()` (via `#[clap_mcp(...)]` on the derive).
2740/// Prefer [`ParseOrServeMcp::parse_or_serve_mcp`] when the trait is in scope.
2741pub fn parse_or_serve_mcp_with<T>(options: ClapMcpRunOptions) -> T
2742where
2743    T: ClapMcpSchemaMetadataProvider
2744        + ClapMcpToolExecutor
2745        + clap::Parser
2746        + clap::CommandFactory
2747        + clap::FromArgMatches
2748        + 'static,
2749{
2750    parse_or_serve_mcp_common::<T>(options, |config, serve_options| {
2751        prepare_derive_mcp_serve::<T>(config, serve_options)
2752    })
2753}
2754
2755/// Like [`parse_or_serve_mcp_with`] but uses [`clap::Parser::parse`] when argv does not request
2756/// clap-mcp entry, preserving native clap error formatting for normal shell invocations.
2757pub fn parse_or_serve_mcp_preserve_cli_with<T>(options: ClapMcpRunOptions) -> T
2758where
2759    T: ClapMcpSchemaMetadataProvider
2760        + ClapMcpToolExecutor
2761        + clap::Parser
2762        + clap::CommandFactory
2763        + clap::FromArgMatches
2764        + 'static,
2765{
2766    let args: Vec<String> = std::env::args().skip(1).collect();
2767    if argv_contains_clap_mcp_flags(&args, &options.config.builtin_flags) {
2768        parse_or_serve_mcp_with(options)
2769    } else {
2770        T::parse()
2771    }
2772}
2773
2774/// Stateful derive entrypoint: like [`parse_or_serve_mcp_with`] but captures `state` in the
2775/// in-process tool handler for the MCP server lifetime.
2776///
2777/// `state` is stored as [`Arc`] internally; your `run` function receives `&T::State` on each
2778/// tool call. Requires [`ClapMcpConfig::reinvocation_safe`](ClapMcpConfig::reinvocation_safe).
2779///
2780/// Session state is shared for the server process lifetime, not per MCP client. See
2781/// [`ClapMcpToolExecutorWithState`] for multi-user and untrusted-remote guidance.
2782pub fn parse_or_serve_mcp_with_state<T>(options: ClapMcpRunOptions, state: Arc<T::State>) -> T
2783where
2784    T: ClapMcpSchemaMetadataProvider
2785        + ClapMcpToolExecutorWithState
2786        + clap::Parser
2787        + clap::CommandFactory
2788        + clap::FromArgMatches
2789        + 'static,
2790{
2791    parse_or_serve_mcp_common::<T>(options, |config, serve_options| {
2792        prepare_derive_mcp_serve_with_state::<T>(config, serve_options, Arc::clone(&state))
2793    })
2794}
2795
2796/// Like [`parse_or_serve_mcp_with_state`] but uses [`clap::Parser::parse`] when argv does not
2797/// request clap-mcp entry.
2798pub fn parse_or_serve_mcp_with_state_preserve_cli<T>(
2799    options: ClapMcpRunOptions,
2800    state: Arc<T::State>,
2801) -> T
2802where
2803    T: ClapMcpSchemaMetadataProvider
2804        + ClapMcpToolExecutorWithState
2805        + clap::Parser
2806        + clap::CommandFactory
2807        + clap::FromArgMatches
2808        + 'static,
2809{
2810    let args: Vec<String> = std::env::args().skip(1).collect();
2811    if argv_contains_clap_mcp_flags(&args, &options.config.builtin_flags) {
2812        parse_or_serve_mcp_with_state(options, state)
2813    } else {
2814        T::parse()
2815    }
2816}
2817
2818/// Parse CLI or serve MCP with shared session state when `--mcp` is present.
2819///
2820/// Requires [`ClapMcpToolExecutorWithState`] on the derive target (see trait docs for setup).
2821/// Session state is shared for the server process lifetime; see that trait for security scope.
2822pub trait ParseOrServeMcpWithState: ClapMcpToolExecutorWithState + Sized {
2823    /// Parse argv or start MCP with `state` captured for the server lifetime.
2824    fn parse_or_serve_mcp_with_state(state: Arc<Self::State>) -> Self;
2825}
2826
2827impl<T> ParseOrServeMcpWithState for T
2828where
2829    T: ClapMcpConfigProvider
2830        + ClapMcpSchemaMetadataProvider
2831        + ClapMcpToolExecutorWithState
2832        + clap::Parser
2833        + clap::CommandFactory
2834        + clap::FromArgMatches
2835        + 'static,
2836{
2837    fn parse_or_serve_mcp_with_state(state: Arc<T::State>) -> Self {
2838        parse_or_serve_mcp_with_state::<T>(
2839            ClapMcpRunOptions {
2840                config: T::clap_mcp_config(),
2841                serve: ClapMcpServeOptions::default(),
2842            },
2843            state,
2844        )
2845    }
2846}
2847
2848fn arg_to_schema(arg: &clap::Arg) -> ClapArg {
2849    let value_names = arg
2850        .get_value_names()
2851        .map(|names| names.iter().map(|n| n.to_string()).collect())
2852        .unwrap_or_default();
2853
2854    ClapArg {
2855        id: arg.get_id().to_string(),
2856        long: arg.get_long().map(|s| s.to_string()),
2857        short: arg.get_short(),
2858        help: arg.get_help().map(|s| s.to_string()),
2859        long_help: arg.get_long_help().map(|s| s.to_string()),
2860        required: arg.is_required_set(),
2861        global: arg.is_global_set(),
2862        index: arg.get_index(),
2863        action: Some(format!("{:?}", arg.get_action())),
2864        value_names,
2865        num_args: arg.get_num_args().map(|r| format!("{r:?}")),
2866    }
2867}
2868
2869/// Validates that all required args for the command are present in the arguments map.
2870/// Returns Err with a clear message if any required arg is missing.
2871pub(crate) fn validate_required_args(
2872    schema: &ClapSchema,
2873    command_name: &str,
2874    arguments: &serde_json::Map<String, serde_json::Value>,
2875) -> Result<(), String> {
2876    if command_path(schema, command_name).is_none() {
2877        return Ok(());
2878    }
2879    let effective_args = effective_args_for_tool(schema, command_name);
2880    let missing: Vec<_> = effective_args
2881        .iter()
2882        .filter(|a| {
2883            if !a.required {
2884                return false;
2885            }
2886            let has_value = arguments.get(&a.id).map(|v| {
2887                let action = a.action.as_deref().unwrap_or("Set");
2888                if matches!(action, "SetTrue" | "SetFalse" | "Count") {
2889                    // Flag/count: key present is enough (value can be false/0)
2890                    true
2891                } else if action == "Append" || v.is_array() {
2892                    !value_to_strings(v).is_some_and(|s| s.is_empty())
2893                } else {
2894                    value_to_string(v).is_some_and(|s| !s.is_empty())
2895                }
2896            });
2897            !has_value.unwrap_or(false)
2898        })
2899        .map(|a| a.id.clone())
2900        .collect();
2901    if missing.is_empty() {
2902        Ok(())
2903    } else {
2904        Err(format!(
2905            "Missing required argument(s): {}. The MCP tool schema marks these as required.",
2906            missing.join(", ")
2907        ))
2908    }
2909}
2910
2911/// Builds full argv for clap's `get_matches_from` (program name + subcommand + args).
2912fn build_argv_for_clap(
2913    schema: &ClapSchema,
2914    command_name: &str,
2915    arguments: serde_json::Map<String, serde_json::Value>,
2916) -> Vec<String> {
2917    let args = build_tool_argv(schema, command_name, arguments);
2918    let mut argv = vec!["cli".to_string()]; // program name for parsing
2919    if let Some(path) = command_path(schema, command_name) {
2920        argv.extend(path.into_iter().skip(1));
2921    }
2922    argv.extend(args);
2923    argv
2924}
2925
2926pub(crate) fn command_path(schema: &ClapSchema, command_name: &str) -> Option<Vec<String>> {
2927    fn walk(cmd: &ClapCommand, command_name: &str, path: &mut Vec<String>) -> bool {
2928        path.push(cmd.name.clone());
2929        if cmd.name == command_name {
2930            return true;
2931        }
2932        for subcommand in &cmd.subcommands {
2933            if walk(subcommand, command_name, path) {
2934                return true;
2935            }
2936        }
2937        path.pop();
2938        false
2939    }
2940
2941    let mut path = Vec::new();
2942    if walk(&schema.root, command_name, &mut path) {
2943        Some(path)
2944    } else {
2945        None
2946    }
2947}
2948
2949/// Builds argv for the executable from the schema and tool arguments.
2950///
2951/// Positional args (no long form) are passed in index order; optional args as `--long value`.
2952pub(crate) fn build_tool_argv(
2953    schema: &ClapSchema,
2954    command_name: &str,
2955    arguments: serde_json::Map<String, serde_json::Value>,
2956) -> Vec<String> {
2957    if command_path(schema, command_name).is_none() {
2958        return Vec::new();
2959    }
2960    let effective_args = effective_args_for_tool(schema, command_name);
2961
2962    let mut positionals: Vec<&ClapArg> = effective_args
2963        .iter()
2964        .filter(|a| a.long.is_none() && !a.num_args.as_deref().is_some_and(|n| n.contains("..")))
2965        .collect();
2966    positionals.sort_by_key(|a| a.index.unwrap_or(0));
2967    let trailing_positionals: Vec<&ClapArg> = effective_args
2968        .iter()
2969        .filter(|a| a.long.is_none() && a.num_args.as_deref().is_some_and(|n| n.contains("..")))
2970        .collect();
2971    let optionals: Vec<&ClapArg> = effective_args.iter().filter(|a| a.long.is_some()).collect();
2972
2973    let mut out = Vec::new();
2974
2975    for arg in positionals {
2976        if let Some(v) = arguments.get(&arg.id)
2977            && let Some(strings) = value_to_strings(v)
2978        {
2979            for s in strings {
2980                out.push(s);
2981            }
2982        }
2983    }
2984    for arg in optionals {
2985        if let Some(long) = &arg.long {
2986            let action = arg.action.as_deref().unwrap_or("Set");
2987            let v = arguments.get(&arg.id);
2988            match action {
2989                "SetTrue" => {
2990                    if v.and_then(value_to_string).is_some_and(|s| s == "true")
2991                        || v.and_then(|x| x.as_bool()).is_some_and(|b| b)
2992                    {
2993                        out.push(format!("--{long}"));
2994                    }
2995                }
2996                "SetFalse" => {
2997                    if v.and_then(value_to_string).is_some_and(|s| s == "false")
2998                        || v.and_then(|x| x.as_bool()).is_some_and(|b| !b)
2999                    {
3000                        out.push(format!("--{long}"));
3001                    }
3002                }
3003                "Count" => {
3004                    let n = v.and_then(|x| x.as_i64()).unwrap_or(0).clamp(0, i64::MAX) as usize;
3005                    for _ in 0..n {
3006                        out.push(format!("--{long}"));
3007                    }
3008                }
3009                "Append" => {
3010                    if let Some(v) = v.and_then(value_to_strings) {
3011                        for s in v {
3012                            if !s.is_empty() {
3013                                out.push(format!("--{long}"));
3014                                out.push(s);
3015                            }
3016                        }
3017                    } else if let Some(s) = v.and_then(value_to_string)
3018                        && !s.is_empty()
3019                    {
3020                        out.push(format!("--{long}"));
3021                        out.push(s);
3022                    }
3023                }
3024                _ => {
3025                    if let Some(s) = v.and_then(value_to_string)
3026                        && !s.is_empty()
3027                    {
3028                        out.push(format!("--{long}"));
3029                        out.push(s);
3030                    }
3031                }
3032            }
3033        }
3034    }
3035
3036    let mut trailing_values = Vec::new();
3037    for arg in &trailing_positionals {
3038        if let Some(v) = arguments.get(&arg.id)
3039            && let Some(strings) = value_to_strings(v)
3040        {
3041            trailing_values.extend(strings);
3042        }
3043    }
3044    if !trailing_values.is_empty() {
3045        out.push("--".to_string());
3046        out.extend(trailing_values);
3047    }
3048
3049    out
3050}
3051
3052/// Type for in-process tool execution handler.
3053///
3054/// Called with `(command_name, arguments)` and returns `Result<ClapMcpToolOutput, ClapMcpToolError>`.
3055/// Used when `reinvocation_safe` is true to avoid spawning subprocesses.
3056pub type InProcessToolHandler = Arc<
3057    dyn Fn(
3058            &str,
3059            serde_json::Map<String, serde_json::Value>,
3060        ) -> Result<ClapMcpToolOutput, ClapMcpToolError>
3061        + Send
3062        + Sync,
3063>;
3064
3065fn merge_captured_stdout(
3066    result: Result<ClapMcpToolOutput, ClapMcpToolError>,
3067    captured: String,
3068) -> Result<ClapMcpToolOutput, ClapMcpToolError> {
3069    match result {
3070        Ok(ClapMcpToolOutput::Text(text)) if !captured.is_empty() => {
3071            let merged = if text.is_empty() {
3072                captured.trim().to_string()
3073            } else {
3074                let cap = captured.trim();
3075                if cap.is_empty() {
3076                    text
3077                } else {
3078                    format!("{text}\n{cap}")
3079                }
3080            };
3081            Ok(ClapMcpToolOutput::Text(merged))
3082        }
3083        other => other,
3084    }
3085}
3086
3087fn parse_cli_from_tool_args<T>(
3088    schema: &ClapSchema,
3089    command_name: &str,
3090    arguments: serde_json::Map<String, serde_json::Value>,
3091) -> Result<T, ClapMcpToolError>
3092where
3093    T: clap::CommandFactory + clap::FromArgMatches,
3094{
3095    validate_required_args(schema, command_name, &arguments).map_err(ClapMcpToolError::text)?;
3096    let argv = build_argv_for_clap(schema, command_name, arguments);
3097    let matches = T::command()
3098        .try_get_matches_from(&argv)
3099        .map_err(|e| ClapMcpToolError::text(e.to_string()))?;
3100    T::from_arg_matches(&matches).map_err(|e| ClapMcpToolError::text(e.to_string()))
3101}
3102
3103fn execute_in_process_command<T>(
3104    schema: &ClapSchema,
3105    command_name: &str,
3106    arguments: serde_json::Map<String, serde_json::Value>,
3107    capture_stdout: bool,
3108    execute: impl FnOnce(T) -> Result<ClapMcpToolOutput, ClapMcpToolError>,
3109) -> Result<ClapMcpToolOutput, ClapMcpToolError>
3110where
3111    T: clap::CommandFactory + clap::FromArgMatches,
3112{
3113    let cli = parse_cli_from_tool_args::<T>(schema, command_name, arguments)?;
3114    if capture_stdout {
3115        let (result, captured) = run_with_stdout_capture(|| execute(cli));
3116        merge_captured_stdout(result, captured)
3117    } else {
3118        execute(cli)
3119    }
3120}
3121
3122fn execute_in_process_command_stateless<T>(
3123    schema: &ClapSchema,
3124    command_name: &str,
3125    arguments: serde_json::Map<String, serde_json::Value>,
3126    capture_stdout: bool,
3127) -> Result<ClapMcpToolOutput, ClapMcpToolError>
3128where
3129    T: ClapMcpToolExecutor + clap::CommandFactory + clap::FromArgMatches,
3130{
3131    execute_in_process_command::<T>(schema, command_name, arguments, capture_stdout, |cli| {
3132        <T as ClapMcpToolExecutor>::execute_for_mcp(cli)
3133    })
3134}
3135
3136fn execute_in_process_command_stateful<T>(
3137    schema: &ClapSchema,
3138    command_name: &str,
3139    arguments: serde_json::Map<String, serde_json::Value>,
3140    state: &T::State,
3141    capture_stdout: bool,
3142) -> Result<ClapMcpToolOutput, ClapMcpToolError>
3143where
3144    T: ClapMcpToolExecutorWithState + clap::CommandFactory + clap::FromArgMatches,
3145{
3146    execute_in_process_command::<T>(schema, command_name, arguments, capture_stdout, |cli| {
3147        <T as ClapMcpToolExecutorWithState>::execute_for_mcp_with_state(cli, state)
3148    })
3149}
3150
3151/// Builds an in-process tool handler for type `T` when using [`ServeMcpBuilder`],
3152/// [`serve_mcp`], or [`serve_mcp_blocking`] with `reinvocation_safe`.
3153/// [`ServeMcpBuilder::for_cli`] sets this automatically when appropriate.
3154pub fn in_process_tool_handler_for<T>(
3155    schema: ClapSchema,
3156    capture_stdout: bool,
3157) -> InProcessToolHandler
3158where
3159    T: ClapMcpToolExecutor + clap::CommandFactory + clap::FromArgMatches + 'static,
3160{
3161    make_in_process_handler::<T>(schema, capture_stdout)
3162}
3163
3164pub(crate) fn make_in_process_handler<T>(
3165    schema: ClapSchema,
3166    capture_stdout: bool,
3167) -> InProcessToolHandler
3168where
3169    T: ClapMcpToolExecutor + clap::CommandFactory + clap::FromArgMatches + 'static,
3170{
3171    Arc::new(
3172        move |cmd: &str, args: serde_json::Map<String, serde_json::Value>| {
3173            execute_in_process_command_stateless::<T>(&schema, cmd, args, capture_stdout)
3174        },
3175    ) as InProcessToolHandler
3176}
3177
3178pub(crate) fn make_in_process_handler_with_state<T>(
3179    schema: ClapSchema,
3180    state: Arc<T::State>,
3181    capture_stdout: bool,
3182) -> InProcessToolHandler
3183where
3184    T: ClapMcpToolExecutorWithState + clap::CommandFactory + clap::FromArgMatches + 'static,
3185{
3186    Arc::new(
3187        move |cmd: &str, args: serde_json::Map<String, serde_json::Value>| {
3188            execute_in_process_command_stateful::<T>(
3189                &schema,
3190                cmd,
3191                args,
3192                state.as_ref(),
3193                capture_stdout,
3194            )
3195        },
3196    ) as InProcessToolHandler
3197}
3198
3199pub(crate) fn format_panic_payload(payload: &(dyn std::any::Any + Send)) -> String {
3200    if let Some(s) = payload.downcast_ref::<&str>() {
3201        return (*s).to_string();
3202    }
3203    if let Some(s) = payload.downcast_ref::<String>() {
3204        return s.clone();
3205    }
3206    "<panic>".to_string()
3207}
3208
3209fn value_to_string(v: &serde_json::Value) -> Option<String> {
3210    if v.is_null() {
3211        return None;
3212    }
3213    Some(match v {
3214        serde_json::Value::String(s) => s.clone(),
3215        serde_json::Value::Number(n) => n.to_string(),
3216        serde_json::Value::Bool(b) => b.to_string(),
3217        other => other.to_string(),
3218    })
3219}
3220
3221/// Stable string for one MCP argument value when building topical lock keys.
3222pub(crate) fn canonical_lock_arg_value(v: &serde_json::Value) -> Option<String> {
3223    if v.is_null() {
3224        return None;
3225    }
3226    match v {
3227        serde_json::Value::Array(arr) => {
3228            let mut parts = Vec::with_capacity(arr.len());
3229            for item in arr {
3230                parts.push(canonical_lock_arg_value(item)?);
3231            }
3232            Some(format!("[{}]", parts.join(",")))
3233        }
3234        serde_json::Value::Object(map) => {
3235            let mut keys: Vec<_> = map.keys().cloned().collect();
3236            keys.sort();
3237            let mut out = String::from("{");
3238            for (i, key) in keys.iter().enumerate() {
3239                if i > 0 {
3240                    out.push(',');
3241                }
3242                let val = canonical_lock_arg_value(map.get(key)?)?;
3243                out.push_str(key);
3244                out.push(':');
3245                out.push_str(&val);
3246            }
3247            out.push('}');
3248            Some(out)
3249        }
3250        _ => value_to_string(v),
3251    }
3252}
3253
3254/// Builds a topical lock key for a tool call when the tool has serialize metadata.
3255pub(crate) fn serialize_lock_key(
3256    tool_name: &str,
3257    args: &serde_json::Map<String, serde_json::Value>,
3258    scope: &ClapMcpSerializeScope,
3259    topic_fns: Option<&std::collections::HashMap<String, SerializeTopicSegmentFn>>,
3260) -> String {
3261    let tool_prefix = format!("tool:{tool_name}");
3262    match scope {
3263        ClapMcpSerializeScope::Tool => tool_prefix,
3264        ClapMcpSerializeScope::Args(arg_ids) => {
3265            let mut sorted_ids: Vec<_> = arg_ids.clone();
3266            sorted_ids.sort();
3267            let mut segments = Vec::with_capacity(sorted_ids.len());
3268            for id in &sorted_ids {
3269                match args.get(id) {
3270                    Some(value) => {
3271                        let segment = topic_fns
3272                            .and_then(|fns| fns.get(id))
3273                            .and_then(|f| f(value))
3274                            .or_else(|| canonical_lock_arg_value(value));
3275                        match segment {
3276                            Some(value) => segments.push(format!("{id}={value}")),
3277                            None => return tool_prefix,
3278                        }
3279                    }
3280                    None => return tool_prefix,
3281                }
3282            }
3283            format!("{tool_prefix}:{}", segments.join(":"))
3284        }
3285    }
3286}
3287
3288/// Returns one or more string values for MCP input. For arrays, returns each element as string; otherwise single value.
3289fn value_to_strings(v: &serde_json::Value) -> Option<Vec<String>> {
3290    if v.is_null() {
3291        return None;
3292    }
3293    match v {
3294        serde_json::Value::Array(arr) => {
3295            let out: Vec<String> = arr
3296                .iter()
3297                .filter_map(value_to_string)
3298                .filter(|s| !s.is_empty())
3299                .collect();
3300            Some(out)
3301        }
3302        _ => value_to_string(v).map(|s| vec![s]),
3303    }
3304}
3305
3306/// Runs an async future for MCP tool execution, respecting `share_runtime` in config.
3307///
3308/// **Idiomatic approach:** with `#[clap_mcp_output_from = "run"]`, do async work inside your
3309/// `run` function (e.g. use a runtime handle or call this function). The closure must return
3310/// a `Future` that produces the tool output.
3311///
3312/// Returns [`Ok`] with the future's output, or [`Err`](ClapMcpError) if the runtime could
3313/// not be created, the current context is invalid (`share_runtime` without a tokio runtime),
3314/// or the async thread panicked.
3315///
3316/// # Runtime selection
3317///
3318/// | `reinvocation_safe` | `share_runtime` | Behavior |
3319/// |---------------------|----------------|----------|
3320/// | `false` | any | Dedicated thread (subprocess mode; `share_runtime` ignored) |
3321/// | `true` | `false` | Dedicated thread with its own tokio runtime (default, recommended) |
3322/// | `true` | `true` | Uses `Handle::current().block_on()` on the MCP server's runtime |
3323///
3324/// When `parallel_safe` is true and `share_runtime` is false, `run_async_tool` uses
3325/// `block_in_place` so the MCP server's multi-thread runtime can process overlapping calls
3326/// while dedicated-thread work runs.
3327///
3328/// When `share_runtime` is true, uses `block_in_place` + `block_on` so the async
3329/// work runs on the MCP server's multi-thread runtime without deadlock.
3330///
3331/// # Task logging (`meta.taskId`)
3332///
3333/// When MCP task-augmented `tools/call` is active, the MCP server wraps tool
3334/// bodies with [`crate::logging::run_with_mcp_task_id`]. For **`share_runtime =
3335/// true`**, this function captures [`crate::logging::current_mcp_task_id`] before
3336/// `block_on` and re-installs it inside the nested future. Tokio task-local from
3337/// the outer MCP task body does not always propagate into futures polled by
3338/// `block_on` (especially under concurrent `parallel_safe` load), so the
3339/// re-scope keeps `meta.taskId` on forwarded log notifications. The dedicated-
3340/// thread path (`share_runtime = false`) uses [`crate::logging::McpTaskIdGuard`]
3341/// instead. This behavior is platform-independent.
3342///
3343/// # Example (async inside `run`)
3344///
3345/// ```rust,ignore
3346/// fn run(cmd: Cli) -> SleepResult {
3347///     match cmd {
3348///         Cli::SleepDemo => clap_mcp::run_async_tool(&Cli::clap_mcp_config(), run_sleep_demo).expect("async tool failed"),
3349///     }
3350/// }
3351/// ```
3352pub fn run_async_tool<Fut, O>(
3353    config: &ClapMcpConfig,
3354    f: impl FnOnce() -> Fut + Send,
3355) -> std::result::Result<O, ClapMcpError>
3356where
3357    Fut: std::future::Future<Output = O> + Send,
3358    O: Send,
3359{
3360    if config.reinvocation_safe && config.share_runtime {
3361        tokio::task::block_in_place(|| {
3362            let handle = tokio::runtime::Handle::try_current()
3363                .map_err(|e| ClapMcpError::RuntimeContext(e.to_string()))?;
3364            // Capture before `block_on`: task-local from the MCP task body does not always
3365            // propagate into the nested future polled by `block_on` (notably under concurrent
3366            // `parallel_safe` load). Re-install via `run_with_mcp_task_id`, mirroring the
3367            // dedicated-thread `McpTaskIdGuard` path below.
3368            let task_id = crate::logging::current_mcp_task_id();
3369            Ok(handle.block_on(async move {
3370                match task_id {
3371                    Some(id) => crate::logging::run_with_mcp_task_id(id, f()).await,
3372                    None => f().await,
3373                }
3374            }))
3375        })
3376    } else {
3377        let catch_panics = config.catch_in_process_panics;
3378        let run_on_dedicated_thread = || {
3379            let task_id = crate::logging::current_mcp_task_id();
3380            std::thread::scope(|s| {
3381                let join_handle = s.spawn(move || {
3382                    let _task_id_guard = task_id.map(crate::logging::McpTaskIdGuard::new);
3383                    let rt = tokio::runtime::Builder::new_current_thread()
3384                        .enable_all()
3385                        .build()?;
3386                    Ok(rt.block_on(f()))
3387                });
3388                match join_handle.join() {
3389                    Ok(inner) => inner,
3390                    Err(payload) if catch_panics => {
3391                        let msg = format_panic_payload(payload.as_ref());
3392                        Err(ClapMcpError::ToolThread(format!("Tool panicked: {msg}")))
3393                    }
3394                    Err(payload) => std::panic::resume_unwind(payload),
3395                }
3396            })
3397        };
3398        if config.reinvocation_safe && config.parallel_safe {
3399            tokio::task::block_in_place(run_on_dedicated_thread)
3400        } else {
3401            run_on_dedicated_thread()
3402        }
3403    }
3404}
3405
3406#[cfg(test)]
3407mod tests {
3408    use super::*;
3409    use crate::server::{
3410        ClapMcpServer, build_clap_mcp_server, build_execution_command,
3411        call_tool_result_from_output, call_tool_result_from_panic,
3412        call_tool_result_from_tool_error, command_launch_failure_result, get_prompt_result,
3413        list_prompts_result, list_resources_result, placeholder_tool_result, read_resource_result,
3414        schema_parse_failure_result, subprocess_stderr_log_params, validate_tool_argument_names,
3415    };
3416    use async_trait::async_trait;
3417    use clap::{Arg, ArgAction, ArgGroup, Command, CommandFactory};
3418    use rmcp::ServerHandler;
3419    use rmcp::model::{
3420        Content, GetPromptRequestParams, PromptMessage, PromptMessageContent, PromptMessageRole,
3421        RawContent, ReadResourceRequestParams, ResourceContents, Tool,
3422    };
3423    use serde::Deserialize;
3424    use serde_json::json;
3425    use std::error::Error;
3426    use std::sync::{Arc, Mutex};
3427
3428    fn content_text(content: &Content) -> &str {
3429        match &content.raw {
3430            RawContent::Text(text) => &text.text,
3431            _ => panic!("expected text content"),
3432        }
3433    }
3434
3435    fn prompt_text(content: &PromptMessageContent) -> &str {
3436        match content {
3437            PromptMessageContent::Text { text, .. } => text,
3438            _ => panic!("expected text prompt content"),
3439        }
3440    }
3441
3442    #[cfg(unix)]
3443    use std::os::unix::process::ExitStatusExt;
3444
3445    #[cfg(unix)]
3446    use crate::server::call_tool_result_from_subprocess_output;
3447
3448    fn sample_helper_schema() -> ClapSchema {
3449        schema_from_command(
3450            &Command::new("sample")
3451                .arg(Arg::new("input").help("Input file").required(true).index(1))
3452                .arg(
3453                    Arg::new("verbose")
3454                        .long("verbose")
3455                        .help("Verbose mode")
3456                        .action(ArgAction::SetTrue),
3457                )
3458                .arg(
3459                    Arg::new("no-cache")
3460                        .long("no-cache")
3461                        .help("Disable cache")
3462                        .action(ArgAction::SetFalse),
3463                )
3464                .arg(
3465                    Arg::new("level")
3466                        .long("level")
3467                        .help("Verbosity level")
3468                        .action(ArgAction::Count),
3469                )
3470                .arg(
3471                    Arg::new("tag")
3472                        .long("tag")
3473                        .help("Tags to include")
3474                        .action(ArgAction::Append)
3475                        .value_name("TAG"),
3476                )
3477                .arg(
3478                    Arg::new("mode")
3479                        .long("mode")
3480                        .help("Execution mode")
3481                        .action(ArgAction::Set),
3482                )
3483                .subcommand(Command::new("serve").about("Serve the sample app")),
3484        )
3485    }
3486
3487    fn nested_schema() -> ClapSchema {
3488        schema_from_command(
3489            &Command::new("sample")
3490                .subcommand(
3491                    Command::new("parent")
3492                        .subcommand(Command::new("child").arg(Arg::new("value").long("value"))),
3493                )
3494                .subcommand(Command::new("echo").arg(Arg::new("message").long("message"))),
3495        )
3496    }
3497
3498    #[derive(Debug)]
3499    struct TestError(&'static str);
3500
3501    impl std::fmt::Display for TestError {
3502        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3503            f.write_str(self.0)
3504        }
3505    }
3506
3507    impl Error for TestError {}
3508
3509    struct TestPromptProvider {
3510        response: Result<Vec<PromptMessage>, &'static str>,
3511        seen: Mutex<Vec<(String, serde_json::Map<String, serde_json::Value>)>>,
3512    }
3513
3514    #[async_trait]
3515    impl content::PromptContentProvider for TestPromptProvider {
3516        async fn get(
3517            &self,
3518            name: &str,
3519            arguments: &serde_json::Map<String, serde_json::Value>,
3520        ) -> std::result::Result<Vec<PromptMessage>, Box<dyn Error + Send + Sync>> {
3521            self.seen
3522                .lock()
3523                .expect("prompt provider mutex should lock")
3524                .push((name.to_string(), arguments.clone()));
3525            match &self.response {
3526                Ok(messages) => Ok(messages.clone()),
3527                Err(message) => Err(Box::new(TestError(message))),
3528            }
3529        }
3530    }
3531
3532    struct TestResourceProvider {
3533        response: Result<String, &'static str>,
3534    }
3535
3536    #[async_trait]
3537    impl content::ResourceContentProvider for TestResourceProvider {
3538        async fn read(
3539            &self,
3540            _uri: &str,
3541        ) -> std::result::Result<String, Box<dyn Error + Send + Sync>> {
3542            match &self.response {
3543                Ok(text) => Ok(text.clone()),
3544                Err(message) => Err(Box::new(TestError(message))),
3545            }
3546        }
3547    }
3548
3549    #[derive(Debug, clap::Parser)]
3550    #[command(name = "exec-cli", subcommand_required = true)]
3551    enum ExecCli {
3552        PrintOnly,
3553        PrintAndText,
3554        Structured,
3555        Echo {
3556            #[arg(long)]
3557            value: String,
3558        },
3559    }
3560
3561    impl ClapMcpToolExecutor for ExecCli {
3562        fn execute_for_mcp(self) -> Result<ClapMcpToolOutput, ClapMcpToolError> {
3563            match self {
3564                Self::PrintOnly => {
3565                    print!("captured only");
3566                    Ok(ClapMcpToolOutput::Text(String::new()))
3567                }
3568                Self::PrintAndText => {
3569                    print!("captured extra");
3570                    Ok(ClapMcpToolOutput::Text("returned text".to_string()))
3571                }
3572                Self::Structured => {
3573                    print!("ignored capture");
3574                    Ok(ClapMcpToolOutput::Structured(json!({ "status": "ok" })))
3575                }
3576                Self::Echo { value } => Ok(ClapMcpToolOutput::Text(value)),
3577            }
3578        }
3579    }
3580
3581    #[test]
3582    fn test_format_panic_payload() {
3583        let s: Box<dyn std::any::Any + Send> = Box::new("hello");
3584        assert_eq!(format_panic_payload(s.as_ref()), "hello");
3585        let s: Box<dyn std::any::Any + Send> = Box::new("world".to_string());
3586        assert_eq!(format_panic_payload(s.as_ref()), "world");
3587        let n: Box<dyn std::any::Any + Send> = Box::new(42i32);
3588        assert_eq!(format_panic_payload(n.as_ref()), "<panic>");
3589    }
3590
3591    #[test]
3592    fn test_mcp_type_for_arg_and_description_hints() {
3593        let boolean_arg = ClapArg {
3594            id: "verbose".to_string(),
3595            long: Some("verbose".to_string()),
3596            short: None,
3597            help: Some("Verbose mode".to_string()),
3598            long_help: None,
3599            required: false,
3600            global: false,
3601            index: None,
3602            action: Some("SetTrue".to_string()),
3603            value_names: vec![],
3604            num_args: None,
3605        };
3606        let (json_type, items) = mcp_type_for_arg(&boolean_arg);
3607        assert_eq!(json_type, json!("boolean"));
3608        assert!(items.is_none());
3609        assert_eq!(
3610            mcp_action_description_hint(&boolean_arg),
3611            Some(" Boolean flag: set to true to pass this flag.".to_string())
3612        );
3613
3614        let false_arg = ClapArg {
3615            action: Some("SetFalse".to_string()),
3616            ..boolean_arg.clone()
3617        };
3618        assert_eq!(mcp_type_for_arg(&false_arg).0, json!("boolean"));
3619        assert_eq!(
3620            mcp_action_description_hint(&false_arg),
3621            Some(" Boolean flag: set to false to pass this flag (e.g. --no-xxx).".to_string())
3622        );
3623
3624        let count_arg = ClapArg {
3625            action: Some("Count".to_string()),
3626            ..boolean_arg.clone()
3627        };
3628        assert_eq!(mcp_type_for_arg(&count_arg).0, json!("integer"));
3629        assert_eq!(
3630            mcp_action_description_hint(&count_arg),
3631            Some(" Number of times the flag is passed (e.g. -vvv).".to_string())
3632        );
3633
3634        let append_arg = ClapArg {
3635            action: Some("Append".to_string()),
3636            value_names: vec!["TAG".to_string()],
3637            ..boolean_arg
3638        };
3639        let (json_type, items) = mcp_type_for_arg(&append_arg);
3640        assert_eq!(json_type, json!("array"));
3641        assert_eq!(
3642            items,
3643            Some(json!({ "type": "string", "description": "A TAG value" }))
3644        );
3645        assert_eq!(
3646            mcp_action_description_hint(&append_arg),
3647            Some(" List of TAG values; pass a JSON array (e.g. [\"a\", \"b\"]).".to_string())
3648        );
3649
3650        let multi_value_arg = ClapArg {
3651            id: "names".to_string(),
3652            long: Some("name".to_string()),
3653            short: None,
3654            help: None,
3655            long_help: None,
3656            required: false,
3657            global: false,
3658            index: None,
3659            action: Some("Set".to_string()),
3660            value_names: vec!["NAME".to_string()],
3661            num_args: Some("1..".to_string()),
3662        };
3663        let (json_type, items) = mcp_type_for_arg(&multi_value_arg);
3664        assert_eq!(json_type, json!("array"));
3665        assert_eq!(
3666            items,
3667            Some(json!({ "type": "string", "description": "A NAME value" }))
3668        );
3669    }
3670
3671    #[test]
3672    fn test_command_to_tool_with_config_reflects_arg_shapes() {
3673        let schema = sample_helper_schema();
3674        let tool = command_to_tool_with_config(
3675            &schema,
3676            &schema.root,
3677            &ClapMcpConfig {
3678                reinvocation_safe: true,
3679                parallel_safe: false,
3680                share_runtime: true,
3681                ..Default::default()
3682            },
3683            &ClapMcpSchemaMetadata::default(),
3684            None,
3685        );
3686
3687        assert_eq!(tool.name, "sample");
3688        assert_eq!(tool.description, None);
3689
3690        let props = tool
3691            .input_schema
3692            .get("properties")
3693            .and_then(|value| value.as_object())
3694            .expect("tool should include input schema properties");
3695        let required = tool
3696            .input_schema
3697            .get("required")
3698            .and_then(|value| value.as_array())
3699            .expect("tool should include required keys");
3700        assert_eq!(
3701            required
3702                .iter()
3703                .filter_map(|value| value.as_str())
3704                .collect::<Vec<_>>(),
3705            vec!["input"]
3706        );
3707        assert_eq!(
3708            props["verbose"]
3709                .get("type")
3710                .and_then(|value| value.as_str()),
3711            Some("boolean")
3712        );
3713        assert!(
3714            props["verbose"]["description"]
3715                .as_str()
3716                .expect("verbose description")
3717                .contains("Boolean flag")
3718        );
3719        assert_eq!(
3720            props["level"].get("type").and_then(|value| value.as_str()),
3721            Some("integer")
3722        );
3723        assert_eq!(
3724            props["tag"].get("type").and_then(|value| value.as_str()),
3725            Some("array")
3726        );
3727        assert_eq!(
3728            props["tag"]["items"]["description"].as_str(),
3729            Some("A TAG value")
3730        );
3731        assert_eq!(
3732            tool.meta
3733                .as_ref()
3734                .and_then(|meta| meta.get("clapMcp"))
3735                .and_then(|value| value.get("shareRuntime"))
3736                .and_then(|value| value.as_bool()),
3737            Some(true)
3738        );
3739    }
3740
3741    #[test]
3742    fn test_validate_required_args_handles_missing_empty_and_flag_values() {
3743        let schema = sample_helper_schema();
3744        let mut provided = serde_json::Map::new();
3745        provided.insert("verbose".to_string(), json!(false));
3746        provided.insert("level".to_string(), json!(0));
3747        provided.insert("input".to_string(), json!("input.txt"));
3748        assert!(validate_required_args(&schema, "sample", &provided).is_ok());
3749
3750        let mut missing_text = serde_json::Map::new();
3751        missing_text.insert("input".to_string(), json!(""));
3752        let error = validate_required_args(&schema, "sample", &missing_text)
3753            .expect_err("empty required string should fail");
3754        assert!(error.contains("Missing required argument(s): input"));
3755
3756        let mut missing_array = serde_json::Map::new();
3757        missing_array.insert("input".to_string(), json!([]));
3758        let error = validate_required_args(&schema, "sample", &missing_array)
3759            .expect_err("empty array should fail");
3760        assert!(error.contains("input"));
3761
3762        assert!(validate_required_args(&schema, "unknown", &serde_json::Map::new()).is_ok());
3763    }
3764
3765    #[test]
3766    fn test_serialize_lock_key_tool_wide() {
3767        let scope = ClapMcpSerializeScope::Tool;
3768        let args = serde_json::Map::new();
3769        assert_eq!(
3770            serialize_lock_key("flush", &args, &scope, None),
3771            "tool:flush"
3772        );
3773    }
3774
3775    #[test]
3776    fn test_serialize_lock_key_arg_scoped() {
3777        let scope = ClapMcpSerializeScope::Args(vec!["output".into()]);
3778        let mut args = serde_json::Map::new();
3779        args.insert("output".into(), json!("abc"));
3780        assert_eq!(
3781            serialize_lock_key("flush", &args, &scope, None),
3782            "tool:flush:output=abc"
3783        );
3784    }
3785
3786    #[test]
3787    fn test_serialize_lock_key_missing_arg_falls_back_to_tool_wide() {
3788        let scope = ClapMcpSerializeScope::Args(vec!["output".into()]);
3789        let args = serde_json::Map::new();
3790        assert_eq!(
3791            serialize_lock_key("flush", &args, &scope, None),
3792            "tool:flush"
3793        );
3794    }
3795
3796    #[test]
3797    fn test_serialize_lock_key_multi_arg_sorted() {
3798        let scope = ClapMcpSerializeScope::Args(vec!["bucket".into(), "region".into()]);
3799        let mut args = serde_json::Map::new();
3800        args.insert("region".into(), json!("us-east"));
3801        args.insert("bucket".into(), json!("logs"));
3802        assert_eq!(
3803            serialize_lock_key("sync", &args, &scope, None),
3804            "tool:sync:bucket=logs:region=us-east"
3805        );
3806    }
3807
3808    #[test]
3809    fn test_serialize_lock_key_typed_topic_fn() {
3810        fn topic(value: &serde_json::Value) -> Option<String> {
3811            String::serialize_topic_segment(value)
3812        }
3813        let scope = ClapMcpSerializeScope::Args(vec!["output".into()]);
3814        let mut fns = std::collections::HashMap::new();
3815        fns.insert("output".to_string(), topic as SerializeTopicSegmentFn);
3816        let mut args = serde_json::Map::new();
3817        args.insert("output".into(), json!("a"));
3818        let key = serialize_lock_key("flush", &args, &scope, Some(&fns));
3819        assert!(key.starts_with("tool:flush:output="));
3820        assert_ne!(key, "tool:flush");
3821    }
3822
3823    #[test]
3824    fn test_serialize_lock_key_typed_topic_fallback_to_json() {
3825        fn bad_parse(_: &serde_json::Value) -> Option<String> {
3826            None
3827        }
3828        let scope = ClapMcpSerializeScope::Args(vec!["output".into()]);
3829        let mut fns = std::collections::HashMap::new();
3830        fns.insert("output".to_string(), bad_parse as SerializeTopicSegmentFn);
3831        let mut args = serde_json::Map::new();
3832        args.insert("output".into(), json!("plain"));
3833        assert_eq!(
3834            serialize_lock_key("flush", &args, &scope, Some(&fns)),
3835            "tool:flush:output=plain"
3836        );
3837    }
3838
3839    #[test]
3840    fn test_serialize_topic_hash_eq_differs_from_json_for_equivalent_values() {
3841        #[derive(Hash, Eq, PartialEq, Deserialize)]
3842        struct Topic(u32);
3843        impl_serialize_topic_hash_eq!(Topic);
3844        let json_key = canonical_lock_arg_value(&json!(1)).unwrap();
3845        let typed_key = Topic::serialize_topic_segment(&json!(1)).unwrap();
3846        assert_ne!(json_key, typed_key);
3847        assert_eq!(
3848            Topic::serialize_topic_segment(&json!(1)),
3849            Topic::serialize_topic_segment(&json!(1))
3850        );
3851    }
3852
3853    #[test]
3854    fn test_canonical_lock_arg_value_object_key_order() {
3855        let a = json!({"b": 2, "a": 1});
3856        let b = json!({"a": 1, "b": 2});
3857        assert_eq!(canonical_lock_arg_value(&a), canonical_lock_arg_value(&b));
3858    }
3859
3860    #[test]
3861    fn test_canonical_lock_arg_value_array_order_matters() {
3862        assert_ne!(
3863            canonical_lock_arg_value(&json!(["a", "b"])),
3864            canonical_lock_arg_value(&json!(["b", "a"]))
3865        );
3866    }
3867
3868    fn command_with_arg_group() -> Command {
3869        Command::new("exec-modes")
3870            .arg(Arg::new("exec").long("exec"))
3871            .arg(Arg::new("exec_batch").long("exec-batch"))
3872            .group(
3873                ArgGroup::new("execs")
3874                    .args(["exec", "exec_batch"])
3875                    .required(true),
3876            )
3877    }
3878
3879    #[test]
3880    fn test_arg_groups_extracted_from_command() {
3881        let schema = schema_from_command(&command_with_arg_group());
3882        assert_eq!(schema.root.name, "exec-modes");
3883        assert_eq!(schema.root.arg_groups.len(), 1);
3884        let group = &schema.root.arg_groups[0];
3885        assert_eq!(group.id, "execs");
3886        assert_eq!(group.args, vec!["exec", "exec_batch"]);
3887        assert!(group.required);
3888        assert!(!group.multiple);
3889    }
3890
3891    #[test]
3892    fn test_arg_groups_meta_in_list_tools() {
3893        let schema = schema_from_command(&command_with_arg_group());
3894        let tools = tools_from_schema_with_metadata(
3895            &schema,
3896            &ClapMcpConfig::default(),
3897            &ClapMcpSchemaMetadata::default(),
3898        );
3899        let tool = tools
3900            .iter()
3901            .find(|t| t.name == "exec-modes")
3902            .expect("exec-modes tool");
3903        let arg_groups = tool
3904            .meta
3905            .as_ref()
3906            .and_then(|meta| meta.get("clapMcp"))
3907            .and_then(|value| value.get("argGroups"))
3908            .and_then(|value| value.as_array())
3909            .expect("argGroups meta");
3910        assert_eq!(arg_groups.len(), 1);
3911        assert_eq!(
3912            arg_groups[0].get("id").and_then(|v| v.as_str()),
3913            Some("execs")
3914        );
3915        let args = arg_groups[0]
3916            .get("args")
3917            .and_then(|v| v.as_array())
3918            .expect("args array");
3919        assert_eq!(
3920            args.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>(),
3921            vec!["exec", "exec_batch"]
3922        );
3923    }
3924
3925    #[test]
3926    fn test_arg_groups_skip_filters_members() {
3927        let mut metadata = ClapMcpSchemaMetadata::default();
3928        metadata
3929            .skip_args
3930            .insert("exec-modes".into(), vec!["exec_batch".into()]);
3931        let schema = schema_from_command_with_metadata(&command_with_arg_group(), &metadata);
3932        assert!(
3933            schema.root.arg_groups.is_empty(),
3934            "group with one visible member should be omitted"
3935        );
3936    }
3937
3938    #[test]
3939    fn test_arg_groups_omitted_when_empty() {
3940        let schema = sample_helper_schema();
3941        let tools = tools_from_schema_with_metadata(
3942            &schema,
3943            &ClapMcpConfig::default(),
3944            &ClapMcpSchemaMetadata::default(),
3945        );
3946        for tool in &tools {
3947            let has_arg_groups = tool
3948                .meta
3949                .as_ref()
3950                .and_then(|meta| meta.get("clapMcp"))
3951                .and_then(|value| value.get("argGroups"))
3952                .is_some();
3953            assert!(!has_arg_groups, "tool {} should omit argGroups", tool.name);
3954        }
3955    }
3956
3957    #[test]
3958    fn test_arg_group_description_suffix() {
3959        let schema = schema_from_command(&command_with_arg_group());
3960        let tool = command_to_tool_with_config(
3961            &schema,
3962            &schema.root,
3963            &ClapMcpConfig::default(),
3964            &ClapMcpSchemaMetadata::default(),
3965            None,
3966        );
3967        let description = tool
3968            .description
3969            .as_ref()
3970            .map(|d| d.to_string())
3971            .expect("description");
3972        assert!(description.contains("Arg groups (parse-time)"));
3973        assert!(description.contains("`execs` requires one of"));
3974        assert!(description.contains("`exec`"));
3975        assert!(description.contains("`exec_batch`"));
3976    }
3977
3978    #[test]
3979    fn test_arg_groups_per_command_node() {
3980        let cmd = Command::new("root")
3981            .arg(Arg::new("root_a").long("root-a"))
3982            .arg(Arg::new("root_b").long("root-b"))
3983            .group(ArgGroup::new("root_group").args(["root_a", "root_b"]))
3984            .subcommand(
3985                Command::new("leaf")
3986                    .arg(Arg::new("leaf_x").long("leaf-x"))
3987                    .arg(Arg::new("leaf_y").long("leaf-y"))
3988                    .group(ArgGroup::new("leaf_group").args(["leaf_x", "leaf_y"])),
3989            );
3990        let schema = schema_from_command(&cmd);
3991        assert_eq!(schema.root.arg_groups.len(), 1);
3992        assert_eq!(schema.root.arg_groups[0].id, "root_group");
3993        let leaf = schema
3994            .root
3995            .subcommands
3996            .iter()
3997            .find(|c| c.name == "leaf")
3998            .expect("leaf subcommand");
3999        assert_eq!(leaf.arg_groups.len(), 1);
4000        assert_eq!(leaf.arg_groups[0].id, "leaf_group");
4001
4002        let tools = tools_from_schema_with_metadata(
4003            &schema,
4004            &ClapMcpConfig::default(),
4005            &ClapMcpSchemaMetadata::default(),
4006        );
4007        let root_tool = tools.iter().find(|t| t.name == "root").expect("root tool");
4008        let leaf_tool = tools.iter().find(|t| t.name == "leaf").expect("leaf tool");
4009        let root_groups = root_tool
4010            .meta
4011            .as_ref()
4012            .and_then(|m| m.get("clapMcp"))
4013            .and_then(|v| v.get("argGroups"))
4014            .and_then(|v| v.as_array())
4015            .expect("root argGroups");
4016        assert_eq!(
4017            root_groups[0].get("id").and_then(|v| v.as_str()),
4018            Some("root_group")
4019        );
4020        let leaf_groups = leaf_tool
4021            .meta
4022            .as_ref()
4023            .and_then(|m| m.get("clapMcp"))
4024            .and_then(|v| v.get("argGroups"))
4025            .and_then(|v| v.as_array())
4026            .expect("leaf argGroups");
4027        assert_eq!(
4028            leaf_groups[0].get("id").and_then(|v| v.as_str()),
4029            Some("leaf_group")
4030        );
4031    }
4032
4033    #[test]
4034    fn test_tools_from_schema_serializes_meta() {
4035        let schema = sample_helper_schema();
4036        let config = ClapMcpConfig {
4037            reinvocation_safe: true,
4038            parallel_safe: true,
4039            ..Default::default()
4040        };
4041        let mut metadata = ClapMcpSchemaMetadata::default();
4042        metadata.serialize_tools.insert(
4043            "sample".into(),
4044            ClapMcpSerializeScope::Args(vec!["input".into()]),
4045        );
4046        let tools = tools_from_schema_with_metadata(&schema, &config, &metadata);
4047        let tool = tools
4048            .iter()
4049            .find(|t| t.name == "sample")
4050            .expect("sample tool");
4051        let clap_mcp = tool
4052            .meta
4053            .as_ref()
4054            .and_then(|meta| meta.get("clapMcp"))
4055            .and_then(|value| value.as_object())
4056            .expect("clapMcp meta");
4057        assert_eq!(
4058            clap_mcp.get("serialized").and_then(|v| v.as_bool()),
4059            Some(true)
4060        );
4061        assert_eq!(
4062            clap_mcp.get("serializeScope").and_then(|v| v.as_str()),
4063            Some("args")
4064        );
4065        assert_eq!(
4066            clap_mcp.get("serializeArgs").and_then(|v| v.as_array()),
4067            Some(&vec![json!("input")])
4068        );
4069    }
4070
4071    #[test]
4072    fn test_build_tool_argv_handles_positional_flags_and_lists() {
4073        let schema = sample_helper_schema();
4074        let arguments = serde_json::Map::from_iter([
4075            ("input".to_string(), json!("input.txt")),
4076            ("verbose".to_string(), json!(true)),
4077            ("no-cache".to_string(), json!(false)),
4078            ("level".to_string(), json!(2)),
4079            ("tag".to_string(), json!(["alpha", "", "beta"])),
4080            ("mode".to_string(), json!("fast")),
4081        ]);
4082
4083        let argv = build_tool_argv(&schema, "sample", arguments);
4084        assert_eq!(
4085            argv,
4086            vec![
4087                "input.txt",
4088                "--level",
4089                "--level",
4090                "--mode",
4091                "fast",
4092                "--no-cache",
4093                "--tag",
4094                "alpha",
4095                "--tag",
4096                "beta",
4097                "--verbose",
4098            ]
4099        );
4100    }
4101
4102    fn passthrough_exec_command(allow_hyphen_values: bool) -> Command {
4103        let mut trailing = Arg::new("command").last(true).num_args(1..);
4104        if allow_hyphen_values {
4105            trailing = trailing.allow_hyphen_values(true);
4106        }
4107        Command::new("passthrough-args").subcommand(
4108            Command::new("exec")
4109                .arg(
4110                    Arg::new("dry_run")
4111                        .long("dry-run")
4112                        .action(ArgAction::SetTrue),
4113                )
4114                .arg(trailing),
4115        )
4116    }
4117
4118    #[test]
4119    fn test_build_tool_argv_trailing_vec_with_hyphen_tokens() {
4120        let schema = schema_from_command(&passthrough_exec_command(true));
4121        let arguments = serde_json::Map::from_iter([
4122            ("dry_run".to_string(), json!(false)),
4123            ("command".to_string(), json!(["-v", "--mcp", "hello"])),
4124        ]);
4125        let argv = build_tool_argv(&schema, "exec", arguments);
4126        assert_eq!(argv, vec!["--", "-v", "--mcp", "hello"]);
4127    }
4128
4129    #[test]
4130    fn test_build_argv_round_trip_with_hyphen_trailing_vec() {
4131        let cmd = passthrough_exec_command(true);
4132        let schema = schema_from_command(&cmd);
4133        let arguments = serde_json::Map::from_iter([
4134            ("dry_run".to_string(), json!(true)),
4135            ("command".to_string(), json!(["-v", "hello"])),
4136        ]);
4137        let argv = build_argv_for_clap(&schema, "exec", arguments);
4138        let matches = cmd
4139            .try_get_matches_from(argv)
4140            .expect("trailing vec with -- separator should parse hyphen tokens");
4141        let sub = matches.subcommand().expect("exec subcommand");
4142        assert!(sub.1.get_flag("dry_run"));
4143        assert_eq!(
4144            sub.1
4145                .get_many::<String>("command")
4146                .into_iter()
4147                .flatten()
4148                .map(|s| s.as_str())
4149                .collect::<Vec<_>>(),
4150            vec!["-v", "hello"]
4151        );
4152    }
4153
4154    #[test]
4155    fn test_build_argv_round_trip_with_trailing_vec() {
4156        let cmd = passthrough_exec_command(true);
4157        let schema = schema_from_command(&cmd);
4158        let arguments = serde_json::Map::from_iter([
4159            ("dry_run".to_string(), json!(true)),
4160            ("command".to_string(), json!(["echo", "hello"])),
4161        ]);
4162        let argv = build_argv_for_clap(&schema, "exec", arguments);
4163        assert_eq!(
4164            argv,
4165            vec![
4166                "cli".to_string(),
4167                "exec".to_string(),
4168                "--dry-run".to_string(),
4169                "--".to_string(),
4170                "echo".to_string(),
4171                "hello".to_string(),
4172            ]
4173        );
4174        let matches = cmd
4175            .try_get_matches_from(argv)
4176            .expect("trailing vec after flags should parse");
4177        let sub = matches.subcommand().expect("exec subcommand");
4178        assert_eq!(sub.0, "exec");
4179        assert!(sub.1.get_flag("dry_run"));
4180        assert_eq!(
4181            sub.1
4182                .get_many::<String>("command")
4183                .into_iter()
4184                .flatten()
4185                .map(|s| s.as_str())
4186                .collect::<Vec<_>>(),
4187            vec!["echo", "hello"]
4188        );
4189    }
4190
4191    #[test]
4192    fn test_build_argv_hyphen_trailing_fails_without_end_of_opts() {
4193        let cmd = passthrough_exec_command(false);
4194        let err = cmd
4195            .try_get_matches_from(["cli", "exec", "-v", "hello"])
4196            .expect_err("trailing hyphen tokens without -- should fail clap parse");
4197        assert!(
4198            err.to_string().contains("unexpected argument")
4199                || err.to_string().contains("unknown argument")
4200                || err.to_string().contains("found argument"),
4201            "unexpected error: {err}"
4202        );
4203    }
4204
4205    #[test]
4206    fn test_value_to_string_and_value_to_strings_cover_scalar_and_array_inputs() {
4207        assert_eq!(value_to_string(&json!("hello")), Some("hello".to_string()));
4208        assert_eq!(value_to_string(&json!(3)), Some("3".to_string()));
4209        assert_eq!(value_to_string(&json!(false)), Some("false".to_string()));
4210        assert_eq!(value_to_string(&serde_json::Value::Null), None);
4211        assert_eq!(
4212            value_to_string(&json!({"name":"sample"})),
4213            Some("{\"name\":\"sample\"}".to_string())
4214        );
4215
4216        assert_eq!(
4217            value_to_strings(&json!(["alpha", "", 3, null, false])),
4218            Some(vec![
4219                "alpha".to_string(),
4220                "3".to_string(),
4221                "false".to_string()
4222            ])
4223        );
4224        assert_eq!(
4225            value_to_strings(&json!("solo")),
4226            Some(vec!["solo".to_string()])
4227        );
4228        assert_eq!(value_to_strings(&serde_json::Value::Null), None);
4229    }
4230
4231    #[test]
4232    fn test_command_flag_helpers_are_idempotent() {
4233        let cmd = command_with_mcp_flag(command_with_mcp_flag(Command::new("sample")));
4234        let mcp_args = cmd
4235            .get_arguments()
4236            .filter(|arg| arg.get_long() == Some(MCP_FLAG_LONG))
4237            .count();
4238        assert_eq!(mcp_args, 1);
4239
4240        let cmd = command_with_export_skills_flag(command_with_export_skills_flag(Command::new(
4241            "sample",
4242        )));
4243        let export_args = cmd
4244            .get_arguments()
4245            .filter(|arg| arg.get_long() == Some(EXPORT_SKILLS_FLAG_LONG))
4246            .count();
4247        assert_eq!(export_args, 1);
4248
4249        let cmd = command_with_mcp_and_export_skills_flags(Command::new("bare"));
4250        assert_eq!(
4251            cmd.get_arguments()
4252                .filter(|arg| arg.get_long() == Some(MCP_FLAG_LONG))
4253                .count(),
4254            1
4255        );
4256        assert_eq!(
4257            cmd.get_arguments()
4258                .filter(|arg| arg.get_long() == Some(EXPORT_SKILLS_FLAG_LONG))
4259                .count(),
4260            1
4261        );
4262    }
4263
4264    #[test]
4265    fn test_argv_export_skills_dir_from_args() {
4266        let flags = ClapMcpBuiltinFlags::default();
4267        assert!(argv_export_skills_dir_from_args(&[], &flags).is_none());
4268        assert!(argv_export_skills_dir_from_args(&["--other".to_string()], &flags).is_none());
4269        assert_eq!(
4270            argv_export_skills_dir_from_args(&["--export-skills".to_string()], &flags),
4271            Some(None)
4272        );
4273        assert_eq!(
4274            argv_export_skills_dir_from_args(
4275                &["--export-skills".to_string(), "out".to_string()],
4276                &flags
4277            ),
4278            Some(Some(std::path::PathBuf::from("out")))
4279        );
4280        assert_eq!(
4281            argv_export_skills_dir_from_args(
4282                &["--export-skills".to_string(), "--mcp".to_string()],
4283                &flags
4284            ),
4285            Some(None)
4286        );
4287        assert_eq!(
4288            argv_export_skills_dir_from_args(&["--export-skills=out".to_string()], &flags),
4289            Some(Some(std::path::PathBuf::from("out")))
4290        );
4291        assert!(
4292            argv_export_skills_dir_from_args(
4293                &[
4294                    "run".to_string(),
4295                    "--".to_string(),
4296                    "--export-skills".to_string()
4297                ],
4298                &flags
4299            )
4300            .is_none(),
4301            "export-skills after -- must not trigger"
4302        );
4303    }
4304
4305    #[test]
4306    fn test_argv_before_end_of_opts() {
4307        let args = vec!["run".to_string(), "--".to_string(), "--mcp".to_string()];
4308        assert_eq!(
4309            argv_before_end_of_opts(&args),
4310            &["run".to_string()] as &[String]
4311        );
4312    }
4313
4314    #[test]
4315    fn test_argv_contains_clap_mcp_flags() {
4316        let flags = ClapMcpBuiltinFlags::default();
4317        assert!(!argv_contains_clap_mcp_flags(&[], &flags));
4318        assert!(!argv_contains_clap_mcp_flags(
4319            &["run".to_string(), "hello".to_string()],
4320            &flags
4321        ));
4322        assert!(argv_contains_clap_mcp_flags(&["--mcp".to_string()], &flags));
4323        assert!(argv_contains_clap_mcp_flags(
4324            &["--export-skills".to_string()],
4325            &flags
4326        ));
4327        assert!(argv_contains_clap_mcp_flags(
4328            &["run".to_string(), "--mcp".to_string()],
4329            &flags
4330        ));
4331        assert!(!argv_contains_clap_mcp_flags(
4332            &["run".to_string(), "--".to_string(), "--mcp".to_string()],
4333            &flags
4334        ));
4335        let custom = ClapMcpBuiltinFlags::default().with_stdio_long("modelcontextprotocol");
4336        assert!(argv_contains_clap_mcp_flags(
4337            &["--modelcontextprotocol".to_string()],
4338            &custom
4339        ));
4340        assert!(!argv_contains_clap_mcp_flags(
4341            &["--mcp".to_string()],
4342            &custom
4343        ));
4344        #[cfg(feature = "http")]
4345        {
4346            assert!(argv_contains_clap_mcp_flags(
4347                &["--mcp-http".to_string()],
4348                &flags
4349            ));
4350            assert!(argv_contains_clap_mcp_flags(
4351                &["--mcp-http".to_string(), "127.0.0.1:8080".to_string()],
4352                &flags
4353            ));
4354        }
4355    }
4356
4357    #[test]
4358    fn test_argv_requests_mcp_without_subcommand_from_args() {
4359        let cmd = Command::new("app").subcommand(Command::new("run"));
4360        let flags = ClapMcpBuiltinFlags::default();
4361        assert!(argv_requests_mcp_without_subcommand_from_args(
4362            &["--mcp".to_string()],
4363            &cmd,
4364            &flags
4365        ));
4366        assert!(!argv_requests_mcp_without_subcommand_from_args(
4367            &["--mcp".to_string(), "run".to_string()],
4368            &cmd,
4369            &flags
4370        ));
4371        assert!(!argv_requests_mcp_without_subcommand_from_args(
4372            &["run".to_string()],
4373            &cmd,
4374            &flags
4375        ));
4376        assert!(!argv_requests_mcp_without_subcommand_from_args(
4377            &[],
4378            &cmd,
4379            &flags
4380        ));
4381        assert!(!argv_requests_mcp_without_subcommand_from_args(
4382            &["run".to_string(), "--".to_string(), "--mcp".to_string()],
4383            &cmd,
4384            &flags
4385        ));
4386        assert!(!argv_requests_mcp_without_subcommand_from_args(
4387            &["run".to_string(), "--mcp".to_string()],
4388            &cmd,
4389            &flags
4390        ));
4391        assert!(argv_requests_mcp_without_subcommand_from_args(
4392            &["--mcp".to_string(), "--".to_string(), "--mcp".to_string()],
4393            &cmd,
4394            &flags
4395        ));
4396    }
4397
4398    #[test]
4399    fn test_argv_requests_mcp_custom_stdio_long() {
4400        let cmd = Command::new("app");
4401        let flags = ClapMcpBuiltinFlags::default().with_stdio_long("modelcontextprotocol");
4402        assert!(argv_requests_mcp_without_subcommand_from_args(
4403            &["--modelcontextprotocol".to_string()],
4404            &cmd,
4405            &flags
4406        ));
4407        assert!(!argv_requests_mcp_without_subcommand_from_args(
4408            &["--mcp".to_string()],
4409            &cmd,
4410            &flags
4411        ));
4412        assert!(!argv_requests_mcp_without_subcommand_from_args(
4413            &[
4414                "run".to_string(),
4415                "--".to_string(),
4416                "--modelcontextprotocol".to_string(),
4417            ],
4418            &cmd,
4419            &flags
4420        ));
4421    }
4422
4423    #[test]
4424    fn test_is_builtin_arg() {
4425        assert!(is_builtin_arg("help"));
4426        assert!(is_builtin_arg("version"));
4427        assert!(is_builtin_arg(CLAP_MCP_STDIO_FLAG_ID));
4428        assert!(!is_builtin_arg(CLAP_MCP_STDIO_FLAG_ID_LEGACY));
4429        assert!(is_builtin_arg(EXPORT_SKILLS_FLAG_LONG));
4430        assert!(!is_builtin_arg("input"));
4431        assert!(!is_builtin_arg("path"));
4432        assert!(!is_builtin_arg("mcp"), "user mcp field must not be builtin");
4433    }
4434
4435    #[test]
4436    fn test_tools_from_schema_with_metadata() {
4437        let schema = sample_helper_schema();
4438        let tools = tools_from_schema_with_metadata(
4439            &schema,
4440            &ClapMcpConfig::default(),
4441            &ClapMcpSchemaMetadata::default(),
4442        );
4443        assert!(!tools.is_empty());
4444    }
4445
4446    #[cfg(feature = "http")]
4447    #[test]
4448    fn test_mcp_http_listen_from_env_and_flag_alone() {
4449        let listen_key = MCP_HTTP_LISTEN_ENV;
4450        let bind_key = MCP_HTTP_BIND_ENV;
4451        let port_key = MCP_HTTP_PORT_ENV;
4452
4453        let flags = ClapMcpBuiltinFlags::default();
4454        unsafe {
4455            std::env::set_var(listen_key, "127.0.0.1:9090");
4456        }
4457        assert_eq!(
4458            argv_mcp_http_listen_from_args(&["--mcp-http".to_string()], &flags),
4459            Some("127.0.0.1:9090".to_string())
4460        );
4461        unsafe {
4462            std::env::remove_var(listen_key);
4463        }
4464
4465        unsafe {
4466            std::env::set_var(bind_key, "127.0.0.1");
4467            std::env::set_var(port_key, "9091");
4468        }
4469        assert_eq!(
4470            argv_mcp_http_listen_from_args(&["--mcp-http".to_string()], &flags),
4471            Some("127.0.0.1:9091".to_string())
4472        );
4473        unsafe {
4474            std::env::remove_var(bind_key);
4475            std::env::remove_var(port_key);
4476        }
4477    }
4478
4479    #[cfg(feature = "http")]
4480    #[test]
4481    fn test_http_flag_helpers_cover_command_and_argv_shapes() {
4482        use clap::Command;
4483
4484        let cmd = command_with_mcp_http_flag(Command::new("app"));
4485        assert!(
4486            cmd.get_arguments()
4487                .any(|a| a.get_long() == Some(MCP_HTTP_FLAG_LONG))
4488        );
4489
4490        let flags = ClapMcpBuiltinFlags::default();
4491        assert_eq!(
4492            argv_mcp_http_listen_from_args(
4493                &["--mcp-http".to_string(), "127.0.0.1:4242".to_string()],
4494                &flags
4495            ),
4496            Some("127.0.0.1:4242".to_string())
4497        );
4498        assert_eq!(
4499            argv_mcp_http_listen_from_args(&["--mcp-http=10.0.0.1:9".to_string()], &flags),
4500            Some("10.0.0.1:9".to_string())
4501        );
4502
4503        let mut cmd = Command::new("app");
4504        cmd = cmd.arg(
4505            clap::Arg::new(CLAP_MCP_HTTP_FLAG_ID)
4506                .long(MCP_HTTP_FLAG_LONG)
4507                .global(true),
4508        );
4509        let unchanged = command_with_mcp_http_flag_with_flags(cmd, &flags);
4510        assert_eq!(unchanged.get_arguments().count(), 1);
4511
4512        let matches = Command::new("app")
4513            .arg(
4514                clap::Arg::new(CLAP_MCP_HTTP_FLAG_ID)
4515                    .long(MCP_HTTP_FLAG_LONG)
4516                    .value_name("ADDR")
4517                    .global(true),
4518            )
4519            .get_matches_from(["app", "--mcp-http", "127.0.0.1:1"]);
4520        assert!(matches_http_flag(&matches, &flags));
4521        assert!(is_builtin_arg(CLAP_MCP_HTTP_FLAG_ID));
4522    }
4523
4524    #[cfg(feature = "http")]
4525    #[test]
4526    fn test_parse_mcp_http_listen_reports_invalid_addresses() {
4527        let err = parse_mcp_http_listen("not-an-address").expect_err("invalid listen");
4528        assert!(
4529            matches!(err, ClapMcpError::InvalidConfig(message) if message.contains("invalid MCP HTTP listen"))
4530        );
4531        assert!(
4532            mcp_http_listen_error_message(&ClapMcpBuiltinFlags::default())
4533                .contains(MCP_HTTP_LISTEN_ENV)
4534        );
4535    }
4536
4537    #[cfg(feature = "http")]
4538    #[test]
4539    fn test_resolve_mcp_http_listen_from_args_covers_success_and_errors() {
4540        let flags = ClapMcpBuiltinFlags::default();
4541        assert_eq!(
4542            resolve_mcp_http_listen_from_args(
4543                &["--mcp-http".to_string(), "127.0.0.1:4242".to_string()],
4544                &flags
4545            )
4546            .expect("listen should parse")
4547            .map(|addr| addr.to_string()),
4548            Some("127.0.0.1:4242".to_string())
4549        );
4550        assert!(
4551            resolve_mcp_http_listen_from_args(&["--help".to_string()], &flags)
4552                .expect("no http flag")
4553                .is_none()
4554        );
4555        let missing = resolve_mcp_http_listen_from_args(&["--mcp-http".to_string()], &flags)
4556            .expect_err("missing host:port should error");
4557        assert!(
4558            matches!(missing, ClapMcpError::InvalidConfig(message) if message.contains(MCP_HTTP_LISTEN_ENV))
4559        );
4560    }
4561
4562    #[cfg(feature = "http")]
4563    #[test]
4564    fn test_argv_requests_mcp_http_without_subcommand_from_args() {
4565        let cmd = Command::new("app").subcommand(Command::new("run"));
4566        let flags = ClapMcpBuiltinFlags::default();
4567        assert!(argv_requests_mcp_http_without_subcommand_from_args(
4568            &["--mcp-http".to_string(), "127.0.0.1:1".to_string()],
4569            &cmd,
4570            &flags
4571        ));
4572        assert!(!argv_requests_mcp_http_without_subcommand_from_args(
4573            &[
4574                "run".to_string(),
4575                "--mcp-http".to_string(),
4576                "127.0.0.1:1".to_string()
4577            ],
4578            &cmd,
4579            &flags
4580        ));
4581    }
4582
4583    #[tokio::test]
4584    async fn test_serve_mcp_fails_fast_on_invalid_schema_json() {
4585        let err = serve_mcp(
4586            McpListen::Stdio,
4587            "not-json".to_string(),
4588            None,
4589            ClapMcpConfig::default(),
4590            None,
4591            ClapMcpServeOptions::default(),
4592            &ClapMcpSchemaMetadata::default(),
4593        )
4594        .await
4595        .expect_err("invalid schema should fail");
4596        assert!(matches!(err, ClapMcpError::SchemaJson(_)));
4597    }
4598
4599    #[test]
4600    fn test_ambiguous_positional_scalars_build_swapped_argv() {
4601        use clap::{FromArgMatches, Parser, Subcommand};
4602
4603        #[derive(Debug, Subcommand)]
4604        enum Cmd {
4605            Edit { task_id: String, state: String },
4606        }
4607
4608        #[derive(Debug, Parser)]
4609        #[command(subcommand_required = true)]
4610        struct App {
4611            #[command(subcommand)]
4612            cmd: Cmd,
4613        }
4614
4615        let schema = schema_from_command(&App::command());
4616        let args = serde_json::Map::from_iter([
4617            ("task_id".to_string(), json!("done")),
4618            ("state".to_string(), json!("TASK-0")),
4619        ]);
4620        let argv = build_argv_for_clap(&schema, "edit", args);
4621        assert_eq!(argv, vec!["cli", "edit", "TASK-0", "done"]);
4622
4623        let matches = App::command().get_matches_from(argv);
4624        let parsed = App::from_arg_matches(&matches).expect("app should parse");
4625        match parsed.cmd {
4626            Cmd::Edit { task_id, state } => {
4627                assert_eq!(task_id, "TASK-0");
4628                assert_eq!(state, "done");
4629            }
4630        }
4631    }
4632
4633    #[test]
4634    fn test_command_path_and_build_argv_for_clap() {
4635        let schema = nested_schema();
4636        assert_eq!(command_path(&schema, "sample"), Some(vec!["sample".into()]));
4637        assert_eq!(
4638            command_path(&schema, "child"),
4639            Some(vec!["sample".into(), "parent".into(), "child".into()])
4640        );
4641        assert_eq!(command_path(&schema, "nonexistent"), None);
4642
4643        let args = serde_json::Map::from_iter([("value".to_string(), json!("v"))]);
4644        let argv = build_argv_for_clap(&schema, "child", args);
4645        assert_eq!(argv[0], "cli");
4646        assert_eq!(argv[1], "parent");
4647        assert_eq!(argv[2], "child");
4648        assert!(argv.contains(&"--value".to_string()));
4649        assert!(argv.contains(&"v".to_string()));
4650
4651        let empty_argv = build_tool_argv(&schema, "nonexistent", serde_json::Map::new());
4652        assert!(empty_argv.is_empty());
4653    }
4654
4655    #[cfg(not(feature = "output-schema"))]
4656    #[test]
4657    fn test_output_schema_for_type_without_schemars() {
4658        assert!(output_schema_for_type::<()>().is_none());
4659    }
4660
4661    #[cfg(feature = "output-schema")]
4662    #[test]
4663    fn test_output_schema_for_type_with_schemars() {
4664        use schemars::JsonSchema;
4665        #[derive(JsonSchema)]
4666        struct Dummy {
4667            _x: i32,
4668        }
4669        let schema = output_schema_for_type::<Dummy>();
4670        assert!(schema.is_some());
4671    }
4672
4673    #[tokio::test]
4674    async fn test_resource_helpers_cover_builtin_custom_and_error_paths() {
4675        let custom = vec![content::CustomResource {
4676            uri: "test://dynamic".to_string(),
4677            name: "dynamic".to_string(),
4678            title: None,
4679            description: Some("dynamic resource".to_string()),
4680            mime_type: Some("text/plain".to_string()),
4681            content: content::ResourceContent::Dynamic(Arc::new(TestResourceProvider {
4682                response: Ok("dynamic body".to_string()),
4683            })),
4684        }];
4685
4686        let listed = list_resources_result(&custom);
4687        assert_eq!(listed.resources.len(), 2);
4688        assert_eq!(listed.resources[0].uri, MCP_RESOURCE_URI_SCHEMA);
4689        assert_eq!(listed.resources[1].uri, "test://dynamic");
4690
4691        let schema_read = read_resource_result(
4692            "{\"name\":\"sample\"}",
4693            &custom,
4694            ReadResourceRequestParams::new(MCP_RESOURCE_URI_SCHEMA),
4695        )
4696        .await
4697        .expect("schema resource should resolve");
4698        let text = match &schema_read.contents[0] {
4699            ResourceContents::TextResourceContents { text, .. } => text,
4700            other => panic!("unexpected content: {other:?}"),
4701        };
4702        assert!(text.contains("\"name\":\"sample\""));
4703
4704        let custom_read = read_resource_result(
4705            "{}",
4706            &custom,
4707            ReadResourceRequestParams::new("test://dynamic"),
4708        )
4709        .await
4710        .expect("custom resource should resolve");
4711        let text = match &custom_read.contents[0] {
4712            ResourceContents::TextResourceContents { text, .. } => text,
4713            other => panic!("unexpected content: {other:?}"),
4714        };
4715        assert_eq!(text, "dynamic body");
4716
4717        let missing = read_resource_result(
4718            "{}",
4719            &custom,
4720            ReadResourceRequestParams::new("test://missing"),
4721        )
4722        .await
4723        .expect_err("missing resource should error");
4724        assert!(missing.message.contains("unknown resource uri"));
4725
4726        let failing_resources = vec![content::CustomResource {
4727            uri: "test://broken".to_string(),
4728            name: "broken".to_string(),
4729            title: None,
4730            description: None,
4731            mime_type: None,
4732            content: content::ResourceContent::Dynamic(Arc::new(TestResourceProvider {
4733                response: Err("read failed"),
4734            })),
4735        }];
4736        let failing = read_resource_result(
4737            "{}",
4738            &failing_resources,
4739            ReadResourceRequestParams::new("test://broken"),
4740        )
4741        .await
4742        .expect_err("provider failure should map to rpc error");
4743        assert_eq!(failing.message, "read failed");
4744    }
4745
4746    #[tokio::test]
4747    async fn test_prompt_helpers_cover_logging_custom_and_error_paths() {
4748        let provider = Arc::new(TestPromptProvider {
4749            response: Ok(vec![PromptMessage::new_text(
4750                PromptMessageRole::User,
4751                "dynamic prompt",
4752            )]),
4753            seen: Mutex::new(Vec::new()),
4754        });
4755        let prompts = vec![content::CustomPrompt {
4756            name: "dynamic".to_string(),
4757            title: Some("Dynamic".to_string()),
4758            description: Some("dynamic prompt".to_string()),
4759            arguments: vec![],
4760            content: content::PromptContent::Dynamic(provider.clone()),
4761        }];
4762
4763        let listed = list_prompts_result(true, &prompts);
4764        assert_eq!(listed.prompts.len(), 2);
4765        assert_eq!(listed.prompts[0].name, PROMPT_LOGGING_GUIDE);
4766        assert_eq!(listed.prompts[1].name, "dynamic");
4767
4768        let logging_prompt = get_prompt_result(
4769            true,
4770            &prompts,
4771            GetPromptRequestParams::new(PROMPT_LOGGING_GUIDE),
4772        )
4773        .await
4774        .expect("logging guide should resolve");
4775        assert!(prompt_text(&logging_prompt.messages[0].content).contains("logger"));
4776
4777        let mut topic_args = serde_json::Map::new();
4778        topic_args.insert("topic".into(), json!("coverage"));
4779        let dynamic_prompt = get_prompt_result(
4780            false,
4781            &prompts,
4782            GetPromptRequestParams::new("dynamic").with_arguments(topic_args),
4783        )
4784        .await
4785        .expect("dynamic prompt should resolve");
4786        assert_eq!(
4787            dynamic_prompt.description.as_deref(),
4788            Some("dynamic prompt")
4789        );
4790        assert_eq!(
4791            provider
4792                .seen
4793                .lock()
4794                .expect("provider seen mutex should lock")[0]
4795                .1
4796                .get("topic")
4797                .and_then(|value| value.as_str()),
4798            Some("coverage")
4799        );
4800
4801        let unknown_logging = get_prompt_result(
4802            false,
4803            &prompts,
4804            GetPromptRequestParams::new(PROMPT_LOGGING_GUIDE),
4805        )
4806        .await
4807        .expect_err("logging guide should error when logging disabled");
4808        assert!(unknown_logging.message.contains("unknown prompt"));
4809
4810        let failing_prompts = vec![content::CustomPrompt {
4811            name: "broken".to_string(),
4812            title: None,
4813            description: None,
4814            arguments: vec![],
4815            content: content::PromptContent::Dynamic(Arc::new(TestPromptProvider {
4816                response: Err("prompt failed"),
4817                seen: Mutex::new(Vec::new()),
4818            })),
4819        }];
4820        let failing = get_prompt_result(
4821            false,
4822            &failing_prompts,
4823            GetPromptRequestParams::new("broken"),
4824        )
4825        .await
4826        .expect_err("provider failure should map to rpc error");
4827        assert_eq!(failing.message, "prompt failed");
4828    }
4829
4830    #[test]
4831    fn test_call_tool_result_helpers_cover_text_structured_errors_and_panics() {
4832        let text = call_tool_result_from_output(ClapMcpToolOutput::Text("hello".to_string()));
4833        assert_ne!(text.is_error, Some(true));
4834        assert_eq!(content_text(&text.content[0]), "hello");
4835
4836        let structured = call_tool_result_from_output(ClapMcpToolOutput::Structured(json!({
4837            "sum": 5
4838        })));
4839        assert_eq!(
4840            structured
4841                .structured_content
4842                .as_ref()
4843                .and_then(|content| content.get("sum"))
4844                .and_then(|value| value.as_i64()),
4845            Some(5)
4846        );
4847        assert!(content_text(&structured.content[0]).contains("\"sum\": 5"));
4848
4849        let array_value = call_tool_result_from_output(ClapMcpToolOutput::Structured(json!(["a"])));
4850        assert_eq!(array_value.structured_content.as_ref(), Some(&json!(["a"])));
4851
4852        let error = call_tool_result_from_tool_error(ClapMcpToolError::structured(
4853            "bad",
4854            json!({ "code": 7 }),
4855        ));
4856        assert_eq!(error.is_error, Some(true));
4857        assert_eq!(
4858            error
4859                .structured_content
4860                .as_ref()
4861                .and_then(|content| content.get("code"))
4862                .and_then(|value| value.as_i64()),
4863            Some(7)
4864        );
4865
4866        let panic_payload: Box<dyn std::any::Any + Send> = Box::new("boom");
4867        let panic_result = call_tool_result_from_panic(panic_payload.as_ref());
4868        assert_eq!(panic_result.is_error, Some(true));
4869        assert!(content_text(&panic_result.content[0]).contains("Tool panicked: boom"));
4870    }
4871
4872    #[test]
4873    fn test_subprocess_helpers_cover_command_building_logging_and_result_shapes() {
4874        let schema = nested_schema();
4875        let args = serde_json::Map::from_iter([(
4876            "value".to_string(),
4877            serde_json::Value::String("ok".to_string()),
4878        )]);
4879        let command = build_execution_command(
4880            std::path::Path::new("/tmp/example"),
4881            &schema,
4882            "sample",
4883            "child",
4884            &args,
4885        );
4886        assert_eq!(command.get_program(), std::ffi::OsStr::new("/tmp/example"));
4887        let actual_args: Vec<_> = command.get_args().collect();
4888        assert_eq!(
4889            actual_args,
4890            vec![
4891                std::ffi::OsStr::new("parent"),
4892                std::ffi::OsStr::new("child"),
4893                std::ffi::OsStr::new("--value"),
4894                std::ffi::OsStr::new("ok"),
4895            ]
4896        );
4897
4898        let log_params = subprocess_stderr_log_params("child", "warning on stderr\n")
4899            .expect("stderr should produce logging params");
4900        assert_eq!(log_params.logger.as_deref(), Some("stderr"));
4901        assert_eq!(
4902            log_params.meta.as_ref().and_then(|meta| meta.get("tool")),
4903            Some(&serde_json::Value::String("child".to_string()))
4904        );
4905        assert!(subprocess_stderr_log_params("child", "   ").is_none());
4906
4907        #[cfg(unix)]
4908        {
4909            let success_output = std::process::Output {
4910                status: std::process::ExitStatus::from_raw(0),
4911                stdout: b"done\n".to_vec(),
4912                stderr: b"note\n".to_vec(),
4913            };
4914            let success = call_tool_result_from_subprocess_output(&success_output);
4915            assert_ne!(success.is_error, Some(true));
4916            assert!(content_text(&success.content[0]).contains("stderr:\nnote"));
4917
4918            let failure_output = std::process::Output {
4919                status: std::process::ExitStatus::from_raw(256),
4920                stdout: Vec::new(),
4921                stderr: b"boom\n".to_vec(),
4922            };
4923            let failure = call_tool_result_from_subprocess_output(&failure_output);
4924            assert_eq!(failure.is_error, Some(true));
4925            assert!(content_text(&failure.content[0]).contains("non-zero status"));
4926        }
4927
4928        let launch_error = command_launch_failure_result(&std::io::Error::new(
4929            std::io::ErrorKind::NotFound,
4930            "missing",
4931        ));
4932        assert_eq!(launch_error.is_error, Some(true));
4933        assert!(content_text(&launch_error.content[0]).contains("Failed to run command"));
4934
4935        let placeholder = placeholder_tool_result(
4936            "echo",
4937            &serde_json::Map::from_iter([("message".to_string(), json!("hi"))]),
4938        );
4939        assert!(content_text(&placeholder.content[0]).contains("Would invoke clap command 'echo'"));
4940
4941        let parse_failure = schema_parse_failure_result();
4942        assert_eq!(parse_failure.is_error, Some(true));
4943        assert_eq!(
4944            content_text(&parse_failure.content[0]),
4945            "Failed to parse schema"
4946        );
4947    }
4948
4949    #[test]
4950    fn test_validate_tool_argument_names_rejects_unknown_keys() {
4951        let schema = sample_helper_schema();
4952        let tool = command_to_tool_with_config(
4953            &schema,
4954            &schema.root,
4955            &ClapMcpConfig::default(),
4956            &ClapMcpSchemaMetadata::default(),
4957            None,
4958        );
4959        let ok_args = serde_json::Map::from_iter([("input".to_string(), json!("in.txt"))]);
4960        assert!(validate_tool_argument_names(&tool, &tool.name, &ok_args).is_ok());
4961
4962        let bad_args = serde_json::Map::from_iter([("bogus".to_string(), json!(1))]);
4963        let err = validate_tool_argument_names(&tool, &tool.name, &bad_args)
4964            .expect_err("unknown key should error");
4965        assert!(format!("{err:?}").contains("unknown argument: bogus"));
4966    }
4967
4968    #[test]
4969    fn test_into_clap_mcp_result_and_error_impls_cover_basic_conversions() {
4970        assert!(matches!(
4971            String::from("hello")
4972                .into_tool_result()
4973                .expect("string should convert"),
4974            ClapMcpToolOutput::Text(text) if text == "hello"
4975        ));
4976        assert!(matches!(
4977            "world"
4978                .into_tool_result()
4979                .expect("str should convert"),
4980            ClapMcpToolOutput::Text(text) if text == "world"
4981        ));
4982
4983        let structured = AsStructured(json!({ "ok": true }))
4984            .into_tool_result()
4985            .expect("structured value should convert");
4986        assert!(matches!(structured, ClapMcpToolOutput::Structured(_)));
4987
4988        let empty = Option::<String>::None
4989            .into_tool_result()
4990            .expect("none should convert");
4991        assert!(matches!(empty, ClapMcpToolOutput::Text(text) if text.is_empty()));
4992
4993        let some = Some("x").into_tool_result().expect("some should convert");
4994        assert!(matches!(some, ClapMcpToolOutput::Text(text) if text == "x"));
4995
4996        let ok_result: Result<&str, &str> = Ok("done");
4997        assert!(matches!(
4998            ok_result.into_tool_result().expect("ok result should convert"),
4999            ClapMcpToolOutput::Text(text) if text == "done"
5000        ));
5001
5002        let err_result: Result<&str, &str> = Err("boom");
5003        let err = err_result
5004            .into_tool_result()
5005            .expect_err("err result should map to tool error");
5006        assert_eq!(err.message, "boom");
5007
5008        assert_eq!(ClapMcpToolError::from("oops").message, "oops");
5009        assert_eq!(ClapMcpToolError::from(String::from("ouch")).message, "ouch");
5010        assert_eq!(String::from("bad").into_tool_error().message, "bad");
5011        assert_eq!("worse".into_tool_error().message, "worse");
5012    }
5013
5014    #[test]
5015    fn test_merge_captured_stdout_only_changes_text_outputs() {
5016        let merged = merge_captured_stdout(
5017            Ok(ClapMcpToolOutput::Text(String::new())),
5018            "captured only\n".to_string(),
5019        )
5020        .expect("merge should succeed");
5021        assert!(matches!(merged, ClapMcpToolOutput::Text(text) if text == "captured only"));
5022
5023        let appended = merge_captured_stdout(
5024            Ok(ClapMcpToolOutput::Text("returned".to_string())),
5025            "captured\n".to_string(),
5026        )
5027        .expect("append should succeed");
5028        assert!(matches!(appended, ClapMcpToolOutput::Text(text) if text == "returned\ncaptured"));
5029
5030        let structured = merge_captured_stdout(
5031            Ok(ClapMcpToolOutput::Structured(json!({"ok": true}))),
5032            "captured\n".to_string(),
5033        )
5034        .expect("structured output should pass through");
5035        assert!(matches!(structured, ClapMcpToolOutput::Structured(_)));
5036    }
5037
5038    #[test]
5039    fn test_execute_in_process_command_and_handler_cover_capture_stdout_paths() {
5040        let schema = schema_from_command(&ExecCli::command());
5041
5042        let structured = execute_in_process_command_stateless::<ExecCli>(
5043            &schema,
5044            "structured",
5045            serde_json::Map::new(),
5046            false,
5047        )
5048        .expect("structured should execute");
5049        assert!(matches!(structured, ClapMcpToolOutput::Structured(_)));
5050
5051        let echo_args = serde_json::Map::from_iter([("value".to_string(), json!("hello"))]);
5052        let handler = make_in_process_handler::<ExecCli>(schema.clone(), false);
5053        let echoed = handler("echo", echo_args).expect("handler should execute");
5054        assert!(matches!(echoed, ClapMcpToolOutput::Text(text) if text == "hello"));
5055
5056        let missing = execute_in_process_command_stateless::<ExecCli>(
5057            &schema,
5058            "echo",
5059            serde_json::Map::new(),
5060            false,
5061        )
5062        .expect_err("missing required arg should fail");
5063        assert!(
5064            missing
5065                .message
5066                .contains("Missing required argument(s): value")
5067        );
5068    }
5069
5070    fn build_test_server(
5071        config: ClapMcpConfig,
5072        metadata: ClapMcpSchemaMetadata,
5073        serve_options: ClapMcpServeOptions,
5074        handler: Option<InProcessToolHandler>,
5075        executable_path: Option<std::path::PathBuf>,
5076    ) -> ClapMcpServer {
5077        let schema = nested_schema();
5078        let schema_json = serde_json::to_string(&schema).expect("schema json");
5079        let tools = tools_from_schema_with_metadata(&schema, &config, &metadata);
5080        build_clap_mcp_server(
5081            schema_json,
5082            tools,
5083            executable_path,
5084            handler,
5085            schema.root.name.clone(),
5086            &config,
5087            &serve_options,
5088            &metadata,
5089        )
5090        .expect("server should build")
5091    }
5092
5093    #[test]
5094    fn test_build_clap_mcp_server_rejects_task_augmented_without_reinvocation_safe() {
5095        let config = ClapMcpConfig {
5096            reinvocation_safe: false,
5097            ..Default::default()
5098        };
5099        let metadata = ClapMcpSchemaMetadata {
5100            task_augmented_tools: true,
5101            ..Default::default()
5102        };
5103        let schema = nested_schema();
5104        let schema_json = serde_json::to_string(&schema).expect("schema json");
5105        let tools = tools_from_schema_with_metadata(&schema, &config, &metadata);
5106        assert!(matches!(
5107            build_clap_mcp_server(
5108                schema_json,
5109                tools,
5110                None,
5111                Some(Arc::new(|_, _| Ok(ClapMcpToolOutput::Text("ok".into())))),
5112                schema.root.name,
5113                &config,
5114                &ClapMcpServeOptions::default(),
5115                &metadata,
5116            ),
5117            Err(ClapMcpError::InvalidConfig(_))
5118        ));
5119    }
5120
5121    #[test]
5122    fn test_build_clap_mcp_server_rejects_task_augmented_without_in_process_handler() {
5123        let config = ClapMcpConfig {
5124            reinvocation_safe: true,
5125            ..Default::default()
5126        };
5127        let metadata = ClapMcpSchemaMetadata {
5128            task_augmented_tools: true,
5129            ..Default::default()
5130        };
5131        let schema = nested_schema();
5132        let schema_json = serde_json::to_string(&schema).expect("schema json");
5133        let tools = tools_from_schema_with_metadata(&schema, &config, &metadata);
5134        assert!(matches!(
5135            build_clap_mcp_server(
5136                schema_json,
5137                tools,
5138                None,
5139                None,
5140                schema.root.name,
5141                &config,
5142                &ClapMcpServeOptions::default(),
5143                &metadata,
5144            ),
5145            Err(ClapMcpError::InvalidConfig(_))
5146        ));
5147    }
5148
5149    #[test]
5150    fn test_build_clap_mcp_server_accepts_parallel_and_serial_configs() {
5151        let handler: InProcessToolHandler =
5152            Arc::new(|_, _| Ok(ClapMcpToolOutput::Text("ok".into())));
5153        for parallel_safe in [true, false] {
5154            let config = ClapMcpConfig {
5155                reinvocation_safe: true,
5156                parallel_safe,
5157                ..Default::default()
5158            };
5159            build_test_server(
5160                config,
5161                ClapMcpSchemaMetadata::default(),
5162                ClapMcpServeOptions::default(),
5163                Some(handler.clone()),
5164                None,
5165            );
5166        }
5167    }
5168
5169    #[test]
5170    fn test_get_info_capability_matrix_and_instructions() {
5171        let handler: InProcessToolHandler =
5172            Arc::new(|_, _| Ok(ClapMcpToolOutput::Text("ok".into())));
5173        let config = ClapMcpConfig {
5174            reinvocation_safe: true,
5175            parallel_safe: true,
5176            ..Default::default()
5177        };
5178
5179        let (tx, rx) = logging::log_channel(4);
5180        drop(tx);
5181        let with_logging = build_test_server(
5182            config.clone(),
5183            ClapMcpSchemaMetadata::default(),
5184            ClapMcpServeOptions {
5185                log_rx: Some(rx),
5186                ..Default::default()
5187            },
5188            Some(handler.clone()),
5189            None,
5190        );
5191        let info = with_logging.get_info();
5192        assert!(info.capabilities.logging.is_some());
5193        assert!(info.capabilities.tasks.is_none());
5194        assert_eq!(
5195            info.instructions.as_deref(),
5196            Some(LOG_INTERPRETATION_INSTRUCTIONS)
5197        );
5198
5199        let with_tasks = build_test_server(
5200            config.clone(),
5201            ClapMcpSchemaMetadata {
5202                task_augmented_tools: true,
5203                ..Default::default()
5204            },
5205            ClapMcpServeOptions::default(),
5206            Some(handler.clone()),
5207            None,
5208        );
5209        let info = with_tasks.get_info();
5210        assert!(info.capabilities.logging.is_none());
5211        assert!(info.capabilities.tasks.is_some());
5212        assert!(info.instructions.is_none());
5213
5214        let with_both = build_test_server(
5215            config,
5216            ClapMcpSchemaMetadata {
5217                task_augmented_tools: true,
5218                ..Default::default()
5219            },
5220            ClapMcpServeOptions {
5221                log_rx: Some(logging::log_channel(4).1),
5222                ..Default::default()
5223            },
5224            Some(handler),
5225            None,
5226        );
5227        let info = with_both.get_info();
5228        assert!(info.capabilities.logging.is_some());
5229        assert!(info.capabilities.tasks.is_some());
5230        assert_eq!(
5231            info.instructions.as_deref(),
5232            Some(LOG_INTERPRETATION_INSTRUCTIONS)
5233        );
5234    }
5235
5236    #[test]
5237    fn test_build_execution_command_root_tool_skips_extra_segment() {
5238        let schema =
5239            schema_from_command(&Command::new("sample").arg(Arg::new("value").long("value")));
5240        let args = serde_json::Map::from_iter([("value".to_string(), json!("ok"))]);
5241        let command = build_execution_command(
5242            std::path::Path::new("/tmp/example"),
5243            &schema,
5244            "sample",
5245            "sample",
5246            &args,
5247        );
5248        let actual_args: Vec<_> = command.get_args().collect();
5249        assert_eq!(
5250            actual_args,
5251            vec![std::ffi::OsStr::new("--value"), std::ffi::OsStr::new("ok"),]
5252        );
5253    }
5254
5255    #[test]
5256    fn test_validate_tool_argument_names_allows_extra_when_no_properties() {
5257        let tool = Tool::new(
5258            "freeform",
5259            "no schema properties",
5260            Arc::new(serde_json::Map::new()),
5261        );
5262        let args = serde_json::Map::from_iter([("anything".to_string(), json!(1))]);
5263        assert!(validate_tool_argument_names(&tool, "freeform", &args).is_ok());
5264    }
5265}