use anyhow::{Context, Result};
use mcpmesh_net::framing::{FrameReader, Inbound, write_frame};
use serde_json::{Value, json};
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
use crate::client::ensure_daemon;
use crate::ipc::MAX_FRAME_BYTES;
pub fn split_target(target: &str) -> Result<(String, String)> {
let (peer, service) = target
.split_once('/')
.with_context(|| format!("target '{target}' must be <peer>/<service>"))?;
anyhow::ensure!(
!peer.is_empty() && !service.is_empty(),
"target '{target}' must be <peer>/<service>"
);
Ok((peer.to_string(), service.to_string()))
}
pub fn server_object(peer: &str, service: &str) -> Value {
json!({ "command": "mcpmesh", "args": ["connect", format!("{peer}/{service}")] })
}
pub fn setup_entry(peer: &str, service: &str) -> Value {
json!({ "mcpServers": { format!("{peer}-{service}"): server_object(peer, service) } })
}
pub async fn run(peer: String, service: String) -> Result<()> {
let client = ensure_daemon().await?;
let (control_reader, control_writer) = client.open_session(peer, service).await?;
pump_stdio(
tokio::io::stdin(),
tokio::io::stdout(),
control_reader,
control_writer,
)
.await
}
pub(crate) async fn pump_stdio<SI, SO, CR, CW>(
stdin: SI,
mut stdout: SO,
mut control_reader: FrameReader<CR>,
mut control_writer: CW,
) -> Result<()>
where
SI: AsyncRead + Unpin + Send,
SO: AsyncWrite + Unpin + Send,
CR: AsyncRead + Unpin + Send,
CW: AsyncWrite + Unpin + Send,
{
let mut stdin = FrameReader::new(BufReader::new(stdin), MAX_FRAME_BYTES);
let to_control = async {
loop {
match stdin.next().await {
Ok(Some(Inbound::Frame(frame))) => {
if write_frame(&mut control_writer, &frame).await.is_err() {
break; }
}
Ok(Some(Inbound::Violation(_))) => break,
Ok(None) | Err(_) => break, }
}
};
let to_stdout = async {
loop {
match control_reader.next().await {
Ok(Some(Inbound::Frame(frame))) => {
if write_frame(&mut stdout, &frame).await.is_err() {
break; }
}
Ok(Some(Inbound::Violation(_))) => break,
Ok(None) | Err(_) => break, }
}
};
tokio::select! {
() = to_control => {}
() = to_stdout => {}
}
let _ = stdout.shutdown().await;
Ok(())
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use serde_json::json;
use tokio::io::{duplex, split};
use tokio::time::timeout;
use super::*;
#[test]
fn setup_entry_has_the_exact_wire_shape() {
assert_eq!(
setup_entry("alice", "notes"),
json!({
"mcpServers": {
"alice-notes": {
"command": "mcpmesh",
"args": ["connect", "alice/notes"]
}
}
})
);
}
#[test]
fn split_target_requires_both_halves() {
assert_eq!(
split_target("alice/notes").unwrap(),
("alice".into(), "notes".into())
);
assert!(split_target("alice").is_err());
assert!(split_target("alice/").is_err());
assert!(split_target("/notes").is_err());
}
#[tokio::test]
async fn pump_relays_both_directions_verbatim() {
timeout(Duration::from_secs(10), async {
let (mut a_in, b_in) = duplex(64 * 1024);
let (a_out, b_out) = duplex(64 * 1024);
let (pc, dc) = duplex(64 * 1024);
let (pc_r, pc_w) = split(pc);
let (dc_r, dc_w) = split(dc);
let pump = tokio::spawn(pump_stdio(
b_in,
b_out,
FrameReader::new(pc_r, MAX_FRAME_BYTES),
pc_w,
));
let mut daemon_reader = FrameReader::new(dc_r, MAX_FRAME_BYTES);
let mut dc_w = dc_w;
let mut a_out_reader = FrameReader::new(a_out, MAX_FRAME_BYTES);
let init = json!({"jsonrpc":"2.0","id":1,"method":"initialize","params":{}});
write_frame(&mut a_in, &init).await.unwrap();
match daemon_reader.next().await.unwrap().unwrap() {
Inbound::Frame(f) => assert_eq!(f, init, "stdin frame must reach control verbatim"),
other => panic!("expected a frame, got {other:?}"),
}
let err = json!({"jsonrpc":"2.0","id":null,"error":{"code":-32055,"message":"peer unreachable","data":{"source":"mcpmesh"}}});
write_frame(&mut dc_w, &err).await.unwrap();
match a_out_reader.next().await.unwrap().unwrap() {
Inbound::Frame(f) => assert_eq!(f, err, "control frame must reach stdout verbatim"),
other => panic!("expected a frame, got {other:?}"),
}
drop(a_in);
pump.await.unwrap().unwrap();
})
.await
.expect("pump test timed out");
}
}