#![cfg(unix)]
use std::process::Stdio;
use std::sync::Arc;
use std::time::Duration;
use iroh::address_lookup::MemoryLookup;
use mcpmesh::Request;
use mcpmesh::allowlist::{AllowlistGate, PeerStore};
use mcpmesh::client::connect_control;
use mcpmesh::config::Config;
use mcpmesh::control::{DaemonState, serve_control};
use mcpmesh::daemon::{self, MeshState, build_services, spawn_accept_loop};
use mcpmesh::pairing::sas::short_auth_code;
use mcpmesh::pairing::{Invite, LiveInvites};
use mcpmesh::roster::gate::RosterGate;
use mcpmesh_local_api::{InviteParams, InviteResult, PairParams, PairResult, StatusResult};
use mcpmesh_net::framing::{FrameReader, Inbound, write_frame};
use mcpmesh_net::registry::ConnRegistry;
use mcpmesh_net::{TrustGate, connect};
use serde_json::{Value, json};
use tokio::io::{AsyncWriteExt, BufReader};
use tokio::process::Command;
use tokio::time::timeout;
const STUB: &str = env!("CARGO_BIN_EXE_echo_mcp_stub");
const MCPMESH: &str = env!("CARGO_BIN_EXE_mcpmesh");
const MAX_FRAME: usize = 16 * 1024 * 1024;
async fn dual_alpn_endpoint() -> iroh::Endpoint {
iroh::Endpoint::builder(iroh::endpoint::presets::Minimal)
.relay_mode(iroh::RelayMode::Disabled)
.alpns(vec![
mcpmesh_net::ALPN_MCP.to_vec(),
mcpmesh_net::ALPN_PAIR.to_vec(),
])
.bind()
.await
.expect("bind dual-ALPN endpoint")
}
async fn mesh_endpoint() -> iroh::Endpoint {
iroh::Endpoint::builder(iroh::endpoint::presets::Minimal)
.relay_mode(iroh::RelayMode::Disabled)
.alpns(vec![mcpmesh_net::ALPN_MCP.to_vec()])
.bind()
.await
.expect("bind mesh endpoint")
}
#[tokio::test(flavor = "multi_thread")]
async fn four_command_hero_flow() {
timeout(Duration::from_secs(90), async {
let alice_dir = tempfile::tempdir().unwrap();
let bob_dir = tempfile::tempdir().unwrap();
let alice_ep = dual_alpn_endpoint().await;
let alice_id = *alice_ep.id().as_bytes();
let alice_addr = alice_ep.addr();
let alice_config = alice_dir.path().join("config.toml");
std::fs::write(
&alice_config,
format!("[services.notes]\nrun = ['{STUB}']\nallow = []\n"),
)
.unwrap();
let alice_store = Arc::new(PeerStore::open(&alice_dir.path().join("state.redb")).unwrap());
let alice_gate: Arc<dyn TrustGate> = Arc::new(AllowlistGate::new(alice_store.clone()));
let alice_cfg = Config::load(&alice_config).unwrap();
let alice_mesh = MeshState::new(
alice_ep,
alice_gate,
alice_store.clone(),
Arc::new(LiveInvites::new()),
"alice".into(),
alice_config.clone(),
Arc::new(RosterGate::empty()),
Arc::new(ConnRegistry::new()),
None,
None,
None,
None,
);
let alice_task = spawn_accept_loop(alice_mesh.clone(), Arc::new(build_services(&alice_cfg)));
alice_mesh.set_accept_task(alice_task).await;
let alice_socket = alice_dir.path().join("control.sock");
let alice_listener = mcpmesh::ipc::bind_control_socket(&alice_socket).await.unwrap();
let alice_state = Arc::new(DaemonState::with_mesh(daemon::STACK_VERSION, alice_mesh));
let alice_control = tokio::spawn(serve_control(alice_listener, alice_state));
let bob_ep = mesh_endpoint().await;
let bob_id = *bob_ep.id().as_bytes();
let bob_eid = format!("eid:{}", bob_ep.id());
let mem = MemoryLookup::new();
mem.add_endpoint_info(alice_addr.clone());
bob_ep
.address_lookup()
.expect("address lookup services")
.add(mem);
let bob_store = Arc::new(PeerStore::open(&bob_dir.path().join("state.redb")).unwrap());
let bob_gate: Arc<dyn TrustGate> = Arc::new(AllowlistGate::new(bob_store.clone()));
let bob_mesh = MeshState::new(
bob_ep,
bob_gate,
bob_store.clone(),
Arc::new(LiveInvites::new()),
"bob".into(),
bob_dir.path().join("config.toml"),
Arc::new(RosterGate::empty()),
Arc::new(ConnRegistry::new()),
None,
None,
None,
None,
);
let bob_socket = bob_dir.path().join("mcpmesh").join("mcpmesh.sock");
let bob_listener = mcpmesh::ipc::bind_control_socket(&bob_socket).await.unwrap();
let bob_state = Arc::new(DaemonState::with_mesh(daemon::STACK_VERSION, bob_mesh));
let bob_control = tokio::spawn(serve_control(bob_listener, bob_state));
let mut alice_client = connect_control(&alice_socket)
.await
.expect("raw connect_control to Alice");
assert_eq!(
alice_client.hello().api,
"mcpmesh-local/1",
"the non-porcelain client is told the api at connect"
);
assert!(
!alice_client.hello().api_version.is_empty(),
"a non-empty api version is delivered at connect: {:?}",
alice_client.hello()
);
let status = alice_client
.request(Request::Status)
.await
.expect("status over mcpmesh-local/1");
assert_eq!(status["stack_version"], daemon::STACK_VERSION);
let invite_value = alice_client
.request(Request::Invite(InviteParams {
services: vec!["notes".into()],
app_label: None,
max_uses: None,
}))
.await
.expect("invite over mcpmesh-local/1");
let invite: InviteResult =
serde_json::from_value(invite_value).expect("typed InviteResult decodes");
assert!(
invite.invite_line.starts_with("mcpmesh-invite:"),
"the invite line is the §1.5 surface-#2 copyable artifact: {}",
invite.invite_line
);
let mut bob_client = connect_control(&bob_socket)
.await
.expect("raw connect_control to Bob");
assert_eq!(bob_client.hello().api, "mcpmesh-local/1");
let pair_value = bob_client
.request(Request::Pair(PairParams {
invite_line: invite.invite_line.clone(),
}))
.await
.expect("pair over mcpmesh-local/1");
let paired: PairResult =
serde_json::from_value(pair_value).expect("typed PairResult decodes");
assert_eq!(paired.peer_nickname, "alice", "Bob's local name for the inviter");
assert_eq!(
paired.services,
vec!["notes".to_string()],
"the pairing granted Bob `notes` (mountable as alice/notes)"
);
let decoded = Invite::decode(&invite.invite_line).unwrap();
assert_eq!(decoded.inviter_id, alice_id, "the invite names Alice's id");
let alice_sas = short_auth_code(&decoded.inviter_id, &bob_id, &decoded.secret);
assert_eq!(
paired.sas_code, alice_sas,
"Bob's SAS equals the code Alice computes"
);
assert_eq!(
short_auth_code(&bob_id, &decoded.inviter_id, &decoded.secret),
alice_sas,
"the SAS is endpoint-order-independent (both sides read the same words)"
);
let status_after = alice_client
.request(Request::Status)
.await
.expect("status over mcpmesh-local/1 after pairing");
let status_after: StatusResult =
serde_json::from_value(status_after).expect("typed StatusResult decodes");
let recent = status_after
.recent_pairings
.first()
.expect("the inviter's status must surface the completed pairing");
assert_eq!(recent.peer_nickname, "bob", "the pairing is listed under Bob's nickname");
assert_eq!(
recent.sas_code, paired.sas_code,
"the inviter's status shows the SAME code the redeemer's pair printed"
);
let bob_side = bob_store
.resolve(&alice_id)
.unwrap()
.expect("Bob's store has an alice entry");
assert_eq!(bob_side.nickname, "alice");
assert_eq!(bob_side.services, vec!["notes".to_string()]);
assert!(bob_side.paired_at.is_some());
let alice_side = alice_store
.resolve(&bob_id)
.unwrap()
.expect("Alice's store has a bob dial-back entry");
assert_eq!(alice_side.nickname, "bob");
assert!(
alice_side.services.is_empty(),
"Alice's dial-back entry carries no service grants (§4.2): {:?}",
alice_side.services
);
assert!(alice_side.paired_at.is_some());
let after = Config::load(&alice_config).unwrap();
assert_eq!(
after.services.get("notes").unwrap().allow,
vec![bob_eid.clone()],
"the pairing grant appended Bob's eid: principal to [services.notes].allow"
);
drop(bob_client);
let mut child = Command::new(MCPMESH)
.arg("connect")
.arg("alice/notes")
.env("XDG_RUNTIME_DIR", bob_dir.path())
.env("XDG_CONFIG_HOME", bob_dir.path())
.env("XDG_DATA_HOME", bob_dir.path())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.expect("spawn mcpmesh connect alice/notes");
let mut child_in = child.stdin.take().unwrap();
let mut child_out =
FrameReader::new(BufReader::new(child.stdout.take().unwrap()), MAX_FRAME);
write_frame(
&mut child_in,
&json!({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": "2025-11-25", "capabilities": {},
"clientInfo": {"name": "ai", "version": "0"}}
}),
)
.await
.unwrap();
let init = next_frame(&mut child_out).await;
assert_eq!(
init["result"]["serverInfo"]["name"], "echo-stub",
"the paired peer reached Alice's served notes child (not -32054'd): {init}"
);
write_frame(
&mut child_in,
&json!({
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "echo", "arguments": {"text": "four-command hero flow"}}
}),
)
.await
.unwrap();
let call = next_frame(&mut child_out).await;
assert_eq!(
call["result"]["content"][0]["text"], "four-command hero flow",
"the echoed tools/call payload round-tripped byte-faithfully: {call}"
);
assert_eq!(
call["result"]["peer_name"], "bob",
"the served child saw the paired identity `bob` across the mesh: {call}"
);
child_in.shutdown().await.unwrap();
drop(child_in);
let exit = timeout(Duration::from_secs(10), child.wait())
.await
.expect("proxy did not exit after stdin close")
.unwrap();
assert!(exit.success(), "proxy exited non-zero: {exit}");
let stranger_ep = mesh_endpoint().await;
match connect(&stranger_ep, alice_addr.clone(), "notes").await.map(|(t, _)| t) {
Err(_) => {}
Ok(mut transport) => {
let _ = transport
.send_value(json!({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"_meta": {"mcpmesh/service": "notes"}, "capabilities": {}}
}))
.await;
let outcome = transport.recv_value().await;
assert!(
matches!(outcome, Err(_) | Ok(None)),
"unpaired stranger got an MCP frame — the gate did not refuse pre-MCP: {outcome:?}"
);
}
}
alice_control.abort();
bob_control.abort();
drop(alice_dir);
drop(bob_dir);
})
.await
.expect("four-command hero-flow test timed out");
}
async fn next_frame<R: tokio::io::AsyncRead + Unpin>(reader: &mut FrameReader<R>) -> Value {
match reader.next().await.unwrap() {
Some(Inbound::Frame(v)) => v,
other => panic!("expected a frame, got {other:?}"),
}
}