use std::process::ExitCode;
use clap::{error::ErrorKind, Parser};
use crate::app::options::{CliCommand, CliOptions, LlmCommand, MultiServerArg};
use crate::app::runtime::{run_cli_mode, run_compressed_server, run_just_bash_mode};
use crate::oauth::clear_oauth_store;
use crate::server::{
BackendConfigSource, BackendServerConfig, CompressedServer, CompressedServerConfig,
ProxyTransformMode,
};
pub fn main_exit_code() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(CliError::Display(message)) => {
print!("{message}");
ExitCode::SUCCESS
}
Err(CliError::Usage(message)) => {
eprintln!("error: {message}");
ExitCode::from(2)
}
Err(CliError::Runtime(message)) => {
eprintln!("error: {message}");
ExitCode::from(1)
}
}
}
pub fn run() -> Result<(), CliError> {
run_from(std::env::args())
}
pub fn run_from<I, T>(args: I) -> Result<(), CliError>
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
{
let cli = CliOptions::try_parse_from(args).map_err(|error| {
if matches!(error.kind(), ErrorKind::DisplayHelp | ErrorKind::DisplayVersion) {
CliError::Display(error.to_string())
} else {
CliError::Usage(error.to_string())
}
})?;
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(|error| CliError::Runtime(error.to_string()))?;
runtime.block_on(async move { run_async(cli).await })
}
async fn run_async(cli: CliOptions) -> Result<(), CliError> {
cli.validate().map_err(CliError::Usage)?;
if let Some(command) = &cli.command_kind {
return run_command(command).await;
}
let server = build_server(&cli).await?;
match cli.transform_mode() {
ProxyTransformMode::Cli => run_cli_mode(cli, server).await,
ProxyTransformMode::CompressedTools => run_compressed_server(&cli, server).await,
ProxyTransformMode::JustBash => run_just_bash_mode(cli, server).await,
}
.map_err(CliError::Runtime)
}
async fn build_server(cli: &CliOptions) -> Result<CompressedServer, CliError> {
let config = CompressedServerConfig {
level: cli.compression(),
server_name: cli.server_name.clone(),
include_tools: cli.include_tools.clone(),
exclude_tools: cli.exclude_tools.clone(),
toonify: cli.toonify,
transform_mode: cli.transform_mode(),
config_source: BackendConfigSource::Command,
};
if let Some(config_path) = &cli.config_path {
let json = std::fs::read_to_string(config_path)
.map_err(|error| CliError::Runtime(error.to_string()))?;
let mut config = config;
let parsed = crate::config::topology::MCPConfig::from_json(&json)
.map_err(|error| CliError::Runtime(error.to_string()))?;
config.config_source = if parsed.server_names().len() == 1 {
BackendConfigSource::SingleServerJsonConfig
} else {
BackendConfigSource::MultiServerJsonConfig
};
CompressedServer::connect_mcp_config_json(config, &json)
.await
.map_err(|error| CliError::Runtime(error.to_string()))
} else {
if !cli.multi_server.is_empty() {
return CompressedServer::connect_multi_stdio(
config,
cli.multi_server
.iter()
.cloned()
.map(MultiServerArg::into_backend)
.collect(),
)
.await
.map_err(|error| CliError::Runtime(error.to_string()));
}
let (command, args) = cli
.command
.split_first()
.ok_or_else(|| CliError::Usage("backend command is required".to_string()))?;
let backend_name = cli
.server_name
.clone()
.unwrap_or_else(|| "server".to_string());
CompressedServer::connect_stdio(
config,
BackendServerConfig::new(backend_name, command.clone(), args.to_vec()),
)
.await
.map_err(|error| CliError::Runtime(error.to_string()))
}
}
async fn run_command(command: &CliCommand) -> Result<(), CliError> {
if let Some(llm_command) = command.llm_command() {
return run_llm_command(llm_command).await;
}
let removed = clear_oauth_store(command.clear_oauth_target())
.map_err(|error| CliError::Runtime(error.to_string()))?;
if removed.is_empty() {
println!("No stored OAuth credentials found.");
} else {
println!(
"Removed {} OAuth store entr{}.",
removed.len(),
if removed.len() == 1 { "y" } else { "ies" }
);
}
Ok(())
}
async fn run_llm_command(command: &LlmCommand) -> Result<(), CliError> {
match command {
LlmCommand::Status(options) => {
let config = options.config(false).map_err(CliError::Usage)?;
let status = crate::llm_assist::install_status(&config)
.map_err(|error| CliError::Runtime(error.to_string()))?;
println!(
"llama-server: {}",
if status.llama_server_ready {
"ready"
} else {
"missing"
}
);
if let Some(path) = status.llama_server_path {
println!("llama-server path: {}", path.display());
}
println!("model: {}", status.model_ref);
println!(
"model status: {}",
if status.model_ready {
"ready"
} else {
"missing"
}
);
println!("model path: {}", status.model_path.display());
}
LlmCommand::Pull(options) => {
let config = options.config(true).map_err(CliError::Usage)?;
let prepared = crate::llm_assist::pull_llm_assets(config)
.await
.map_err(|error| CliError::Runtime(error.to_string()))?;
println!(
"llama-server ready: {}",
prepared.llama_server_path.display()
);
println!("model ready: {}", prepared.model_path.display());
}
LlmCommand::Remove(options) => {
crate::llm_assist::remove_managed_llm_assets(options.cache_dir())
.map_err(|error| CliError::Runtime(error.to_string()))?;
println!("Removed managed LLM assets from the mcp-compressor cache.");
}
LlmCommand::Test(options) => {
let config = options.config(true).map_err(CliError::Usage)?;
let assistant = crate::llm_assist::LlmAssistant::from_config(config);
assistant.start_background_preparation();
let response = assistant
.complete("You are a concise local utility model.", options.prompt())
.await
.map_err(|error| CliError::Runtime(error.to_string()))?;
println!("{response}");
}
}
Ok(())
}
#[derive(Debug)]
pub enum CliError {
Display(String),
Usage(String),
Runtime(String),
}