1use crate::ffi::FfiClientArtifactKind;
2use crate::llm_assist::{ModelRef, DEFAULT_MODEL_REF};
3use std::path::PathBuf;
4use std::str::FromStr;
5
6use clap::{ArgAction, Parser, Subcommand, ValueEnum};
7
8use crate::compression::CompressionLevel;
9use crate::server::{BackendServerConfig, ProxyTransformMode};
10
11#[derive(Debug, Parser)]
12#[command(
13 name = "mcp-compressor",
14 about = "Standalone Rust MCP compressor core binary",
15 disable_help_subcommand = true,
16 version,
17 override_usage = "mcp-compressor [OPTIONS] [COMMAND] -- <URL_OR_COMMAND> [BACKEND_ARGS]..."
18)]
19pub struct CliOptions {
20 #[command(subcommand)]
21 pub command_kind: Option<CliCommand>,
22
23 #[arg(short = 'c', long, value_enum, default_value = "medium")]
25 compression: CompressionLevelArg,
26
27 #[arg(long = "config")]
29 pub config_path: Option<PathBuf>,
30
31 #[arg(short = 'n', long)]
33 pub server_name: Option<String>,
34
35 #[arg(long, value_enum, default_value = "compressed-tools")]
37 transform_mode: TransformModeArg,
38
39 #[arg(long, action = ArgAction::SetTrue)]
41 cli_mode: bool,
42
43 #[arg(long, action = ArgAction::SetTrue)]
45 just_bash: bool,
46
47 #[arg(long = "just-bash-mode", action = ArgAction::SetTrue)]
49 just_bash_mode: bool,
50
51 #[arg(long = "code-mode", value_enum, value_name = "LANGUAGE")]
53 code_mode: Option<CodeModeArg>,
54
55 #[arg(long = "python-mode", action = ArgAction::SetTrue, hide = true)]
57 python_mode: bool,
58
59 #[arg(long = "typescript-mode", action = ArgAction::SetTrue, hide = true)]
61 typescript_mode: bool,
62
63 #[arg(long, value_delimiter = ',')]
65 pub include_tools: Vec<String>,
66
67 #[arg(long, value_delimiter = ',')]
69 pub exclude_tools: Vec<String>,
70
71 #[arg(long, action = ArgAction::SetTrue)]
73 pub toonify: bool,
74
75 #[arg(long = "output-dir")]
77 pub output_dir: Option<PathBuf>,
78
79 #[arg(long = "multi-server", value_name = "NAME=COMMAND [ARGS...]", action = ArgAction::Append)]
81 pub multi_server: Vec<MultiServerArg>,
82
83 #[arg(long, value_enum, default_value = "stdio")]
85 pub transport: FrontendTransport,
86
87 #[arg(long, default_value_t = 8000)]
89 pub port: u16,
90
91 #[arg(value_name = "URL_OR_COMMAND", allow_hyphen_values = true, last = true)]
93 pub command: Vec<String>,
94}
95
96impl CliOptions {
97 pub fn validate(&self) -> Result<(), String> {
98 if self.config_path.is_some() && self.server_name.is_some() {
99 return Err("--server-name cannot be used with --config; MCP config server names come from mcpServers keys".to_string());
100 }
101
102 let mode_aliases = [
103 self.cli_mode,
104 self.just_bash || self.just_bash_mode,
105 self.code_mode.is_some() || self.python_mode || self.typescript_mode,
106 ]
107 .into_iter()
108 .filter(|enabled| *enabled)
109 .count();
110 if mode_aliases > 1 {
111 return Err(
112 "choose only one of --cli-mode, --just-bash-mode, or --code-mode".to_string(),
113 );
114 }
115
116 let code_mode_aliases = [
117 self.code_mode.is_some(),
118 self.python_mode,
119 self.typescript_mode,
120 ]
121 .into_iter()
122 .filter(|enabled| *enabled)
123 .count();
124 if code_mode_aliases > 1 {
125 return Err(
126 "choose only one code mode: --code-mode python or --code-mode typescript"
127 .to_string(),
128 );
129 }
130 Ok(())
131 }
132
133 pub fn compression(&self) -> CompressionLevel {
134 self.compression.into()
135 }
136
137 pub fn transform_mode(&self) -> ProxyTransformMode {
138 if self.just_bash || self.just_bash_mode {
139 ProxyTransformMode::JustBash
140 } else if self.cli_mode
141 || self.python_mode
142 || self.typescript_mode
143 || self.code_mode.is_some()
144 {
145 ProxyTransformMode::Cli
146 } else {
147 self.transform_mode.into()
148 }
149 }
150
151 pub fn client_artifact_kind(&self) -> Option<FfiClientArtifactKind> {
152 if let Some(code_mode) = self.code_mode {
153 return Some(code_mode.into());
154 }
155 if self.python_mode {
156 Some(FfiClientArtifactKind::Python)
157 } else if self.typescript_mode {
158 Some(FfiClientArtifactKind::TypeScript)
159 } else {
160 None
161 }
162 }
163}
164
165#[derive(Debug, Clone, Subcommand)]
166pub enum CliCommand {
167 ClearOauth {
169 target: Option<String>,
171 },
172
173 Llm {
175 #[command(subcommand)]
176 command: LlmCommand,
177 },
178}
179
180impl CliCommand {
181 pub fn clear_oauth_target(&self) -> Option<&str> {
182 match self {
183 Self::ClearOauth { target } => target.as_deref(),
184 Self::Llm { .. } => None,
185 }
186 }
187
188 pub fn llm_command(&self) -> Option<&LlmCommand> {
189 match self {
190 Self::Llm { command } => Some(command),
191 Self::ClearOauth { .. } => None,
192 }
193 }
194}
195
196#[derive(Debug, Clone, Subcommand)]
197pub enum LlmCommand {
198 Status(LlmCommandOptions),
200 Pull(LlmCommandOptions),
202 Remove(LlmCommandOptions),
204 Test(LlmTestOptions),
206}
207
208#[derive(Debug, Clone, Parser)]
209pub struct LlmCommandOptions {
210 #[arg(long = "model", default_value = DEFAULT_MODEL_REF)]
212 model: String,
213
214 #[arg(long = "cache-dir")]
216 cache_dir: Option<PathBuf>,
217
218 #[arg(long = "llama-server")]
220 llama_server_path: Option<PathBuf>,
221}
222
223impl LlmCommandOptions {
224 pub fn config(
225 &self,
226 allow_download: bool,
227 ) -> Result<crate::llm_assist::LlmRuntimeConfig, String> {
228 let mut config = crate::llm_assist::LlmRuntimeConfig::local_default(allow_download)
229 .with_model(ModelRef::parse(&self.model).map_err(|error| error.to_string())?);
230 if let Some(cache_dir) = &self.cache_dir {
231 config = config.with_cache_dir(cache_dir.clone());
232 }
233 if let Some(path) = &self.llama_server_path {
234 config = config.with_llama_server_path(path.clone());
235 }
236 Ok(config)
237 }
238
239 pub fn cache_dir(&self) -> Option<PathBuf> {
240 self.cache_dir.clone()
241 }
242}
243
244#[derive(Debug, Clone, Parser)]
245pub struct LlmTestOptions {
246 #[command(flatten)]
247 options: LlmCommandOptions,
248
249 #[arg(long = "prompt", default_value = "Say hello in one short sentence.")]
251 prompt: String,
252}
253
254impl LlmTestOptions {
255 pub fn config(
256 &self,
257 allow_download: bool,
258 ) -> Result<crate::llm_assist::LlmRuntimeConfig, String> {
259 self.options.config(allow_download)
260 }
261
262 pub fn prompt(&self) -> &str {
263 &self.prompt
264 }
265}
266
267#[derive(Debug, Clone)]
268pub struct MultiServerArg {
269 name: String,
270 command: String,
271 args: Vec<String>,
272}
273
274impl MultiServerArg {
275 pub fn into_backend(self) -> BackendServerConfig {
276 BackendServerConfig::new(self.name, self.command, self.args)
277 }
278}
279
280impl FromStr for MultiServerArg {
281 type Err = String;
282
283 fn from_str(value: &str) -> Result<Self, Self::Err> {
284 let mut parts = value.split_whitespace();
285 let spec = parts
286 .next()
287 .ok_or_else(|| "expected name=command".to_string())?;
288 let (name, command) = spec
289 .split_once('=')
290 .filter(|(name, command)| !name.is_empty() && !command.is_empty())
291 .ok_or_else(|| "expected name=command".to_string())?;
292 Ok(Self {
293 name: name.to_string(),
294 command: command.to_string(),
295 args: parts.map(ToString::to_string).collect(),
296 })
297 }
298}
299
300#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
301enum CompressionLevelArg {
302 Low,
303 Medium,
304 High,
305 Max,
306}
307
308impl std::fmt::Display for CompressionLevelArg {
309 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
310 formatter.write_str(match self {
311 Self::Low => "low",
312 Self::Medium => "medium",
313 Self::High => "high",
314 Self::Max => "max",
315 })
316 }
317}
318
319impl From<CompressionLevelArg> for CompressionLevel {
320 fn from(value: CompressionLevelArg) -> Self {
321 match value {
322 CompressionLevelArg::Low => CompressionLevel::Low,
323 CompressionLevelArg::Medium => CompressionLevel::Medium,
324 CompressionLevelArg::High => CompressionLevel::High,
325 CompressionLevelArg::Max => CompressionLevel::Max,
326 }
327 }
328}
329
330#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
331enum CodeModeArg {
332 Python,
333 #[value(name = "typescript")]
334 TypeScript,
335}
336
337impl From<CodeModeArg> for FfiClientArtifactKind {
338 fn from(value: CodeModeArg) -> Self {
339 match value {
340 CodeModeArg::Python => FfiClientArtifactKind::Python,
341 CodeModeArg::TypeScript => FfiClientArtifactKind::TypeScript,
342 }
343 }
344}
345
346#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
347enum TransformModeArg {
348 CompressedTools,
349 Cli,
350 JustBash,
351}
352
353impl std::fmt::Display for TransformModeArg {
354 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
355 formatter.write_str(match self {
356 Self::CompressedTools => "compressed-tools",
357 Self::Cli => "cli",
358 Self::JustBash => "just-bash",
359 })
360 }
361}
362
363impl From<TransformModeArg> for ProxyTransformMode {
364 fn from(value: TransformModeArg) -> Self {
365 match value {
366 TransformModeArg::CompressedTools => ProxyTransformMode::CompressedTools,
367 TransformModeArg::Cli => ProxyTransformMode::Cli,
368 TransformModeArg::JustBash => ProxyTransformMode::JustBash,
369 }
370 }
371}
372
373#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
374pub enum FrontendTransport {
375 Stdio,
376 StreamableHttp,
377}
378
379impl std::fmt::Display for FrontendTransport {
380 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
381 formatter.write_str(match self {
382 Self::Stdio => "stdio",
383 Self::StreamableHttp => "streamable-http",
384 })
385 }
386}