#![recursion_limit = "256"]
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};
const HELP: &str = "\
tsafe-mcp — first-party MCP server for tsafe
Usage:
tsafe-mcp serve --profile <profile> --contract <contract> --workdir <path>
tsafe-mcp config codex --name <name> --profile <profile> --contract <contract> --workdir <path>
tsafe-mcp install <host> --profile <profile> [--allowed-keys <glob,glob>]
tsafe-mcp uninstall <host> --profile <profile>
tsafe-mcp status
Bound mode fixes one profile, one contract, one workdir at process startup.
It speaks stdio JSON-RPC to an MCP host. It does not return secret values to
the model; secret material may enter only the bounded child process environment
when a contract-authorized command is run.
Privacy:
stdio JSON-RPC is local host IPC, not product telemetry. CloudEvents and audit
receipts remain local/export-selected tsafe surfaces, separate from this MCP
transport.
Tools in bound contract mode:
show_exec_plan
run_contract_command
tsafe_mcp_status
";
#[derive(Debug)]
enum Subcommand {
Serve(Vec<String>),
Install {
host: String,
rest: Vec<String>,
},
Config {
host: String,
rest: Vec<String>,
},
Uninstall {
host: String,
rest: Vec<String>,
},
Status,
Help,
}
pub fn run() {
if let Err(e) = run_inner() {
eprintln!("tsafe-mcp: {e:#}");
if let Some(data) = e.data {
eprintln!("resolution: {data}");
}
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::Help => {
print!("{HELP}");
Ok(())
}
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::Config { host, rest } => {
let mut opts = parse_install_opts(&rest, false)?;
opts.dry_run = true;
if host.eq_ignore_ascii_case("codex") {
install::validate_codex_bound_config(&opts)?;
let rendered = install::codex::render_config(&opts)?;
print!("{rendered}");
Ok(())
} else {
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() {
"--help" | "-h" | "help" => Ok(Subcommand::Help),
"serve" if argv[2..].iter().any(|arg| arg == "--help" || arg == "-h") => {
Ok(Subcommand::Help)
}
"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(),
})
}
"config" => {
if argv.len() < 3 {
return Err(McpError::new(
McpErrorKind::InvalidRequest,
"config: missing host argument (codex)",
));
}
Ok(Subcommand::Config {
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 workdir: Option<std::path::PathBuf> = 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")
})?);
}
"--workdir" => {
i += 1;
let dir = rest.get(i).cloned().ok_or_else(|| {
McpError::new(McpErrorKind::InvalidRequest, "--workdir requires a value")
})?;
workdir = Some(std::path::PathBuf::from(dir));
}
"--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,
workdir,
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!(" --workdir <path>");
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"]);
}
}