use std::path::PathBuf;
use anyhow::Context;
use clap::{Parser, Subcommand};
use mcpmesh::enrollcmd::{load_device_key, split_csv, with_daemon};
use mcpmesh::render::{self, SERVE_EXAMPLE};
use mcpmesh::{config, doctor, enrollcmd, proxy, util};
use mcpmesh_local_api::{BackendSpec, PeerAddParams};
use mcpmesh_trust::paths;
fn serve_after_help() -> String {
format!(
"Example — share a folder of notes (needs npx; no MCP server of your own required):\n \
{SERVE_EXAMPLE}\n\nThen `mcpmesh invite notes` mints an invite to send whoever you're \
sharing with."
)
}
#[derive(Parser)]
#[command(name = "mcpmesh", version)]
struct Cli {
#[arg(long, value_name = "dir", global = true, visible_alias = "home")]
profile: Option<PathBuf>,
#[arg(long, global = true)]
json: bool,
#[command(subcommand)]
cmd: Option<Cmd>,
}
#[derive(Subcommand)]
enum Cmd {
Status,
Doctor,
#[command(after_help = serve_after_help())]
Serve {
name: String,
#[arg(long)]
allow: Option<String>,
#[arg(last = true, required = true)]
cmd: Vec<String>,
},
Up {
#[arg(long, value_name = "secs")]
timeout: Option<u64>,
},
Connect {
target: String,
},
Invite {
services: Vec<String>,
#[arg(long, value_name = "text")]
label: Option<String>,
},
Pair {
invite: Option<String>,
#[arg(long, value_name = "nickname")]
remove: Option<String>,
},
Use {
target: String,
},
Join {
org_invite: String,
#[arg(long)]
name: Option<String>,
#[arg(long)]
user_id: Option<String>,
#[arg(long, default_value = "laptop")]
label: String,
},
Org {
#[command(subcommand)]
command: OrgCmd,
},
Devices {
#[command(subcommand)]
command: DevicesCmd,
},
Completions {
shell: clap_complete::Shell,
},
Internal {
#[command(subcommand)]
command: Internal,
},
}
#[derive(Subcommand)]
enum OrgCmd {
Create {
name: String,
#[arg(long)]
expires: Option<String>,
#[arg(long)]
roster_url: Option<String>,
},
Approve {
join_code: String,
#[arg(long)]
groups: String,
#[arg(long)]
user_id: Option<String>,
},
Revoke {
target: String,
#[arg(long)]
user_key: bool,
},
}
#[derive(Subcommand)]
enum DevicesCmd {
Code {
#[arg(long, default_value = "desktop")]
label: String,
},
Add {
device_code: String,
},
}
#[derive(Subcommand)]
enum Internal {
Daemon,
Id,
Peer {
#[command(subcommand)]
command: PeerCmd,
},
Roster {
#[command(subcommand)]
command: RosterCmd,
},
Blob {
#[command(subcommand)]
command: BlobCmd,
},
Audit {
#[command(subcommand)]
command: AuditCmd,
},
Watch,
Man {
dir: PathBuf,
},
}
#[derive(Subcommand)]
enum BlobCmd {
Publish {
scope: String,
file: PathBuf,
},
Grant { scope: String, principal: String },
List {
#[arg(long)]
scope: Option<String>,
#[arg(long)]
limit: Option<usize>,
#[arg(long)]
offset: Option<usize>,
#[arg(long)]
counts_only: bool,
},
Fetch {
ticket: String,
dest: PathBuf,
},
}
#[derive(clap::Subcommand)]
enum AuditCmd {
Tail {
#[arg(long, default_value_t = 20)]
lines: usize,
#[arg(long)]
kind: Option<String>,
#[arg(long)]
peer: Option<String>,
},
List,
Prune {
#[arg(long, value_name = "YYYY-MM")]
before: String,
},
}
#[derive(Subcommand)]
enum RosterCmd {
Install {
file: PathBuf,
#[arg(long)]
org_root_pk: Option<String>,
},
}
#[derive(Subcommand)]
enum PeerCmd {
Add {
nickname: String,
endpoint_id: String,
#[arg(long)]
allow: Option<String>,
},
State {
peer: String,
#[arg(long)]
json: bool,
},
}
fn main() -> std::process::ExitCode {
let cli = Cli::parse();
if let Some(dir) = &cli.profile {
let abs = if dir.is_absolute() {
dir.clone()
} else {
std::env::current_dir()
.map(|c| c.join(dir))
.unwrap_or_else(|_| dir.clone())
};
let _ = paths::set_root(abs);
}
let json = cli.json;
match run(cli) {
Ok(()) => std::process::ExitCode::SUCCESS,
Err(err) => {
if json {
eprintln!("{}", mcpmesh::json::error_json(&err));
} else {
for line in render::error_lines(&err) {
eprintln!("{line}");
}
}
std::process::ExitCode::FAILURE
}
}
}
fn run(cli: Cli) -> anyhow::Result<()> {
match cli.cmd {
Some(Cmd::Internal {
command: Internal::Daemon,
}) => mcpmesh::daemonshell::run(),
Some(Cmd::Internal {
command: Internal::Id,
}) => run_internal_id(),
Some(Cmd::Serve { name, allow, cmd }) => run_serve(name, allow, cmd, cli.json),
Some(Cmd::Connect { target }) => run_connect(target),
Some(Cmd::Invite { services, label }) => run_invite(services, label, cli.json),
Some(Cmd::Pair { invite, remove }) => run_pair(invite, remove, cli.json),
Some(Cmd::Use { target }) => run_use(target, cli.json),
Some(Cmd::Join {
org_invite,
name,
user_id,
label,
}) => enrollcmd::run_join(org_invite, name, user_id, label, cli.json),
Some(Cmd::Org {
command:
OrgCmd::Create {
name,
expires,
roster_url,
},
}) => enrollcmd::run_org_create(name, expires, roster_url, cli.json),
Some(Cmd::Org {
command:
OrgCmd::Approve {
join_code,
groups,
user_id,
},
}) => enrollcmd::run_org_approve(join_code, groups, user_id, cli.json),
Some(Cmd::Org {
command: OrgCmd::Revoke { target, user_key },
}) => enrollcmd::run_org_revoke(target, user_key, cli.json),
Some(Cmd::Devices {
command: DevicesCmd::Code { label },
}) => enrollcmd::run_devices_code(label, cli.json),
Some(Cmd::Devices {
command: DevicesCmd::Add { device_code },
}) => enrollcmd::run_devices_add(device_code, cli.json),
Some(Cmd::Internal {
command:
Internal::Peer {
command:
PeerCmd::Add {
nickname,
endpoint_id,
allow,
},
},
}) => run_peer_add(nickname, endpoint_id, allow, cli.json),
Some(Cmd::Internal {
command:
Internal::Peer {
command: PeerCmd::State { peer, json },
},
}) => run_peer_state(peer, json || cli.json),
Some(Cmd::Internal {
command:
Internal::Roster {
command: RosterCmd::Install { file, org_root_pk },
},
}) => run_roster_install(file, org_root_pk, cli.json),
Some(Cmd::Internal {
command: Internal::Blob { command },
}) => run_internal_blob(command, cli.json),
Some(Cmd::Internal {
command: Internal::Audit { command },
}) => run_internal_audit(command, cli.json),
Some(Cmd::Internal {
command: Internal::Watch,
}) => run_watch(cli.json),
Some(Cmd::Completions { shell }) => {
use clap::CommandFactory;
clap_complete::generate(
shell,
&mut Cli::command(),
"mcpmesh",
&mut std::io::stdout(),
);
Ok(())
}
Some(Cmd::Internal {
command: Internal::Man { dir },
}) => run_internal_man(dir),
Some(Cmd::Doctor) => doctor::run_doctor(cli.json),
Some(Cmd::Up { timeout }) => run_up(timeout, cli.json),
Some(Cmd::Status) | None => run_status(cli.json),
}
}
fn run_up(timeout: Option<u64>, json: bool) -> anyhow::Result<()> {
let launch = mcpmesh::client::DaemonLaunch::ambient()?;
let ready = timeout
.map(std::time::Duration::from_secs)
.unwrap_or(mcpmesh::client::DEFAULT_READY_TIMEOUT);
let socket = launch.socket.clone();
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
rt.block_on(async move {
let _client = mcpmesh::client::ensure_daemon_with_timeout(&launch, ready).await?;
Ok::<(), anyhow::Error>(())
})?;
if json {
println!("{}", mcpmesh::json::up_json(&socket));
} else {
println!("{}", socket.display());
}
Ok(())
}
fn run_serve(
name: String,
allow: Option<String>,
cmd: Vec<String>,
json: bool,
) -> anyhow::Result<()> {
let allow = split_csv(allow);
with_daemon(async move |mut client| {
client
.register_service(
&name,
BackendSpec::Run {
cmd,
env: Default::default(),
cwd: None,
},
allow,
)
.await?;
if json {
println!("{}", mcpmesh::json::serve_json(&name));
return Ok(());
}
println!("serving '{name}'");
println!(
"Next: run `mcpmesh invite {name}` to mint a one-time invite, and send it to the \
person you want to share it with."
);
Ok(())
})
}
fn run_connect(target: String) -> anyhow::Result<()> {
let (peer, service) = proxy::split_target(&target)?;
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
rt.block_on(proxy::run(peer, service))
}
fn run_invite(services: Vec<String>, label: Option<String>, json: bool) -> anyhow::Result<()> {
if services.is_empty() {
anyhow::bail!("specify at least one service to grant (e.g. `mcpmesh invite notes`)");
}
with_daemon(async move |mut client| {
let invite = client.invite_with(services.clone(), label).await?;
if json {
println!("{}", mcpmesh::json::invite_json(&invite, &services));
return Ok(());
}
for line in render::invite_lines(&invite, &services, util::epoch_now_u64()) {
println!("{line}");
}
Ok(())
})
}
fn run_pair(invite: Option<String>, remove: Option<String>, json: bool) -> anyhow::Result<()> {
match (invite, remove) {
(Some(_), Some(_)) => {
anyhow::bail!("provide an invite to redeem OR --remove <nickname>, not both")
}
(None, None) => {
anyhow::bail!("provide an invite to redeem, or --remove <nickname> to unpair")
}
(Some(invite_line), None) => with_daemon(async move |mut client| {
let paired = client.pair(&invite_line).await?;
if json {
println!("{}", mcpmesh::json::pair_json(&paired));
return Ok(());
}
for line in render::pair_lines(&paired) {
println!("{line}");
}
Ok(())
}),
(None, Some(nickname)) => with_daemon(async move |mut client| {
client.peer_remove(&nickname).await?;
if json {
println!("{}", mcpmesh::json::unpair_json(&nickname));
} else {
println!("Unpaired {nickname}.");
}
Ok(())
}),
}
}
fn run_use(target: String, json: bool) -> anyhow::Result<()> {
let (peer, service) = proxy::split_target(&target)?;
with_daemon(async move |mut client| {
let status = client.status().await?;
if let Some(message) = render::use_target_error(&peer, &service, &status.peers) {
anyhow::bail!("{message}");
}
if json {
println!("{}", mcpmesh::json::use_json(&peer, &[service]));
return Ok(());
}
for line in proxy::client_instruction_lines(&peer, &[service]) {
println!("{line}");
}
Ok(())
})
}
fn run_peer_add(
nickname: String,
endpoint_id: String,
allow: Option<String>,
json: bool,
) -> anyhow::Result<()> {
let allow = split_csv(allow);
with_daemon(async move |mut client| {
client
.request(mcpmesh::Request::PeerAdd(PeerAddParams {
nickname: nickname.clone(),
endpoint_id,
allow,
}))
.await?;
if json {
println!("{}", serde_json::json!({"peer": nickname, "added": true}));
} else {
println!("added peer '{nickname}'");
}
Ok(())
})
}
fn run_peer_state(peer: String, json: bool) -> anyhow::Result<()> {
with_daemon(async move |mut client| {
let v = client
.request(mcpmesh::Request::PeerDiagnostics(
mcpmesh_local_api::PeerDiagnosticsParams { peer: peer.clone() },
))
.await?;
if json {
println!("{v}");
return Ok(());
}
let d: mcpmesh_local_api::PeerDiagnosticsResult = serde_json::from_value(v)?;
println!("peer: {} ({})", d.nickname, d.principal);
if let Some(u) = &d.user_id {
println!("user: {u}");
}
println!(
"paired_at: {}",
d.paired_at.as_deref().unwrap_or("(not recorded)")
);
match (&d.last_addr, d.hint_usable) {
(None, _) => println!(
"dial hint: (none) — this node dials by id alone, exactly as a freshly paired \
identity does"
),
(Some(_), false) => println!(
"dial hint: PRESENT BUT UNUSABLE — it does not parse, or its embedded id is not \
this peer, so every dial silently discards it and falls back to id-only"
),
(Some(_), true) => {
println!("dial hint: {}", d.hint_addrs.join(", "));
if !d.hint_addrs.iter().any(|a| !a.starts_with("relay ")) {
println!(
" RELAY-ONLY — this hint can never punch. It is what an invite \
minted while only the relay path was up leaves behind."
);
}
println!(
" merged with discovery as an extra candidate — but iroh SKIPS \
that lookup while a path is already selected, so on a pair holding an open \
relayed connection this hint is the only addressing a dial contributes."
);
}
}
match &d.reachability {
None => println!(
"live: never probed (this is a cache read — it does not dial, so a fresh \
daemon reports this until something else probes)"
),
Some(r) => {
let path = match &r.path {
mcpmesh_local_api::PeerPath::Direct => "direct".to_string(),
mcpmesh_local_api::PeerPath::Relay { url } => match url {
Some(u) => format!("relay ({u})"),
None => "relay".to_string(),
},
_ => "unknown".to_string(),
};
println!(
"live: {} via {path}{}",
if r.reachable {
"reachable"
} else {
"UNREACHABLE"
},
match r.rtt_ms {
Some(ms) => format!(", {ms} ms"),
None => String::new(),
}
);
}
}
Ok(())
})
}
fn run_roster_install(
file: PathBuf,
org_root_pk: Option<String>,
json: bool,
) -> anyhow::Result<()> {
let path = file.to_string_lossy().into_owned();
with_daemon(async move |mut client| {
let installed = client.roster_install(&path, org_root_pk).await?;
if json {
println!("{}", serde_json::to_value(&installed)?);
} else {
println!("{}", render::roster_install_line(&installed));
}
Ok(())
})
}
fn run_internal_blob(command: BlobCmd, json: bool) -> anyhow::Result<()> {
with_daemon(async move |mut client| {
match command {
BlobCmd::Publish { scope, file } => {
let path = file.to_string_lossy().into_owned();
let r = client.blob_publish(&scope, &path).await?;
if json {
println!(
"{}",
serde_json::json!({"hash": r.hash, "ticket": r.ticket})
);
} else {
println!("Published (hash {}).", r.hash);
println!("{}", r.ticket);
}
}
BlobCmd::Grant { scope, principal } => {
client.blob_grant(&scope, &principal).await?;
if json {
println!(
"{}",
serde_json::json!({"scope": scope, "principal": principal, "granted": true})
);
} else {
println!("Granted scope '{scope}' to '{principal}'.");
}
}
BlobCmd::List {
limit,
offset,
counts_only,
scope,
} => {
let r = client
.blob_list_paged(mcpmesh_local_api::BlobListParams {
scope,
hash: None,
limit,
offset,
counts_only,
})
.await?;
if json {
println!("{}", serde_json::to_value(&r)?);
} else {
let shown = r.scopes.len();
for s in r.scopes {
let grants: Vec<String> = s
.grants
.iter()
.map(|g| {
if g.starts_with("eid:") || g.starts_with("b64u:") {
"a paired peer".to_owned()
} else {
g.clone()
}
})
.collect();
println!(
"{}: {} blob(s), granted to [{}]",
s.name,
s.hash_count,
grants.join(", ")
);
}
if r.truncated {
println!(
"\n… showing {} of {} scope(s). Use --offset to page, or --limit to \
raise the cap.",
shown, r.total
);
}
}
}
BlobCmd::Fetch { ticket, dest } => {
let dest_path = dest.to_string_lossy().into_owned();
let r = client.blob_fetch(&ticket, &dest_path).await?;
if json {
println!(
"{}",
serde_json::json!({
"bytes_len": r.bytes_len,
"hash": r.hash,
"dest": dest.display().to_string(),
})
);
} else {
println!(
"Fetched {} bytes (hash {}) → {}",
r.bytes_len,
r.hash,
dest.display()
);
}
}
}
Ok(())
})
}
fn run_internal_audit(command: AuditCmd, json: bool) -> anyhow::Result<()> {
use mcpmesh::audit;
let dir = paths::default_audit_dir()?;
match command {
AuditCmd::Tail { lines, kind, peer } => {
let kind_filter = match kind.as_deref() {
Some(s) => {
Some(audit::parse_kind(s).with_context(|| format!("unknown --kind '{s}'"))?)
}
None => None,
};
let all = audit::read_all_records(&dir)?;
let filtered = audit::filter_records(&all, kind_filter, peer.as_deref());
let start = filtered.len().saturating_sub(lines);
for rec in &filtered[start..] {
println!("{}", serde_json::to_string(rec)?);
}
}
AuditCmd::List => {
if json {
let months: Vec<serde_json::Value> = audit::list_month_files(&dir)?
.into_iter()
.map(|(month, _, size)| serde_json::json!({"month": month, "bytes": size}))
.collect();
println!("{}", serde_json::Value::Array(months));
} else {
for (month, _, size) in audit::list_month_files(&dir)? {
println!("{month} {size} bytes");
}
}
}
AuditCmd::Prune { before } => {
anyhow::ensure!(
audit::valid_month_key(&before),
"--before must be a zero-padded YYYY-MM month key, got '{before}'"
);
let deleted = audit::prune_before(&dir, &before)?;
if json {
println!("{}", serde_json::json!({"pruned": deleted}));
} else if deleted.is_empty() {
println!("Nothing to prune before {before}.");
} else {
println!("Pruned {} month(s): {}.", deleted.len(), deleted.join(", "));
}
}
}
Ok(())
}
fn run_status(json: bool) -> anyhow::Result<()> {
let fingerprint = load_device_key()?.fingerprint();
let has_roster_url = paths::default_config_path()
.ok()
.and_then(|p| config::Config::load(&p).ok())
.map(|c| c.roster.url.is_some())
.unwrap_or(false);
with_daemon(async move |mut client| {
let hello = client.hello().clone();
let status = client.status().await?;
if json {
println!(
"{}",
mcpmesh::json::status_json(&fingerprint, &hello, &status)
);
} else {
render::render_status(&fingerprint, &hello, &status, has_roster_url);
}
Ok(())
})
}
fn run_watch(json: bool) -> anyhow::Result<()> {
with_daemon(async move |client| {
let mut stream = client.subscribe().await?;
if !json {
println!("watching the mesh — Ctrl-C to stop");
}
while let Some(frame) = stream.next().await? {
if json {
println!("{}", serde_json::to_string(&frame)?);
} else {
println!("{}", render::render_frame(&frame));
}
}
Ok(())
})
}
fn run_internal_man(dir: PathBuf) -> anyhow::Result<()> {
use clap::CommandFactory;
std::fs::create_dir_all(&dir)?;
let mut count = 0usize;
write_man_tree(&dir, &Cli::command(), "mcpmesh", &mut count)?;
println!("wrote {count} man pages to {}", dir.display());
Ok(())
}
fn write_man_tree(
dir: &std::path::Path,
cmd: &clap::Command,
stem: &str,
count: &mut usize,
) -> anyhow::Result<()> {
let mut buf = Vec::new();
clap_mangen::Man::new(cmd.clone().name(stem.to_string())).render(&mut buf)?;
std::fs::write(dir.join(format!("{stem}.1")), buf)?;
*count += 1;
for sub in cmd
.get_subcommands()
.filter(|s| !s.is_hide_set() && s.get_name() != "help")
{
write_man_tree(dir, sub, &format!("{stem}-{}", sub.get_name()), count)?;
}
Ok(())
}
fn run_internal_id() -> anyhow::Result<()> {
let key = load_device_key()?;
let endpoint_id = mcpmesh_net::iroh::SecretKey::from_bytes(&key.secret_bytes()).public();
println!("{endpoint_id}");
Ok(())
}
#[cfg(test)]
mod tests {
use mcpmesh::roster;
#[test]
fn org_invite_carries_and_round_trips_the_roster_url() {
let url = "https://intranet.acme.com/roster.json";
let code = roster::enroll::OrgInviteCode {
org_id: "acme".into(),
org_root_pk: "b64u:AAAA".into(),
roster_url: Some(url.to_string()),
};
let decoded = roster::enroll::OrgInviteCode::decode(&code.encode()).unwrap();
assert_eq!(decoded.roster_url.as_deref(), Some(url));
assert_eq!(decoded.org_id, "acme");
let bare = roster::enroll::OrgInviteCode {
org_id: "acme".into(),
org_root_pk: "b64u:AAAA".into(),
roster_url: None,
};
assert!(
roster::enroll::OrgInviteCode::decode(&bare.encode())
.unwrap()
.roster_url
.is_none()
);
}
}