Skip to main content

mcp_compressor_core/app/
options.rs

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    /// Compression level: low, medium, high, or max.
24    #[arg(short = 'c', long, value_enum, default_value = "medium")]
25    compression: CompressionLevelArg,
26
27    /// MCP config JSON file.
28    #[arg(long = "config")]
29    pub config_path: Option<PathBuf>,
30
31    /// Frontend server name/prefix.
32    #[arg(short = 'n', long)]
33    pub server_name: Option<String>,
34
35    /// Frontend transform mode.
36    #[arg(long, value_enum, default_value = "compressed-tools")]
37    transform_mode: TransformModeArg,
38
39    /// Alias for --transform-mode cli.
40    #[arg(long, action = ArgAction::SetTrue)]
41    cli_mode: bool,
42
43    /// Alias for --transform-mode just-bash.
44    #[arg(long, action = ArgAction::SetTrue)]
45    just_bash: bool,
46
47    /// Alias for --transform-mode just-bash.
48    #[arg(long = "just-bash-mode", action = ArgAction::SetTrue)]
49    just_bash_mode: bool,
50
51    /// Generate a Python or TypeScript code client that talks to the local proxy.
52    #[arg(long = "code-mode", value_enum, value_name = "LANGUAGE")]
53    code_mode: Option<CodeModeArg>,
54
55    /// Deprecated alias for --code-mode python.
56    #[arg(long = "python-mode", action = ArgAction::SetTrue, hide = true)]
57    python_mode: bool,
58
59    /// Deprecated alias for --code-mode typescript.
60    #[arg(long = "typescript-mode", action = ArgAction::SetTrue, hide = true)]
61    typescript_mode: bool,
62
63    /// Comma-separated backend tool names to include.
64    #[arg(long, value_delimiter = ',')]
65    pub include_tools: Vec<String>,
66
67    /// Comma-separated backend tool names to exclude.
68    #[arg(long, value_delimiter = ',')]
69    pub exclude_tools: Vec<String>,
70
71    /// Convert JSON text outputs to TOON where possible.
72    #[arg(long, action = ArgAction::SetTrue)]
73    pub toonify: bool,
74
75    /// Output directory for generated Python/TypeScript code clients.
76    #[arg(long = "output-dir")]
77    pub output_dir: Option<PathBuf>,
78
79    /// Multi-server backend spec: name=command [args...]. Repeat for each backend.
80    #[arg(long = "multi-server", value_name = "NAME=COMMAND [ARGS...]", action = ArgAction::Append)]
81    pub multi_server: Vec<MultiServerArg>,
82
83    /// Frontend transport.
84    #[arg(long, value_enum, default_value = "stdio")]
85    pub transport: FrontendTransport,
86
87    /// Port for streamable-http frontend; 0 chooses an available port.
88    #[arg(long, default_value_t = 8000)]
89    pub port: u16,
90
91    /// Backend URL or command plus backend arguments. All backend server arguments belong after `--`.
92    #[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    /// Clear stored OAuth credentials.
168    ClearOauth {
169        /// Backend server name or URL to clear. If omitted, all Rust OAuth state is removed.
170        target: Option<String>,
171    },
172
173    /// Manage optional local LLM assistance assets.
174    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    /// Show local llama-server/model installation status.
199    Status(LlmCommandOptions),
200    /// Download/install llama-server and the configured model.
201    Pull(LlmCommandOptions),
202    /// Remove managed LLM runtime and model assets from the cache.
203    Remove(LlmCommandOptions),
204    /// Download/install assets and run a test prompt.
205    Test(LlmTestOptions),
206}
207
208#[derive(Debug, Clone, Parser)]
209pub struct LlmCommandOptions {
210    /// Model reference, for example LiquidAI/LFM2.5-350M-GGUF:Q4_K_M.
211    #[arg(long = "model", default_value = DEFAULT_MODEL_REF)]
212    model: String,
213
214    /// Override mcp-compressor LLM cache directory.
215    #[arg(long = "cache-dir")]
216    cache_dir: Option<PathBuf>,
217
218    /// Explicit llama-server binary path.
219    #[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    /// Prompt to send to the local model.
250    #[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}