use std::time::Duration;
use mcpmesh_local_api::BackendSpec;
use mcpmesh_node::{Config, NodeBuilder};
use serde_json::json;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use tokio::time::timeout;
const STUB: &str = env!("CARGO_BIN_EXE_echo_mcp_stub");
fn hermetic() -> Config {
Config::from_toml_str("[network]\nrelay_mode = \"disabled\"\n").expect("valid test config")
}
#[tokio::test(flavor = "multi_thread")]
async fn two_embedded_nodes_pair_and_run_an_mcp_session() {
let (a_root, b_root) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
let a = NodeBuilder::new(a_root.path())
.config(hermetic())
.start()
.await
.expect("node a starts");
let b = NodeBuilder::new(b_root.path())
.config(hermetic())
.start()
.await
.expect("node b starts");
let mut a_ctl = a.control().await.expect("a control");
a_ctl
.register_service(
"notes",
BackendSpec::Run {
cmd: vec![STUB.into()],
env: Default::default(),
cwd: None,
},
vec![],
)
.await
.expect("register notes");
let invite = a_ctl.invite(vec!["notes".into()]).await.expect("invite");
let mut b_ctl = b.control().await.expect("b control");
let paired = timeout(Duration::from_secs(30), b_ctl.pair(&invite.invite_line))
.await
.expect("pair within 30s")
.expect("pair succeeds");
assert!(!paired.sas_code.is_empty(), "redeemer displays a SAS");
assert_eq!(paired.services, vec!["notes".to_string()]);
let a_status = a_ctl.status().await.expect("a status");
assert_eq!(
a_status
.recent_pairings
.last()
.expect("a recorded the pairing")
.sas_code,
paired.sas_code,
"both sides display the same safety code"
);
let session_ctl = b.control().await.expect("b session control");
let (reader, mut writer) = session_ctl
.open_session(paired.peer_nickname.clone(), "notes".into())
.await
.expect("open_session");
let mut lines = reader.into_inner();
let mut line = String::new();
writer
.write_all(
(json!({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}).to_string()
+ "\n")
.as_bytes(),
)
.await
.expect("send initialize");
timeout(Duration::from_secs(30), lines.read_line(&mut line))
.await
.expect("initialize reply within 30s")
.expect("read initialize reply");
assert!(
line.contains("\"result\""),
"initialize must answer a result: {line}"
);
line.clear();
writer
.write_all(
(json!({
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "echo", "arguments": {"text": "hello-embedded"}}
})
.to_string()
+ "\n")
.as_bytes(),
)
.await
.expect("send tools/call");
timeout(Duration::from_secs(30), lines.read_line(&mut line))
.await
.expect("echo reply within 30s")
.expect("read echo reply");
assert!(
line.contains("hello-embedded"),
"the stub must echo the text: {line}"
);
assert!(
line.contains("peer_name"),
"the stub must see the injected caller identity: {line}"
);
b.shutdown().await;
a.shutdown().await;
}