pub mod audit;
pub mod backend;
pub mod errors;
pub mod install;
pub mod server;
pub mod session;
pub mod tools;
use crate::errors::{McpError, McpErrorKind};
use crate::install::{InstallOpts, Scope};
use crate::server::MCP_PROTOCOL_VERSION;
use crate::session::{Session, SessionArgs};
#[derive(Debug)]
enum Subcommand {
Serve(Vec<String>),
Install {
host: String,
rest: Vec<String>,
},
Uninstall {
host: String,
rest: Vec<String>,
},
Status,
}
pub fn run() {
if let Err(e) = run_inner() {
eprintln!("tsafe-mcp: {e:#}");
std::process::exit(1);
}
}
fn run_inner() -> Result<(), McpError> {
let argv: Vec<String> = std::env::args().collect();
let sub = parse_subcommand(&argv)?;
match sub {
Subcommand::Serve(serve_args) => {
let session_args = SessionArgs::parse(&serve_args)?;
let session = Session::from_cli_args(&session_args)?;
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(|e| {
McpError::new(
McpErrorKind::InternalError,
format!("tokio runtime init failed: {e}"),
)
})?
.block_on(async {
server::serve_stdio(session).await.map_err(|e| {
McpError::new(McpErrorKind::InternalError, format!("serve failed: {e}"))
})
})
}
Subcommand::Install { host, rest } => {
let opts = parse_install_opts(&rest, false)?;
install::dispatch(&host, &opts)
}
Subcommand::Uninstall { host, rest } => {
let opts = parse_install_opts(&rest, true)?;
install::dispatch(&host, &opts)
}
Subcommand::Status => {
status_diagnostic();
Ok(())
}
}
}
fn parse_subcommand(argv: &[String]) -> Result<Subcommand, McpError> {
if argv.len() < 2 {
return Ok(Subcommand::Serve(Vec::new()));
}
match argv[1].as_str() {
"serve" => Ok(Subcommand::Serve(argv[2..].to_vec())),
"install" => {
if argv.len() < 3 {
return Err(McpError::new(
McpErrorKind::InvalidRequest,
"install: missing host argument (claude | cursor | continue | windsurf | codex)",
));
}
Ok(Subcommand::Install {
host: argv[2].clone(),
rest: argv[3..].to_vec(),
})
}
"uninstall" => {
if argv.len() < 3 {
return Err(McpError::new(
McpErrorKind::InvalidRequest,
"uninstall: missing host argument",
));
}
Ok(Subcommand::Uninstall {
host: argv[2].clone(),
rest: argv[3..].to_vec(),
})
}
"status" => Ok(Subcommand::Status),
other if other.starts_with("--") => Ok(Subcommand::Serve(argv[1..].to_vec())),
unknown => Err(McpError::new(
McpErrorKind::InvalidRequest,
format!(
"unknown subcommand '{unknown}'. Use: serve | install <host> | uninstall <host> | status"
),
)),
}
}
fn parse_install_opts(rest: &[String], uninstall: bool) -> Result<InstallOpts, McpError> {
let mut profile: Option<String> = None;
let mut allowed_keys: Vec<String> = Vec::new();
let mut denied_keys: Vec<String> = Vec::new();
let mut contract: Option<String> = None;
let mut allow_reveal = false;
let mut name: Option<String> = None;
let mut global = false;
let mut project_dir: Option<std::path::PathBuf> = None;
let mut dry_run = false;
let mut audit_source: Option<String> = None;
let mut i = 0;
while i < rest.len() {
let arg = &rest[i];
match arg.as_str() {
"--profile" => {
i += 1;
profile = Some(rest.get(i).cloned().ok_or_else(|| {
McpError::new(McpErrorKind::InvalidRequest, "--profile requires a value")
})?);
}
"--allowed-keys" => {
i += 1;
let raw = rest.get(i).cloned().ok_or_else(|| {
McpError::new(
McpErrorKind::InvalidRequest,
"--allowed-keys requires a value",
)
})?;
allowed_keys = split_csv(&raw);
}
"--denied-keys" => {
i += 1;
let raw = rest.get(i).cloned().ok_or_else(|| {
McpError::new(
McpErrorKind::InvalidRequest,
"--denied-keys requires a value",
)
})?;
denied_keys = split_csv(&raw);
}
"--contract" => {
i += 1;
contract = Some(rest.get(i).cloned().ok_or_else(|| {
McpError::new(McpErrorKind::InvalidRequest, "--contract requires a value")
})?);
}
"--allow-reveal" => allow_reveal = true,
"--name" => {
i += 1;
name = Some(rest.get(i).cloned().ok_or_else(|| {
McpError::new(McpErrorKind::InvalidRequest, "--name requires a value")
})?);
}
"--global" => global = true,
"--project" => {
i += 1;
let dir = rest.get(i).cloned().ok_or_else(|| {
McpError::new(
McpErrorKind::InvalidRequest,
"--project requires a directory path",
)
})?;
project_dir = Some(std::path::PathBuf::from(dir));
}
"--dry-run" => dry_run = true,
"--audit-source" => {
i += 1;
audit_source = Some(rest.get(i).cloned().ok_or_else(|| {
McpError::new(
McpErrorKind::InvalidRequest,
"--audit-source requires a value",
)
})?);
}
other => {
return Err(McpError::new(
McpErrorKind::InvalidRequest,
format!("unknown install flag: '{other}'"),
));
}
}
i += 1;
}
let scope = match project_dir {
Some(dir) => Scope::Project { dir },
None if global => Scope::Global,
None => Scope::Global, };
let profile = profile.ok_or_else(|| {
McpError::new(
McpErrorKind::InvalidRequest,
"install/uninstall: --profile <name> is required",
)
})?;
Ok(InstallOpts {
profile,
allowed_keys,
denied_keys,
contract,
allow_reveal,
name,
scope,
dry_run,
uninstall,
audit_source,
})
}
fn split_csv(raw: &str) -> Vec<String> {
raw.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
}
fn status_diagnostic() {
println!("tsafe-mcp {}", env!("CARGO_PKG_VERSION"));
println!("protocol: MCP {MCP_PROTOCOL_VERSION}");
println!();
println!("Resolve scope at runtime by invoking `tsafe-mcp serve` with one of:");
println!(" --profile <name>");
println!(" --allowed-keys <glob,glob>");
println!(" --contract <name>");
println!(" --denied-keys <glob,glob>");
println!(" --allow-reveal");
println!(" --audit-source <host-label>");
println!();
println!("See ADR-006 / docs/architecture/mcp-server-design.md for the full surface.");
}
#[cfg(test)]
mod tests {
use super::*;
fn argv(items: &[&str]) -> Vec<String> {
items.iter().map(|s| s.to_string()).collect()
}
#[test]
fn parse_subcommand_defaults_to_serve_with_no_args() {
let parsed = parse_subcommand(&argv(&["tsafe-mcp"])).unwrap();
match parsed {
Subcommand::Serve(rest) => assert!(rest.is_empty()),
other => panic!("expected Serve, got {other:?}"),
}
}
#[test]
fn parse_subcommand_serve_passes_through_remaining_args() {
let parsed = parse_subcommand(&argv(&["tsafe-mcp", "serve", "--profile", "demo"])).unwrap();
match parsed {
Subcommand::Serve(rest) => {
assert_eq!(rest, vec!["--profile", "demo"]);
}
other => panic!("expected Serve, got {other:?}"),
}
}
#[test]
fn parse_subcommand_bare_flag_routes_to_serve() {
let parsed = parse_subcommand(&argv(&["tsafe-mcp", "--profile", "demo"])).unwrap();
match parsed {
Subcommand::Serve(rest) => {
assert_eq!(rest, vec!["--profile", "demo"]);
}
other => panic!("expected Serve, got {other:?}"),
}
}
#[test]
fn parse_subcommand_install_without_host_returns_invalid_request() {
let err = parse_subcommand(&argv(&["tsafe-mcp", "install"])).unwrap_err();
assert_eq!(err.kind, McpErrorKind::InvalidRequest);
assert!(err.message.contains("install"));
}
#[test]
fn parse_subcommand_uninstall_without_host_returns_invalid_request() {
let err = parse_subcommand(&argv(&["tsafe-mcp", "uninstall"])).unwrap_err();
assert_eq!(err.kind, McpErrorKind::InvalidRequest);
}
#[test]
fn parse_subcommand_status_parses_cleanly() {
let parsed = parse_subcommand(&argv(&["tsafe-mcp", "status"])).unwrap();
assert!(matches!(parsed, Subcommand::Status));
}
#[test]
fn parse_subcommand_unknown_returns_invalid_request_with_hint() {
let err = parse_subcommand(&argv(&["tsafe-mcp", "diagnose"])).unwrap_err();
assert_eq!(err.kind, McpErrorKind::InvalidRequest);
assert!(
err.message.contains("serve")
&& err.message.contains("install")
&& err.message.contains("status"),
"hint should list supported subcommands: {}",
err.message
);
}
#[test]
fn parse_install_opts_round_trips_all_flags() {
let rest = argv(&[
"--profile",
"demo",
"--allowed-keys",
"demo/*,shared/*",
"--denied-keys",
"demo/secret",
"--contract",
"deploy",
"--allow-reveal",
"--name",
"testsrv",
"--audit-source",
"mcp:claude:proof",
]);
let opts = parse_install_opts(&rest, false).unwrap();
assert_eq!(opts.profile, "demo");
assert_eq!(opts.allowed_keys, vec!["demo/*", "shared/*"]);
assert_eq!(opts.denied_keys, vec!["demo/secret"]);
assert_eq!(opts.contract.as_deref(), Some("deploy"));
assert!(opts.allow_reveal);
assert_eq!(opts.name.as_deref(), Some("testsrv"));
assert_eq!(opts.audit_source.as_deref(), Some("mcp:claude:proof"));
assert!(!opts.uninstall);
assert!(!opts.dry_run);
}
#[test]
fn parse_install_opts_project_scope() {
let rest = argv(&[
"--profile",
"demo",
"--allowed-keys",
"demo/*",
"--project",
"/tmp/myproject",
"--dry-run",
]);
let opts = parse_install_opts(&rest, false).unwrap();
match opts.scope {
crate::install::Scope::Project { dir } => {
assert_eq!(dir, std::path::PathBuf::from("/tmp/myproject"));
}
crate::install::Scope::Global => panic!("expected Project scope, got Global"),
}
assert!(opts.dry_run);
}
#[test]
fn parse_install_opts_missing_profile_value() {
let rest = argv(&["--profile"]);
let err = parse_install_opts(&rest, false).unwrap_err();
assert_eq!(err.kind, McpErrorKind::InvalidRequest);
assert!(err.message.contains("--profile"));
}
#[test]
fn parse_install_opts_missing_profile_entirely() {
let rest = argv(&["--allowed-keys", "demo/*"]);
let err = parse_install_opts(&rest, false).unwrap_err();
assert_eq!(err.kind, McpErrorKind::InvalidRequest);
assert!(err.message.contains("--profile"));
}
#[test]
fn parse_install_opts_rejects_unknown_flag() {
let rest = argv(&["--profile", "demo", "--mystery-flag"]);
let err = parse_install_opts(&rest, false).unwrap_err();
assert_eq!(err.kind, McpErrorKind::InvalidRequest);
assert!(err.message.contains("unknown install flag"));
}
#[test]
fn parse_install_opts_uninstall_sets_flag() {
let rest = argv(&["--profile", "demo"]);
let opts = parse_install_opts(&rest, true).unwrap();
assert!(opts.uninstall);
}
#[test]
fn split_csv_handles_whitespace_and_empties() {
assert_eq!(split_csv("demo/*, , shared/*"), vec!["demo/*", "shared/*"]);
assert!(split_csv("").is_empty());
assert_eq!(split_csv(" only "), vec!["only"]);
}
}