use std::net::SocketAddr;
use std::sync::Arc;
use clap::{Args, Parser, Subcommand};
use emperor_folder::{
Config, Folder, RootSpec, ServeOptions, default_allowed_hosts, doctor, serve_http, serve_stdio,
};
#[derive(Parser)]
#[command(
name = "emperor-folder",
version,
about = "MCP server confined to one folder"
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Serve(Opts),
Doctor(Opts),
}
#[derive(Args)]
struct Opts {
#[arg(long = "root", value_name = "[NAME=]DIR", required = true)]
roots: Vec<RootSpec>,
#[arg(long, default_value = "127.0.0.1:8031")]
bind: SocketAddr,
#[arg(long)]
writable: bool,
#[arg(long)]
stdio: bool,
#[arg(long)]
hidden: bool,
#[arg(long)]
no_ignore: bool,
#[arg(long, default_value_t = emperor_folder::folder::DEFAULT_MAX_FILE_BYTES)]
max_file_bytes: u64,
#[arg(long, default_value_t = emperor_folder::folder::DEFAULT_MAX_RESULTS)]
max_results: usize,
#[arg(long, default_value_t = emperor_folder::folder::DEFAULT_MAX_CHARS)]
max_chars: usize,
#[arg(long = "allowed-host", value_name = "HOST")]
allowed_hosts: Vec<String>,
#[arg(long = "allowed-origin", value_name = "ORIGIN")]
allowed_origins: Vec<String>,
}
impl Opts {
fn config(&self) -> Config {
let mut config = Config::with_roots(self.roots.clone()).max_chars(self.max_chars);
config.writable = self.writable;
config.hidden = self.hidden;
config.no_ignore = self.no_ignore;
config.max_file_bytes = self.max_file_bytes;
config.max_results = self.max_results.max(1);
config
}
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
Command::Doctor(opts) => {
let report = doctor::report(&opts.config(), (!opts.stdio).then_some(opts.bind));
report.print();
std::process::exit(report.exit_code());
}
Command::Serve(opts) => {
init_tracing();
let folder = Arc::new(Folder::open(opts.config())?);
for (name, path) in folder.roots() {
tracing::info!(root = %path.display(), name, "root folder");
}
tracing::info!(writable = opts.writable, "emperor-folder starting");
let runtime = tokio::runtime::Runtime::new()?;
runtime.block_on(async move {
if opts.stdio {
serve_stdio(folder).await
} else {
let hosts = if opts.allowed_hosts.is_empty() {
default_allowed_hosts(opts.bind)
} else {
opts.allowed_hosts.clone()
};
let options = ServeOptions::new(opts.bind)
.with_allowed_hosts(hosts)
.with_allowed_origins(opts.allowed_origins.clone());
serve_http(folder, options).await
}
})
}
}
}
fn init_tracing() {
use tracing_subscriber::{EnvFilter, fmt};
let _ = fmt()
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()))
.with_writer(std::io::stderr)
.try_init();
}