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 #[arg(short = 'c', long, value_enum, default_value = "medium")]
24 compression: CompressionLevelArg,
25
26 #[arg(long = "config")]
28 pub config_path: Option<PathBuf>,
29
30 #[arg(short = 'n', long)]
32 pub server_name: Option<String>,
33
34 #[arg(long, value_enum, default_value = "compressed-tools")]
36 transform_mode: TransformModeArg,
37
38 #[arg(long, action = ArgAction::SetTrue)]
40 cli_mode: bool,
41
42 #[arg(long, action = ArgAction::SetTrue)]
44 just_bash: bool,
45
46 #[arg(long = "just-bash-mode", action = ArgAction::SetTrue)]
48 just_bash_mode: bool,
49
50 #[arg(long = "code-mode", value_enum, value_name = "LANGUAGE")]
52 code_mode: Option<CodeModeArg>,
53
54 #[arg(long = "python-mode", action = ArgAction::SetTrue, hide = true)]
56 python_mode: bool,
57
58 #[arg(long = "typescript-mode", action = ArgAction::SetTrue, hide = true)]
60 typescript_mode: bool,
61
62 #[arg(long, value_delimiter = ',')]
64 pub include_tools: Vec<String>,
65
66 #[arg(long, value_delimiter = ',')]
68 pub exclude_tools: Vec<String>,
69
70 #[arg(long, action = ArgAction::SetTrue)]
72 pub toonify: bool,
73
74 #[arg(long = "output-dir")]
76 pub output_dir: Option<PathBuf>,
77
78 #[arg(long = "multi-server", value_name = "NAME=COMMAND [ARGS...]", action = ArgAction::Append)]
80 pub multi_server: Vec<MultiServerArg>,
81
82 #[arg(long, value_enum, default_value = "stdio")]
84 pub transport: FrontendTransport,
85
86 #[arg(long, default_value_t = 8000)]
88 pub port: u16,
89
90 #[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 ClearOauth {
168 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}