#![cfg(unix)]
use std::process::Stdio;
use std::sync::Arc;
use std::time::Duration;
use iroh::address_lookup::MemoryLookup;
use mcpmesh::allowlist::{AllowlistGate, PeerEntry, PeerStore};
use mcpmesh::client::connect_control;
use mcpmesh::config::Config;
use mcpmesh::daemon::{self, STACK_VERSION, build_services};
use mcpmesh_net::framing::{FrameReader, Inbound, write_frame};
use mcpmesh_net::{TrustGate, connect, serve};
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 local_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 localhost endpoint")
}
#[tokio::test(flavor = "multi_thread")]
async fn hero_flow_minus_pairing() {
timeout(Duration::from_secs(30), async {
let dir = tempfile::tempdir().unwrap();
let alice_ep = local_endpoint().await;
let alice_id = *alice_ep.id().as_bytes();
let alice_addr = alice_ep.addr();
let bob_ep = local_endpoint().await;
let bob_id = *bob_ep.id().as_bytes();
let bob_principal = format!("eid:{}", bob_ep.id());
let alice_cfg = Config::from_toml_str(&format!(
"[services.files]\nrun = ['{STUB}']\nallow = [\"{bob_principal}\"]\n"
))
.expect("parse alice config");
let alice_store = PeerStore::open(&dir.path().join("alice_state.redb")).unwrap();
alice_store
.add(PeerEntry {
endpoint_id: bob_id,
nickname: "bob".into(),
services: vec!["files".into()],
paired_at: None,
user_id: None,
last_addr: None,
})
.unwrap();
let alice_gate: Arc<dyn TrustGate> = Arc::new(AllowlistGate::new(Arc::new(alice_store)));
let _alice_handle = serve(alice_ep, alice_gate, build_services(&alice_cfg), Arc::new(mcpmesh_net::ConnRegistry::new()));
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(&dir.path().join("bob_state.redb")).unwrap());
bob_store
.add(PeerEntry {
endpoint_id: alice_id,
nickname: "alice".into(),
services: vec!["files".into()],
paired_at: None,
user_id: None,
last_addr: None,
})
.unwrap();
let socket = dir.path().join("mcpmesh").join("mcpmesh.sock");
let listener = mcpmesh::ipc::bind_control_socket(&socket).await.unwrap();
let bob_state = daemon::serving_state(bob_ep, bob_store);
let control = tokio::spawn(mcpmesh::control::serve_control(listener, bob_state));
let mut child = Command::new(MCPMESH)
.arg("connect")
.arg("alice/files")
.env("XDG_RUNTIME_DIR", dir.path())
.env("XDG_CONFIG_HOME", dir.path())
.env("XDG_DATA_HOME", dir.path())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.expect("spawn mcpmesh connect");
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",
"Alice's served child answered initialize back through the proxy: {init}"
);
write_frame(
&mut child_in,
&json!({
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "echo", "arguments": {"text": "hero flow, minus pairing"}}
}),
)
.await
.unwrap();
let call = next_frame(&mut child_out).await;
assert_eq!(
call["result"]["content"][0]["text"], "hero flow, minus pairing",
"the echoed tools/call payload round-tripped byte-faithfully: {call}"
);
assert_eq!(
call["result"]["peer_name"], "bob",
"the served child saw the gate-resolved caller identity (bob) across the mesh: {call}"
);
child_in.shutdown().await.unwrap();
drop(child_in);
let status = timeout(Duration::from_secs(10), child.wait())
.await
.expect("proxy did not exit after stdin close")
.unwrap();
assert!(status.success(), "proxy exited non-zero: {status}");
let stranger_ep = local_endpoint().await;
match connect(&stranger_ep, alice_addr, "files").await.map(|(t, _)| t) {
Err(_) => {}
Ok(mut transport) => {
write_frame_value(
&mut transport,
json!({"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"_meta": {"mcpmesh/service": "files"}, "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:?}"
);
}
}
let mut raw = connect_control(&socket).await.expect("raw connect_control");
assert_eq!(raw.hello().api, "mcpmesh-local/1", "api identified at connect");
assert!(
!raw.hello().api_version.is_empty(),
"a non-empty api version is delivered at connect: {:?}",
raw.hello()
);
let result = raw
.request(mcpmesh::Request::Status)
.await
.expect("status request");
let s: mcpmesh::StatusResult =
serde_json::from_value(result).expect("StatusResult deserializes");
assert_eq!(s.stack_version, STACK_VERSION);
control.abort();
})
.await
.expect("hero-flow-minus-pairing 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:?}"),
}
}
async fn write_frame_value(transport: &mut mcpmesh_net::SessionTransport, v: Value) {
let _ = transport.send_value(v).await;
}