xianyun_mcp/lib.rs
1//! Library half of `xianyun-mcp`: the allowlist/annotation/middleware
2//! configuration ([`config`]) and the `clap::Command` tree-building logic
3//! shared between the real `xianyun-mcp` binary (`src/main.rs`) and this
4//! crate's tests (golden-file snapshot, guardrail, in-process protocol).
5//!
6//! See `src/main.rs`'s module doc comment for the full story on why the
7//! binary routes argv between the `mcp` subtree and real `xianyun`
8//! command dispatch, and why [`mounted_root`] un-globals `Cli::command()`'s
9//! global arguments before mounting `brontes::command` onto it.
10
11pub mod config;
12
13use clap::{Command, CommandFactory as _};
14use xianyun_cli::Cli;
15
16/// Group name `brontes::command` mounts its subtree under; matches
17/// `brontes`'s own default (see `brontes::Config::command_name`, which
18/// this crate does not override).
19pub const MCP_GROUP: &str = "mcp";
20
21/// Builds the full `xianyun` command tree with `brontes::command` mounted
22/// onto it, after un-globaling `Cli::command()`'s own global arguments so
23/// they don't collide with `brontes`'s own `--log-level` flag on its
24/// mounted subcommands. See the crate-level doc comment for the full
25/// story.
26#[must_use]
27pub fn mounted_root(cfg: &brontes::Config) -> Command {
28 let mut root = Cli::command();
29 let global_ids: Vec<clap::Id> = root
30 .get_arguments()
31 .filter(|a| a.is_global_set())
32 .map(|a| a.get_id().clone())
33 .collect();
34 for id in global_ids {
35 root = root.mut_arg(id, |a| a.global(false));
36 }
37 root.subcommand(brontes::command(Some(cfg)))
38}
39
40/// `true` when `argv[1]` is exactly `"mcp"`, or `argv` carries no arguments
41/// beyond the binary name (the no-arguments-given default).
42#[must_use]
43pub fn is_mcp_invocation<S: AsRef<std::ffi::OsStr>>(argv: &[S]) -> bool {
44 argv.len() == 1 || argv.get(1).is_some_and(|a| a.as_ref() == MCP_GROUP)
45}