use std::sync::Arc;
use std::time::Duration;
use mcpmesh::allowlist::{AllowlistGate, PeerEntry, PeerStore};
use mcpmesh::config::Config;
use mcpmesh::control::{DaemonState, serve_control_io};
use mcpmesh::daemon::{MeshState, STACK_VERSION, build_services, spawn_accept_loop};
use mcpmesh::pairing::LiveInvites;
use mcpmesh::roster::gate::RosterGate;
use mcpmesh_local_api::{BackendSpec, connect_control_io};
use mcpmesh_net::{SessionTransport, TrustGate, connect, serve};
use serde_json::{Value, json};
use tokio::time::timeout;
const STUB: &str = env!("CARGO_BIN_EXE_echo_mcp_stub");
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]
async fn daemon_serves_run_service_and_injects_caller_identity_over_the_mesh() {
timeout(Duration::from_secs(60), async {
let dir = tempfile::tempdir().unwrap();
let client = local_endpoint().await;
let client_id = *client.id().as_bytes();
let client_eid = format!("eid:{}", client.id());
let cfg = Config::from_toml_str(&format!(
"[services.echo]\nrun = ['{STUB}']\nallow = [\"{client_eid}\"]\n"
))
.expect("parse config");
let store = PeerStore::open(&dir.path().join("state.redb")).unwrap();
store
.add(PeerEntry {
endpoint_id: client_id,
nickname: "tester".into(),
services: vec!["echo".into()],
paired_at: None,
user_id: None,
last_addr: None,
})
.unwrap();
let gate: Arc<dyn TrustGate> = Arc::new(AllowlistGate::new(Arc::new(store)));
let server = local_endpoint().await;
let addr = server.addr();
let _handle = serve(
server,
gate,
build_services(&cfg),
Arc::new(mcpmesh_net::ConnRegistry::new()),
);
let mut transport = connect(&client, addr, "echo").await.unwrap().0;
transport
.send_value(json!({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"_meta": {"mcpmesh/service": "echo"},
"capabilities": {}, "clientInfo": {"name": "tester", "version": "0"}
}
}))
.await
.unwrap();
let init_res = transport.recv_value().await.unwrap().unwrap();
assert_eq!(
init_res["result"]["serverInfo"]["name"], "echo-stub",
"the served child answered initialize over the mesh"
);
transport
.send_value(json!({
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "echo", "arguments": {"text": "over the mesh"}}
}))
.await
.unwrap();
let call_res = transport.recv_value().await.unwrap().unwrap();
assert_eq!(
call_res["result"]["content"][0]["text"], "over the mesh",
"the served child echoed the tools/call payload"
);
assert_eq!(
call_res["result"]["peer_name"], "tester",
"the child's MCPMESH_PEER_NAME carried the gate-resolved nickname across the mesh"
);
assert_eq!(
call_res["result"]["peer_eid"], client_eid,
"the child's MCPMESH_PEER_EID carried the authenticated endpoint principal"
);
assert_eq!(
call_res["result"]["peer_user"], "",
"setup: an unbound pairing caller — exactly the case that had no stable principal"
);
})
.await
.expect("daemon serve integration test timed out");
}
async fn send_initialize(transport: &mut SessionTransport, service: &str) -> Value {
transport
.send_value(json!({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"_meta": {"mcpmesh/service": service},
"capabilities": {}
}
}))
.await
.unwrap();
transport.recv_value().await.unwrap().unwrap()
}
#[tokio::test]
async fn hot_reload_serves_a_newly_registered_service_over_the_mesh() {
timeout(Duration::from_secs(60), async {
let dir = tempfile::tempdir().unwrap();
let before = local_endpoint().await;
let after = local_endpoint().await;
let tester_eid = format!("eid:{}", after.id());
let store = Arc::new(PeerStore::open(&dir.path().join("state.redb")).unwrap());
for (ep, nick) in [(&before, "prober"), (&after, "tester")] {
store
.add(PeerEntry {
endpoint_id: *ep.id().as_bytes(),
nickname: nick.into(),
services: vec!["echo".into()],
paired_at: None,
user_id: None,
last_addr: None,
})
.unwrap();
}
let gate: Arc<dyn TrustGate> = Arc::new(AllowlistGate::new(store.clone()));
let server = local_endpoint().await;
let addr = server.addr();
let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, "").unwrap();
let mesh = MeshState::new(
server,
gate,
store.clone(),
Arc::new(LiveInvites::new()),
"server".into(),
config_path.clone(),
Arc::new(RosterGate::empty()),
Arc::new(mcpmesh_net::ConnRegistry::new()),
None,
None,
None,
None,
);
let accept_task = spawn_accept_loop(
mesh.clone(),
Arc::new(build_services(&Config::load(&config_path).unwrap())),
);
mesh.set_accept_task(accept_task).await;
let mut t_before = connect(&before, addr.clone(), "echo").await.unwrap().0;
let refused = send_initialize(&mut t_before, "echo").await;
assert_eq!(
refused["error"]["code"], -32054,
"echo must be UNSERVED before the reload: {refused}"
);
let state = Arc::new(DaemonState::with_mesh(STACK_VERSION, mesh.clone()));
let (ctl_side, daemon_side) = tokio::io::duplex(64 * 1024);
let (d_read, d_write) = tokio::io::split(daemon_side);
tokio::spawn(serve_control_io(d_read, d_write, state));
let (c_read, c_write) = tokio::io::split(ctl_side);
let mut ctl = connect_control_io(c_read, c_write).await.unwrap();
ctl.register_service(
"echo",
BackendSpec::Run {
cmd: vec![STUB.into()],
env: Default::default(),
cwd: None,
},
vec![tester_eid.clone(), "ghost".into()],
)
.await
.expect("register_service");
let persisted = Config::load(&config_path).unwrap();
assert_eq!(
persisted.services["echo"].allow,
vec![tester_eid.clone(), "ghost".to_string()],
"operator-typed allow is stored verbatim (#38)"
);
let mut t_after = connect(&after, addr.clone(), "echo").await.unwrap().0;
let init = send_initialize(&mut t_after, "echo").await;
assert_eq!(
init["result"]["serverInfo"]["name"], "echo-stub",
"echo must be SERVED after the reload: {init}"
);
t_after
.send_value(json!({
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"arguments": {"text": "after reload"}}
}))
.await
.unwrap();
let call = t_after.recv_value().await.unwrap().unwrap();
assert_eq!(call["result"]["content"][0]["text"], "after reload");
assert_eq!(
call["result"]["peer_name"], "tester",
"identity injection still holds through the reloaded serve loop"
);
let ghost = local_endpoint().await;
store
.add(PeerEntry {
endpoint_id: *ghost.id().as_bytes(),
nickname: "ghost".into(),
services: vec![],
paired_at: None,
user_id: None,
last_addr: None,
})
.unwrap();
let mut t_ghost = connect(&ghost, addr, "echo").await.unwrap().0;
let ghost_refused = send_initialize(&mut t_ghost, "echo").await;
assert_eq!(
ghost_refused["error"]["code"], -32054,
"a bare allow entry kept verbatim must not admit a peer by NICKNAME: {ghost_refused}"
);
})
.await
.expect("hot-reload serve test timed out");
}