Skip to main content

mcp_compressor_core/app/
options.rs

1use std::path::PathBuf;
2use crate::ffi::FfiClientArtifactKind;
3use std::str::FromStr;
4
5use clap::{ArgAction, Parser, Subcommand, ValueEnum};
6
7use crate::compression::CompressionLevel;
8use crate::server::{BackendServerConfig, ProxyTransformMode};
9
10#[derive(Debug, Parser)]
11#[command(
12    name = "mcp-compressor",
13    about = "Standalone Rust MCP compressor core binary",
14    disable_help_subcommand = true,
15    version,
16    override_usage = "mcp-compressor [OPTIONS] [COMMAND] -- <URL_OR_COMMAND> [BACKEND_ARGS]..."
17)]
18pub struct CliOptions {
19    #[command(subcommand)]
20    pub command_kind: Option<CliCommand>,
21
22    /// Compression level: low, medium, high, or max.
23    #[arg(short = 'c', long, value_enum, default_value = "medium")]
24    compression: CompressionLevelArg,
25
26    /// MCP config JSON file.
27    #[arg(long = "config")]
28    pub config_path: Option<PathBuf>,
29
30    /// Frontend server name/prefix.
31    #[arg(short = 'n', long)]
32    pub server_name: Option<String>,
33
34    /// Frontend transform mode.
35    #[arg(long, value_enum, default_value = "compressed-tools")]
36    transform_mode: TransformModeArg,
37
38    /// Alias for --transform-mode cli.
39    #[arg(long, action = ArgAction::SetTrue)]
40    cli_mode: bool,
41
42    /// Alias for --transform-mode just-bash.
43    #[arg(long, action = ArgAction::SetTrue)]
44    just_bash: bool,
45
46    /// Alias for --transform-mode just-bash.
47    #[arg(long = "just-bash-mode", action = ArgAction::SetTrue)]
48    just_bash_mode: bool,
49
50    /// Generate a Python or TypeScript code client that talks to the local proxy.
51    #[arg(long = "code-mode", value_enum, value_name = "LANGUAGE")]
52    code_mode: Option<CodeModeArg>,
53
54    /// Deprecated alias for --code-mode python.
55    #[arg(long = "python-mode", action = ArgAction::SetTrue, hide = true)]
56    python_mode: bool,
57
58    /// Deprecated alias for --code-mode typescript.
59    #[arg(long = "typescript-mode", action = ArgAction::SetTrue, hide = true)]
60    typescript_mode: bool,
61
62    /// Comma-separated backend tool names to include.
63    #[arg(long, value_delimiter = ',')]
64    pub include_tools: Vec<String>,
65
66    /// Comma-separated backend tool names to exclude.
67    #[arg(long, value_delimiter = ',')]
68    pub exclude_tools: Vec<String>,
69
70    /// Convert JSON text outputs to TOON where possible.
71    #[arg(long, action = ArgAction::SetTrue)]
72    pub toonify: bool,
73
74    /// Output directory for generated Python/TypeScript code clients.
75    #[arg(long = "output-dir")]
76    pub output_dir: Option<PathBuf>,
77
78    /// Multi-server backend spec: name=command [args...]. Repeat for each backend.
79    #[arg(long = "multi-server", value_name = "NAME=COMMAND [ARGS...]", action = ArgAction::Append)]
80    pub multi_server: Vec<MultiServerArg>,
81
82    /// Frontend transport.
83    #[arg(long, value_enum, default_value = "stdio")]
84    pub transport: FrontendTransport,
85
86    /// Port for streamable-http frontend; 0 chooses an available port.
87    #[arg(long, default_value_t = 8000)]
88    pub port: u16,
89
90    /// Backend URL or command plus backend arguments. All backend server arguments belong after `--`.
91    #[arg(value_name = "URL_OR_COMMAND", allow_hyphen_values = true, last = true)]
92    pub command: Vec<String>,
93}
94
95impl CliOptions {
96    pub fn validate(&self) -> Result<(), String> {
97        if self.config_path.is_some() && self.server_name.is_some() {
98            return Err("--server-name cannot be used with --config; MCP config server names come from mcpServers keys".to_string());
99        }
100
101        let mode_aliases = [
102            self.cli_mode,
103            self.just_bash || self.just_bash_mode,
104            self.code_mode.is_some() || self.python_mode || self.typescript_mode,
105        ]
106        .into_iter()
107        .filter(|enabled| *enabled)
108        .count();
109        if mode_aliases > 1 {
110            return Err(
111                "choose only one of --cli-mode, --just-bash-mode, or --code-mode".to_string(),
112            );
113        }
114
115        let code_mode_aliases = [
116            self.code_mode.is_some(),
117            self.python_mode,
118            self.typescript_mode,
119        ]
120        .into_iter()
121        .filter(|enabled| *enabled)
122        .count();
123        if code_mode_aliases > 1 {
124            return Err(
125                "choose only one code mode: --code-mode python or --code-mode typescript"
126                    .to_string(),
127            );
128        }
129        Ok(())
130    }
131
132    pub fn compression(&self) -> CompressionLevel {
133        self.compression.into()
134    }
135
136    pub fn transform_mode(&self) -> ProxyTransformMode {
137        if self.just_bash || self.just_bash_mode {
138            ProxyTransformMode::JustBash
139        } else if self.cli_mode
140            || self.python_mode
141            || self.typescript_mode
142            || self.code_mode.is_some()
143        {
144            ProxyTransformMode::Cli
145        } else {
146            self.transform_mode.into()
147        }
148    }
149
150    pub fn client_artifact_kind(&self) -> Option<FfiClientArtifactKind> {
151        if let Some(code_mode) = self.code_mode {
152            return Some(code_mode.into());
153        }
154        if self.python_mode {
155            Some(FfiClientArtifactKind::Python)
156        } else if self.typescript_mode {
157            Some(FfiClientArtifactKind::TypeScript)
158        } else {
159            None
160        }
161    }
162}
163
164#[derive(Debug, Clone, Subcommand)]
165pub enum CliCommand {
166    /// Clear stored OAuth credentials.
167    ClearOauth {
168        /// Backend server name or URL to clear. If omitted, all Rust OAuth state is removed.
169        target: Option<String>,
170    },
171}
172
173impl CliCommand {
174    pub fn clear_oauth_target(&self) -> Option<&str> {
175        match self {
176            Self::ClearOauth { target } => target.as_deref(),
177        }
178    }
179}
180
181#[derive(Debug, Clone)]
182pub struct MultiServerArg {
183    name: String,
184    command: String,
185    args: Vec<String>,
186}
187
188impl MultiServerArg {
189    pub fn into_backend(self) -> BackendServerConfig {
190        BackendServerConfig::new(self.name, self.command, self.args)
191    }
192}
193
194impl FromStr for MultiServerArg {
195    type Err = String;
196
197    fn from_str(value: &str) -> Result<Self, Self::Err> {
198        let mut parts = value.split_whitespace();
199        let spec = parts
200            .next()
201            .ok_or_else(|| "expected name=command".to_string())?;
202        let (name, command) = spec
203            .split_once('=')
204            .filter(|(name, command)| !name.is_empty() && !command.is_empty())
205            .ok_or_else(|| "expected name=command".to_string())?;
206        Ok(Self {
207            name: name.to_string(),
208            command: command.to_string(),
209            args: parts.map(ToString::to_string).collect(),
210        })
211    }
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
215enum CompressionLevelArg {
216    Low,
217    Medium,
218    High,
219    Max,
220}
221
222impl std::fmt::Display for CompressionLevelArg {
223    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224        formatter.write_str(match self {
225            Self::Low => "low",
226            Self::Medium => "medium",
227            Self::High => "high",
228            Self::Max => "max",
229        })
230    }
231}
232
233impl From<CompressionLevelArg> for CompressionLevel {
234    fn from(value: CompressionLevelArg) -> Self {
235        match value {
236            CompressionLevelArg::Low => CompressionLevel::Low,
237            CompressionLevelArg::Medium => CompressionLevel::Medium,
238            CompressionLevelArg::High => CompressionLevel::High,
239            CompressionLevelArg::Max => CompressionLevel::Max,
240        }
241    }
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
245enum CodeModeArg {
246    Python,
247    #[value(name = "typescript")]
248    TypeScript,
249}
250
251impl From<CodeModeArg> for FfiClientArtifactKind {
252    fn from(value: CodeModeArg) -> Self {
253        match value {
254            CodeModeArg::Python => FfiClientArtifactKind::Python,
255            CodeModeArg::TypeScript => FfiClientArtifactKind::TypeScript,
256        }
257    }
258}
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
261enum TransformModeArg {
262    CompressedTools,
263    Cli,
264    JustBash,
265}
266
267impl std::fmt::Display for TransformModeArg {
268    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269        formatter.write_str(match self {
270            Self::CompressedTools => "compressed-tools",
271            Self::Cli => "cli",
272            Self::JustBash => "just-bash",
273        })
274    }
275}
276
277impl From<TransformModeArg> for ProxyTransformMode {
278    fn from(value: TransformModeArg) -> Self {
279        match value {
280            TransformModeArg::CompressedTools => ProxyTransformMode::CompressedTools,
281            TransformModeArg::Cli => ProxyTransformMode::Cli,
282            TransformModeArg::JustBash => ProxyTransformMode::JustBash,
283        }
284    }
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
288pub enum FrontendTransport {
289    Stdio,
290    StreamableHttp,
291}
292
293impl std::fmt::Display for FrontendTransport {
294    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295        formatter.write_str(match self {
296            Self::Stdio => "stdio",
297            Self::StreamableHttp => "streamable-http",
298        })
299    }
300}