//! MCP Gateway - Universal Model Context Protocol Gateway
//!
//! Single-port multiplexing with Meta-MCP for ~95% context token savings.
use std::path::Path;
use std::process::ExitCode;
use std::sync::Arc;
use clap::Parser;
use tracing::{error, info};
use mcp_gateway::{
capability::{
AuthTemplate, CapabilityExecutor, CapabilityLoader, OpenApiConverter,
parse_capability_file, validate_capability,
},
cli::{CapCommand, Cli, Command, TlsCommand},
config::Config,
discovery::AutoDiscovery,
gateway::Gateway,
mtls::{CaParams, CertGenerator, LeafCertParams},
registry::Registry,
setup_tracing,
validator::ValidateConfig,
};
#[tokio::main]
async fn main() -> ExitCode {
let cli = Cli::parse();
// Setup tracing
if let Err(e) = setup_tracing(&cli.log_level, cli.log_format.as_deref()) {
eprintln!("Failed to setup tracing: {e}");
return ExitCode::FAILURE;
}
// Handle subcommands
match cli.command {
Some(Command::Init {
output,
with_examples,
}) => run_init_command(&output, with_examples),
Some(Command::Cap(cap_cmd)) => run_cap_command(cap_cmd).await,
Some(Command::Tls(tls_cmd)) => run_tls_command(tls_cmd),
Some(Command::Stats { url, price }) => run_stats_command(&url, price).await,
Some(Command::Validate {
paths,
format,
severity,
fix,
no_color,
}) => {
let config = ValidateConfig {
format,
min_severity: severity,
auto_fix: fix,
color: !no_color,
};
mcp_gateway::validator::cli_handler::run_validate_command(&paths, &config).await
}
Some(Command::Serve) | None => run_server(cli).await,
}
}
/// Generate a starter gateway configuration file.
///
/// Writes a commented YAML configuration to `output` that includes sensible
/// defaults and, when `with_examples` is true, three free-tier capability
/// definitions (weather, Wikipedia, Hacker News) so the user can start
/// experimenting immediately.
fn run_init_command(output: &Path, with_examples: bool) -> ExitCode {
if output.exists() {
eprintln!(
"Error: {} already exists. Remove it first or choose a different path with --output.",
output.display()
);
return ExitCode::FAILURE;
}
let examples_section = if with_examples {
r#"
# Capabilities - direct REST API integration (no MCP server needed)
# These free capabilities work out of the box with zero API keys.
capabilities:
enabled: true
directories:
- capabilities
# Example MCP backends (uncomment to enable)
# backends:
# filesystem:
# command: "npx -y @anthropic/mcp-server-filesystem /path/to/dir"
# description: "File system access"
# brave-search:
# command: "npx -y @anthropic/mcp-server-brave-search"
# description: "Web search via Brave"
# env:
# BRAVE_API_KEY: "${BRAVE_API_KEY}"
"#
} else {
r#"
# Capabilities - direct REST API integration
capabilities:
enabled: true
directories:
- capabilities
# Add your MCP backends here:
# backends:
# my-server:
# command: "npx -y @my/mcp-server"
# description: "My MCP server"
"#
};
let config_content = format!(
r#"# MCP Gateway Configuration
# ========================
# Generated by: mcp-gateway init
#
# Documentation: https://github.com/MikkoParkkola/mcp-gateway#readme
# Server settings
server:
host: "127.0.0.1"
port: 3000
# Meta-MCP mode - exposes 4 meta-tools for dynamic tool discovery
# Reduces context tokens by ~95% compared to loading all tools upfront
meta_mcp:
enabled: true
cache_tools: true
cache_ttl: 300s
{examples_section}"#
);
match std::fs::write(output, config_content) {
Ok(()) => {
println!("Created {}", output.display());
println!();
println!("Next steps:");
println!(" 1. Review and edit {}", output.display());
println!(" 2. Start the gateway:");
println!(" mcp-gateway -c {}", output.display());
println!(" 3. Add to your MCP client (e.g. Claude Desktop):");
println!(" {{");
println!(" \"mcpServers\": {{");
println!(" \"gateway\": {{");
println!(" \"url\": \"http://127.0.0.1:3000/mcp\"");
println!(" }}");
println!(" }}");
println!(" }}");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("Error: Failed to write {}: {e}", output.display());
ExitCode::FAILURE
}
}
}
/// Run `mcp-gateway tls` subcommands.
#[allow(clippy::too_many_lines)]
fn run_tls_command(cmd: TlsCommand) -> ExitCode {
match cmd {
TlsCommand::InitCa {
cn,
validity_days,
out,
} => {
println!("Generating Root CA: {cn}");
let params = CaParams {
cn: &cn,
validity_days,
};
match CertGenerator::init_ca(¶ms) {
Ok(cert) => {
if let Err(e) = CertGenerator::write_to_dir(&cert, &out, "ca") {
eprintln!("Error: Failed to write CA files: {e}");
return ExitCode::FAILURE;
}
println!(" CA cert: {}", out.join("ca.crt").display());
println!(" CA key: {}", out.join("ca.key").display());
println!();
println!("Keep the CA key offline or in a vault.");
println!("Add to gateway.yaml:");
println!(" mtls:");
println!(" ca_cert: \"{}\"", out.join("ca.crt").display());
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("Error: CA generation failed: {e}");
ExitCode::FAILURE
}
}
}
TlsCommand::IssueServer {
ca_cert,
ca_key,
cn,
san_dns,
validity_days,
out,
} => {
println!("Issuing server certificate for: {cn}");
let ca_cert_pem = match std::fs::read_to_string(&ca_cert) {
Ok(s) => s,
Err(e) => {
eprintln!("Error: Cannot read CA cert '{}': {e}", ca_cert.display());
return ExitCode::FAILURE;
}
};
let ca_key_pem = match std::fs::read_to_string(&ca_key) {
Ok(s) => s,
Err(e) => {
eprintln!("Error: Cannot read CA key '{}': {e}", ca_key.display());
return ExitCode::FAILURE;
}
};
let sans: Vec<String> = san_dns
.split(',')
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect();
let params = LeafCertParams {
cn: &cn,
ou: None,
san_dns: sans,
san_uris: vec![],
validity_days,
};
match CertGenerator::issue_leaf(¶ms, &ca_cert_pem, &ca_key_pem) {
Ok(cert) => {
if let Err(e) = CertGenerator::write_to_dir(&cert, &out, "server") {
eprintln!("Error: Failed to write server cert files: {e}");
return ExitCode::FAILURE;
}
println!(" Cert: {}", out.join("server.crt").display());
println!(" Key: {}", out.join("server.key").display());
println!();
println!("Add to gateway.yaml:");
println!(" mtls:");
println!(" enabled: true");
println!(" server_cert: \"{}\"", out.join("server.crt").display());
println!(" server_key: \"{}\"", out.join("server.key").display());
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("Error: Server cert generation failed: {e}");
ExitCode::FAILURE
}
}
}
TlsCommand::IssueClient {
ca_cert,
ca_key,
cn,
ou,
spiffe_uri,
validity_days,
out,
} => {
println!("Issuing client certificate for: {cn}");
let ca_cert_pem = match std::fs::read_to_string(&ca_cert) {
Ok(s) => s,
Err(e) => {
eprintln!("Error: Cannot read CA cert '{}': {e}", ca_cert.display());
return ExitCode::FAILURE;
}
};
let ca_key_pem = match std::fs::read_to_string(&ca_key) {
Ok(s) => s,
Err(e) => {
eprintln!("Error: Cannot read CA key '{}': {e}", ca_key.display());
return ExitCode::FAILURE;
}
};
let san_uris = spiffe_uri.map(|u| vec![u]).unwrap_or_default();
let params = LeafCertParams {
cn: &cn,
ou: ou.as_deref(),
san_dns: vec![],
san_uris,
validity_days,
};
let stem = cn.replace(['/', ' '], "-");
match CertGenerator::issue_leaf(¶ms, &ca_cert_pem, &ca_key_pem) {
Ok(cert) => {
if let Err(e) = CertGenerator::write_to_dir(&cert, &out, &stem) {
eprintln!("Error: Failed to write client cert files: {e}");
return ExitCode::FAILURE;
}
println!(" Cert: {}", out.join(format!("{stem}.crt")).display());
println!(" Key: {}", out.join(format!("{stem}.key")).display());
println!();
println!("Configure the agent:");
println!(
" export MCP_GATEWAY_CLIENT_CERT={}",
out.join(format!("{stem}.crt")).display()
);
println!(
" export MCP_GATEWAY_CLIENT_KEY={}",
out.join(format!("{stem}.key")).display()
);
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("Error: Client cert generation failed: {e}");
ExitCode::FAILURE
}
}
}
}
}
/// Run stats command
async fn run_stats_command(url: &str, price: f64) -> ExitCode {
use serde_json::json;
let client = reqwest::Client::new();
let request_body = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "gateway_get_stats",
"arguments": {
"price_per_million": price
}
}
});
let url = format!("{}/mcp", url.trim_end_matches('/'));
match client.post(&url).json(&request_body).send().await {
Ok(response) => {
if !response.status().is_success() {
eprintln!("❌ Gateway returned error: {}", response.status());
return ExitCode::FAILURE;
}
match response.json::<serde_json::Value>().await {
Ok(body) => {
if let Some(result) = body.get("result") {
if let Some(content) = result.get("content") {
if let Some(arr) = content.as_array() {
if let Some(first) = arr.first() {
if let Some(text) = first.get("text").and_then(|v| v.as_str()) {
if let Ok(stats) = serde_json::from_str::<serde_json::Value>(text) {
println!("📊 Gateway Statistics\n");
println!("Invocations: {}", stats["invocations"]);
println!("Cache Hits: {}", stats["cache_hits"]);
println!("Cache Hit Rate: {}", stats["cache_hit_rate"]);
println!("Tools Discovered: {}", stats["tools_discovered"]);
println!("Tools Available: {}", stats["tools_available"]);
println!("Tokens Saved: {}", stats["tokens_saved"].as_u64().unwrap_or(0));
println!("Estimated Savings: {}", stats["estimated_savings_usd"]);
if let Some(top_tools) = stats["top_tools"].as_array() {
if !top_tools.is_empty() {
println!("\n🏆 Top Tools:");
for tool in top_tools {
println!(" • {}:{} - {} calls",
tool["server"].as_str().unwrap_or(""),
tool["tool"].as_str().unwrap_or(""),
tool["count"]);
}
}
}
return ExitCode::SUCCESS;
}
}
}
}
}
}
if let Some(error) = body.get("error") {
eprintln!("❌ Error: {}", error.get("message").and_then(|v| v.as_str()).unwrap_or("Unknown"));
return ExitCode::FAILURE;
}
eprintln!("❌ Unexpected response format");
ExitCode::FAILURE
}
Err(e) => {
eprintln!("❌ Failed to parse response: {e}");
ExitCode::FAILURE
}
}
}
Err(e) => {
eprintln!("❌ Failed to connect to gateway: {e}");
eprintln!(" Make sure the gateway is running at {url}");
ExitCode::FAILURE
}
}
}
/// Run a capability subcommand (install, search, etc.).
#[allow(clippy::too_many_lines)]
async fn run_cap_command(cmd: CapCommand) -> ExitCode {
match cmd {
CapCommand::Validate { file } => match parse_capability_file(&file).await {
Ok(cap) => {
if let Err(e) = validate_capability(&cap) {
eprintln!("❌ Validation failed: {e}");
return ExitCode::FAILURE;
}
println!("✅ {} - valid", cap.name);
if !cap.description.is_empty() {
println!(" {}", cap.description);
}
if let Some(provider) = cap.primary_provider() {
println!(
" Provider: {} ({})",
provider.service, provider.config.method
);
println!(
" URL: {}{}",
provider.config.base_url, provider.config.path
);
}
if cap.auth.required {
println!(" Auth: {} ({})", cap.auth.auth_type, cap.auth.key);
}
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("❌ Failed to parse: {e}");
ExitCode::FAILURE
}
},
CapCommand::List { directory } => {
let path = directory.to_string_lossy();
match CapabilityLoader::load_directory(&path).await {
Ok(caps) => {
if caps.is_empty() {
println!("No capabilities found in {path}");
} else {
println!("Found {} capabilities in {}:\n", caps.len(), path);
for cap in caps {
let auth_info = if cap.auth.required {
format!(" [{}]", cap.auth.auth_type)
} else {
String::new()
};
println!(" {} - {}{}", cap.name, cap.description, auth_info);
}
}
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("❌ Failed to load: {e}");
ExitCode::FAILURE
}
}
}
CapCommand::Import {
spec,
output,
prefix,
auth_key,
} => {
let mut converter = OpenApiConverter::new();
if let Some(p) = prefix {
converter = converter.with_prefix(&p);
}
if let Some(key) = auth_key {
converter = converter.with_default_auth(AuthTemplate {
auth_type: "bearer".to_string(),
key,
description: "API authentication".to_string(),
});
}
let spec_path = spec.to_string_lossy();
match converter.convert_file(&spec_path) {
Ok(caps) => {
let out_path = output.to_string_lossy();
println!("Generated {} capabilities from {}\n", caps.len(), spec_path);
for cap in caps {
if let Err(e) = cap.write_to_file(&out_path) {
eprintln!("❌ Failed to write {}: {e}", cap.name);
} else {
println!(" ✅ {}.yaml", cap.name);
}
}
println!("\nCapabilities written to {out_path}/");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("❌ Failed to convert: {e}");
ExitCode::FAILURE
}
}
}
CapCommand::Test { file, args } => {
// Parse capability
let cap = match parse_capability_file(&file).await {
Ok(c) => c,
Err(e) => {
eprintln!("❌ Failed to parse capability: {e}");
return ExitCode::FAILURE;
}
};
// Parse arguments
let params: serde_json::Value = match serde_json::from_str(&args) {
Ok(v) => v,
Err(e) => {
eprintln!("❌ Invalid JSON arguments: {e}");
return ExitCode::FAILURE;
}
};
println!("Testing capability: {}", cap.name);
println!(
"Arguments: {}",
serde_json::to_string_pretty(¶ms).unwrap_or_default()
);
println!();
// Execute
let executor = Arc::new(CapabilityExecutor::new());
match executor.execute(&cap, params).await {
Ok(result) => {
println!("✅ Success:\n");
println!(
"{}",
serde_json::to_string_pretty(&result).unwrap_or_default()
);
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("❌ Execution failed: {e}");
ExitCode::FAILURE
}
}
}
CapCommand::Discover {
format,
write_config,
config_path,
} => {
let discovery = AutoDiscovery::new();
println!("🔍 Discovering MCP servers...\n");
match discovery.discover_all().await {
Ok(servers) => {
if servers.is_empty() {
println!("No MCP servers found.");
println!("\nSearched locations:");
println!(" • Claude Desktop config");
println!(" • VS Code/Cursor MCP configs");
println!(" • Windsurf config");
println!(" • ~/.config/mcp/*.json");
println!(" • Running processes (pieces, surreal, etc.)");
println!(" • Environment variables (MCP_SERVER_*_URL)");
return ExitCode::SUCCESS;
}
match format.as_str() {
"json" => {
println!(
"{}",
serde_json::to_string_pretty(&servers).unwrap_or_default()
);
}
"yaml" => {
println!(
"{}",
serde_yaml::to_string(&servers).unwrap_or_default()
);
}
_ => {
// Table format
println!("Discovered {} MCP server(s):\n", servers.len());
for server in &servers {
println!("📦 {}", server.name);
println!(" Description: {}", server.description);
println!(" Source: {:?}", server.source);
match &server.transport {
mcp_gateway::config::TransportConfig::Stdio {
command,
..
} => {
println!(" Transport: stdio");
println!(" Command: {command}");
}
mcp_gateway::config::TransportConfig::Http {
http_url,
..
} => {
println!(" Transport: http");
println!(" URL: {http_url}");
}
}
if let Some(ref path) = server.metadata.config_path {
println!(" Config: {}", path.display());
}
if let Some(pid) = server.metadata.pid {
println!(" PID: {pid}");
}
println!();
}
}
}
if write_config {
println!("\n📝 Writing discovered servers to config...");
let result = write_discovered_to_config(&servers, config_path.as_deref());
match result {
Ok(path) => {
println!("✅ Config written to {}", path.display());
println!(
"\nTo use discovered servers, start gateway with: mcp-gateway -c {}",
path.display()
);
}
Err(e) => {
eprintln!("❌ Failed to write config: {e}");
return ExitCode::FAILURE;
}
}
} else {
println!("\n💡 To add these servers to your gateway config, run:");
println!(" mcp-gateway cap discover --write-config");
}
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("❌ Discovery failed: {e}");
ExitCode::FAILURE
}
}
}
CapCommand::Install {
name,
from_github,
repo,
branch,
output,
} => {
if from_github {
println!("📦 Installing {name} from GitHub ({repo})...");
let registry = Registry::new(&output);
match registry
.install_from_github(&name, &repo, &branch)
.await
{
Ok(path) => {
println!("✅ Installed to {}", path.display());
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("❌ Installation failed: {e}");
ExitCode::FAILURE
}
}
} else {
// All capabilities are already in the capabilities directory
println!("ℹ️ All capabilities are already available in the capabilities directory.");
println!(" Use 'cap list' to see available capabilities.");
ExitCode::SUCCESS
}
}
CapCommand::Search { query, capabilities } => {
let reg = Registry::new(&capabilities);
match reg.build_index().await {
Ok(index) => {
let results = index.search(&query);
if results.is_empty() {
println!("No capabilities found matching '{query}'");
} else {
println!("Found {} capability(ies) matching '{query}':\n", results.len());
for entry in results {
let auth = if entry.requires_key { " 🔑" } else { "" };
println!(" {} - {}{}", entry.name, entry.description, auth);
if !entry.tags.is_empty() {
println!(" Tags: {}", entry.tags.join(", "));
}
println!();
}
println!("All capabilities are already available in the capabilities directory.");
}
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("❌ Failed to build registry index: {e}");
ExitCode::FAILURE
}
}
}
CapCommand::RegistryList { capabilities } => {
let reg = Registry::new(&capabilities);
match reg.build_index().await {
Ok(index) => {
println!("Available capabilities ({}):\n", index.capabilities.len());
for entry in &index.capabilities {
let auth = if entry.requires_key { " 🔑" } else { "" };
println!(" {} - {}{}", entry.name, entry.description, auth);
if !entry.tags.is_empty() {
println!(" Tags: {}", entry.tags.join(", "));
}
println!();
}
println!("All capabilities are available in the capabilities directory.");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("❌ Failed to build registry index: {e}");
ExitCode::FAILURE
}
}
}
}
}
/// Apply CLI overrides to a loaded configuration.
///
/// Merges CLI-provided port, host, and meta-mcp settings into `config`.
fn apply_cli_overrides(config: &mut Config, cli: &Cli) {
if let Some(port) = cli.port {
config.server.port = port;
}
if let Some(ref host) = cli.host {
config.server.host.clone_from(host);
}
if cli.no_meta_mcp {
config.meta_mcp.enabled = false;
}
}
/// Run the gateway server
async fn run_server(cli: Cli) -> ExitCode {
// Load configuration
let config = match Config::load(cli.config.as_deref()) {
Ok(mut config) => {
apply_cli_overrides(&mut config, &cli);
config
}
Err(e) => {
error!("Failed to load configuration: {e}");
return ExitCode::FAILURE;
}
};
info!(
version = env!("CARGO_PKG_VERSION"),
port = config.server.port,
backends = config.backends.len(),
meta_mcp = config.meta_mcp.enabled,
"Starting MCP Gateway"
);
// Create and run gateway (pass config path for hot-reload support)
let config_path = cli.config.as_deref().map(std::path::Path::to_path_buf);
let gateway = match Gateway::new_with_path(config, config_path).await {
Ok(g) => g,
Err(e) => {
error!("Failed to create gateway: {e}");
return ExitCode::FAILURE;
}
};
// Run with graceful shutdown
if let Err(e) = gateway.run().await {
error!("Gateway error: {e}");
return ExitCode::FAILURE;
}
info!("Gateway shutdown complete");
ExitCode::SUCCESS
}
/// Write discovered servers to a config file
fn write_discovered_to_config(
servers: &[mcp_gateway::discovery::DiscoveredServer],
config_path: Option<&std::path::Path>,
) -> mcp_gateway::Result<std::path::PathBuf> {
// Determine config path
let path = if let Some(p) = config_path {
p.to_path_buf()
} else {
std::path::PathBuf::from("mcp-gateway-discovered.yaml")
};
// Load existing config or create new
let mut config = if path.exists() {
Config::load(Some(&path))?
} else {
Config::default()
};
// Add discovered servers to backends
for server in servers {
let backend_config = server.to_backend_config();
config.backends.insert(server.name.clone(), backend_config);
}
// Serialize to YAML
let yaml = serde_yaml::to_string(&config)
.map_err(|e| mcp_gateway::Error::Config(format!("Failed to serialize config: {e}")))?;
// Write to file
std::fs::write(&path, yaml)
.map_err(|e| mcp_gateway::Error::Config(format!("Failed to write config: {e}")))?;
Ok(path)
}
#[cfg(test)]
mod tests {
use super::*;
use mcp_gateway::cli::Cli;
use mcp_gateway::config::{BackendConfig, Config};
/// Build a `Cli` struct with optional overrides for testing.
fn make_cli(
port: Option<u16>,
host: Option<String>,
no_meta_mcp: bool,
) -> Cli {
Cli {
config: None,
port,
host,
log_level: "info".to_string(),
log_format: None,
no_meta_mcp,
command: None,
}
}
// =====================================================================
// apply_cli_overrides
// =====================================================================
#[test]
fn apply_cli_overrides_no_overrides_preserves_defaults() {
let mut config = Config::default();
let cli = make_cli(None, None, false);
let original_port = config.server.port;
let original_host = config.server.host.clone();
let original_meta = config.meta_mcp.enabled;
apply_cli_overrides(&mut config, &cli);
assert_eq!(config.server.port, original_port);
assert_eq!(config.server.host, original_host);
assert_eq!(config.meta_mcp.enabled, original_meta);
}
#[test]
fn apply_cli_overrides_port_override() {
let mut config = Config::default();
let cli = make_cli(Some(9999), None, false);
apply_cli_overrides(&mut config, &cli);
assert_eq!(config.server.port, 9999);
}
#[test]
fn apply_cli_overrides_host_override() {
let mut config = Config::default();
let cli = make_cli(None, Some("0.0.0.0".to_string()), false);
apply_cli_overrides(&mut config, &cli);
assert_eq!(config.server.host, "0.0.0.0");
}
#[test]
fn apply_cli_overrides_disable_meta_mcp() {
let mut config = Config::default();
assert!(config.meta_mcp.enabled); // default is enabled
let cli = make_cli(None, None, true);
apply_cli_overrides(&mut config, &cli);
assert!(!config.meta_mcp.enabled);
}
#[test]
fn apply_cli_overrides_all_at_once() {
let mut config = Config::default();
let cli = make_cli(Some(8080), Some("192.168.1.1".to_string()), true);
apply_cli_overrides(&mut config, &cli);
assert_eq!(config.server.port, 8080);
assert_eq!(config.server.host, "192.168.1.1");
assert!(!config.meta_mcp.enabled);
}
#[test]
fn apply_cli_overrides_no_meta_mcp_false_keeps_enabled() {
let mut config = Config::default();
let cli = make_cli(None, None, false);
apply_cli_overrides(&mut config, &cli);
assert!(config.meta_mcp.enabled);
}
#[test]
fn apply_cli_overrides_port_zero_is_valid() {
let mut config = Config::default();
let cli = make_cli(Some(0), None, false);
apply_cli_overrides(&mut config, &cli);
assert_eq!(config.server.port, 0);
}
#[test]
fn apply_cli_overrides_host_empty_string() {
let mut config = Config::default();
let cli = make_cli(None, Some(String::new()), false);
apply_cli_overrides(&mut config, &cli);
assert_eq!(config.server.host, "");
}
#[test]
fn apply_cli_overrides_preserves_other_config_fields() {
let mut config = Config::default();
config.backends.insert("test".to_string(), BackendConfig::default());
config.server.request_timeout = std::time::Duration::from_secs(60);
let cli = make_cli(Some(3000), None, false);
apply_cli_overrides(&mut config, &cli);
assert_eq!(config.server.port, 3000);
assert!(config.backends.contains_key("test"));
assert_eq!(
config.server.request_timeout,
std::time::Duration::from_secs(60)
);
}
// =====================================================================
// Config::default sanity checks
// =====================================================================
#[test]
fn default_config_has_expected_defaults() {
let config = Config::default();
assert_eq!(config.server.port, 39400);
assert_eq!(config.server.host, "127.0.0.1");
assert!(config.meta_mcp.enabled);
assert!(config.backends.is_empty());
}
// =====================================================================
// run_init_command
// =====================================================================
#[test]
fn init_command_creates_config_file() {
let dir = tempfile::tempdir().unwrap();
let output = dir.path().join("gateway.yaml");
let result = run_init_command(&output, true);
assert_eq!(result, ExitCode::SUCCESS);
assert!(output.exists());
let content = std::fs::read_to_string(&output).unwrap();
assert!(content.contains("server:"));
assert!(content.contains("host: \"127.0.0.1\""));
assert!(content.contains("port: 3000"));
assert!(content.contains("meta_mcp:"));
assert!(content.contains("enabled: true"));
}
#[test]
fn init_command_with_examples_includes_capabilities() {
let dir = tempfile::tempdir().unwrap();
let output = dir.path().join("gateway.yaml");
let result = run_init_command(&output, true);
assert_eq!(result, ExitCode::SUCCESS);
let content = std::fs::read_to_string(&output).unwrap();
assert!(content.contains("capabilities:"));
assert!(content.contains("directories:"));
}
#[test]
fn init_command_without_examples_omits_sample_backends() {
let dir = tempfile::tempdir().unwrap();
let output = dir.path().join("gateway.yaml");
let result = run_init_command(&output, false);
assert_eq!(result, ExitCode::SUCCESS);
let content = std::fs::read_to_string(&output).unwrap();
assert!(content.contains("capabilities:"));
// Should not contain the filesystem/brave-search example backends
assert!(!content.contains("filesystem:"));
}
#[test]
fn init_command_refuses_to_overwrite_existing() {
let dir = tempfile::tempdir().unwrap();
let output = dir.path().join("gateway.yaml");
std::fs::write(&output, "existing content").unwrap();
let result = run_init_command(&output, true);
assert_eq!(result, ExitCode::FAILURE);
// Original content should be preserved
let content = std::fs::read_to_string(&output).unwrap();
assert_eq!(content, "existing content");
}
#[test]
fn init_command_custom_output_path() {
let dir = tempfile::tempdir().unwrap();
let output = dir.path().join("custom-config.yaml");
let result = run_init_command(&output, true);
assert_eq!(result, ExitCode::SUCCESS);
assert!(output.exists());
}
}