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 = clap_mcp::parse_or_serve_mcp_attr::<Cli>();
29//!     println!("{}", run(cli));
30//! }
31//! ```
32//!
33//! Run with `--mcp` to start the MCP server instead of executing the CLI.
34
35use async_trait::async_trait;
36use clap::{Arg, ArgAction, Command};
37use rust_mcp_sdk::{
38    McpServer, StdioTransport, TransportOptions,
39    mcp_server::{McpServerOptions, ServerHandler, ToMcpServerHandler, server_runtime},
40    schema::{
41        CallToolError, CallToolRequestParams, CallToolResult, ContentBlock, GetPromptRequestParams,
42        GetPromptResult, Implementation, InitializeResult, LATEST_PROTOCOL_VERSION,
43        ListPromptsResult, ListResourcesResult, ListToolsResult, LoggingLevel,
44        LoggingMessageNotificationParams, PaginatedRequestParams, Prompt, PromptMessage,
45        ReadResourceContent, ReadResourceRequestParams, ReadResourceResult, Resource, Role,
46        RpcError, ServerCapabilities, ServerCapabilitiesPrompts, ServerCapabilitiesResources,
47        ServerCapabilitiesTools, TextResourceContents, Tool, ToolInputSchema, schema_utils,
48    },
49};
50use serde::{Deserialize, Serialize};
51use std::{collections::HashMap, path::PathBuf, sync::Arc, time::Duration};
52
53#[cfg(any(feature = "tracing", feature = "log"))]
54pub mod logging;
55
56/// Custom MCP resources and prompts, and skill export.
57pub mod content;
58
59#[cfg(feature = "derive")]
60pub use clap_mcp_macros::ClapMcp;
61
62/// Convenience macro for struct root + subcommand CLIs: parse root then run.
63///
64/// Expands to: parse the root with [`parse_or_serve_mcp_attr`], then evaluate the given
65/// expression (which can use `args` for the parsed root). Use in `main` so the pattern
66/// is one line and hard to forget.
67///
68/// # Example
69///
70/// ```rust,ignore
71/// fn main() {
72///     clap_mcp_main!(Cli, |args| match args.command {
73///         None => println!("No subcommand"),
74///         Some(cmd) => println!("{}", run(cmd)),
75///     });
76/// }
77/// ```
78///
79/// For `Result`-returning run logic, use `?` in main or call [`run_or_serve_mcp`].
80#[macro_export]
81macro_rules! clap_mcp_main {
82    ($root:ty, |$args:ident| $run_expr:expr) => {{
83        let $args = $crate::parse_or_serve_mcp_attr::<$root>();
84        $run_expr
85    }};
86    ($root:ty, $run_expr:expr) => {{
87        macro_rules! __clap_mcp_with_args {
88            ($args:ident, $expr:expr) => {{
89                let $args = $crate::parse_or_serve_mcp_attr::<$root>();
90                $expr
91            }};
92        }
93        __clap_mcp_with_args!(args, $run_expr)
94    }};
95}
96
97/// Long flag that triggers MCP server mode. Add to your CLI via [`command_with_mcp_flag`].
98pub const MCP_FLAG_LONG: &str = "mcp";
99
100/// Long flag that triggers [Agent Skills](https://agentskills.io/specification) export (generates SKILL.md). Add via [`command_with_export_skills_flag`].
101pub const EXPORT_SKILLS_FLAG_LONG: &str = "export-skills";
102
103/// URI for the clap schema resource exposed by the MCP server.
104pub const MCP_RESOURCE_URI_SCHEMA: &str = "clap://schema";
105
106/// Provides MCP execution safety configuration from `#[clap_mcp(...)]` attributes.
107/// Implemented by the `#[derive(ClapMcp)]` macro.
108///
109/// # Example
110///
111/// ```rust
112/// use clap::Parser;
113/// use clap_mcp::ClapMcpConfigProvider;
114/// use clap_mcp::ClapMcp;
115///
116/// #[derive(Debug, Parser, ClapMcp)]
117/// #[clap_mcp(reinvocation_safe, parallel_safe = false)]
118/// #[clap_mcp_output_from = "run"]
119/// enum MyCli { Foo }
120///
121/// fn run(cmd: MyCli) -> String {
122///     match cmd { MyCli::Foo => "ok".to_string() }
123/// }
124///
125/// let config = MyCli::clap_mcp_config();
126/// assert!(config.reinvocation_safe);
127/// assert!(!config.parallel_safe);
128/// ```
129pub trait ClapMcpConfigProvider {
130    fn clap_mcp_config() -> ClapMcpConfig;
131}
132
133/// Provides MCP schema metadata (skip, requires) from `#[clap_mcp(skip)]` and
134/// `#[clap_mcp(requires = "arg_name")]` attributes.
135///
136/// Implemented by the `#[derive(ClapMcp)]` macro. For custom types, implement
137/// with `fn clap_mcp_schema_metadata() -> ClapMcpSchemaMetadata { ClapMcpSchemaMetadata::default() }`.
138pub trait ClapMcpSchemaMetadataProvider {
139    fn clap_mcp_schema_metadata() -> ClapMcpSchemaMetadata;
140}
141
142/// Produces the output string for a parsed CLI value.
143/// Used for in-process MCP tool execution when `reinvocation_safe` is true.
144/// Implemented by the `#[derive(ClapMcp)]` macro via the blanket impl for `ClapMcpToolExecutor`.
145pub trait ClapMcpRunnable {
146    fn run(self) -> String;
147}
148
149/// Error produced when a tool's `run` function returns `Err(e)` (e.g. `Result<O, E>`).
150///
151/// When your `run` returns `Result<O, E>`, `Err(e)` is converted via [`IntoClapMcpToolError`]
152/// into this type. Implement that trait for your error type to get structured JSON in the
153/// response when `E: Serialize`.
154#[derive(Debug, Clone)]
155pub struct ClapMcpToolError {
156    /// Human-readable error message for MCP content.
157    pub message: String,
158    /// Optional structured JSON when `E: Serialize` and [`IntoClapMcpToolError`] provides it.
159    pub structured: Option<serde_json::Value>,
160}
161
162impl ClapMcpToolError {
163    /// Create a plain text error.
164    pub fn text(message: impl Into<String>) -> Self {
165        Self {
166            message: message.into(),
167            structured: None,
168        }
169    }
170
171    /// Create an error with structured serialization.
172    pub fn structured(message: impl Into<String>, value: serde_json::Value) -> Self {
173        Self {
174            message: message.into(),
175            structured: Some(value),
176        }
177    }
178}
179
180impl From<String> for ClapMcpToolError {
181    fn from(s: String) -> Self {
182        Self::text(s)
183    }
184}
185
186impl From<&str> for ClapMcpToolError {
187    fn from(s: &str) -> Self {
188        Self::text(s)
189    }
190}
191
192/// Converts the return value of a `run` function (used with `#[clap_mcp_output_from]`) into
193/// MCP tool output or error.
194///
195/// Implemented for:
196/// - `String` / `&str` → text output
197/// - [`AsStructured`]`<T>` where `T: Serialize` → structured JSON output
198/// - `Option<O>` → `None` → empty text; `Some(o)` → `o.into_tool_result()`
199/// - `Result<O, E>` → `Ok(o)` → output; `Err(e)` → `ClapMcpToolError`
200///
201/// `Result<AsStructured<T>, E>` is fully supported as a `run` return type; use it when you want
202/// structured success payloads and a separate error type.
203pub trait IntoClapMcpResult {
204    fn into_tool_result(self) -> std::result::Result<ClapMcpToolOutput, ClapMcpToolError>;
205}
206
207impl IntoClapMcpResult for String {
208    fn into_tool_result(self) -> std::result::Result<ClapMcpToolOutput, ClapMcpToolError> {
209        Ok(ClapMcpToolOutput::Text(self))
210    }
211}
212
213impl IntoClapMcpResult for &str {
214    fn into_tool_result(self) -> std::result::Result<ClapMcpToolOutput, ClapMcpToolError> {
215        Ok(ClapMcpToolOutput::Text(self.to_string()))
216    }
217}
218
219/// Wrapper for structured (JSON) output when using `#[clap_mcp_output_from]`.
220/// Use when your `run` function returns a type that implements `Serialize` but is not `String`/`&str`.
221///
222/// Fully supported when used as the `Ok` type in `Result<AsStructured<T>, E>`; there are no known
223/// limitations for mixed success/error types. [`IntoClapMcpResult`] is implemented for
224/// `AsStructured<T>` where `T: Serialize`.
225///
226/// # Example
227///
228/// ```rust,ignore
229/// fn run(cmd: Cli) -> Result<clap_mcp::AsStructured<SubcommandResult>, Error> {
230///     match cmd { ... }
231/// }
232/// ```
233#[derive(Debug, Clone)]
234pub struct AsStructured<T>(pub T);
235
236impl<T: Serialize> IntoClapMcpResult for AsStructured<T> {
237    fn into_tool_result(self) -> std::result::Result<ClapMcpToolOutput, ClapMcpToolError> {
238        serde_json::to_value(&self.0)
239            .map(ClapMcpToolOutput::Structured)
240            .map_err(|e| ClapMcpToolError::text(e.to_string()))
241    }
242}
243
244impl<O: IntoClapMcpResult> IntoClapMcpResult for Option<O> {
245    fn into_tool_result(self) -> std::result::Result<ClapMcpToolOutput, ClapMcpToolError> {
246        match self {
247            None => Ok(ClapMcpToolOutput::Text(String::new())),
248            Some(o) => o.into_tool_result(),
249        }
250    }
251}
252
253/// Converts an error type from a `run` function into [`ClapMcpToolError`].
254/// Used when `run` returns `Result<O, E>` and the `Err` branch is taken.
255///
256/// Implement this for your error type when you need custom formatting or structured errors.
257/// For plain string errors, you can use `String` or `&str`, which have built-in impls.
258pub trait IntoClapMcpToolError {
259    fn into_tool_error(self) -> ClapMcpToolError;
260}
261
262impl IntoClapMcpToolError for String {
263    fn into_tool_error(self) -> ClapMcpToolError {
264        ClapMcpToolError::text(self)
265    }
266}
267
268impl IntoClapMcpToolError for &str {
269    fn into_tool_error(self) -> ClapMcpToolError {
270        ClapMcpToolError::text(self.to_string())
271    }
272}
273
274impl<O: IntoClapMcpResult, E: IntoClapMcpToolError> IntoClapMcpResult for Result<O, E> {
275    fn into_tool_result(self) -> std::result::Result<ClapMcpToolOutput, ClapMcpToolError> {
276        match self {
277            Ok(o) => o.into_tool_result(),
278            Err(e) => Err(e.into_tool_error()),
279        }
280    }
281}
282
283/// Runs a closure with stdout captured. Returns `(result, captured_stdout)`.
284/// Unix-only; on Windows returns empty captured string.
285#[cfg(unix)]
286fn run_with_stdout_capture<R, F>(f: F) -> (R, String)
287where
288    F: FnOnce() -> R,
289{
290    use std::io::{Read, Write};
291    use std::os::unix::io::FromRawFd;
292
293    // SAFETY: We use a pipe and dup2 to temporarily redirect stdout. All fds are either
294    // created by pipe()/dup() or are well-known (STDOUT_FILENO). We close or restore every
295    // fd on every path (success or error); from_raw_fd(read_fd) takes ownership of read_fd
296    // so it is not double-closed. No fd is used after being closed.
297    let mut fds: [libc::c_int; 2] = [0, 0];
298    if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
299        return (f(), String::new());
300    }
301    let (read_fd, write_fd) = (fds[0], fds[1]);
302
303    let stdout_fd = libc::STDOUT_FILENO;
304    let saved_stdout = unsafe { libc::dup(stdout_fd) };
305    if saved_stdout < 0 {
306        unsafe {
307            libc::close(read_fd);
308            libc::close(write_fd);
309        }
310        return (f(), String::new());
311    }
312
313    if unsafe { libc::dup2(write_fd, stdout_fd) } < 0 {
314        unsafe {
315            libc::close(saved_stdout);
316            libc::close(read_fd);
317            libc::close(write_fd);
318        }
319        return (f(), String::new());
320    }
321
322    let result = f();
323
324    let _ = std::io::stdout().flush();
325    unsafe {
326        libc::dup2(saved_stdout, stdout_fd);
327        libc::close(saved_stdout);
328        libc::close(write_fd);
329    }
330
331    let mut reader = unsafe { std::fs::File::from_raw_fd(read_fd) };
332    let mut captured = String::new();
333    let _ = reader.read_to_string(&mut captured);
334
335    (result, captured)
336}
337
338#[cfg(not(unix))]
339fn run_with_stdout_capture<R, F>(f: F) -> (R, String)
340where
341    F: FnOnce() -> R,
342{
343    (f(), String::new())
344}
345
346/// Output produced by a CLI command for MCP tool results.
347///
348/// Use `Text` for plain string output; use `Structured` for serializable JSON
349/// (e.g. when using `#[clap_mcp_output_from = "run"]` with `AsStructured<T>`, or
350/// (e.g. when using `#[clap_mcp_output_from = "run"]` with `AsStructured<T>`).
351///
352/// # Example
353///
354/// ```
355/// use clap_mcp::ClapMcpToolOutput;
356///
357/// let text = ClapMcpToolOutput::Text("hello".into());
358/// assert_eq!(text.into_string(), "hello");
359///
360/// let structured = ClapMcpToolOutput::Structured(serde_json::json!({"x": 1}));
361/// assert!(structured.as_structured().unwrap().get("x").is_some());
362/// ```
363#[derive(Debug, Clone)]
364pub enum ClapMcpToolOutput {
365    /// Plain text output (stdout-style).
366    Text(String),
367    /// Structured JSON output for machine consumption.
368    Structured(serde_json::Value),
369}
370
371impl ClapMcpToolOutput {
372    /// Returns the text content if this is `Text`, or the JSON string if `Structured`.
373    ///
374    /// # Example
375    ///
376    /// ```
377    /// use clap_mcp::ClapMcpToolOutput;
378    ///
379    /// assert_eq!(ClapMcpToolOutput::Text("hi".into()).into_string(), "hi");
380    /// assert!(ClapMcpToolOutput::Structured(serde_json::json!({"a":1})).into_string().contains("a"));
381    /// ```
382    pub fn into_string(self) -> String {
383        match self {
384            ClapMcpToolOutput::Text(s) => s,
385            ClapMcpToolOutput::Structured(v) => {
386                serde_json::to_string(&v).unwrap_or_else(|_| v.to_string())
387            }
388        }
389    }
390
391    /// Returns `Some(&str)` for `Text`, `None` for `Structured`.
392    ///
393    /// # Example
394    ///
395    /// ```
396    /// use clap_mcp::ClapMcpToolOutput;
397    ///
398    /// assert_eq!(ClapMcpToolOutput::Text("hi".into()).as_text(), Some("hi"));
399    /// assert!(ClapMcpToolOutput::Structured(serde_json::json!(1)).as_text().is_none());
400    /// ```
401    pub fn as_text(&self) -> Option<&str> {
402        match self {
403            ClapMcpToolOutput::Text(s) => Some(s),
404            ClapMcpToolOutput::Structured(_) => None,
405        }
406    }
407
408    /// Returns `Some(&Value)` for `Structured`, `None` for `Text`.
409    ///
410    /// # Example
411    ///
412    /// ```
413    /// use clap_mcp::ClapMcpToolOutput;
414    ///
415    /// let v = serde_json::json!({"sum": 10});
416    /// assert_eq!(ClapMcpToolOutput::Structured(v.clone()).as_structured(), Some(&v));
417    /// assert!(ClapMcpToolOutput::Text("x".into()).as_structured().is_none());
418    /// ```
419    pub fn as_structured(&self) -> Option<&serde_json::Value> {
420        match self {
421            ClapMcpToolOutput::Text(_) => None,
422            ClapMcpToolOutput::Structured(v) => Some(v),
423        }
424    }
425}
426
427/// Produces MCP tool output (text or structured) for a parsed CLI value.
428///
429/// Implemented by the `#[derive(ClapMcp)]` macro. Used for in-process execution.
430///
431/// When using **`#[clap_mcp_output_from = "run"]`** on the enum (required), the macro
432/// implements this trait by calling `run(self)` and converting the result via [`IntoClapMcpResult`].
433/// CLI and MCP share a single implementation.
434pub trait ClapMcpToolExecutor {
435    fn execute_for_mcp(self) -> std::result::Result<ClapMcpToolOutput, ClapMcpToolError>;
436}
437
438impl<T: ClapMcpToolExecutor> ClapMcpRunnable for T {
439    fn run(self) -> String {
440        self.execute_for_mcp()
441            .unwrap_or_else(|e| ClapMcpToolOutput::Text(e.message))
442            .into_string()
443    }
444}
445
446/// Errors that can occur when running the MCP server.
447#[derive(Debug, thiserror::Error)]
448pub enum ClapMcpError {
449    #[error("failed to serialize clap schema to JSON: {0}")]
450    SchemaJson(#[from] serde_json::Error),
451    #[error("MCP transport error: {0}")]
452    Transport(#[from] rust_mcp_sdk::TransportError),
453    #[error("MCP runtime error: {0}")]
454    McpSdk(#[from] rust_mcp_sdk::error::McpSdkError),
455    #[error("I/O error during skill export: {0}")]
456    Io(#[from] std::io::Error),
457    #[error("tokio runtime context: {0}")]
458    RuntimeContext(String),
459    #[error("async tool thread panicked or failed: {0}")]
460    ToolThread(String),
461}
462
463/// Configuration for execution safety when exposing a CLI over MCP.
464///
465/// Use this to declare whether your CLI tool can be safely invoked multiple times,
466/// whether it can run in parallel with other tool calls, and how async tools run.
467///
468/// # Crash and panic behavior
469///
470/// - **Subprocess (`reinvocation_safe` = false):** If the tool process exits with a non-zero
471///   status, the server returns an MCP tool result with `is_error: true` and a message
472///   that includes the exit code (and stderr when non-empty).
473/// - **In-process (`reinvocation_safe` = true), `catch_in_process_panics` = false:** Any panic
474///   in tool code (including from [`run_async_tool`]) crashes the server.
475/// - **In-process, `catch_in_process_panics` = true:** Panics are caught and returned as an
476///   MCP error; the server stays up. After a caught panic, the process may no longer be
477///   reinvocation_safe (global state may be corrupted); consider restarting the server.
478///
479/// # Example
480///
481/// ```
482/// use clap_mcp::ClapMcpConfig;
483///
484/// // Default: subprocess per call, serialized
485/// let config = ClapMcpConfig::default();
486///
487/// // In-process, parallel-safe
488/// let config = ClapMcpConfig {
489///     reinvocation_safe: true,
490///     parallel_safe: true,
491///     ..Default::default()
492/// };
493/// ```
494#[derive(Debug, Clone)]
495pub struct ClapMcpConfig {
496    /// If true, the CLI can be invoked multiple times without tearing down the process.
497    /// When false (default), each tool call spawns a fresh subprocess.
498    /// When true, uses in-process execution (no subprocess).
499    pub reinvocation_safe: bool,
500
501    /// If true, tool calls may run concurrently. When false, calls are serialized.
502    /// Default is false (serialize by default) for safety.
503    pub parallel_safe: bool,
504
505    /// When `reinvocation_safe` is true, controls how async tool execution runs.
506    /// Only applies to in-process execution; ignored when `reinvocation_safe` is false.
507    ///
508    /// | Value | Behavior | When to use |
509    /// |-------|----------|-------------|
510    /// | `false` (default) | Dedicated thread with its own tokio runtime per tool call. No nesting, no special setup. | **Recommended.** Use unless you need deep integration. |
511    /// | `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. |
512    ///
513    /// Use with [`run_async_tool`] in `#[clap_mcp_output]` for async subcommands.
514    pub share_runtime: bool,
515
516    /// When true and `reinvocation_safe` is true, panics in tool code are caught and returned
517    /// as an MCP error (`is_error: true`) instead of crashing the server. Default is `false` (opt-in).
518    ///
519    /// **Warning:** After a caught panic, the process may no longer be reinvocation_safe: global
520    /// state (e.g. static or process-wide resources) could be left in an inconsistent state.
521    /// For reliability, restart the MCP server after a caught panic when using in-process execution.
522    pub catch_in_process_panics: bool,
523
524    /// When true (default), `myapp --mcp` starts the MCP server even when the root has
525    /// `subcommand_required = true`, by checking argv before calling clap. Set to false to
526    /// require a subcommand (and thus `Option<Commands>` + `subcommand_required = false`) for
527    /// `--mcp` to parse.
528    pub allow_mcp_without_subcommand: bool,
529}
530
531impl Default for ClapMcpConfig {
532    fn default() -> Self {
533        Self {
534            reinvocation_safe: false,
535            parallel_safe: false,
536            share_runtime: false,
537            catch_in_process_panics: false,
538            allow_mcp_without_subcommand: true,
539        }
540    }
541}
542
543/// Optional configuration for MCP serve behavior (logging, etc.).
544///
545/// Pass to [`serve_schema_json_over_stdio`] or [`serve_schema_json_over_stdio_blocking`].
546/// When `log_rx` is set, enables the logging capability and forwards messages to the MCP client.
547///
548/// # Example
549///
550/// ```rust,ignore
551/// use clap_mcp::{ClapMcpServeOptions, logging::log_channel};
552///
553/// let (log_tx, log_rx) = log_channel(32);
554/// let mut opts = ClapMcpServeOptions::default();
555/// opts.log_rx = Some(log_rx);
556/// // Pass opts to parse_or_serve_mcp_with_config_and_options or serve_schema_json_over_stdio_blocking
557/// ```
558#[derive(Debug, Default)]
559pub struct ClapMcpServeOptions {
560    /// When set, log messages received on this channel are forwarded to the MCP client
561    /// via `notifications/message`. Enables the logging capability and instructions.
562    pub log_rx: Option<tokio::sync::mpsc::Receiver<LoggingMessageNotificationParams>>,
563
564    /// When true and running in-process, capture stdout written during tool execution
565    /// and merge it with Text output. Only has effect when `reinvocation_safe` is true.
566    /// Unix only; **not available on Windows** (this field does not exist there; code
567    /// setting it will fail to compile on Windows).
568    #[cfg(unix)]
569    pub capture_stdout: bool,
570
571    /// Custom MCP resources (static or async dynamic). Merged with the built-in `clap://schema` resource.
572    pub custom_resources: Vec<content::CustomResource>,
573
574    /// Custom MCP prompts (static or async dynamic). Merged with the built-in logging guide when logging is enabled.
575    pub custom_prompts: Vec<content::CustomPrompt>,
576}
577
578/// Log interpretation hint for MCP clients (included in `instructions` when logging is enabled).
579///
580/// When changing logging behavior (logger names in `logging`, subprocess stderr handling below),
581/// update this and [`LOGGING_GUIDE_CONTENT`].
582pub const LOG_INTERPRETATION_INSTRUCTIONS: &str = r#"When this server emits log messages (notifications/message), the `logger` field indicates the source:
583- "stderr": Subprocess stderr (CLI tools run as subprocesses)
584- "app": In-process application logs
585- Other: Application-defined logger names"#;
586
587/// Name of the logging guide prompt.
588pub const PROMPT_LOGGING_GUIDE: &str = "clap-mcp-logging-guide";
589
590/// Full content for the logging guide prompt (returned when clients request `PROMPT_LOGGING_GUIDE`).
591///
592/// When changing logging behavior (logger names in `logging`, subprocess stderr handling below),
593/// update this and [`LOG_INTERPRETATION_INSTRUCTIONS`].
594pub const LOGGING_GUIDE_CONTENT: &str = r#"# clap-mcp Logging Guide
595
596When this server emits log messages (notifications/message), use the `logger` field to interpret the source:
597
598- **"stderr"**: Output from subprocess stderr (CLI tools run as subprocesses). The `meta` field may include `tool` for the command name.
599- **"app"**: In-process application logs.
600- **Other**: Application-defined logger names.
601
602The `level` field uses RFC 5424 syslog severity: debug, info, notice, warning, error, critical, alert, emergency.
603The `data` field contains the message (string or JSON object)."#;
604
605/// Metadata for filtering and adjusting the MCP schema.
606///
607/// Use with [`schema_from_command_with_metadata`] to exclude commands/args from MCP
608/// or to make optional args required in the MCP tool schema.
609///
610/// # Example (imperative)
611///
612/// ```rust
613/// use clap::Command;
614/// use clap_mcp::{schema_from_command_with_metadata, ClapMcpSchemaMetadata};
615///
616/// let mut metadata = ClapMcpSchemaMetadata::default();
617/// metadata.skip_commands.push("internal".into());
618/// metadata.skip_args.insert("mycmd".into(), vec!["verbose".into()]);
619/// metadata.requires_args.insert("mycmd".into(), vec!["path".into()]);
620///
621/// let cmd = Command::new("myapp").subcommand(Command::new("mycmd").arg(clap::Arg::new("path")));
622/// let schema = schema_from_command_with_metadata(&cmd, &metadata);
623/// ```
624#[derive(Debug, Clone, Default)]
625pub struct ClapMcpSchemaMetadata {
626    /// Command names to exclude from MCP exposure.
627    pub skip_commands: Vec<String>,
628    /// Per-command arg ids to exclude (command_name -> arg_ids).
629    pub skip_args: std::collections::HashMap<String, Vec<String>>,
630    /// Per-command arg ids to treat as required in MCP (command_name -> arg_ids).
631    pub requires_args: std::collections::HashMap<String, Vec<String>>,
632    /// When `true` and the root command has subcommands, the root is excluded from the
633    /// MCP tool list (only subcommands become tools). Use when the meaningful tools are
634    /// the leaf subcommands (e.g. explain, compare, sort) and the root is rarely invoked.
635    pub skip_root_command_when_subcommands: bool,
636    /// Optional JSON schema for tool output. When set (e.g. via `#[clap_mcp_output_type]` or
637    /// `#[clap_mcp_output_one_of]` with the `output-schema` feature), this schema is attached
638    /// to each tool's `output_schema` field.
639    pub output_schema: Option<serde_json::Value>,
640}
641
642/// Builds a JSON schema for a single type. Used by the derive macro when `#[clap_mcp_output_type = "T"]` is set.
643/// When the `output-schema` feature is enabled and `T: schemars::JsonSchema`, returns the schema; otherwise returns `None`.
644#[cfg(feature = "output-schema")]
645pub fn output_schema_for_type<T: schemars::JsonSchema>() -> Option<serde_json::Value> {
646    serde_json::to_value(schemars::schema_for!(T)).ok()
647}
648
649#[cfg(not(feature = "output-schema"))]
650pub fn output_schema_for_type<T>() -> Option<serde_json::Value> {
651    let _ = std::marker::PhantomData::<T>;
652    None
653}
654
655/// Builds a JSON schema with `oneOf` for the given types. Used by the derive macro when
656/// `#[clap_mcp_output_one_of = "T1, T2, T3"]` is set. Requires the `output-schema` feature
657/// and each type must implement `schemars::JsonSchema`.
658#[macro_export]
659macro_rules! output_schema_one_of {
660    ($($T:ty),+ $(,)?) => {{
661        #[cfg(feature = "output-schema")]
662        {
663            let mut one_of = vec![];
664            $( one_of.push(serde_json::to_value(&schemars::schema_for!($T)).unwrap()); )+
665            Some(serde_json::json!({ "oneOf": one_of }))
666        }
667        #[cfg(not(feature = "output-schema"))]
668        {
669            None::<serde_json::Value>
670        }
671    }};
672}
673
674/// Serializable schema extracted from a clap `Command`.
675/// Used to build MCP tools and invoke the CLI.
676#[derive(Debug, Clone, Serialize, Deserialize)]
677pub struct ClapSchema {
678    pub root: ClapCommand,
679}
680
681/// A command or subcommand in the schema.
682#[derive(Debug, Clone, Serialize, Deserialize)]
683pub struct ClapCommand {
684    pub name: String,
685    pub about: Option<String>,
686    pub long_about: Option<String>,
687    pub version: Option<String>,
688    pub args: Vec<ClapArg>,
689    pub subcommands: Vec<ClapCommand>,
690}
691
692impl ClapCommand {
693    /// Returns this command and all subcommands in depth-first order.
694    pub fn all_commands(&self) -> Vec<&ClapCommand> {
695        let mut out = Vec::new();
696        fn walk<'a>(cmd: &'a ClapCommand, acc: &mut Vec<&'a ClapCommand>) {
697            acc.push(cmd);
698            for sub in &cmd.subcommands {
699                walk(sub, acc);
700            }
701        }
702        walk(self, &mut out);
703        out
704    }
705}
706
707/// Arg IDs that are omitted from MCP tool arguments (built-in / default options).
708pub(crate) fn is_builtin_arg(id: &str) -> bool {
709    matches!(
710        id,
711        "help" | "version" | MCP_FLAG_LONG | EXPORT_SKILLS_FLAG_LONG
712    )
713}
714
715/// Builds MCP tools from a clap schema.
716///
717/// One tool per command (root + every subcommand). Tool names match command names;
718/// descriptions use the same text as `--help`; each tool's input schema lists the
719/// command's arguments (excluding help/version/mcp).
720///
721/// # Example
722///
723/// ```rust
724/// use clap::{CommandFactory, Parser};
725/// use clap_mcp::{schema_from_command, tools_from_schema};
726///
727/// #[derive(Parser)]
728/// #[command(name = "mycli")]
729/// enum Cli { Foo }
730///
731/// let cmd = Cli::command();
732/// let schema = schema_from_command(&cmd);
733/// let tools = tools_from_schema(&schema);
734/// assert!(!tools.is_empty());
735/// ```
736pub fn tools_from_schema(schema: &ClapSchema) -> Vec<Tool> {
737    tools_from_schema_with_config(schema, &ClapMcpConfig::default())
738}
739
740/// Builds MCP tools from a clap schema with execution safety annotations.
741///
742/// Tools include `meta.clapMcp` with `reinvocationSafe` and `parallelSafe` hints
743/// for MCP clients to make informed execution decisions.
744///
745/// # Example
746///
747/// ```rust
748/// use clap::{CommandFactory, Parser};
749/// use clap_mcp::{schema_from_command, tools_from_schema_with_config, ClapMcpConfig};
750///
751/// #[derive(Parser)]
752/// #[command(name = "mycli")]
753/// enum Cli { Foo }
754///
755/// let schema = schema_from_command(&Cli::command());
756/// let config = ClapMcpConfig { reinvocation_safe: true, parallel_safe: false, ..Default::default() };
757/// let tools = tools_from_schema_with_config(&schema, &config);
758/// ```
759pub fn tools_from_schema_with_config(schema: &ClapSchema, config: &ClapMcpConfig) -> Vec<Tool> {
760    tools_from_schema_with_config_and_metadata(schema, config, &ClapMcpSchemaMetadata::default())
761}
762
763/// Builds MCP tools from a clap schema with config and optional metadata.
764/// When `metadata.output_schema` is set, each tool's `output_schema` field is set to that value.
765/// When `metadata.skip_root_command_when_subcommands` is true and the root has subcommands,
766/// the root command is excluded from the tool list (only subcommands become tools).
767pub fn tools_from_schema_with_config_and_metadata(
768    schema: &ClapSchema,
769    config: &ClapMcpConfig,
770    metadata: &ClapMcpSchemaMetadata,
771) -> Vec<Tool> {
772    let commands: Vec<&ClapCommand> =
773        if metadata.skip_root_command_when_subcommands && !schema.root.subcommands.is_empty() {
774            schema
775                .root
776                .subcommands
777                .iter()
778                .flat_map(|c| c.all_commands())
779                .collect()
780        } else {
781            schema.root.all_commands()
782        };
783    commands
784        .into_iter()
785        .map(|cmd| command_to_tool_with_config(cmd, config, metadata.output_schema.as_ref()))
786        .collect()
787}
788
789fn command_to_tool_with_config(
790    cmd: &ClapCommand,
791    config: &ClapMcpConfig,
792    output_schema: Option<&serde_json::Value>,
793) -> Tool {
794    let args: Vec<&ClapArg> = cmd
795        .args
796        .iter()
797        .filter(|a| !is_builtin_arg(a.id.as_str()))
798        .collect();
799
800    let mut properties: HashMap<String, serde_json::Map<String, serde_json::Value>> =
801        HashMap::new();
802    for arg in &args {
803        let mut prop = serde_json::Map::new();
804        let (json_type, items) = mcp_type_for_arg(arg);
805        prop.insert("type".to_string(), json_type);
806        if let Some(items) = items {
807            prop.insert("items".to_string(), items);
808        }
809        let desc = arg
810            .long_help
811            .as_deref()
812            .or(arg.help.as_deref())
813            .map(String::from);
814        let mut desc = desc.unwrap_or_default();
815        if let Some(hint) = mcp_action_description_hint(arg) {
816            desc.push_str(&hint);
817        }
818        if !desc.is_empty() {
819            prop.insert("description".to_string(), serde_json::Value::String(desc));
820        }
821        properties.insert(arg.id.clone(), prop);
822    }
823
824    let required: Vec<String> = args
825        .iter()
826        .filter(|a| a.required)
827        .map(|a| a.id.clone())
828        .collect();
829
830    let input_schema = ToolInputSchema::new(required, Some(properties), None);
831
832    let description = cmd
833        .long_about
834        .as_deref()
835        .or(cmd.about.as_deref())
836        .map(String::from);
837    let title = cmd.about.as_ref().map(String::from);
838
839    let meta = {
840        let mut m = serde_json::Map::new();
841        m.insert(
842            "clapMcp".into(),
843            serde_json::json!({
844                "reinvocationSafe": config.reinvocation_safe,
845                "parallelSafe": config.parallel_safe,
846                "shareRuntime": config.share_runtime,
847            }),
848        );
849        Some(m)
850    };
851
852    Tool {
853        name: cmd.name.clone(),
854        title,
855        description,
856        input_schema,
857        annotations: None,
858        execution: None,
859        icons: vec![],
860        meta,
861        output_schema: output_schema
862            .cloned()
863            .and_then(|v| serde_json::from_value::<rust_mcp_sdk::schema::ToolOutputSchema>(v).ok()),
864    }
865}
866
867/// Serializable representation of a clap argument.
868#[derive(Debug, Clone, Serialize, Deserialize)]
869pub struct ClapArg {
870    pub id: String,
871    pub long: Option<String>,
872    pub short: Option<char>,
873    pub help: Option<String>,
874    pub long_help: Option<String>,
875    pub required: bool,
876    pub global: bool,
877    pub index: Option<usize>,
878    pub action: Option<String>,
879    pub value_names: Vec<String>,
880    pub num_args: Option<String>,
881}
882
883/// Returns the MCP input schema type for an argument based on its action (and num_args).
884/// - SetTrue / SetFalse: boolean
885/// - Count: integer
886/// - Append (or multi-value num_args): array of strings
887/// - Set / default: string
888///
889/// When the arg has a single value_name (e.g. VERSION), the array items schema gets a description
890/// so clients know what each element represents.
891fn mcp_type_for_arg(arg: &ClapArg) -> (serde_json::Value, Option<serde_json::Value>) {
892    let action = arg.action.as_deref().unwrap_or("Set");
893    let is_multi = matches!(action, "Append")
894        || arg
895            .num_args
896            .as_deref()
897            .is_some_and(|n| n.contains("..") && !n.contains("=1"));
898    let (json_type, items) = if matches!(action, "SetTrue" | "SetFalse") {
899        (serde_json::json!("boolean"), None)
900    } else if action == "Count" {
901        (serde_json::json!("integer"), None)
902    } else if is_multi {
903        let item_desc = arg
904            .value_names
905            .first()
906            .map(|name| format!("A {} value", name));
907        let items_schema = match item_desc {
908            Some(desc) => serde_json::json!({ "type": "string", "description": desc }),
909            None => serde_json::json!({ "type": "string" }),
910        };
911        (serde_json::json!("array"), Some(items_schema))
912    } else {
913        (serde_json::json!("string"), None)
914    };
915    (json_type, items)
916}
917
918/// Optional description suffix so MCP clients know what to pass for flags/count/list.
919fn mcp_action_description_hint(arg: &ClapArg) -> Option<String> {
920    let action = arg.action.as_deref()?;
921    let hint: String = match action {
922        "SetTrue" => " Boolean flag: set to true to pass this flag.".into(),
923        "SetFalse" => " Boolean flag: set to false to pass this flag (e.g. --no-xxx).".into(),
924        "Count" => " Number of times the flag is passed (e.g. -vvv).".into(),
925        "Append" => {
926            if let Some(name) = arg.value_names.first() {
927                format!(
928                    " List of {} values; pass a JSON array (e.g. [\"a\", \"b\"]).",
929                    name
930                )
931            } else {
932                " List of values; pass a JSON array (e.g. [\"a\", \"b\"]).".into()
933            }
934        }
935        _ => return None,
936    };
937    Some(hint)
938}
939
940/// Adds a root-level `--mcp` flag to a `clap::Command` (imperative clap usage).
941///
942/// When present, the CLI should start an MCP server instead of normal execution.
943/// If an arg with `--mcp` already exists, this is a no-op.
944///
945/// # Example
946///
947/// ```rust
948/// use clap::Command;
949/// use clap_mcp::command_with_mcp_flag;
950///
951/// let cmd = Command::new("myapp");
952/// let cmd = command_with_mcp_flag(cmd);
953/// assert!(cmd.get_arguments().any(|a| a.get_long() == Some("mcp")));
954/// ```
955pub fn command_with_mcp_flag(mut cmd: Command) -> Command {
956    let already = cmd
957        .get_arguments()
958        .any(|a| a.get_long().is_some_and(|l| l == MCP_FLAG_LONG));
959    if already {
960        return cmd;
961    }
962
963    cmd = cmd.arg(
964        Arg::new(MCP_FLAG_LONG)
965            .long(MCP_FLAG_LONG)
966            .help("Run an MCP server over stdio that exposes this CLI's clap schema")
967            .action(ArgAction::SetTrue)
968            .global(true),
969    );
970
971    cmd
972}
973
974/// Adds a root-level `--export-skills` flag (optional value for output directory) to a `clap::Command`.
975///
976/// When present, the CLI should generate [Agent Skills](https://agentskills.io/specification)
977/// (SKILL.md) and exit. If an arg with `--export-skills` already exists, this is a no-op.
978///
979/// # Example
980///
981/// ```rust
982/// use clap::Command;
983/// use clap_mcp::command_with_export_skills_flag;
984///
985/// let cmd = Command::new("myapp");
986/// let cmd = command_with_export_skills_flag(cmd);
987/// ```
988pub fn command_with_export_skills_flag(mut cmd: Command) -> Command {
989    let already = cmd
990        .get_arguments()
991        .any(|a| a.get_long().is_some_and(|l| l == EXPORT_SKILLS_FLAG_LONG));
992    if already {
993        return cmd;
994    }
995
996    cmd = cmd.arg(
997        Arg::new(EXPORT_SKILLS_FLAG_LONG)
998            .long(EXPORT_SKILLS_FLAG_LONG)
999            .value_name("DIR")
1000            .help("Generate Agent Skills (SKILL.md) from tools, resources, and prompts, then exit")
1001            .action(ArgAction::Set)
1002            .required(false)
1003            .global(true),
1004    );
1005
1006    cmd
1007}
1008
1009/// Adds both `--mcp` and `--export-skills` flags to the command.
1010/// Use this so schema extraction omits both; check for export-skills before mcp in the parse flow.
1011pub fn command_with_mcp_and_export_skills_flags(cmd: Command) -> Command {
1012    command_with_export_skills_flag(command_with_mcp_flag(cmd))
1013}
1014
1015/// Returns true if argv contains `--mcp` and no token is a root-level subcommand name.
1016/// Used to start MCP server before calling get_matches() when subcommand_required would otherwise fail.
1017fn argv_requests_mcp_without_subcommand(cmd: &Command) -> bool {
1018    let args: Vec<String> = std::env::args().skip(1).collect();
1019    argv_requests_mcp_without_subcommand_from_args(&args, cmd)
1020}
1021
1022/// Pure helper for argv_requests_mcp_without_subcommand; testable with arbitrary args.
1023pub(crate) fn argv_requests_mcp_without_subcommand_from_args(
1024    args: &[String],
1025    cmd: &Command,
1026) -> bool {
1027    let subcommand_names: std::collections::HashSet<String> = cmd
1028        .get_subcommands()
1029        .map(|s| s.get_name().to_string())
1030        .collect();
1031    let has_mcp = args.iter().any(|a| a == "--mcp");
1032    let has_subcommand = args.iter().any(|a| subcommand_names.contains(a.as_str()));
1033    has_mcp && !has_subcommand
1034}
1035
1036/// Returns `Some(None)` if argv contains `--export-skills` with no value (use default dir),
1037/// `Some(Some(path))` if `--export-skills=DIR` is present, and `None` if the flag is not present.
1038fn argv_export_skills_dir() -> Option<Option<std::path::PathBuf>> {
1039    let args: Vec<String> = std::env::args().skip(1).collect();
1040    argv_export_skills_dir_from_args(&args)
1041}
1042
1043/// Pure helper for argv_export_skills_dir; testable with arbitrary args.
1044pub(crate) fn argv_export_skills_dir_from_args(
1045    args: &[String],
1046) -> Option<Option<std::path::PathBuf>> {
1047    for (i, arg) in args.iter().enumerate() {
1048        if arg == "--export-skills" {
1049            return Some(
1050                args.get(i + 1)
1051                    .filter(|s| !s.starts_with('-'))
1052                    .map(std::path::PathBuf::from),
1053            );
1054        }
1055        if let Some(dir) = arg.strip_prefix("--export-skills=") {
1056            return Some(Some(std::path::PathBuf::from(dir)));
1057        }
1058    }
1059    None
1060}
1061
1062/// Extracts a serializable schema from a `clap::Command` (imperative clap usage).
1063///
1064/// The schema reflects the CLI as defined by the application. Any `--mcp` flag
1065/// added via [`command_with_mcp_flag`] is intentionally omitted.
1066///
1067/// # Example
1068///
1069/// ```rust
1070/// use clap::{CommandFactory, Parser};
1071/// use clap_mcp::schema_from_command;
1072///
1073/// #[derive(Parser)]
1074/// #[command(name = "mycli")]
1075/// enum Cli { Foo }
1076///
1077/// let schema = schema_from_command(&Cli::command());
1078/// assert_eq!(schema.root.name, "mycli");
1079/// ```
1080pub fn schema_from_command(cmd: &Command) -> ClapSchema {
1081    schema_from_command_with_metadata(cmd, &ClapMcpSchemaMetadata::default())
1082}
1083
1084/// Extracts a schema from a `clap::Command` with MCP metadata applied.
1085///
1086/// Use [`ClapMcpSchemaMetadata`] to skip commands/args or make optional args required in MCP.
1087pub fn schema_from_command_with_metadata(
1088    cmd: &Command,
1089    metadata: &ClapMcpSchemaMetadata,
1090) -> ClapSchema {
1091    let skip_commands: std::collections::HashSet<_> =
1092        metadata.skip_commands.iter().cloned().collect();
1093    ClapSchema {
1094        root: command_to_schema_with_metadata(cmd, metadata, &skip_commands),
1095    }
1096}
1097
1098fn command_to_schema_with_metadata(
1099    cmd: &Command,
1100    metadata: &ClapMcpSchemaMetadata,
1101    skip_commands: &std::collections::HashSet<String>,
1102) -> ClapCommand {
1103    let mut args: Vec<ClapArg> = cmd
1104        .get_arguments()
1105        .filter(|a| {
1106            let long = a.get_long();
1107            long != Some(MCP_FLAG_LONG) && long != Some(EXPORT_SKILLS_FLAG_LONG)
1108        })
1109        .map(arg_to_schema)
1110        .collect();
1111
1112    let cmd_name = cmd.get_name().to_string();
1113    let skip_args: std::collections::HashSet<_> = metadata
1114        .skip_args
1115        .get(&cmd_name)
1116        .map(|v| v.iter().cloned().collect())
1117        .unwrap_or_default();
1118
1119    let requires_args: std::collections::HashSet<_> = metadata
1120        .requires_args
1121        .get(&cmd_name)
1122        .map(|v| v.iter().cloned().collect())
1123        .unwrap_or_default();
1124
1125    args.retain(|a| !skip_args.contains(&a.id));
1126    for arg in &mut args {
1127        if requires_args.contains(&arg.id) {
1128            arg.required = true;
1129        }
1130    }
1131    args.sort_by(|a, b| a.id.cmp(&b.id));
1132
1133    let subcommands: Vec<ClapCommand> = cmd
1134        .get_subcommands()
1135        .filter(|s| !skip_commands.contains(&s.get_name().to_string()))
1136        .map(|s| command_to_schema_with_metadata(s, metadata, skip_commands))
1137        .collect();
1138
1139    ClapCommand {
1140        name: cmd.get_name().to_string(),
1141        about: cmd.get_about().map(|s| s.to_string()),
1142        long_about: cmd.get_long_about().map(|s| s.to_string()),
1143        version: cmd.get_version().map(|s| s.to_string()),
1144        args,
1145        subcommands,
1146    }
1147}
1148
1149/// Imperative clap entrypoint.
1150///
1151/// - Adds `--mcp` to the command (if not already present)
1152/// - If `--mcp` is present, starts an MCP stdio server and exits the process
1153/// - Otherwise, returns `ArgMatches` for normal app execution
1154///
1155/// # Example
1156///
1157/// ```rust,ignore
1158/// use clap::Command;
1159/// use clap_mcp::{command_with_mcp_flag, get_matches_or_serve_mcp};
1160///
1161/// let cmd = command_with_mcp_flag(Command::new("myapp"));
1162/// let matches = get_matches_or_serve_mcp(cmd);
1163/// // If we get here, --mcp was not passed
1164/// ```
1165pub fn get_matches_or_serve_mcp(cmd: Command) -> clap::ArgMatches {
1166    get_matches_or_serve_mcp_with_config(cmd, ClapMcpConfig::default())
1167}
1168
1169/// Imperative clap entrypoint with execution safety configuration.
1170///
1171/// See [`get_matches_or_serve_mcp`] for behavior. Use `config` to declare
1172/// reinvocation and parallel execution safety for tool execution.
1173pub fn get_matches_or_serve_mcp_with_config(
1174    cmd: Command,
1175    config: ClapMcpConfig,
1176) -> clap::ArgMatches {
1177    get_matches_or_serve_mcp_with_config_and_metadata(
1178        cmd,
1179        config,
1180        &ClapMcpSchemaMetadata::default(),
1181    )
1182}
1183
1184/// Imperative clap entrypoint with execution safety configuration and schema metadata.
1185///
1186/// Use `metadata` for `#[clap_mcp(skip)]` and `#[clap_mcp(requires = "arg_name")]` behavior.
1187pub fn get_matches_or_serve_mcp_with_config_and_metadata(
1188    cmd: Command,
1189    config: ClapMcpConfig,
1190    metadata: &ClapMcpSchemaMetadata,
1191) -> clap::ArgMatches {
1192    let schema = schema_from_command_with_metadata(&cmd, metadata);
1193    let cmd = command_with_mcp_and_export_skills_flags(cmd);
1194
1195    if let Some(maybe_dir) = argv_export_skills_dir() {
1196        let tools = tools_from_schema_with_config_and_metadata(&schema, &config, metadata);
1197        let output_dir = maybe_dir.unwrap_or_else(|| PathBuf::from(".agents").join("skills"));
1198        let app_name = schema.root.name.as_str();
1199        let serve_options = ClapMcpServeOptions::default();
1200        if let Err(e) = content::export_skills(
1201            &schema,
1202            metadata,
1203            &tools,
1204            &serve_options.custom_resources,
1205            &serve_options.custom_prompts,
1206            &output_dir,
1207            app_name,
1208        ) {
1209            eprintln!("export-skills failed: {}", e);
1210            std::process::exit(1);
1211        }
1212        std::process::exit(0);
1213    }
1214
1215    if config.allow_mcp_without_subcommand && argv_requests_mcp_without_subcommand(&cmd) {
1216        let schema_json = match serde_json::to_string_pretty(&schema) {
1217            Ok(s) => s,
1218            Err(e) => {
1219                eprintln!("Failed to serialize CLI schema: {}", e);
1220                std::process::exit(1);
1221            }
1222        };
1223        if let Err(e) = serve_schema_json_over_stdio_blocking(
1224            schema_json,
1225            None,
1226            config,
1227            None,
1228            ClapMcpServeOptions::default(),
1229            metadata,
1230        ) {
1231            eprintln!("MCP server error: {}", e);
1232            std::process::exit(1);
1233        }
1234        std::process::exit(0);
1235    }
1236
1237    let matches = cmd.get_matches();
1238    if matches.get_flag(MCP_FLAG_LONG) {
1239        let schema_json = match serde_json::to_string_pretty(&schema) {
1240            Ok(s) => s,
1241            Err(e) => {
1242                eprintln!("Failed to serialize CLI schema: {}", e);
1243                std::process::exit(1);
1244            }
1245        };
1246        if let Err(e) = serve_schema_json_over_stdio_blocking(
1247            schema_json,
1248            None,
1249            config,
1250            None,
1251            ClapMcpServeOptions::default(),
1252            metadata,
1253        ) {
1254            eprintln!("MCP server error: {}", e);
1255            std::process::exit(1);
1256        }
1257        std::process::exit(0);
1258    }
1259
1260    matches
1261}
1262
1263/// Canonical entrypoint for derive-based CLIs: parse (or serve if `--mcp`) and return self.
1264///
1265/// With the trait in scope, use `Args::parse_or_serve_mcp()` instead of
1266/// `parse_or_serve_mcp_attr::<Args>()`. Equivalent to calling [`parse_or_serve_mcp_attr`];
1267/// that free function remains available if you prefer it.
1268///
1269/// # Example
1270///
1271/// ```rust,ignore
1272/// use clap::Parser;
1273/// use clap_mcp::{ClapMcp, ParseOrServeMcp};
1274///
1275/// #[derive(Parser, ClapMcp)]
1276/// #[clap_mcp(reinvocation_safe, parallel_safe = false)]
1277/// enum Cli { Foo }
1278///
1279/// fn main() {
1280///     let cli = Cli::parse_or_serve_mcp();
1281///     // ...
1282/// }
1283/// ```
1284pub trait ParseOrServeMcp {
1285    fn parse_or_serve_mcp() -> Self;
1286}
1287
1288impl<T> ParseOrServeMcp for T
1289where
1290    T: ClapMcpConfigProvider
1291        + ClapMcpSchemaMetadataProvider
1292        + ClapMcpToolExecutor
1293        + clap::Parser
1294        + clap::CommandFactory
1295        + clap::FromArgMatches
1296        + 'static,
1297{
1298    fn parse_or_serve_mcp() -> Self {
1299        parse_or_serve_mcp_attr::<T>()
1300    }
1301}
1302
1303/// High-level helper for `clap` derive-based CLIs.
1304///
1305/// - Adds `--mcp` to the command
1306/// - If `--mcp` is present, starts an MCP stdio server and exits the process
1307/// - Otherwise, returns the parsed CLI type
1308///
1309/// Uses default [`ClapMcpConfig`]. For config from `#[clap_mcp(...)]` attributes,
1310/// use [`parse_or_serve_mcp_attr`].
1311///
1312/// For a **struct root with subcommand**, parse the root type then call your run
1313/// logic on the subcommand (e.g. `run(args.command)`). See the crate README
1314/// section "Struct root with subcommand".
1315///
1316/// # Example
1317///
1318/// ```rust,ignore
1319/// use clap::Parser;
1320/// use clap_mcp::ClapMcp;
1321///
1322/// #[derive(Parser, ClapMcp)]
1323/// enum Cli { Foo }
1324///
1325/// fn main() {
1326///     let cli = clap_mcp::parse_or_serve_mcp::<Cli>();
1327///     // If we get here, --mcp was not passed
1328/// }
1329/// ```
1330pub fn parse_or_serve_mcp<T>() -> T
1331where
1332    T: ClapMcpSchemaMetadataProvider
1333        + ClapMcpToolExecutor
1334        + clap::Parser
1335        + clap::CommandFactory
1336        + clap::FromArgMatches
1337        + 'static,
1338{
1339    parse_or_serve_mcp_with_config::<T>(ClapMcpConfig::default())
1340}
1341
1342/// High-level helper for `clap` derive-based CLIs with config from `#[clap_mcp(...)]` attributes.
1343///
1344/// Use `#[derive(ClapMcp)]` and `#[clap_mcp(reinvocation_safe, parallel_safe = false)]` on your CLI type,
1345/// then call this instead of [`parse_or_serve_mcp`]. Config is taken from `T::clap_mcp_config()`.
1346///
1347/// For a **struct root with subcommand**, parse the root type then call your run
1348/// logic on the subcommand (e.g. `run(args.command)` or `match args.command { ... }`).
1349/// See the crate README section "Struct root with subcommand" and [`ParseOrServeMcp`].
1350///
1351/// # Example
1352///
1353/// ```rust,ignore
1354/// use clap::Parser;
1355/// use clap_mcp::ClapMcp;
1356///
1357/// #[derive(Parser, ClapMcp)]
1358/// #[clap_mcp(reinvocation_safe, parallel_safe = false)]
1359/// #[clap_mcp_output_from = "run"]
1360/// enum Cli { Foo }
1361///
1362/// fn run(cmd: Cli) -> String {
1363///     match cmd { Cli::Foo => "done".to_string() }
1364/// }
1365///
1366/// fn main() {
1367///     let cli = clap_mcp::parse_or_serve_mcp_attr::<Cli>();
1368///     println!("{}", run(cli));
1369/// }
1370/// ```
1371pub fn parse_or_serve_mcp_attr<T>() -> T
1372where
1373    T: ClapMcpConfigProvider
1374        + ClapMcpSchemaMetadataProvider
1375        + ClapMcpToolExecutor
1376        + clap::Parser
1377        + clap::CommandFactory
1378        + clap::FromArgMatches
1379        + 'static,
1380{
1381    parse_or_serve_mcp_with_config::<T>(T::clap_mcp_config())
1382}
1383
1384/// Run parsed CLI through a closure, or serve MCP if `--mcp` is present.
1385///
1386/// If `--mcp` is passed, starts the MCP server and does not return. Otherwise,
1387/// parses the CLI type `A`, calls `f(args)`, and returns the result. Use this
1388/// when you want the "parse then run" flow in one place (e.g. `run_or_serve_mcp::<Cli, _>(|c| Ok(run(c)))`)
1389/// instead of parsing and then calling `run` in main. For a simple "parse then branch"
1390/// style, use [`ParseOrServeMcp::parse_or_serve_mcp`] or [`parse_or_serve_mcp_attr`].
1391///
1392/// # Example
1393///
1394/// ```rust,ignore
1395/// fn main() -> Result<(), Box<dyn std::error::Error>> {
1396///     clap_mcp::run_or_serve_mcp::<Cli, _, _, _>(|cli| Ok(run(cli)))
1397/// }
1398/// ```
1399pub fn run_or_serve_mcp<A, F, R, E>(f: F) -> Result<R, E>
1400where
1401    A: ClapMcpConfigProvider
1402        + ClapMcpSchemaMetadataProvider
1403        + ClapMcpToolExecutor
1404        + clap::Parser
1405        + clap::CommandFactory
1406        + clap::FromArgMatches
1407        + 'static,
1408    F: FnOnce(A) -> Result<R, E>,
1409{
1410    let args = parse_or_serve_mcp_attr::<A>();
1411    f(args)
1412}
1413
1414/// High-level helper for `clap` derive-based CLIs with execution safety configuration.
1415///
1416/// See [`parse_or_serve_mcp`] for behavior. Use `config` to declare reinvocation
1417/// and parallel execution safety. When `reinvocation_safe` is true, uses in-process
1418/// execution; requires `T: ClapMcpToolExecutor`.
1419pub fn parse_or_serve_mcp_with_config<T>(config: ClapMcpConfig) -> T
1420where
1421    T: ClapMcpSchemaMetadataProvider
1422        + ClapMcpToolExecutor
1423        + clap::Parser
1424        + clap::CommandFactory
1425        + clap::FromArgMatches
1426        + 'static,
1427{
1428    parse_or_serve_mcp_with_config_and_options::<T>(config, ClapMcpServeOptions::default())
1429}
1430
1431/// Like [`parse_or_serve_mcp_with_config`] but with custom serve options (e.g. logging).
1432///
1433/// Use `serve_options.log_rx` to forward log messages to the MCP client.
1434/// See [`ClapMcpServeOptions`] and the `logging` module.
1435pub fn parse_or_serve_mcp_with_config_and_options<T>(
1436    config: ClapMcpConfig,
1437    serve_options: ClapMcpServeOptions,
1438) -> T
1439where
1440    T: ClapMcpSchemaMetadataProvider
1441        + ClapMcpToolExecutor
1442        + clap::Parser
1443        + clap::CommandFactory
1444        + clap::FromArgMatches
1445        + 'static,
1446{
1447    let mut cmd = T::command();
1448    cmd = command_with_mcp_and_export_skills_flags(cmd);
1449
1450    if let Some(maybe_dir) = argv_export_skills_dir() {
1451        let base_cmd = T::command();
1452        let metadata = T::clap_mcp_schema_metadata();
1453        let schema = schema_from_command_with_metadata(&base_cmd, &metadata);
1454        let tools = tools_from_schema_with_config_and_metadata(&schema, &config, &metadata);
1455        let output_dir = maybe_dir.unwrap_or_else(|| PathBuf::from(".agents").join("skills"));
1456        let app_name = schema.root.name.as_str();
1457        if let Err(e) = content::export_skills(
1458            &schema,
1459            &metadata,
1460            &tools,
1461            &serve_options.custom_resources,
1462            &serve_options.custom_prompts,
1463            &output_dir,
1464            app_name,
1465        ) {
1466            eprintln!("export-skills failed: {}", e);
1467            std::process::exit(1);
1468        }
1469        std::process::exit(0);
1470    }
1471
1472    if config.allow_mcp_without_subcommand && argv_requests_mcp_without_subcommand(&cmd) {
1473        let base_cmd = T::command();
1474        let metadata = T::clap_mcp_schema_metadata();
1475        let schema = schema_from_command_with_metadata(&base_cmd, &metadata);
1476        let schema_json = match serde_json::to_string_pretty(&schema) {
1477            Ok(s) => s,
1478            Err(e) => {
1479                eprintln!("Failed to serialize CLI schema: {}", e);
1480                std::process::exit(1);
1481            }
1482        };
1483        let exe = std::env::current_exe().ok();
1484
1485        let in_process_handler = if config.reinvocation_safe {
1486            #[cfg(unix)]
1487            let capture_stdout = serve_options.capture_stdout;
1488            #[cfg(not(unix))]
1489            let capture_stdout = false;
1490            Some(make_in_process_handler::<T>(schema.clone(), capture_stdout))
1491        } else {
1492            None
1493        };
1494
1495        if let Err(e) = serve_schema_json_over_stdio_blocking(
1496            schema_json,
1497            if config.reinvocation_safe { None } else { exe },
1498            config,
1499            in_process_handler,
1500            serve_options,
1501            &metadata,
1502        ) {
1503            eprintln!("MCP server error: {}", e);
1504            std::process::exit(1);
1505        }
1506
1507        std::process::exit(0);
1508    }
1509
1510    let matches = cmd.get_matches();
1511    let mcp_requested = matches.get_flag(MCP_FLAG_LONG);
1512
1513    if mcp_requested {
1514        let base_cmd = T::command();
1515        let metadata = T::clap_mcp_schema_metadata();
1516        let schema = schema_from_command_with_metadata(&base_cmd, &metadata);
1517        let schema_json = match serde_json::to_string_pretty(&schema) {
1518            Ok(s) => s,
1519            Err(e) => {
1520                eprintln!("Failed to serialize CLI schema: {}", e);
1521                std::process::exit(1);
1522            }
1523        };
1524        let exe = std::env::current_exe().ok();
1525
1526        let in_process_handler = if config.reinvocation_safe {
1527            #[cfg(unix)]
1528            let capture_stdout = serve_options.capture_stdout;
1529            #[cfg(not(unix))]
1530            let capture_stdout = false;
1531            Some(make_in_process_handler::<T>(schema.clone(), capture_stdout))
1532        } else {
1533            None
1534        };
1535
1536        if let Err(e) = serve_schema_json_over_stdio_blocking(
1537            schema_json,
1538            if config.reinvocation_safe { None } else { exe },
1539            config,
1540            in_process_handler,
1541            serve_options,
1542            &metadata,
1543        ) {
1544            eprintln!("MCP server error: {}", e);
1545            std::process::exit(1);
1546        }
1547
1548        std::process::exit(0);
1549    }
1550
1551    T::from_arg_matches(&matches).unwrap_or_else(|e| e.exit())
1552}
1553
1554fn arg_to_schema(arg: &clap::Arg) -> ClapArg {
1555    let value_names = arg
1556        .get_value_names()
1557        .map(|names| names.iter().map(|n| n.to_string()).collect())
1558        .unwrap_or_default();
1559
1560    ClapArg {
1561        id: arg.get_id().to_string(),
1562        long: arg.get_long().map(|s| s.to_string()),
1563        short: arg.get_short(),
1564        help: arg.get_help().map(|s| s.to_string()),
1565        long_help: arg.get_long_help().map(|s| s.to_string()),
1566        required: arg.is_required_set(),
1567        global: arg.is_global_set(),
1568        index: arg.get_index(),
1569        action: Some(format!("{:?}", arg.get_action())),
1570        value_names,
1571        num_args: arg.get_num_args().map(|r| format!("{r:?}")),
1572    }
1573}
1574
1575/// Validates that all required args for the command are present in the arguments map.
1576/// Returns Err with a clear message if any required arg is missing.
1577fn validate_required_args(
1578    schema: &ClapSchema,
1579    command_name: &str,
1580    arguments: &serde_json::Map<String, serde_json::Value>,
1581) -> Result<(), String> {
1582    let cmd = schema
1583        .root
1584        .all_commands()
1585        .into_iter()
1586        .find(|c| c.name == command_name);
1587    let Some(cmd) = cmd else {
1588        return Ok(());
1589    };
1590    let missing: Vec<_> = cmd
1591        .args
1592        .iter()
1593        .filter(|a| {
1594            if !a.required || is_builtin_arg(a.id.as_str()) {
1595                return false;
1596            }
1597            let has_value = arguments.get(&a.id).map(|v| {
1598                let action = a.action.as_deref().unwrap_or("Set");
1599                if matches!(action, "SetTrue" | "SetFalse" | "Count") {
1600                    // Flag/count: key present is enough (value can be false/0)
1601                    true
1602                } else if action == "Append" || v.is_array() {
1603                    !value_to_strings(v).is_some_and(|s| s.is_empty())
1604                } else {
1605                    value_to_string(v).is_some_and(|s| !s.is_empty())
1606                }
1607            });
1608            !has_value.unwrap_or(false)
1609        })
1610        .map(|a| a.id.clone())
1611        .collect();
1612    if missing.is_empty() {
1613        Ok(())
1614    } else {
1615        Err(format!(
1616            "Missing required argument(s): {}. The MCP tool schema marks these as required.",
1617            missing.join(", ")
1618        ))
1619    }
1620}
1621
1622/// Builds full argv for clap's `get_matches_from` (program name + subcommand + args).
1623fn build_argv_for_clap(
1624    schema: &ClapSchema,
1625    command_name: &str,
1626    arguments: serde_json::Map<String, serde_json::Value>,
1627) -> Vec<String> {
1628    let args = build_tool_argv(schema, command_name, arguments);
1629    let mut argv = vec!["cli".to_string()]; // program name for parsing
1630    if let Some(path) = command_path(schema, command_name) {
1631        argv.extend(path.into_iter().skip(1));
1632    }
1633    argv.extend(args);
1634    argv
1635}
1636
1637fn command_path(schema: &ClapSchema, command_name: &str) -> Option<Vec<String>> {
1638    fn walk(cmd: &ClapCommand, command_name: &str, path: &mut Vec<String>) -> bool {
1639        path.push(cmd.name.clone());
1640        if cmd.name == command_name {
1641            return true;
1642        }
1643        for subcommand in &cmd.subcommands {
1644            if walk(subcommand, command_name, path) {
1645                return true;
1646            }
1647        }
1648        path.pop();
1649        false
1650    }
1651
1652    let mut path = Vec::new();
1653    if walk(&schema.root, command_name, &mut path) {
1654        Some(path)
1655    } else {
1656        None
1657    }
1658}
1659
1660/// Builds argv for the executable from the schema and tool arguments.
1661///
1662/// Positional args (no long form) are passed in index order; optional args as `--long value`.
1663fn build_tool_argv(
1664    schema: &ClapSchema,
1665    command_name: &str,
1666    arguments: serde_json::Map<String, serde_json::Value>,
1667) -> Vec<String> {
1668    let cmd = schema
1669        .root
1670        .all_commands()
1671        .into_iter()
1672        .find(|c| c.name == command_name);
1673    let Some(cmd) = cmd else {
1674        return Vec::new();
1675    };
1676
1677    let args: Vec<&ClapArg> = cmd
1678        .args
1679        .iter()
1680        .filter(|a| !is_builtin_arg(a.id.as_str()))
1681        .collect();
1682
1683    let mut positionals: Vec<&ClapArg> =
1684        args.iter().filter(|a| a.long.is_none()).copied().collect();
1685    positionals.sort_by_key(|a| a.index.unwrap_or(0));
1686    let optionals: Vec<&ClapArg> = args.iter().filter(|a| a.long.is_some()).copied().collect();
1687
1688    let mut out = Vec::new();
1689
1690    for arg in positionals {
1691        if let Some(v) = arguments.get(&arg.id)
1692            && let Some(strings) = value_to_strings(v)
1693        {
1694            for s in strings {
1695                out.push(s);
1696            }
1697        }
1698    }
1699    for arg in optionals {
1700        if let Some(long) = &arg.long {
1701            let action = arg.action.as_deref().unwrap_or("Set");
1702            let v = arguments.get(&arg.id);
1703            match action {
1704                "SetTrue" => {
1705                    if v.and_then(value_to_string).is_some_and(|s| s == "true")
1706                        || v.and_then(|x| x.as_bool()).is_some_and(|b| b)
1707                    {
1708                        out.push(format!("--{long}"));
1709                    }
1710                }
1711                "SetFalse" => {
1712                    if v.and_then(value_to_string).is_some_and(|s| s == "false")
1713                        || v.and_then(|x| x.as_bool()).is_some_and(|b| !b)
1714                    {
1715                        out.push(format!("--{long}"));
1716                    }
1717                }
1718                "Count" => {
1719                    let n = v.and_then(|x| x.as_i64()).unwrap_or(0).clamp(0, i64::MAX) as usize;
1720                    for _ in 0..n {
1721                        out.push(format!("--{long}"));
1722                    }
1723                }
1724                "Append" => {
1725                    if let Some(v) = v.and_then(value_to_strings) {
1726                        for s in v {
1727                            if !s.is_empty() {
1728                                out.push(format!("--{long}"));
1729                                out.push(s);
1730                            }
1731                        }
1732                    } else if let Some(s) = v.and_then(value_to_string)
1733                        && !s.is_empty()
1734                    {
1735                        out.push(format!("--{long}"));
1736                        out.push(s);
1737                    }
1738                }
1739                _ => {
1740                    if let Some(s) = v.and_then(value_to_string)
1741                        && !s.is_empty()
1742                    {
1743                        out.push(format!("--{long}"));
1744                        out.push(s);
1745                    }
1746                }
1747            }
1748        }
1749    }
1750
1751    out
1752}
1753
1754/// Type for in-process tool execution handler.
1755///
1756/// Called with `(command_name, arguments)` and returns `Result<ClapMcpToolOutput, ClapMcpToolError>`.
1757/// Used when `reinvocation_safe` is true to avoid spawning subprocesses.
1758pub type InProcessToolHandler = Arc<
1759    dyn Fn(
1760            &str,
1761            serde_json::Map<String, serde_json::Value>,
1762        ) -> Result<ClapMcpToolOutput, ClapMcpToolError>
1763        + Send
1764        + Sync,
1765>;
1766
1767fn merge_captured_stdout(
1768    result: Result<ClapMcpToolOutput, ClapMcpToolError>,
1769    captured: String,
1770) -> Result<ClapMcpToolOutput, ClapMcpToolError> {
1771    match result {
1772        Ok(ClapMcpToolOutput::Text(text)) if !captured.is_empty() => {
1773            let merged = if text.is_empty() {
1774                captured.trim().to_string()
1775            } else {
1776                let cap = captured.trim();
1777                if cap.is_empty() {
1778                    text
1779                } else {
1780                    format!("{text}\n{cap}")
1781                }
1782            };
1783            Ok(ClapMcpToolOutput::Text(merged))
1784        }
1785        other => other,
1786    }
1787}
1788
1789fn execute_in_process_command<T>(
1790    schema: &ClapSchema,
1791    command_name: &str,
1792    arguments: serde_json::Map<String, serde_json::Value>,
1793    capture_stdout: bool,
1794) -> Result<ClapMcpToolOutput, ClapMcpToolError>
1795where
1796    T: ClapMcpToolExecutor + clap::CommandFactory + clap::FromArgMatches,
1797{
1798    validate_required_args(schema, command_name, &arguments).map_err(ClapMcpToolError::text)?;
1799    let argv = build_argv_for_clap(schema, command_name, arguments.clone());
1800    let matches = T::command()
1801        .try_get_matches_from(&argv)
1802        .map_err(|e| ClapMcpToolError::text(e.to_string()))?;
1803    let cli = T::from_arg_matches(&matches).map_err(|e| ClapMcpToolError::text(e.to_string()))?;
1804
1805    if capture_stdout {
1806        let (result, captured) =
1807            run_with_stdout_capture(|| <T as ClapMcpToolExecutor>::execute_for_mcp(cli));
1808        merge_captured_stdout(result, captured)
1809    } else {
1810        <T as ClapMcpToolExecutor>::execute_for_mcp(cli)
1811    }
1812}
1813
1814fn make_in_process_handler<T>(schema: ClapSchema, capture_stdout: bool) -> InProcessToolHandler
1815where
1816    T: ClapMcpToolExecutor + clap::CommandFactory + clap::FromArgMatches + 'static,
1817{
1818    Arc::new(
1819        move |cmd: &str, args: serde_json::Map<String, serde_json::Value>| {
1820            execute_in_process_command::<T>(&schema, cmd, args, capture_stdout)
1821        },
1822    ) as InProcessToolHandler
1823}
1824
1825fn format_panic_payload(payload: &(dyn std::any::Any + Send)) -> String {
1826    if let Some(s) = payload.downcast_ref::<&str>() {
1827        return (*s).to_string();
1828    }
1829    if let Some(s) = payload.downcast_ref::<String>() {
1830        return s.clone();
1831    }
1832    "<panic>".to_string()
1833}
1834
1835fn value_to_string(v: &serde_json::Value) -> Option<String> {
1836    if v.is_null() {
1837        return None;
1838    }
1839    Some(match v {
1840        serde_json::Value::String(s) => s.clone(),
1841        serde_json::Value::Number(n) => n.to_string(),
1842        serde_json::Value::Bool(b) => b.to_string(),
1843        other => other.to_string(),
1844    })
1845}
1846
1847/// Returns one or more string values for MCP input. For arrays, returns each element as string; otherwise single value.
1848fn value_to_strings(v: &serde_json::Value) -> Option<Vec<String>> {
1849    if v.is_null() {
1850        return None;
1851    }
1852    match v {
1853        serde_json::Value::Array(arr) => {
1854            let out: Vec<String> = arr
1855                .iter()
1856                .filter_map(value_to_string)
1857                .filter(|s| !s.is_empty())
1858                .collect();
1859            Some(out)
1860        }
1861        _ => value_to_string(v).map(|s| vec![s]),
1862    }
1863}
1864
1865fn clap_schema_resource() -> Resource {
1866    Resource {
1867        name: "clap-schema".into(),
1868        uri: MCP_RESOURCE_URI_SCHEMA.into(),
1869        title: Some("Clap CLI schema".into()),
1870        description: Some("JSON schema extracted from clap Command definitions".into()),
1871        mime_type: Some("application/json".into()),
1872        annotations: None,
1873        icons: vec![],
1874        meta: None,
1875        size: None,
1876    }
1877}
1878
1879fn list_resources_result(custom_resources: &[content::CustomResource]) -> ListResourcesResult {
1880    let mut resources = vec![clap_schema_resource()];
1881    for resource in custom_resources {
1882        resources.push(resource.to_list_resource());
1883    }
1884    ListResourcesResult {
1885        resources,
1886        meta: None,
1887        next_cursor: None,
1888    }
1889}
1890
1891async fn read_resource_result(
1892    schema_json: &str,
1893    custom_resources: &[content::CustomResource],
1894    params: ReadResourceRequestParams,
1895) -> std::result::Result<ReadResourceResult, RpcError> {
1896    if params.uri == MCP_RESOURCE_URI_SCHEMA {
1897        return Ok(ReadResourceResult {
1898            contents: vec![ReadResourceContent::TextResourceContents(
1899                TextResourceContents {
1900                    uri: params.uri,
1901                    mime_type: Some("application/json".into()),
1902                    text: schema_json.to_string(),
1903                    meta: None,
1904                },
1905            )],
1906            meta: None,
1907        });
1908    }
1909    let custom = custom_resources
1910        .iter()
1911        .find(|resource| resource.uri == params.uri);
1912    let Some(resource) = custom else {
1913        return Err(RpcError::invalid_params()
1914            .with_message(format!("unknown resource uri: {}", params.uri)));
1915    };
1916    let text = content::resolve_resource_content(resource, &params.uri).await?;
1917    Ok(ReadResourceResult {
1918        contents: vec![ReadResourceContent::TextResourceContents(
1919            TextResourceContents {
1920                uri: params.uri.clone(),
1921                mime_type: resource.mime_type.clone(),
1922                text,
1923                meta: None,
1924            },
1925        )],
1926        meta: None,
1927    })
1928}
1929
1930fn logging_guide_prompt() -> Prompt {
1931    Prompt {
1932        name: PROMPT_LOGGING_GUIDE.to_string(),
1933        description: Some("How to interpret log messages from this clap-mcp server".to_string()),
1934        arguments: vec![],
1935        icons: vec![],
1936        meta: None,
1937        title: Some("clap-mcp Logging Guide".to_string()),
1938    }
1939}
1940
1941fn list_prompts_result(
1942    logging_enabled: bool,
1943    custom_prompts: &[content::CustomPrompt],
1944) -> ListPromptsResult {
1945    let mut prompts = Vec::new();
1946    if logging_enabled {
1947        prompts.push(logging_guide_prompt());
1948    }
1949    for prompt in custom_prompts {
1950        prompts.push(prompt.to_list_prompt());
1951    }
1952    ListPromptsResult {
1953        prompts,
1954        meta: None,
1955        next_cursor: None,
1956    }
1957}
1958
1959async fn get_prompt_result(
1960    logging_enabled: bool,
1961    custom_prompts: &[content::CustomPrompt],
1962    params: GetPromptRequestParams,
1963) -> std::result::Result<GetPromptResult, RpcError> {
1964    if params.name == PROMPT_LOGGING_GUIDE {
1965        if !logging_enabled {
1966            return Err(
1967                RpcError::invalid_params().with_message(format!("unknown prompt: {}", params.name))
1968            );
1969        }
1970        return Ok(GetPromptResult {
1971            description: Some(
1972                "How to interpret log messages from this clap-mcp server".to_string(),
1973            ),
1974            messages: vec![PromptMessage {
1975                content: ContentBlock::text_content(LOGGING_GUIDE_CONTENT.to_string()),
1976                role: Role::User,
1977            }],
1978            meta: None,
1979        });
1980    }
1981    let custom = custom_prompts
1982        .iter()
1983        .find(|prompt| prompt.name == params.name);
1984    let Some(prompt) = custom else {
1985        return Err(
1986            RpcError::invalid_params().with_message(format!("unknown prompt: {}", params.name))
1987        );
1988    };
1989    let arguments: serde_json::Map<String, serde_json::Value> = params
1990        .arguments
1991        .as_ref()
1992        .map(|map| {
1993            map.iter()
1994                .map(|(key, value)| (key.clone(), serde_json::Value::String(value.clone())))
1995                .collect()
1996        })
1997        .unwrap_or_default();
1998    let messages = content::resolve_prompt_content(prompt, &params.name, &arguments).await?;
1999    Ok(GetPromptResult {
2000        description: prompt.description.clone(),
2001        messages,
2002        meta: None,
2003    })
2004}
2005
2006fn validate_tool_argument_names(
2007    tool: &Tool,
2008    tool_name: &str,
2009    arguments: &serde_json::Map<String, serde_json::Value>,
2010) -> std::result::Result<(), CallToolError> {
2011    if let Some(ref props) = tool.input_schema.properties {
2012        for key in arguments.keys() {
2013            if !props.contains_key(key) {
2014                return Err(CallToolError::invalid_arguments(
2015                    tool_name,
2016                    Some(format!("unknown argument: {key}")),
2017                ));
2018            }
2019        }
2020    }
2021    Ok(())
2022}
2023
2024fn call_tool_result_from_output(output: ClapMcpToolOutput) -> CallToolResult {
2025    let (content, structured_content) = match output {
2026        ClapMcpToolOutput::Text(text) => (vec![ContentBlock::text_content(text)], None),
2027        ClapMcpToolOutput::Structured(value) => {
2028            let json_text =
2029                serde_json::to_string_pretty(&value).unwrap_or_else(|_| value.to_string());
2030            let structured = value.as_object().cloned();
2031            (vec![ContentBlock::text_content(json_text)], structured)
2032        }
2033    };
2034    CallToolResult {
2035        content,
2036        is_error: None,
2037        meta: None,
2038        structured_content,
2039    }
2040}
2041
2042fn call_tool_result_from_tool_error(error: ClapMcpToolError) -> CallToolResult {
2043    let structured_content = error
2044        .structured
2045        .as_ref()
2046        .and_then(|value| value.as_object().cloned());
2047    CallToolResult {
2048        content: vec![ContentBlock::text_content(error.message)],
2049        is_error: Some(true),
2050        meta: None,
2051        structured_content,
2052    }
2053}
2054
2055fn call_tool_result_from_panic(panic_payload: &(dyn std::any::Any + Send)) -> CallToolResult {
2056    let msg = format_panic_payload(panic_payload);
2057    CallToolResult {
2058        content: vec![ContentBlock::text_content(format!(
2059            "Tool panicked: {}",
2060            msg
2061        ))],
2062        is_error: Some(true),
2063        meta: None,
2064        structured_content: None,
2065    }
2066}
2067
2068fn schema_parse_failure_result() -> CallToolResult {
2069    CallToolResult {
2070        content: vec![ContentBlock::text_content("Failed to parse schema".into())],
2071        is_error: Some(true),
2072        meta: None,
2073        structured_content: None,
2074    }
2075}
2076
2077fn command_launch_failure_result(error: &std::io::Error) -> CallToolResult {
2078    CallToolResult {
2079        content: vec![ContentBlock::text_content(format!(
2080            "Failed to run command: {}",
2081            error
2082        ))],
2083        is_error: Some(true),
2084        meta: None,
2085        structured_content: None,
2086    }
2087}
2088
2089fn placeholder_tool_result(
2090    name: &str,
2091    arguments: &serde_json::Map<String, serde_json::Value>,
2092) -> CallToolResult {
2093    let args_json = serde_json::Value::Object(arguments.clone());
2094    CallToolResult::from_content(vec![ContentBlock::text_content(format!(
2095        "Would invoke clap command '{name}' with arguments: {args_json:?}"
2096    ))])
2097}
2098
2099fn build_execution_command(
2100    executable_path: &std::path::Path,
2101    schema: &ClapSchema,
2102    root_name: &str,
2103    tool_name: &str,
2104    arguments: &serde_json::Map<String, serde_json::Value>,
2105) -> std::process::Command {
2106    let argv = build_tool_argv(schema, tool_name, arguments.clone());
2107    let mut command = std::process::Command::new(executable_path);
2108    if let Some(path) = command_path(schema, tool_name) {
2109        for segment in path.into_iter().skip(1) {
2110            command.arg(segment);
2111        }
2112    } else if tool_name != root_name {
2113        command.arg(tool_name);
2114    }
2115    for arg in &argv {
2116        command.arg(arg);
2117    }
2118    command
2119}
2120
2121fn subprocess_stderr_log_params(
2122    tool_name: &str,
2123    stderr: &str,
2124) -> Option<LoggingMessageNotificationParams> {
2125    let trimmed = stderr.trim();
2126    if trimmed.is_empty() {
2127        return None;
2128    }
2129    let mut meta = serde_json::Map::new();
2130    meta.insert(
2131        "tool".to_string(),
2132        serde_json::Value::String(tool_name.to_string()),
2133    );
2134    Some(LoggingMessageNotificationParams {
2135        data: serde_json::Value::String(trimmed.to_string()),
2136        level: LoggingLevel::Info,
2137        logger: Some("stderr".to_string()),
2138        meta: Some(meta),
2139    })
2140}
2141
2142fn call_tool_result_from_subprocess_output(output: &std::process::Output) -> CallToolResult {
2143    let stdout = String::from_utf8_lossy(&output.stdout);
2144    let stderr = String::from_utf8_lossy(&output.stderr);
2145    if !output.status.success() {
2146        let code = output
2147            .status
2148            .code()
2149            .map(|value| value.to_string())
2150            .unwrap_or_else(|| "unknown".to_string());
2151        let mut msg = format!("Tool process exited with non-zero status (code: {})", code);
2152        if !stderr.is_empty() {
2153            msg.push_str("\nstderr:\n");
2154            msg.push_str(stderr.trim());
2155        }
2156        return CallToolResult {
2157            content: vec![ContentBlock::text_content(msg)],
2158            is_error: Some(true),
2159            meta: None,
2160            structured_content: None,
2161        };
2162    }
2163    let text = if stderr.is_empty() {
2164        stdout.trim().to_string()
2165    } else {
2166        format!("{}\nstderr:\n{}", stdout.trim(), stderr.trim())
2167    };
2168    CallToolResult::from_content(vec![ContentBlock::text_content(text)])
2169}
2170
2171/// Starts an MCP server over stdio exposing `clap://schema` with the provided JSON payload.
2172///
2173/// - When `in_process_handler` is `Some`, tool calls use it instead of spawning a subprocess.
2174/// - When `None` and `executable_path` is `Some`, tool calls run that executable.
2175/// - When both are `None`, returns a placeholder message for unknown tools.
2176///
2177/// Use `config` to declare reinvocation and parallel execution safety. When
2178/// `parallel_safe` is false, tool calls are serialized.
2179///
2180/// Use `serve_options.log_rx` to forward log messages to the MCP client.
2181///
2182/// Use `metadata` to attach an optional output schema to each tool (e.g. from
2183/// `#[clap_mcp_output_type]` or `#[clap_mcp_output_one_of]` with the `output-schema`
2184/// feature). Pass [`ClapMcpSchemaMetadata::default()`] when you have none.
2185///
2186/// # Example
2187///
2188/// ```rust,ignore
2189/// let schema_json = serde_json::to_string(&schema)?;
2190/// let metadata = clap_mcp::ClapMcpSchemaMetadata::default();
2191/// clap_mcp::serve_schema_json_over_stdio(
2192///     schema_json,
2193///     Some(std::env::current_exe()?),
2194///     clap_mcp::ClapMcpConfig::default(),
2195///     None,
2196///     clap_mcp::ClapMcpServeOptions::default(),
2197///     &metadata,
2198/// ).await?;
2199/// ```
2200pub async fn serve_schema_json_over_stdio(
2201    schema_json: String,
2202    executable_path: Option<PathBuf>,
2203    config: ClapMcpConfig,
2204    in_process_handler: Option<InProcessToolHandler>,
2205    serve_options: ClapMcpServeOptions,
2206    metadata: &ClapMcpSchemaMetadata,
2207) -> std::result::Result<(), ClapMcpError> {
2208    let schema: ClapSchema = serde_json::from_str(&schema_json)?;
2209    let tools = tools_from_schema_with_config_and_metadata(&schema, &config, metadata);
2210    let root_name = schema.root.name.clone();
2211
2212    let tool_execution_lock: Option<Arc<tokio::sync::Mutex<()>>> = if config.parallel_safe {
2213        None
2214    } else {
2215        Some(Arc::new(tokio::sync::Mutex::new(())))
2216    };
2217
2218    let logging_enabled = serve_options.log_rx.is_some();
2219    let (runtime_tx, runtime_rx) = if logging_enabled {
2220        let (tx, rx) = tokio::sync::oneshot::channel::<Arc<dyn rust_mcp_sdk::McpServer>>();
2221        (
2222            Some(std::sync::Arc::new(std::sync::Mutex::new(Some(tx)))),
2223            Some(rx),
2224        )
2225    } else {
2226        (None, None)
2227    };
2228
2229    if let (Some(mut log_rx), Some(runtime_rx)) = (serve_options.log_rx, runtime_rx) {
2230        tokio::spawn(async move {
2231            let Ok(runtime) = runtime_rx.await else {
2232                return;
2233            };
2234            while let Some(params) = log_rx.recv().await {
2235                let _ = runtime.notify_log_message(params).await;
2236            }
2237        });
2238    }
2239
2240    type RuntimeTx = Option<
2241        Arc<
2242            std::sync::Mutex<
2243                Option<tokio::sync::oneshot::Sender<Arc<dyn rust_mcp_sdk::McpServer>>>,
2244            >,
2245        >,
2246    >;
2247
2248    struct Handler {
2249        schema_json: String,
2250        tools: Vec<Tool>,
2251        executable_path: Option<PathBuf>,
2252        in_process_handler: Option<InProcessToolHandler>,
2253        root_name: String,
2254        tool_execution_lock: Option<Arc<tokio::sync::Mutex<()>>>,
2255        runtime_tx: RuntimeTx,
2256        catch_in_process_panics: bool,
2257        custom_resources: Vec<content::CustomResource>,
2258        custom_prompts: Vec<content::CustomPrompt>,
2259        logging_enabled: bool,
2260    }
2261
2262    #[async_trait]
2263    impl ServerHandler for Handler {
2264        async fn handle_list_resources_request(
2265            &self,
2266            _params: Option<PaginatedRequestParams>,
2267            _runtime: Arc<dyn rust_mcp_sdk::McpServer>,
2268        ) -> std::result::Result<ListResourcesResult, RpcError> {
2269            Ok(list_resources_result(&self.custom_resources))
2270        }
2271
2272        async fn handle_read_resource_request(
2273            &self,
2274            params: ReadResourceRequestParams,
2275            _runtime: Arc<dyn rust_mcp_sdk::McpServer>,
2276        ) -> std::result::Result<ReadResourceResult, RpcError> {
2277            read_resource_result(&self.schema_json, &self.custom_resources, params).await
2278        }
2279
2280        async fn handle_list_tools_request(
2281            &self,
2282            _params: Option<PaginatedRequestParams>,
2283            _runtime: Arc<dyn rust_mcp_sdk::McpServer>,
2284        ) -> std::result::Result<ListToolsResult, RpcError> {
2285            Ok(ListToolsResult {
2286                tools: self.tools.clone(),
2287                meta: None,
2288                next_cursor: None,
2289            })
2290        }
2291
2292        async fn handle_list_prompts_request(
2293            &self,
2294            _params: Option<PaginatedRequestParams>,
2295            _runtime: Arc<dyn rust_mcp_sdk::McpServer>,
2296        ) -> std::result::Result<ListPromptsResult, RpcError> {
2297            Ok(list_prompts_result(
2298                self.logging_enabled,
2299                &self.custom_prompts,
2300            ))
2301        }
2302
2303        async fn handle_get_prompt_request(
2304            &self,
2305            params: GetPromptRequestParams,
2306            _runtime: Arc<dyn rust_mcp_sdk::McpServer>,
2307        ) -> std::result::Result<GetPromptResult, RpcError> {
2308            get_prompt_result(self.logging_enabled, &self.custom_prompts, params).await
2309        }
2310
2311        async fn handle_call_tool_request(
2312            &self,
2313            params: CallToolRequestParams,
2314            runtime: Arc<dyn rust_mcp_sdk::McpServer>,
2315        ) -> std::result::Result<CallToolResult, CallToolError> {
2316            if let Some(ref tx) = self.runtime_tx
2317                && let Ok(mut guard) = tx.lock()
2318                && let Some(sender) = guard.take()
2319            {
2320                let _ = sender.send(runtime.clone());
2321            }
2322
2323            let tool = self.tools.iter().find(|t| t.name == params.name);
2324            let Some(tool) = tool else {
2325                return Err(CallToolError::unknown_tool(params.name.clone()));
2326            };
2327
2328            // Reject unknown argument names — do not trust client to send only schema-defined args
2329            let args_map = params.arguments.unwrap_or_default();
2330            validate_tool_argument_names(tool, &params.name, &args_map)?;
2331
2332            let _guard = if let Some(ref lock) = self.tool_execution_lock {
2333                Some(lock.lock().await)
2334            } else {
2335                None
2336            };
2337
2338            if let Some(ref handler) = self.in_process_handler {
2339                let name = params.name.clone();
2340                let args = args_map;
2341                let result = if self.catch_in_process_panics {
2342                    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| handler(&name, args)))
2343                } else {
2344                    Ok(handler(&name, args))
2345                };
2346                match result {
2347                    Ok(Ok(output)) => return Ok(call_tool_result_from_output(output)),
2348                    Ok(Err(error)) => return Ok(call_tool_result_from_tool_error(error)),
2349                    Err(panic_payload) => {
2350                        return Ok(call_tool_result_from_panic(panic_payload.as_ref()));
2351                    }
2352                }
2353            }
2354
2355            if let Some(ref exe) = self.executable_path {
2356                let schema: ClapSchema = match serde_json::from_str(&self.schema_json) {
2357                    Ok(schema) => schema,
2358                    Err(_) => return Ok(schema_parse_failure_result()),
2359                };
2360                if let Err(e) = validate_required_args(&schema, &params.name, &args_map) {
2361                    return Ok(call_tool_result_from_tool_error(ClapMcpToolError::text(e)));
2362                }
2363                let mut cmd =
2364                    build_execution_command(exe, &schema, &self.root_name, &params.name, &args_map);
2365                match cmd.output() {
2366                    Ok(output) => {
2367                        if let Some(log_params) = subprocess_stderr_log_params(
2368                            &params.name,
2369                            &String::from_utf8_lossy(&output.stderr),
2370                        ) {
2371                            // When changing stderr logging behavior, update LOG_INTERPRETATION_INSTRUCTIONS and LOGGING_GUIDE_CONTENT.
2372                            let _ = runtime.notify_log_message(log_params).await;
2373                        }
2374                        return Ok(call_tool_result_from_subprocess_output(&output));
2375                    }
2376                    Err(error) => return Ok(command_launch_failure_result(&error)),
2377                }
2378            }
2379
2380            Ok(placeholder_tool_result(&params.name, &args_map))
2381        }
2382    }
2383
2384    let meta = {
2385        let mut m = serde_json::Map::new();
2386        m.insert(
2387            "clapMcp".into(),
2388            serde_json::json!({
2389                "version": env!("CARGO_PKG_VERSION"),
2390                "commit": env!("CLAP_MCP_GIT_COMMIT"),
2391                "buildDate": env!("CLAP_MCP_BUILD_DATE"),
2392            }),
2393        );
2394        Some(m)
2395    };
2396
2397    let server_details = InitializeResult {
2398        server_info: Implementation {
2399            name: "clap-mcp".into(),
2400            version: env!("CARGO_PKG_VERSION").into(),
2401            title: Some("clap-mcp".into()),
2402            description: Some("Expose clap CLI schema over MCP (stdio)".into()),
2403            icons: vec![],
2404            website_url: None,
2405        },
2406        capabilities: ServerCapabilities {
2407            resources: Some(ServerCapabilitiesResources {
2408                list_changed: Some(false),
2409                subscribe: Some(false),
2410            }),
2411            tools: Some(ServerCapabilitiesTools {
2412                list_changed: Some(false),
2413            }),
2414            logging: if logging_enabled {
2415                Some(serde_json::Map::new())
2416            } else {
2417                None
2418            },
2419            prompts: Some(ServerCapabilitiesPrompts {
2420                list_changed: Some(false),
2421            }),
2422            ..Default::default()
2423        },
2424        protocol_version: LATEST_PROTOCOL_VERSION.into(),
2425        instructions: if logging_enabled {
2426            Some(LOG_INTERPRETATION_INSTRUCTIONS.to_string())
2427        } else {
2428            None
2429        },
2430        meta,
2431    };
2432
2433    // Conservative timeout; mostly irrelevant for server-side stdio.
2434    let transport_options = TransportOptions {
2435        timeout: Duration::from_secs(30),
2436    };
2437    // For server-side stdio transport, use the ClientMessage dispatcher direction expected by ServerRuntime.
2438    let transport = StdioTransport::<schema_utils::ClientMessage>::new(transport_options)?;
2439
2440    let handler = Handler {
2441        schema_json,
2442        tools,
2443        executable_path,
2444        in_process_handler,
2445        root_name,
2446        tool_execution_lock,
2447        runtime_tx,
2448        catch_in_process_panics: config.catch_in_process_panics,
2449        custom_resources: serve_options.custom_resources.clone(),
2450        custom_prompts: serve_options.custom_prompts.clone(),
2451        logging_enabled,
2452    }
2453    .to_mcp_server_handler();
2454    let server = server_runtime::create_server(McpServerOptions {
2455        server_details,
2456        transport,
2457        handler,
2458        task_store: None,
2459        client_task_store: None,
2460    });
2461
2462    server.start().await?;
2463    Ok(())
2464}
2465
2466/// Convenience wrapper for [`serve_schema_json_over_stdio`] that blocks on a tokio runtime.
2467///
2468/// Use when you cannot use `async fn main`. Spawns a runtime internally.
2469///
2470/// # Runtime selection
2471///
2472/// | `reinvocation_safe` | `share_runtime` | Runtime type |
2473/// |---------------------|----------------|--------------|
2474/// | `false` | any | `current_thread` |
2475/// | `true` | `false` | `current_thread` |
2476/// | `true` | `true` | `multi_thread` (so [`run_async_tool`] with `share_runtime` can use `block_on`) |
2477pub fn serve_schema_json_over_stdio_blocking(
2478    schema_json: String,
2479    executable_path: Option<PathBuf>,
2480    config: ClapMcpConfig,
2481    in_process_handler: Option<InProcessToolHandler>,
2482    serve_options: ClapMcpServeOptions,
2483    metadata: &ClapMcpSchemaMetadata,
2484) -> std::result::Result<(), ClapMcpError> {
2485    let use_multi_thread = config.reinvocation_safe && config.share_runtime;
2486    let rt = if use_multi_thread {
2487        tokio::runtime::Builder::new_multi_thread()
2488            .enable_all()
2489            .build()?
2490    } else {
2491        tokio::runtime::Builder::new_current_thread()
2492            .enable_all()
2493            .build()?
2494    };
2495    rt.block_on(serve_schema_json_over_stdio(
2496        schema_json,
2497        executable_path,
2498        config,
2499        in_process_handler,
2500        serve_options,
2501        metadata,
2502    ))
2503}
2504
2505/// Runs an async future for MCP tool execution, respecting `share_runtime` in config.
2506///
2507/// **Idiomatic approach:** with `#[clap_mcp_output_from = "run"]`, do async work inside your
2508/// `run` function (e.g. use a runtime handle or call this function). The closure must return
2509/// a `Future` that produces the tool output.
2510///
2511/// Returns [`Ok`] with the future's output, or [`Err`](ClapMcpError) if the runtime could
2512/// not be created, the current context is invalid (`share_runtime` without a tokio runtime),
2513/// or the async thread panicked.
2514///
2515/// # Runtime selection
2516///
2517/// | `reinvocation_safe` | `share_runtime` | Behavior |
2518/// |---------------------|----------------|----------|
2519/// | `false` | any | Dedicated thread (subprocess mode; `share_runtime` ignored) |
2520/// | `true` | `false` | Dedicated thread with its own tokio runtime (default, recommended) |
2521/// | `true` | `true` | Uses `Handle::current().block_on()` on the MCP server's runtime |
2522///
2523/// When `share_runtime` is true, uses `block_in_place` + `block_on` so the async
2524/// work runs on the MCP server's multi-thread runtime without deadlock.
2525///
2526/// # Example (async inside `run`)
2527///
2528/// ```rust,ignore
2529/// fn run(cmd: Cli) -> SleepResult {
2530///     match cmd {
2531///         Cli::SleepDemo => clap_mcp::run_async_tool(&Cli::clap_mcp_config(), run_sleep_demo).expect("async tool failed"),
2532///     }
2533/// }
2534/// ```
2535pub fn run_async_tool<Fut, O>(
2536    config: &ClapMcpConfig,
2537    f: impl FnOnce() -> Fut + Send,
2538) -> std::result::Result<O, ClapMcpError>
2539where
2540    Fut: std::future::Future<Output = O> + Send,
2541    O: Send,
2542{
2543    if config.reinvocation_safe && config.share_runtime {
2544        tokio::task::block_in_place(|| {
2545            let handle = tokio::runtime::Handle::try_current()
2546                .map_err(|e| ClapMcpError::RuntimeContext(e.to_string()))?;
2547            Ok(handle.block_on(f()))
2548        })
2549    } else {
2550        std::thread::scope(|s| {
2551            let join_handle = s.spawn(|| {
2552                let rt = tokio::runtime::Builder::new_current_thread()
2553                    .enable_all()
2554                    .build()?;
2555                Ok(rt.block_on(f()))
2556            });
2557            match join_handle.join() {
2558                Ok(inner) => inner,
2559                Err(e) => Err(ClapMcpError::ToolThread(format!("{:?}", e))),
2560            }
2561        })
2562    }
2563}
2564
2565#[cfg(test)]
2566mod tests {
2567    use super::*;
2568    use clap::{ArgAction, CommandFactory};
2569    use serde_json::json;
2570    use std::error::Error;
2571    use std::sync::Mutex;
2572
2573    #[cfg(unix)]
2574    use std::os::unix::process::ExitStatusExt;
2575
2576    fn sample_helper_schema() -> ClapSchema {
2577        schema_from_command(
2578            &Command::new("sample")
2579                .arg(Arg::new("input").help("Input file").required(true).index(1))
2580                .arg(
2581                    Arg::new("verbose")
2582                        .long("verbose")
2583                        .help("Verbose mode")
2584                        .action(ArgAction::SetTrue),
2585                )
2586                .arg(
2587                    Arg::new("no-cache")
2588                        .long("no-cache")
2589                        .help("Disable cache")
2590                        .action(ArgAction::SetFalse),
2591                )
2592                .arg(
2593                    Arg::new("level")
2594                        .long("level")
2595                        .help("Verbosity level")
2596                        .action(ArgAction::Count),
2597                )
2598                .arg(
2599                    Arg::new("tag")
2600                        .long("tag")
2601                        .help("Tags to include")
2602                        .action(ArgAction::Append)
2603                        .value_name("TAG"),
2604                )
2605                .arg(
2606                    Arg::new("mode")
2607                        .long("mode")
2608                        .help("Execution mode")
2609                        .action(ArgAction::Set),
2610                )
2611                .subcommand(Command::new("serve").about("Serve the sample app")),
2612        )
2613    }
2614
2615    fn nested_schema() -> ClapSchema {
2616        schema_from_command(
2617            &Command::new("sample")
2618                .subcommand(
2619                    Command::new("parent")
2620                        .subcommand(Command::new("child").arg(Arg::new("value").long("value"))),
2621                )
2622                .subcommand(Command::new("echo").arg(Arg::new("message").long("message"))),
2623        )
2624    }
2625
2626    #[derive(Debug)]
2627    struct TestError(&'static str);
2628
2629    impl std::fmt::Display for TestError {
2630        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2631            f.write_str(self.0)
2632        }
2633    }
2634
2635    impl Error for TestError {}
2636
2637    struct TestPromptProvider {
2638        response: Result<Vec<PromptMessage>, &'static str>,
2639        seen: Mutex<Vec<(String, serde_json::Map<String, serde_json::Value>)>>,
2640    }
2641
2642    #[async_trait]
2643    impl content::PromptContentProvider for TestPromptProvider {
2644        async fn get(
2645            &self,
2646            name: &str,
2647            arguments: &serde_json::Map<String, serde_json::Value>,
2648        ) -> std::result::Result<Vec<PromptMessage>, Box<dyn Error + Send + Sync>> {
2649            self.seen
2650                .lock()
2651                .expect("prompt provider mutex should lock")
2652                .push((name.to_string(), arguments.clone()));
2653            match &self.response {
2654                Ok(messages) => Ok(messages.clone()),
2655                Err(message) => Err(Box::new(TestError(message))),
2656            }
2657        }
2658    }
2659
2660    struct TestResourceProvider {
2661        response: Result<String, &'static str>,
2662    }
2663
2664    #[async_trait]
2665    impl content::ResourceContentProvider for TestResourceProvider {
2666        async fn read(
2667            &self,
2668            _uri: &str,
2669        ) -> std::result::Result<String, Box<dyn Error + Send + Sync>> {
2670            match &self.response {
2671                Ok(text) => Ok(text.clone()),
2672                Err(message) => Err(Box::new(TestError(message))),
2673            }
2674        }
2675    }
2676
2677    #[derive(Debug, clap::Parser)]
2678    #[command(name = "exec-cli", subcommand_required = true)]
2679    enum ExecCli {
2680        PrintOnly,
2681        PrintAndText,
2682        Structured,
2683        Echo {
2684            #[arg(long)]
2685            value: String,
2686        },
2687    }
2688
2689    impl ClapMcpToolExecutor for ExecCli {
2690        fn execute_for_mcp(self) -> Result<ClapMcpToolOutput, ClapMcpToolError> {
2691            match self {
2692                Self::PrintOnly => {
2693                    print!("captured only");
2694                    Ok(ClapMcpToolOutput::Text(String::new()))
2695                }
2696                Self::PrintAndText => {
2697                    print!("captured extra");
2698                    Ok(ClapMcpToolOutput::Text("returned text".to_string()))
2699                }
2700                Self::Structured => {
2701                    print!("ignored capture");
2702                    Ok(ClapMcpToolOutput::Structured(json!({ "status": "ok" })))
2703                }
2704                Self::Echo { value } => Ok(ClapMcpToolOutput::Text(value)),
2705            }
2706        }
2707    }
2708
2709    #[test]
2710    fn test_format_panic_payload() {
2711        let s: Box<dyn std::any::Any + Send> = Box::new("hello");
2712        assert_eq!(format_panic_payload(s.as_ref()), "hello");
2713        let s: Box<dyn std::any::Any + Send> = Box::new("world".to_string());
2714        assert_eq!(format_panic_payload(s.as_ref()), "world");
2715        let n: Box<dyn std::any::Any + Send> = Box::new(42i32);
2716        assert_eq!(format_panic_payload(n.as_ref()), "<panic>");
2717    }
2718
2719    #[test]
2720    fn test_mcp_type_for_arg_and_description_hints() {
2721        let boolean_arg = ClapArg {
2722            id: "verbose".to_string(),
2723            long: Some("verbose".to_string()),
2724            short: None,
2725            help: Some("Verbose mode".to_string()),
2726            long_help: None,
2727            required: false,
2728            global: false,
2729            index: None,
2730            action: Some("SetTrue".to_string()),
2731            value_names: vec![],
2732            num_args: None,
2733        };
2734        let (json_type, items) = mcp_type_for_arg(&boolean_arg);
2735        assert_eq!(json_type, json!("boolean"));
2736        assert!(items.is_none());
2737        assert_eq!(
2738            mcp_action_description_hint(&boolean_arg),
2739            Some(" Boolean flag: set to true to pass this flag.".to_string())
2740        );
2741
2742        let false_arg = ClapArg {
2743            action: Some("SetFalse".to_string()),
2744            ..boolean_arg.clone()
2745        };
2746        assert_eq!(mcp_type_for_arg(&false_arg).0, json!("boolean"));
2747        assert_eq!(
2748            mcp_action_description_hint(&false_arg),
2749            Some(" Boolean flag: set to false to pass this flag (e.g. --no-xxx).".to_string())
2750        );
2751
2752        let count_arg = ClapArg {
2753            action: Some("Count".to_string()),
2754            ..boolean_arg.clone()
2755        };
2756        assert_eq!(mcp_type_for_arg(&count_arg).0, json!("integer"));
2757        assert_eq!(
2758            mcp_action_description_hint(&count_arg),
2759            Some(" Number of times the flag is passed (e.g. -vvv).".to_string())
2760        );
2761
2762        let append_arg = ClapArg {
2763            action: Some("Append".to_string()),
2764            value_names: vec!["TAG".to_string()],
2765            ..boolean_arg
2766        };
2767        let (json_type, items) = mcp_type_for_arg(&append_arg);
2768        assert_eq!(json_type, json!("array"));
2769        assert_eq!(
2770            items,
2771            Some(json!({ "type": "string", "description": "A TAG value" }))
2772        );
2773        assert_eq!(
2774            mcp_action_description_hint(&append_arg),
2775            Some(" List of TAG values; pass a JSON array (e.g. [\"a\", \"b\"]).".to_string())
2776        );
2777
2778        let multi_value_arg = ClapArg {
2779            id: "names".to_string(),
2780            long: Some("name".to_string()),
2781            short: None,
2782            help: None,
2783            long_help: None,
2784            required: false,
2785            global: false,
2786            index: None,
2787            action: Some("Set".to_string()),
2788            value_names: vec!["NAME".to_string()],
2789            num_args: Some("1..".to_string()),
2790        };
2791        let (json_type, items) = mcp_type_for_arg(&multi_value_arg);
2792        assert_eq!(json_type, json!("array"));
2793        assert_eq!(
2794            items,
2795            Some(json!({ "type": "string", "description": "A NAME value" }))
2796        );
2797    }
2798
2799    #[test]
2800    fn test_command_to_tool_with_config_reflects_arg_shapes() {
2801        let schema = sample_helper_schema();
2802        let tool = command_to_tool_with_config(
2803            &schema.root,
2804            &ClapMcpConfig {
2805                reinvocation_safe: true,
2806                parallel_safe: false,
2807                share_runtime: true,
2808                ..Default::default()
2809            },
2810            None,
2811        );
2812
2813        assert_eq!(tool.name, "sample");
2814        assert_eq!(tool.description, None);
2815
2816        let props = tool
2817            .input_schema
2818            .properties
2819            .expect("tool should include input schema properties");
2820        assert_eq!(tool.input_schema.required, vec!["input".to_string()]);
2821        assert_eq!(
2822            props["verbose"]
2823                .get("type")
2824                .and_then(|value| value.as_str()),
2825            Some("boolean")
2826        );
2827        assert!(
2828            props["verbose"]["description"]
2829                .as_str()
2830                .expect("verbose description")
2831                .contains("Boolean flag")
2832        );
2833        assert_eq!(
2834            props["level"].get("type").and_then(|value| value.as_str()),
2835            Some("integer")
2836        );
2837        assert_eq!(
2838            props["tag"].get("type").and_then(|value| value.as_str()),
2839            Some("array")
2840        );
2841        assert_eq!(
2842            props["tag"]["items"]["description"].as_str(),
2843            Some("A TAG value")
2844        );
2845        assert_eq!(
2846            tool.meta
2847                .as_ref()
2848                .and_then(|meta| meta.get("clapMcp"))
2849                .and_then(|value| value.get("shareRuntime"))
2850                .and_then(|value| value.as_bool()),
2851            Some(true)
2852        );
2853    }
2854
2855    #[test]
2856    fn test_validate_required_args_handles_missing_empty_and_flag_values() {
2857        let schema = sample_helper_schema();
2858        let mut provided = serde_json::Map::new();
2859        provided.insert("verbose".to_string(), json!(false));
2860        provided.insert("level".to_string(), json!(0));
2861        provided.insert("input".to_string(), json!("input.txt"));
2862        assert!(validate_required_args(&schema, "sample", &provided).is_ok());
2863
2864        let mut missing_text = serde_json::Map::new();
2865        missing_text.insert("input".to_string(), json!(""));
2866        let error = validate_required_args(&schema, "sample", &missing_text)
2867            .expect_err("empty required string should fail");
2868        assert!(error.contains("Missing required argument(s): input"));
2869
2870        let mut missing_array = serde_json::Map::new();
2871        missing_array.insert("input".to_string(), json!([]));
2872        let error = validate_required_args(&schema, "sample", &missing_array)
2873            .expect_err("empty array should fail");
2874        assert!(error.contains("input"));
2875
2876        assert!(validate_required_args(&schema, "unknown", &serde_json::Map::new()).is_ok());
2877    }
2878
2879    #[test]
2880    fn test_build_tool_argv_handles_positional_flags_and_lists() {
2881        let schema = sample_helper_schema();
2882        let arguments = serde_json::Map::from_iter([
2883            ("input".to_string(), json!("input.txt")),
2884            ("verbose".to_string(), json!(true)),
2885            ("no-cache".to_string(), json!(false)),
2886            ("level".to_string(), json!(2)),
2887            ("tag".to_string(), json!(["alpha", "", "beta"])),
2888            ("mode".to_string(), json!("fast")),
2889        ]);
2890
2891        let argv = build_tool_argv(&schema, "sample", arguments);
2892        assert_eq!(
2893            argv,
2894            vec![
2895                "input.txt",
2896                "--level",
2897                "--level",
2898                "--mode",
2899                "fast",
2900                "--no-cache",
2901                "--tag",
2902                "alpha",
2903                "--tag",
2904                "beta",
2905                "--verbose",
2906            ]
2907        );
2908    }
2909
2910    #[test]
2911    fn test_value_to_string_and_value_to_strings_cover_scalar_and_array_inputs() {
2912        assert_eq!(value_to_string(&json!("hello")), Some("hello".to_string()));
2913        assert_eq!(value_to_string(&json!(3)), Some("3".to_string()));
2914        assert_eq!(value_to_string(&json!(false)), Some("false".to_string()));
2915        assert_eq!(value_to_string(&serde_json::Value::Null), None);
2916        assert_eq!(
2917            value_to_string(&json!({"name":"sample"})),
2918            Some("{\"name\":\"sample\"}".to_string())
2919        );
2920
2921        assert_eq!(
2922            value_to_strings(&json!(["alpha", "", 3, null, false])),
2923            Some(vec![
2924                "alpha".to_string(),
2925                "3".to_string(),
2926                "false".to_string()
2927            ])
2928        );
2929        assert_eq!(
2930            value_to_strings(&json!("solo")),
2931            Some(vec!["solo".to_string()])
2932        );
2933        assert_eq!(value_to_strings(&serde_json::Value::Null), None);
2934    }
2935
2936    #[test]
2937    fn test_command_flag_helpers_are_idempotent() {
2938        let cmd = command_with_mcp_flag(command_with_mcp_flag(Command::new("sample")));
2939        let mcp_args = cmd
2940            .get_arguments()
2941            .filter(|arg| arg.get_long() == Some(MCP_FLAG_LONG))
2942            .count();
2943        assert_eq!(mcp_args, 1);
2944
2945        let cmd = command_with_export_skills_flag(command_with_export_skills_flag(Command::new(
2946            "sample",
2947        )));
2948        let export_args = cmd
2949            .get_arguments()
2950            .filter(|arg| arg.get_long() == Some(EXPORT_SKILLS_FLAG_LONG))
2951            .count();
2952        assert_eq!(export_args, 1);
2953
2954        let cmd = command_with_mcp_and_export_skills_flags(Command::new("bare"));
2955        assert_eq!(
2956            cmd.get_arguments()
2957                .filter(|arg| arg.get_long() == Some(MCP_FLAG_LONG))
2958                .count(),
2959            1
2960        );
2961        assert_eq!(
2962            cmd.get_arguments()
2963                .filter(|arg| arg.get_long() == Some(EXPORT_SKILLS_FLAG_LONG))
2964                .count(),
2965            1
2966        );
2967    }
2968
2969    #[test]
2970    fn test_argv_export_skills_dir_from_args() {
2971        assert!(argv_export_skills_dir_from_args(&[]).is_none());
2972        assert!(argv_export_skills_dir_from_args(&["--other".to_string()]).is_none());
2973        assert_eq!(
2974            argv_export_skills_dir_from_args(&["--export-skills".to_string()]),
2975            Some(None)
2976        );
2977        assert_eq!(
2978            argv_export_skills_dir_from_args(&["--export-skills".to_string(), "/path".to_string()]),
2979            Some(Some(std::path::PathBuf::from("/path")))
2980        );
2981        assert_eq!(
2982            argv_export_skills_dir_from_args(&["--export-skills".to_string(), "--mcp".to_string()]),
2983            Some(None)
2984        );
2985        assert_eq!(
2986            argv_export_skills_dir_from_args(&["--export-skills=/out".to_string()]),
2987            Some(Some(std::path::PathBuf::from("/out")))
2988        );
2989    }
2990
2991    #[test]
2992    fn test_argv_requests_mcp_without_subcommand_from_args() {
2993        let cmd = Command::new("app").subcommand(Command::new("run"));
2994        assert!(argv_requests_mcp_without_subcommand_from_args(
2995            &["--mcp".to_string()],
2996            &cmd
2997        ));
2998        assert!(!argv_requests_mcp_without_subcommand_from_args(
2999            &["--mcp".to_string(), "run".to_string()],
3000            &cmd
3001        ));
3002        assert!(!argv_requests_mcp_without_subcommand_from_args(
3003            &["run".to_string()],
3004            &cmd
3005        ));
3006        assert!(!argv_requests_mcp_without_subcommand_from_args(&[], &cmd));
3007    }
3008
3009    #[test]
3010    fn test_is_builtin_arg() {
3011        assert!(is_builtin_arg("help"));
3012        assert!(is_builtin_arg("version"));
3013        assert!(is_builtin_arg(MCP_FLAG_LONG));
3014        assert!(is_builtin_arg(EXPORT_SKILLS_FLAG_LONG));
3015        assert!(!is_builtin_arg("input"));
3016        assert!(!is_builtin_arg("path"));
3017    }
3018
3019    #[test]
3020    fn test_tools_from_schema_wrapper() {
3021        let schema = sample_helper_schema();
3022        let tools = tools_from_schema(&schema);
3023        assert!(!tools.is_empty());
3024    }
3025
3026    #[test]
3027    fn test_command_path_and_build_argv_for_clap() {
3028        let schema = nested_schema();
3029        assert_eq!(command_path(&schema, "sample"), Some(vec!["sample".into()]));
3030        assert_eq!(
3031            command_path(&schema, "child"),
3032            Some(vec!["sample".into(), "parent".into(), "child".into()])
3033        );
3034        assert_eq!(command_path(&schema, "nonexistent"), None);
3035
3036        let args = serde_json::Map::from_iter([("value".to_string(), json!("v"))]);
3037        let argv = build_argv_for_clap(&schema, "child", args);
3038        assert_eq!(argv[0], "cli");
3039        assert_eq!(argv[1], "parent");
3040        assert_eq!(argv[2], "child");
3041        assert!(argv.contains(&"--value".to_string()));
3042        assert!(argv.contains(&"v".to_string()));
3043
3044        let empty_argv = build_tool_argv(&schema, "nonexistent", serde_json::Map::new());
3045        assert!(empty_argv.is_empty());
3046    }
3047
3048    #[cfg(not(feature = "output-schema"))]
3049    #[test]
3050    fn test_output_schema_for_type_without_schemars() {
3051        assert!(output_schema_for_type::<()>().is_none());
3052    }
3053
3054    #[cfg(feature = "output-schema")]
3055    #[test]
3056    fn test_output_schema_for_type_with_schemars() {
3057        use schemars::JsonSchema;
3058        #[derive(JsonSchema)]
3059        struct Dummy {
3060            _x: i32,
3061        }
3062        let schema = output_schema_for_type::<Dummy>();
3063        assert!(schema.is_some());
3064    }
3065
3066    #[tokio::test]
3067    async fn test_resource_helpers_cover_builtin_custom_and_error_paths() {
3068        let custom = vec![content::CustomResource {
3069            uri: "test://dynamic".to_string(),
3070            name: "dynamic".to_string(),
3071            title: None,
3072            description: Some("dynamic resource".to_string()),
3073            mime_type: Some("text/plain".to_string()),
3074            content: content::ResourceContent::Dynamic(Arc::new(TestResourceProvider {
3075                response: Ok("dynamic body".to_string()),
3076            })),
3077        }];
3078
3079        let listed = list_resources_result(&custom);
3080        assert_eq!(listed.resources.len(), 2);
3081        assert_eq!(listed.resources[0].uri, MCP_RESOURCE_URI_SCHEMA);
3082        assert_eq!(listed.resources[1].uri, "test://dynamic");
3083
3084        let schema_read = read_resource_result(
3085            "{\"name\":\"sample\"}",
3086            &custom,
3087            ReadResourceRequestParams {
3088                uri: MCP_RESOURCE_URI_SCHEMA.to_string(),
3089                meta: None,
3090            },
3091        )
3092        .await
3093        .expect("schema resource should resolve");
3094        let text = match &schema_read.contents[0] {
3095            ReadResourceContent::TextResourceContents(text) => &text.text,
3096            other => panic!("unexpected content: {other:?}"),
3097        };
3098        assert!(text.contains("\"name\":\"sample\""));
3099
3100        let custom_read = read_resource_result(
3101            "{}",
3102            &custom,
3103            ReadResourceRequestParams {
3104                uri: "test://dynamic".to_string(),
3105                meta: None,
3106            },
3107        )
3108        .await
3109        .expect("custom resource should resolve");
3110        let text = match &custom_read.contents[0] {
3111            ReadResourceContent::TextResourceContents(text) => &text.text,
3112            other => panic!("unexpected content: {other:?}"),
3113        };
3114        assert_eq!(text, "dynamic body");
3115
3116        let missing = read_resource_result(
3117            "{}",
3118            &custom,
3119            ReadResourceRequestParams {
3120                uri: "test://missing".to_string(),
3121                meta: None,
3122            },
3123        )
3124        .await
3125        .expect_err("missing resource should error");
3126        assert!(missing.message.contains("unknown resource uri"));
3127
3128        let failing_resources = vec![content::CustomResource {
3129            uri: "test://broken".to_string(),
3130            name: "broken".to_string(),
3131            title: None,
3132            description: None,
3133            mime_type: None,
3134            content: content::ResourceContent::Dynamic(Arc::new(TestResourceProvider {
3135                response: Err("read failed"),
3136            })),
3137        }];
3138        let failing = read_resource_result(
3139            "{}",
3140            &failing_resources,
3141            ReadResourceRequestParams {
3142                uri: "test://broken".to_string(),
3143                meta: None,
3144            },
3145        )
3146        .await
3147        .expect_err("provider failure should map to rpc error");
3148        assert_eq!(failing.message, "read failed");
3149    }
3150
3151    #[tokio::test]
3152    async fn test_prompt_helpers_cover_logging_custom_and_error_paths() {
3153        let provider = Arc::new(TestPromptProvider {
3154            response: Ok(vec![PromptMessage {
3155                role: Role::User,
3156                content: ContentBlock::text_content("dynamic prompt".to_string()),
3157            }]),
3158            seen: Mutex::new(Vec::new()),
3159        });
3160        let prompts = vec![content::CustomPrompt {
3161            name: "dynamic".to_string(),
3162            title: Some("Dynamic".to_string()),
3163            description: Some("dynamic prompt".to_string()),
3164            arguments: vec![],
3165            content: content::PromptContent::Dynamic(provider.clone()),
3166        }];
3167
3168        let listed = list_prompts_result(true, &prompts);
3169        assert_eq!(listed.prompts.len(), 2);
3170        assert_eq!(listed.prompts[0].name, PROMPT_LOGGING_GUIDE);
3171        assert_eq!(listed.prompts[1].name, "dynamic");
3172
3173        let logging_prompt = get_prompt_result(
3174            true,
3175            &prompts,
3176            GetPromptRequestParams {
3177                name: PROMPT_LOGGING_GUIDE.to_string(),
3178                arguments: None,
3179                meta: None,
3180            },
3181        )
3182        .await
3183        .expect("logging guide should resolve");
3184        assert!(
3185            logging_prompt.messages[0]
3186                .content
3187                .as_text_content()
3188                .expect("logging guide should be text")
3189                .text
3190                .contains("logger")
3191        );
3192
3193        let dynamic_prompt = get_prompt_result(
3194            false,
3195            &prompts,
3196            GetPromptRequestParams {
3197                name: "dynamic".to_string(),
3198                arguments: Some(std::collections::HashMap::from([(
3199                    "topic".to_string(),
3200                    "coverage".to_string(),
3201                )])),
3202                meta: None,
3203            },
3204        )
3205        .await
3206        .expect("dynamic prompt should resolve");
3207        assert_eq!(
3208            dynamic_prompt.description.as_deref(),
3209            Some("dynamic prompt")
3210        );
3211        assert_eq!(
3212            provider
3213                .seen
3214                .lock()
3215                .expect("provider seen mutex should lock")[0]
3216                .1
3217                .get("topic")
3218                .and_then(|value| value.as_str()),
3219            Some("coverage")
3220        );
3221
3222        let unknown_logging = get_prompt_result(
3223            false,
3224            &prompts,
3225            GetPromptRequestParams {
3226                name: PROMPT_LOGGING_GUIDE.to_string(),
3227                arguments: None,
3228                meta: None,
3229            },
3230        )
3231        .await
3232        .expect_err("logging guide should error when logging disabled");
3233        assert!(unknown_logging.message.contains("unknown prompt"));
3234
3235        let failing_prompts = vec![content::CustomPrompt {
3236            name: "broken".to_string(),
3237            title: None,
3238            description: None,
3239            arguments: vec![],
3240            content: content::PromptContent::Dynamic(Arc::new(TestPromptProvider {
3241                response: Err("prompt failed"),
3242                seen: Mutex::new(Vec::new()),
3243            })),
3244        }];
3245        let failing = get_prompt_result(
3246            false,
3247            &failing_prompts,
3248            GetPromptRequestParams {
3249                name: "broken".to_string(),
3250                arguments: None,
3251                meta: None,
3252            },
3253        )
3254        .await
3255        .expect_err("provider failure should map to rpc error");
3256        assert_eq!(failing.message, "prompt failed");
3257    }
3258
3259    #[test]
3260    fn test_call_tool_result_helpers_cover_text_structured_errors_and_panics() {
3261        let text = call_tool_result_from_output(ClapMcpToolOutput::Text("hello".to_string()));
3262        assert_eq!(text.is_error, None);
3263        assert_eq!(
3264            text.content[0]
3265                .as_text_content()
3266                .expect("text result should be text")
3267                .text,
3268            "hello"
3269        );
3270
3271        let structured = call_tool_result_from_output(ClapMcpToolOutput::Structured(json!({
3272            "sum": 5
3273        })));
3274        assert_eq!(
3275            structured
3276                .structured_content
3277                .as_ref()
3278                .and_then(|content| content.get("sum"))
3279                .and_then(|value| value.as_i64()),
3280            Some(5)
3281        );
3282        assert!(
3283            structured.content[0]
3284                .as_text_content()
3285                .expect("structured result should emit text")
3286                .text
3287                .contains("\"sum\": 5")
3288        );
3289
3290        let non_object = call_tool_result_from_output(ClapMcpToolOutput::Structured(json!(["a"])));
3291        assert!(non_object.structured_content.is_none());
3292
3293        let error = call_tool_result_from_tool_error(ClapMcpToolError::structured(
3294            "bad",
3295            json!({ "code": 7 }),
3296        ));
3297        assert_eq!(error.is_error, Some(true));
3298        assert_eq!(
3299            error
3300                .structured_content
3301                .as_ref()
3302                .and_then(|content| content.get("code"))
3303                .and_then(|value| value.as_i64()),
3304            Some(7)
3305        );
3306
3307        let panic_payload: Box<dyn std::any::Any + Send> = Box::new("boom");
3308        let panic_result = call_tool_result_from_panic(panic_payload.as_ref());
3309        assert_eq!(panic_result.is_error, Some(true));
3310        assert!(
3311            panic_result.content[0]
3312                .as_text_content()
3313                .expect("panic result should be text")
3314                .text
3315                .contains("Tool panicked: boom")
3316        );
3317    }
3318
3319    #[test]
3320    fn test_subprocess_helpers_cover_command_building_logging_and_result_shapes() {
3321        let schema = nested_schema();
3322        let args = serde_json::Map::from_iter([(
3323            "value".to_string(),
3324            serde_json::Value::String("ok".to_string()),
3325        )]);
3326        let command = build_execution_command(
3327            std::path::Path::new("/tmp/example"),
3328            &schema,
3329            "sample",
3330            "child",
3331            &args,
3332        );
3333        assert_eq!(command.get_program(), std::ffi::OsStr::new("/tmp/example"));
3334        let actual_args: Vec<_> = command.get_args().collect();
3335        assert_eq!(
3336            actual_args,
3337            vec![
3338                std::ffi::OsStr::new("parent"),
3339                std::ffi::OsStr::new("child"),
3340                std::ffi::OsStr::new("--value"),
3341                std::ffi::OsStr::new("ok"),
3342            ]
3343        );
3344
3345        let log_params = subprocess_stderr_log_params("child", "warning on stderr\n")
3346            .expect("stderr should produce logging params");
3347        assert_eq!(log_params.logger.as_deref(), Some("stderr"));
3348        assert_eq!(
3349            log_params.meta.as_ref().and_then(|meta| meta.get("tool")),
3350            Some(&serde_json::Value::String("child".to_string()))
3351        );
3352        assert!(subprocess_stderr_log_params("child", "   ").is_none());
3353
3354        #[cfg(unix)]
3355        {
3356            let success_output = std::process::Output {
3357                status: std::process::ExitStatus::from_raw(0),
3358                stdout: b"done\n".to_vec(),
3359                stderr: b"note\n".to_vec(),
3360            };
3361            let success = call_tool_result_from_subprocess_output(&success_output);
3362            assert_eq!(success.is_error, None);
3363            assert!(
3364                success.content[0]
3365                    .as_text_content()
3366                    .expect("success result should be text")
3367                    .text
3368                    .contains("stderr:\nnote")
3369            );
3370
3371            let failure_output = std::process::Output {
3372                status: std::process::ExitStatus::from_raw(256),
3373                stdout: Vec::new(),
3374                stderr: b"boom\n".to_vec(),
3375            };
3376            let failure = call_tool_result_from_subprocess_output(&failure_output);
3377            assert_eq!(failure.is_error, Some(true));
3378            assert!(
3379                failure.content[0]
3380                    .as_text_content()
3381                    .expect("failure result should be text")
3382                    .text
3383                    .contains("non-zero status")
3384            );
3385        }
3386
3387        let launch_error = command_launch_failure_result(&std::io::Error::new(
3388            std::io::ErrorKind::NotFound,
3389            "missing",
3390        ));
3391        assert_eq!(launch_error.is_error, Some(true));
3392        assert!(
3393            launch_error.content[0]
3394                .as_text_content()
3395                .expect("launch error should be text")
3396                .text
3397                .contains("Failed to run command")
3398        );
3399
3400        let placeholder = placeholder_tool_result(
3401            "echo",
3402            &serde_json::Map::from_iter([("message".to_string(), json!("hi"))]),
3403        );
3404        assert!(
3405            placeholder.content[0]
3406                .as_text_content()
3407                .expect("placeholder result should be text")
3408                .text
3409                .contains("Would invoke clap command 'echo'")
3410        );
3411
3412        let parse_failure = schema_parse_failure_result();
3413        assert_eq!(parse_failure.is_error, Some(true));
3414        assert_eq!(
3415            parse_failure.content[0]
3416                .as_text_content()
3417                .expect("schema parse failure should be text")
3418                .text,
3419            "Failed to parse schema"
3420        );
3421    }
3422
3423    #[test]
3424    fn test_validate_tool_argument_names_rejects_unknown_keys() {
3425        let tool = command_to_tool_with_config(
3426            &sample_helper_schema().root,
3427            &ClapMcpConfig::default(),
3428            None,
3429        );
3430        let ok_args = serde_json::Map::from_iter([("input".to_string(), json!("in.txt"))]);
3431        assert!(validate_tool_argument_names(&tool, &tool.name, &ok_args).is_ok());
3432
3433        let bad_args = serde_json::Map::from_iter([("bogus".to_string(), json!(1))]);
3434        let err = validate_tool_argument_names(&tool, &tool.name, &bad_args)
3435            .expect_err("unknown key should error");
3436        assert!(format!("{err:?}").contains("unknown argument: bogus"));
3437    }
3438
3439    #[test]
3440    fn test_into_clap_mcp_result_and_error_impls_cover_basic_conversions() {
3441        assert!(matches!(
3442            String::from("hello")
3443                .into_tool_result()
3444                .expect("string should convert"),
3445            ClapMcpToolOutput::Text(text) if text == "hello"
3446        ));
3447        assert!(matches!(
3448            "world"
3449                .into_tool_result()
3450                .expect("str should convert"),
3451            ClapMcpToolOutput::Text(text) if text == "world"
3452        ));
3453
3454        let structured = AsStructured(json!({ "ok": true }))
3455            .into_tool_result()
3456            .expect("structured value should convert");
3457        assert!(matches!(structured, ClapMcpToolOutput::Structured(_)));
3458
3459        let empty = Option::<String>::None
3460            .into_tool_result()
3461            .expect("none should convert");
3462        assert!(matches!(empty, ClapMcpToolOutput::Text(text) if text.is_empty()));
3463
3464        let some = Some("x").into_tool_result().expect("some should convert");
3465        assert!(matches!(some, ClapMcpToolOutput::Text(text) if text == "x"));
3466
3467        let ok_result: Result<&str, &str> = Ok("done");
3468        assert!(matches!(
3469            ok_result.into_tool_result().expect("ok result should convert"),
3470            ClapMcpToolOutput::Text(text) if text == "done"
3471        ));
3472
3473        let err_result: Result<&str, &str> = Err("boom");
3474        let err = err_result
3475            .into_tool_result()
3476            .expect_err("err result should map to tool error");
3477        assert_eq!(err.message, "boom");
3478
3479        assert_eq!(ClapMcpToolError::from("oops").message, "oops");
3480        assert_eq!(ClapMcpToolError::from(String::from("ouch")).message, "ouch");
3481        assert_eq!(String::from("bad").into_tool_error().message, "bad");
3482        assert_eq!("worse".into_tool_error().message, "worse");
3483    }
3484
3485    #[test]
3486    fn test_merge_captured_stdout_only_changes_text_outputs() {
3487        let merged = merge_captured_stdout(
3488            Ok(ClapMcpToolOutput::Text(String::new())),
3489            "captured only\n".to_string(),
3490        )
3491        .expect("merge should succeed");
3492        assert!(matches!(merged, ClapMcpToolOutput::Text(text) if text == "captured only"));
3493
3494        let appended = merge_captured_stdout(
3495            Ok(ClapMcpToolOutput::Text("returned".to_string())),
3496            "captured\n".to_string(),
3497        )
3498        .expect("append should succeed");
3499        assert!(matches!(appended, ClapMcpToolOutput::Text(text) if text == "returned\ncaptured"));
3500
3501        let structured = merge_captured_stdout(
3502            Ok(ClapMcpToolOutput::Structured(json!({"ok": true}))),
3503            "captured\n".to_string(),
3504        )
3505        .expect("structured output should pass through");
3506        assert!(matches!(structured, ClapMcpToolOutput::Structured(_)));
3507    }
3508
3509    #[test]
3510    fn test_execute_in_process_command_and_handler_cover_capture_stdout_paths() {
3511        let schema = schema_from_command(&ExecCli::command());
3512
3513        let structured = execute_in_process_command::<ExecCli>(
3514            &schema,
3515            "structured",
3516            serde_json::Map::new(),
3517            false,
3518        )
3519        .expect("structured should execute");
3520        assert!(matches!(structured, ClapMcpToolOutput::Structured(_)));
3521
3522        let echo_args = serde_json::Map::from_iter([("value".to_string(), json!("hello"))]);
3523        let handler = make_in_process_handler::<ExecCli>(schema.clone(), false);
3524        let echoed = handler("echo", echo_args).expect("handler should execute");
3525        assert!(matches!(echoed, ClapMcpToolOutput::Text(text) if text == "hello"));
3526
3527        let missing =
3528            execute_in_process_command::<ExecCli>(&schema, "echo", serde_json::Map::new(), false)
3529                .expect_err("missing required arg should fail");
3530        assert!(
3531            missing
3532                .message
3533                .contains("Missing required argument(s): value")
3534        );
3535    }
3536}