8 releases
Uses new Rust 2024
| 0.0.3-rc.1 | Mar 8, 2026 |
|---|---|
| 0.0.2-rc.3 | Mar 6, 2026 |
| 0.0.2-rc.2 | Mar 2, 2026 |
| 0.0.1-rc.5 | Feb 28, 2026 |
| 0.0.1-rc.2 | Feb 27, 2026 |
#2234 in Database interfaces
195KB
3.5K
SLoC
clap-mcp
Enrich your Rust CLI with MCP Capabilities
Usage
You can take a look at the examples, but this is a VERY early draft. See examples/README.md for detailed instructions on running them.
Development
Contributors should follow these conventions:
- Format code with
cargo fmt. CI runscargo fmt --all -- --check. - Run
cargo clippy --all-targets --all-features -- -D warningsbefore submitting; CI enforces this. - Document public API items and add a
// SAFETY:comment above anyunsafeblock explaining invariants.
Run all tests (including feature-gated logging tests):
cargo test --all-features
Code coverage
Coverage is measured with cargo-llvm-cov. Install and run:
cargo install cargo-llvm-cov
cargo llvm-cov test --workspace --all-features --summary-only
For an HTML report (opens in browser):
cargo llvm-cov test --workspace --all-features --html
Coverage focuses on the clap-mcp and clap-mcp-macros crates; the examples crate is excluded from coverage targets.
Release prep includes building and running all example binaries (CI runs each
with --help as a smoke test); see examples/README.md.
Design
Compared to a Command Line Interface, I'm not a huge fan of the Model Context Protocol, but my feelings don't represent real world usage patterns. I feel MCP would do better with gRPC and Protobuf as it's "transport." All that being said, I'm not bitter about it, so I'm just letting a model do the development work and deal with it's own self-generated mess.
The intent is generally:
- Make it easy to add a MCP server to current Rust CLIs that use
clap. - Have it work well enough and provide enough guardrails to cover the 95% case.
- If there is structured information available from the CLI as an outcome, we should provide a way to express it naturally via MCP.
- Provide a way to express structured logging information (if available) as part of the response if requested.
Overall, the more you design your CLI around a service pattern, the more naturally this crate will behave as an MCP server, and modern CLIs often do that. At the same time, we shouldn't force CLIs that don't do that, out of the ecosystem.
Quick start
Add clap-mcp to your Cargo.toml (the default derive feature includes the macro):
[dependencies]
clap-mcp = "0.0.3-rc.1"
For derive usage, use clap_mcp::ClapMcp so you can write #[derive(ClapMcp)].
Imperative (existing clap CLI)
If you already have a clap::Command-based CLI, you can add MCP support in one
line. When --mcp is not passed, your CLI works exactly as before:
use clap::Command;
fn main() {
let cmd = Command::new("myapp")
.subcommand(Command::new("hello").about("Say hello"));
let matches = clap_mcp::get_matches_or_serve_mcp(cmd);
// If we reach here, --mcp was not passed — normal CLI execution continues.
}
Derive (minimal)
With #[derive(ClapMcp)], each subcommand is automatically exposed as an MCP
tool. This uses default config (subprocess execution, serialized tool calls):
use clap::Parser;
use clap_mcp::ClapMcp;
#[derive(Debug, Parser, ClapMcp)]
#[clap_mcp_output_from = "run"]
#[command(name = "myapp")]
enum Cli {
/// Say hello.
Greet {
#[arg(long)]
name: Option<String>,
},
}
fn run(cmd: Cli) -> String {
match cmd {
Cli::Greet { name } => format!("Hello, {}!", name.as_deref().unwrap_or("world")),
}
}
fn main() {
let cli = clap_mcp::parse_or_serve_mcp_attr::<Cli>();
println!("{}", run(cli));
}
Derive with attributes (recommended)
Use #[clap_mcp(...)] to declare execution safety, and
parse_or_serve_mcp_attr to pick up that config automatically:
use clap::Parser;
use clap_mcp::ClapMcp;
#[derive(Debug, Parser, ClapMcp)]
#[clap_mcp(reinvocation_safe, parallel_safe = false)]
#[clap_mcp_output_from = "run"]
#[command(name = "myapp")]
enum Cli {
Add { a: i32, b: i32 },
}
fn run(cmd: Cli) -> String {
match cmd {
Cli::Add { a, b } => (a + b).to_string(),
}
}
fn main() {
let cli = clap_mcp::parse_or_serve_mcp_attr::<Cli>();
println!("{}", run(cli));
}
Struct root with subcommand
When your CLI has a struct root with #[command(subcommand)] and an enum of commands, derive ClapMcp on both the root struct and the subcommand enum. Put #[clap_mcp_output_from = "run"] and execution config (#[clap_mcp(...)]) on the subcommand enum. In main, parse the root then call your run logic on the subcommand (e.g. run(cli.command) or match cli.command { ... }).
You can use either subcommand_required = false with command: Option<Commands> (so myapp with no subcommand is valid) or keep subcommand_required = true; in both cases myapp --mcp is valid and starts the MCP server (clap-mcp checks for --mcp before calling clap, so a required subcommand is not demanded when only --mcp is passed).
use clap::{Parser, Subcommand};
use clap_mcp::ClapMcp;
#[derive(Parser, ClapMcp)]
#[clap_mcp(reinvocation_safe, parallel_safe = false)]
#[command(subcommand_required = false)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand, ClapMcp)]
#[clap_mcp_output_from = "run"]
enum Commands {
Greet { #[arg(long)] name: Option<String> },
Add { #[arg(long)] a: i32, #[arg(long)] b: i32 },
}
fn run(cmd: Commands) -> String {
match cmd {
Commands::Greet { name } => format!("Hello, {}!", name.as_deref().unwrap_or("world")),
Commands::Add { a, b } => format!("{}", a + b),
}
}
fn main() {
let cli = clap_mcp::parse_or_serve_mcp_attr::<Cli>();
match cli.command {
None => println!("No subcommand"),
Some(cmd) => println!("{}", run(cmd)),
}
}
See Dual derive (root + subcommand) below and the struct_subcommand example in examples/README.md.
Feature flags
| Flag | Enables |
|---|---|
tracing |
ClapMcpTracingLayer — a tracing_subscriber::Layer that forwards tracing events to MCP clients via notifications/message. |
log |
ClapMcpLogBridge — a log::Log implementation that forwards log crate messages to MCP clients. |
output-schema |
schemars-based JSON schema generation for structured tool output. Enables output_schema_for_type, output_schema_one_of!, and #[clap_mcp_output_type] / #[clap_mcp_output_one_of] to set each tool's output_schema for MCP clients. |
Enable features in Cargo.toml:
[dependencies]
clap-mcp = { version = "0.0.3-rc.1", features = ["tracing"] }
Custom resources and prompts
In addition to the built-in clap://schema resource and the optional logging guide prompt, you can expose custom MCP resources and prompts. Add them to ClapMcpServeOptions and pass that into parse_or_serve_mcp_with_config_and_options or serve_schema_json_over_stdio_blocking.
Custom resources
Set custom_resources to a list of CustomResource values. Each has:
- Identity:
uri,name, optionaltitle,description,mime_type. Use a stable URI (e.g.myapp://config) so clients can list and read. - Content: Either static (
ResourceContent::Static(String)) or dynamic (ResourceContent::Dynamic(Arc<dyn ResourceContentProvider>)). Dynamic content uses the asyncResourceContentProvider::readso the handler can await it.
Example (static):
use clap_mcp::content::{CustomResource, ResourceContent};
let mut opts = clap_mcp::ClapMcpServeOptions::default();
opts.custom_resources.push(CustomResource {
uri: "myapp://readme".into(),
name: "readme".into(),
title: Some("Readme".into()),
description: Some("Project readme".into()),
mime_type: Some("text/markdown".into()),
content: ResourceContent::Static("# Hello\n".into()),
});
For dynamic content, implement ResourceContentProvider (async read(uri)).
Custom prompts
Set custom_prompts to a list of CustomPrompt values. Each has:
- Identity:
name, optionaltitle,description, optionalarguments(MCP prompt argument descriptors). - Content: Either static (
PromptContent::Static(Vec<PromptMessage>)) or dynamic (PromptContent::Dynamic(Arc<dyn PromptContentProvider>)). Dynamic uses the asyncPromptContentProvider::get.
The built-in clap-mcp-logging-guide prompt is only listed when logging is enabled (serve_options.log_rx.is_some()). Custom prompts are always merged into the list.
URI and name conventions
Prefer a stable prefix (e.g. myapp://) for custom resource URIs so they don’t clash with the built-in clap://schema. Prompt names must be unique; avoid clap-mcp-logging-guide for custom prompts.
Exporting agent skills
You can generate Agent Skills (SKILL.md) from the same tools, resources, and prompts that the MCP server exposes. This is useful for documenting your CLI for AI agents.
The --export-skills flag
Add the flag with command_with_export_skills_flag or use command_with_mcp_and_export_skills_flags to add both --mcp and --export-skills:
--export-skills— Generate skills into the default directory (see below) and exit.--export-skills=DIR— Generate skills intoDIR(e.g.--export-skills=./out) and exit.
When both --mcp and --export-skills are present, --export-skills wins: the process exports and exits without starting the MCP server.
Default output directory
Default directory is .agents/skills/, where each skill gets a subdirectory named after the app or tool. Override with --export-skills=DIR.
What gets generated
- One skill per tool (from your clap schema), with name/description and usage hints.
- A combined resources-and-prompts skill when you have custom resources or prompts.
Generated files follow the Agent Skills specification (YAML frontmatter with name, description, and allowed-tools; markdown body with usage instructions). The name field matches the parent directory name as required by the spec. Each tool skill includes allowed-tools listing the MCP tool it describes; note that this field is still experimental in the spec with no defined syntax convention. You can also call content::export_skills programmatically with schema, tools, custom resources, and custom prompts.
Execution safety configuration
CLIs differ in how safely they can be invoked over MCP. Two flags control this:
-
reinvocation_safe(default:false): Controls whether tool calls spawn a fresh subprocess of your binary (false) or run in-process viaClapMcpToolExecutor(true). The name refers to whether the CLI's internal state can survive repeated invocations without a process restart. Most CLIs that don't hold mutable global state can set this totrue. -
parallel_safe(default:false): Controls whether tool calls are serialized behind a tokioMutex(false) or dispatched concurrently (true). Set totrueonly if your CLI logic is safe to run concurrently. -
share_runtime(default:false): Whenreinvocation_safeis true, controls how async tool execution runs. See Async tools and share_runtime below.
Attribute-based config (recommended)
Use #[derive(ClapMcp)] and #[clap_mcp(...)] on your CLI type:
use clap::Parser;
use clap_mcp::ClapMcp;
#[derive(Debug, Parser, ClapMcp)]
#[clap_mcp(reinvocation_safe, parallel_safe = false)]
#[clap_mcp_output_from = "run"]
#[command(...)]
enum Cli {
Add { a: i32, b: i32 },
// ...
}
fn run(cmd: Cli) -> String {
match cmd {
Cli::Add { a, b } => (a + b).to_string(),
// ...
}
}
let cli = clap_mcp::parse_or_serve_mcp_attr::<Cli>();
Schema metadata: skip and requires
Use #[clap_mcp(skip)] to exclude subcommands or arguments from MCP exposure.
Use #[clap_mcp(requires)] or #[clap_mcp(requires = "arg_name")] to make an optional
argument required in the MCP tool schema (useful for positional args that may trigger
stdin behavior when omitted). When the client omits a required arg, a clear error is returned.
For optional positional arguments that might read from stdin when omitted, prefer an
explicit #[clap_mcp(requires)] or #[clap_mcp(skip)] so MCP behavior is intentional.
Argument-level (on each field):
#[derive(Parser, ClapMcp)]
#[clap_mcp_output_from = "run"]
enum Cli {
Read {
#[clap_mcp(requires)] // MCP schema makes path required
#[arg(long)]
path: Option<String>,
},
}
fn run(cmd: Cli) -> String {
match cmd {
Cli::Read { path } => path.unwrap_or_default(),
}
}
Variant-level (one or more args; use a single name or comma-separated list — the MCP schema marks each as required):
#[derive(Parser, ClapMcp)]
#[clap_mcp_output_from = "run"]
enum Cli {
// Single optional positional made required in MCP
#[clap_mcp(requires = "versions")]
Sort { versions: Option<String> },
// Multiple optional args
#[clap_mcp(requires = "path, input")] // both become required in MCP
Process {
#[arg(long)] path: Option<String>,
#[arg(long)] input: Option<String>,
},
}
fn run(cmd: Cli) -> String {
match cmd {
Cli::Sort { versions } => format!("{versions:?}"),
Cli::Process { path, input } => format!("{:?}", (path, input)),
}
}
Skip: (subcommands or variant-level) use #[clap_mcp(skip)] so a variant is hidden from MCP; pair with #[clap_mcp_output_from = "run"] and a single run for the exposed variants.
#[derive(Parser, ClapMcp)]
#[clap_mcp(reinvocation_safe, parallel_safe = false)]
#[clap_mcp_output_from = "run"]
enum Cli {
Public,
#[clap_mcp(skip)]
Internal,
}
fn run(cmd: Cli) -> String {
match cmd {
Cli::Public => "ok".to_string(),
Cli::Internal => "hidden".to_string(),
}
}
You can also use #[clap_mcp(skip)] on root struct fields so options like output format are hidden from MCP (they remain available to the CLI):
#[derive(Parser, ClapMcp)]
#[command(name = "myapp")]
struct Args {
#[clap_mcp(skip)]
#[arg(long)]
out: Option<String>,
#[command(subcommand)]
command: Option<Commands>,
}
Imperative: Use schema_from_command_with_metadata and get_matches_or_serve_mcp_with_config_and_metadata with ClapMcpSchemaMetadata:
let mut metadata = ClapMcpSchemaMetadata::default();
metadata.skip_commands.push("internal".into());
metadata.requires_args.insert("read".into(), vec!["path".into()]);
let schema = schema_from_command_with_metadata(&cmd, &metadata);
When the client omits a required argument, the tool returns a clear error:
"Missing required argument(s): path. The MCP tool schema marks these as required."
Dual derive (root + subcommand)
When you use a struct root with #[command(subcommand)] (e.g. command: Option<Commands>), derive ClapMcp on both the root struct and the subcommand enum. Put #[clap_mcp_output_from = "run"] and execution config (#[clap_mcp(...)]) on the subcommand enum only. The root's derive provides schema metadata and delegates tool execution to the subcommand's executor. In main, parse the root with parse_or_serve_mcp_attr::<Root>() then run with run(cli.command) or match cli.command { ... }. You can keep subcommand_required = true if you want; myapp --mcp alone is valid and starts the MCP server (clap-mcp handles --mcp before clap's subcommand check). See Struct root with subcommand and the struct_subcommand example in examples/README.md.
MCP tool list: The tool list includes the root command and all subcommands. If your CLI has subcommand_required = true, the root command still appears as a tool but has no subcommand in the MCP invocation model and is rarely used by clients; the meaningful tools are the subcommands (e.g. explain, compare, sort). To exclude the root from the tool list when it has subcommands, set ClapMcpSchemaMetadata::skip_root_command_when_subcommands to true via the derive with #[clap_mcp(skip_root_when_subcommands)] on the root struct, or imperatively (e.g. implement ClapMcpSchemaMetadataProvider for the root and set the field, or build metadata manually).
Runtime config
Use ClapMcpConfig with parse_or_serve_mcp_with_config or get_matches_or_serve_mcp_with_config:
clap_mcp::parse_or_serve_mcp_with_config::<Cli>(clap_mcp::ClapMcpConfig {
reinvocation_safe: true, // in-process execution
parallel_safe: false, // serialize tool calls (default)
..Default::default()
})
Tools include meta.clapMcp with these hints for clients.
Crash and panic behavior
- Subprocess (
reinvocation_safe= false): If the tool process exits with a non-zero status, the server returns a tool result withis_error: trueand a message that includes the exit code (and stderr when non-empty). - In-process (
reinvocation_safe= true),catch_in_process_panics= false (default): Any panic in tool code (including fromrun_async_tool) crashes the server. - In-process,
catch_in_process_panics= true: Panics are caught and returned as an MCP error; the server stays up. After a caught panic, the process may no longer be reinvocation_safe (global state may be corrupted) — consider restarting the server. SeeClapMcpConfig::catch_in_process_panicsand the panic_catch_opt_in and subprocess_exit_handling examples in examples/README.md.
Async tools and share_runtime
When your CLI has async subcommands (e.g. tokio::sleep, tokio::spawn), do async work
inside your run function (e.g. call clap_mcp::run_async_tool or use a runtime handle).
Set share_runtime in #[clap_mcp(...)] to share the MCP server's tokio runtime:
share_runtime |
Behavior | When to use |
|---|---|---|
false (default) |
Dedicated thread with its own tokio runtime per tool call. No nesting. | Recommended. Use unless you need deep integration. |
true |
Shares the MCP server's tokio runtime. Requires reinvocation_safe; uses multi-thread runtime. |
Advanced: share runtime state, spawn long-lived tasks, or integrate with other async code. |
Non-shared (default): do async work inside your run function and call
clap_mcp::run_async_tool from there:
fn run(cmd: Cli) -> AsStructured<SleepResult> {
match cmd {
Cli::SleepDemo => AsStructured(
clap_mcp::run_async_tool(&Cli::clap_mcp_config(), run_sleep_demo).expect("async tool failed"),
),
}
}
Shared runtime: same pattern; set share_runtime = true in #[clap_mcp(...)].
share_runtime only applies when reinvocation_safe is true. When tools run
in subprocesses (reinvocation_safe = false), share_runtime is ignored.
Security
The MCP server does not trust the client for tool or argument discovery. Every tool call is validated against the schema before any execution (in-process or subprocess). Unknown tools and unknown argument names are rejected immediately with an error; execution proceeds only for schema-defined tools and arguments.
When reinvocation_safe is false (the default), each tool call spawns a fresh
subprocess of your binary. Consider the following:
Shell injection is not a concern. Arguments are passed via std::process::Command::arg()
directly to the executable as argv — no shell is invoked, so metacharacters
(;, |, $(), etc.) are not interpreted.
Unknown tools and arguments are rejected. The server validates every tool name and
argument name against the schema before execution. Invalid requests fail with
CallToolError::unknown_tool or CallToolError::invalid_arguments; no subprocess
is spawned and no in-process handler is invoked for invalid calls.
Argument values come from the MCP client. The schema constrains which argument names are accepted, but values are passed through unvalidated. If your CLI uses those values unsafely (e.g., in file paths, system calls, or other sensitive operations), a malicious or compromised MCP client could exploit that. Ensure your CLI validates and sanitizes all inputs.
Environment and working directory are inherited. The subprocess inherits the full environment and CWD of the MCP server. Sensitive env vars (API keys, tokens) are visible to every subprocess; relative paths resolve against the server's CWD.
Resource usage. Each tool call spawns a new process. With parallel_safe = true,
many concurrent calls can create many processes. There are no timeouts or resource
limits on subprocess execution.
Tool output attributes
When using #[derive(ClapMcp)], you control how each subcommand's output is
returned to MCP clients. The idiomatic approach is a single output function
(#[clap_mcp_output_from = "run"]): one run implements both CLI and MCP behavior, so you
avoid duplicating logic. Per-variant attributes are available for edge cases but are not
the default.
#[clap_mcp_output_from = "run"] — single output function (recommended)
Put one function in charge of all tool output. The macro generates
execute_for_mcp by calling run(self) and converting the return value.
Use the same run in main so CLI and MCP share the same logic.
Supported return types for run:
Stringor&str→ text outputAsStructured<T>whereT: Serialize→ structured JSON output- A type that implements
IntoClapMcpResult(e.g. a custom enum for mixed text/structured) Option<O>→Nonebecomes empty text;Some(o)→o.into_tool_result()Result<O, E>→Ok(o)→ output;Err(e)→ MCP error.Emust implementIntoClapMcpToolError(e.g.String, or your type for structured errors)
Result<AsStructured<T>, E> is fully supported when you want structured success payloads and a separate error type; IntoClapMcpResult is implemented for AsStructured<T: Serialize>.
Recommended pattern for CLIs with multiple subcommands: have run return Result<AsStructured<SubcommandResult>, ApplicationError> and use #[clap_mcp_output_from = "run"]. Implement IntoClapMcpToolError for your application error type and cover all error variants (e.g. InvalidArgument, validation errors, I/O errors) in that single impl so MCP error responses are consistent across tools.
For run() -> Result<O, E>, ensure E: IntoClapMcpToolError and the macro will convert the return value automatically.
Example:
use clap::Parser;
use clap_mcp::{ClapMcp, AsStructured};
#[derive(Debug, Parser, ClapMcp)]
#[clap_mcp(reinvocation_safe, parallel_safe = false)]
#[clap_mcp_output_from = "run"]
#[command(name = "myapp", subcommand_required = false)]
enum Cli {
Greet { #[arg(long)] name: Option<String> },
Add { a: i32, b: i32 },
}
fn run(cmd: Cli) -> String {
match cmd {
Cli::Greet { name } => format!("Hello, {}!", name.as_deref().unwrap_or("world")),
Cli::Add { a, b } => format!("{}", a + b),
}
}
fn main() {
let cli = clap_mcp::parse_or_serve_mcp_attr::<Cli>();
// Same logic: run(cli) for CLI, run(self) for MCP
println!("{}", run(cli));
}
Tool output is defined only via #[clap_mcp_output_from = "run"] and a single run
function; there are no per-variant output attributes. Use run(Cli) -> T where T
implements IntoClapMcpResult (e.g. String, AsStructured<T>, Result<O, E>).
ClapMcpServeOptions::capture_stdout
When true and running in-process, captures stdout written during tool execution
and merges it with Text output. Only has effect when reinvocation_safe = true
(in-process execution). Unix only — the field is not present on Windows, so
code that sets capture_stdout will not compile on Windows. Subprocess mode
already captures stdout via Command::output().
Output schema (oneOf) for MCP tool discovery
With the output-schema feature enabled, you can attach a JSON schema to each tool's
outputSchema field so MCP clients know the shape of the tool's output.
#[clap_mcp_output_type = "TypeName"]
Use when your tool output is a single type (e.g. an enum or struct). The type must
implement schemars::JsonSchema.
For enums, schemars typically produces a oneOf schema.
// Requires: features = ["output-schema"], and schemars + JsonSchema on the type
#[derive(Serialize, schemars::JsonSchema)]
struct SubcommandResult { result: String }
#[derive(Parser, ClapMcp)]
#[clap_mcp_output_from = "run"]
#[clap_mcp_output_type = "SubcommandResult"]
enum Cli { ... }
#[clap_mcp_output_one_of = "T1, T2, T3"]
Use when you want to list multiple types explicitly for a oneOf schema without
defining a wrapper enum. Each type must implement schemars::JsonSchema.
#[derive(Serialize, schemars::JsonSchema)]
struct AddResult { sum: i32 }
#[derive(Serialize, schemars::JsonSchema)]
struct SubResult { difference: i32 }
#[derive(Parser, ClapMcp)]
#[clap_mcp_output_one_of = "AddResult, SubResult"]
enum Cli { ... }
When either attribute is set, ClapMcpSchemaMetadata::output_schema is populated
(by the derive) and tools_from_schema_with_config_and_metadata attaches it to
each tool. The high-level serve path (parse_or_serve_mcp_attr, etc.) uses metadata
automatically, so tools get output_schema when you use the derive and these attributes.
Logging and observability
clap-mcp can forward application log messages to MCP clients as
notifications/message. Two feature-gated paths are available depending on
your logging ecosystem.
tracing feature
Enable with features = ["tracing"]. ClapMcpTracingLayer is a standard
tracing_subscriber::Layer
and composes with any other layers in your subscriber stack — fmt,
tracing-opentelemetry, file appenders, etc. Adding it does not interfere with
your existing tracing pipeline:
use clap_mcp::logging::{log_channel, ClapMcpTracingLayer};
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
let (log_tx, log_rx) = log_channel(32);
tracing_subscriber::registry()
.with(ClapMcpTracingLayer::new(log_tx))
.with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
// .with(tracing_opentelemetry::layer().with_tracer(tracer)) // works alongside
.init();
let mut opts = clap_mcp::ClapMcpServeOptions::default();
opts.log_rx = Some(log_rx);
Current limitations:
- Only the
messagefield of each tracing event is forwarded. Other structured fields (e.g.tracing::info!(count = 42, "done")—countis dropped) are not yet included. - Span lifecycle events (
on_new_span,on_enter,on_close) are not captured.
log feature
Enable with features = ["log"]. ClapMcpLogBridge implements
log::Log and is installed as
the global logger:
use clap_mcp::logging::{log_channel, ClapMcpLogBridge};
let (log_tx, log_rx) = log_channel(32);
let bridge = ClapMcpLogBridge::new(log_tx);
log::set_logger(Box::leak(Box::new(bridge))).unwrap();
log::set_max_level(log::LevelFilter::Info);
let mut opts = clap_mcp::ClapMcpServeOptions::default();
opts.log_rx = Some(log_rx);
Trade-off: The log crate supports exactly one global logger. Installing
ClapMcpLogBridge replaces any existing logger (e.g. env_logger,
simplelog). If you need to log to both disk and MCP simultaneously, you'll
need a multiplexing wrapper — either a custom Log impl that fans out to
multiple sinks, or a crate like
multi_log.
Dependencies
~14–19MB
~238K SLoC