use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use clap::{Args, Subcommand};
use net_mcp::serve::{MeshGateway, Shim, MSG_NO_DAEMON};
use net_sdk::consent::{CapabilityId, ConsentPolicy};
use net_sdk::pins::{PinState, PinStore};
use serde::Serialize;
use tokio::io::BufReader;
use crate::commands::aggregator::RemoteAttachArgs;
use crate::context::{
build_attached_mesh, load_operator_identity, require_remote_attach, resolve_profile,
};
use crate::error::{generic, invalid_args, CliError};
use crate::prelude::{emit_value, OutputFormat};
#[derive(Subcommand, Debug)]
pub enum McpCommand {
Serve(ServeArgs),
#[command(subcommand)]
Pin(PinCommand),
}
#[derive(Subcommand, Debug)]
pub enum PinCommand {
Approve(PinIdArgs),
Reject(PinIdArgs),
List(PinListArgs),
}
#[derive(Args, Debug)]
pub struct PinIdArgs {
pub cap_id: String,
#[arg(long = "pin-store", value_name = "PATH")]
pub pin_store: Option<PathBuf>,
}
#[derive(Args, Debug)]
pub struct PinListArgs {
#[arg(long = "pin-store", value_name = "PATH")]
pub pin_store: Option<PathBuf>,
}
#[derive(Args, Debug)]
pub struct ServeArgs {
#[arg(long)]
pub identity: Option<PathBuf>,
#[arg(long = "allow-capability", value_name = "PROVIDER/CAP")]
pub allow_capability: Vec<String>,
#[arg(long = "pin-store", value_name = "PATH")]
pub pin_store: Option<PathBuf>,
#[arg(long = "trust-equivalent-providers")]
pub trust_equivalent_providers: bool,
#[command(flatten)]
pub remote: RemoteAttachArgs,
}
pub async fn run(
cmd: McpCommand,
output: Option<OutputFormat>,
config_path: Option<&Path>,
profile_name: &str,
) -> Result<(), CliError> {
match cmd {
McpCommand::Serve(args) => run_serve(args, config_path, profile_name).await,
McpCommand::Pin(cmd) => run_pin(cmd, output).await,
}
}
async fn run_serve(
args: ServeArgs,
config_path: Option<&Path>,
profile_name: &str,
) -> Result<(), CliError> {
let profile = resolve_profile(config_path, profile_name).await?;
let remote = require_remote_attach(&profile, &args.remote, || generic(MSG_NO_DAEMON))?;
let identity_path = args
.identity
.as_deref()
.or(profile.identity.as_deref())
.ok_or_else(|| {
invalid_args(
"net-mesh mcp serve needs an operator identity: pass --identity <PATH> or set \
`identity = \"...\"` in your profile. Wrapped tools admit callers by origin, \
so use the same identity as your `net-mesh wrap` side (or have it `--allow` this \
shim's origin).",
)
})?;
let identity = load_operator_identity(identity_path).await?;
let mesh = build_attached_mesh("0.0.0.0:0", Some(identity), &remote).await?;
let mesh = Arc::new(mesh);
let mut consent = ConsentPolicy::new();
for raw in &args.allow_capability {
let id = CapabilityId::parse(raw)
.map_err(|e| invalid_args(format!("--allow-capability {raw:?}: {e}")))?;
consent.allow(id);
}
let gateway = MeshGateway::new(Arc::clone(&mesh))
.trust_equivalent_providers(args.trust_equivalent_providers);
let shim = Shim::new(gateway)
.with_consent(consent)
.with_pin_store(resolve_pin_store(args.pin_store.as_deref())?);
let reader = BufReader::new(tokio::io::stdin());
let writer = tokio::io::stdout();
let serve_result = tokio::select! {
r = shim.serve(reader, writer) => r,
_ = tokio::signal::ctrl_c() => Ok(()),
};
if let Ok(mesh) = Arc::try_unwrap(mesh) {
mesh.shutdown().await.ok();
}
serve_result.map_err(|e| generic(format!("mcp serve loop: {e}")))?;
Ok(())
}
fn resolve_pin_store(override_: Option<&Path>) -> Result<PathBuf, CliError> {
if let Some(p) = override_ {
return Ok(p.to_path_buf());
}
net_sdk::pins::default_pin_store_path().ok_or_else(|| {
generic(
"could not determine a per-user data directory for the pin store; \
pass --pin-store <PATH>",
)
})
}
#[derive(Serialize)]
struct PinMutation {
cap_id: String,
action: &'static str,
changed: bool,
store: String,
}
#[derive(Serialize)]
struct PinRow {
cap_id: String,
state: &'static str,
}
async fn run_pin(cmd: PinCommand, output: Option<OutputFormat>) -> Result<(), CliError> {
match cmd {
PinCommand::Approve(args) => pin_mutate(args, output, "approved").await,
PinCommand::Reject(args) => pin_mutate(args, output, "rejected").await,
PinCommand::List(args) => pin_list(args, output).await,
}
}
async fn pin_mutate(
args: PinIdArgs,
output: Option<OutputFormat>,
action: &'static str,
) -> Result<(), CliError> {
let id = CapabilityId::parse(&args.cap_id)
.map_err(|e| invalid_args(format!("cap id {:?}: {e}", args.cap_id)))?;
let path = resolve_pin_store(args.pin_store.as_deref())?;
let changed = PinStore::mutate(path.clone(), |store| match action {
"approved" => store.approve(&id),
_ => store.remove(&id),
})
.await
.map_err(|e| generic(format!("update pin store: {e}")))?;
let row = PinMutation {
cap_id: id.display(),
action,
changed,
store: path.display().to_string(),
};
emit_value(OutputFormat::resolve_oneshot(output), &row)
.map_err(|e| generic(format!("write output: {e}")))?;
Ok(())
}
async fn pin_list(args: PinListArgs, output: Option<OutputFormat>) -> Result<(), CliError> {
let path = resolve_pin_store(args.pin_store.as_deref())?;
let store = PinStore::load(&path)
.await
.map_err(|e| generic(format!("load pin store: {e}")))?;
let rows: Vec<PinRow> = store
.list()
.into_iter()
.map(|(id, state)| PinRow {
cap_id: id.display(),
state: match state {
PinState::Approved => "approved",
PinState::Pending => "pending",
},
})
.collect();
emit_value(OutputFormat::resolve_oneshot(output), &rows)
.map_err(|e| generic(format!("write output: {e}")))?;
Ok(())
}