use anyhow::{Context, Result};
use mcpmesh_net::transport::NdjsonTransport;
use mcpmesh_net::{PeerIdentity, SessionBackend, SessionTransport};
use serde_json::Value;
use tokio::io::{AsyncRead, AsyncWrite};
use crate::audit::{AuditSink, RequestAuditor};
pub struct SocketBackend {
pub path: String,
pub service: String,
pub audit: AuditSink,
pub limiter: std::sync::Arc<crate::limits::RateLimiter>,
}
#[async_trait::async_trait]
impl SessionBackend for SocketBackend {
async fn run(
&self,
identity: Option<PeerIdentity>,
initialize: Value,
transport: SessionTransport,
) -> anyhow::Result<()> {
self.run_over(identity, initialize, transport).await
}
}
impl SocketBackend {
pub async fn run_over<R, W>(
&self,
identity: Option<PeerIdentity>,
mut initialize: Value,
mut transport: NdjsonTransport<R, W>,
) -> Result<()>
where
R: AsyncRead + Send + Unpin,
W: AsyncWrite + Send + Unpin,
{
let server = mcpmesh_local_api::transport::connect_local(std::path::Path::new(&self.path))
.await
.with_context(|| format!("dial socket backend at {}", self.path))?;
if let Some(id) = &identity {
if !initialize.is_object() {
initialize = serde_json::json!({});
}
if !initialize["params"].is_object() {
initialize["params"] = serde_json::json!({});
}
if !initialize["params"]["_meta"].is_object() {
initialize["params"]["_meta"] = serde_json::json!({});
}
initialize["params"]["_meta"]["mcpmesh/peer"] = serde_json::json!({
"name": id.name,
"user_id": id.user_id,
"groups": id.groups,
});
}
let peer = identity
.as_ref()
.map(|id| id.user_id.clone().unwrap_or_else(|| id.name.clone()));
let _session = self
.audit
.session(peer.clone().unwrap_or_default(), self.service.clone());
let auditor = RequestAuditor::new(self.audit.clone(), peer.clone(), self.service.clone());
let (server_read, server_write) = mcpmesh_local_api::transport::split_local(server);
let outcome = super::pump(
initialize,
&mut transport,
server_read,
server_write,
auditor,
crate::limits::RateGate::new(
self.limiter.clone(),
identity.as_ref().map(|i| i.endpoint),
),
)
.await;
outcome
}
}
#[cfg(all(test, unix))]
mod tests {
use std::time::Duration;
use mcpmesh_net::PeerIdentity;
use mcpmesh_net::framing::{FrameReader, Inbound, write_frame};
use serde_json::{Value, json};
use tokio::io::{BufReader, duplex, split};
use tokio::net::UnixListener;
use tokio::time::timeout;
use super::SocketBackend;
const MAX_FRAME: usize = 16 * 1024 * 1024;
async fn stub_server(listener: UnixListener) -> Value {
let (stream, _) = listener.accept().await.expect("stub accept");
let (read_half, mut write_half) = stream.into_split();
let mut reader = FrameReader::new(BufReader::new(read_half), MAX_FRAME);
let init = match reader
.next()
.await
.expect("read initialize")
.expect("initialize frame")
{
Inbound::Frame(v) => v,
Inbound::Violation(_) => panic!("stub saw a framing violation on initialize"),
};
write_frame(
&mut write_half,
&json!({
"jsonrpc": "2.0", "id": init["id"].clone(),
"result": {"serverInfo": {"name": "socket-stub"}}
}),
)
.await
.expect("reply initialize");
let call = match reader
.next()
.await
.expect("read tools/call")
.expect("tools/call frame")
{
Inbound::Frame(v) => v,
Inbound::Violation(_) => panic!("stub saw a framing violation on tools/call"),
};
write_frame(
&mut write_half,
&json!({
"jsonrpc": "2.0", "id": call["id"].clone(),
"result": {"content": [{"text": call["params"]["arguments"]["text"].clone()}]}
}),
)
.await
.expect("echo tools/call");
init
}
#[tokio::test]
async fn socket_backend_injects_authoritative_peer_and_echoes() {
timeout(Duration::from_secs(30), async {
let dir = tempfile::tempdir().unwrap();
let sock = dir.path().join("server.sock");
let listener = UnixListener::bind(&sock).unwrap();
let stub = tokio::spawn(stub_server(listener));
let (server_io, client_io) = duplex(64 * 1024);
let (sr, sw) = split(server_io);
let backend_transport = mcpmesh_net::transport::NdjsonTransport::new(sr, sw, MAX_FRAME);
let (cr, cw) = split(client_io);
let mut client = mcpmesh_net::transport::NdjsonTransport::new(cr, cw, MAX_FRAME);
let backend = SocketBackend {
path: sock.to_str().unwrap().to_string(),
service: "test".into(),
audit: crate::audit::AuditSink::disabled(),
limiter: crate::limits::RateLimiter::unlimited_shared(),
};
let identity = Some(PeerIdentity {
endpoint: [0u8; 32],
name: "bob".into(),
user_id: None,
groups: vec![],
});
let initialize = json!({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"_meta": {"mcpmesh/peer": {
"name": "attacker", "groups": ["admin"], "user_id": "root"
}}
}
});
let session = tokio::spawn(async move {
backend
.run_over(identity, initialize, backend_transport)
.await
});
let init_resp = client.recv_value().await.unwrap().unwrap();
assert_eq!(init_resp["id"], 1);
assert_eq!(init_resp["result"]["serverInfo"]["name"], "socket-stub");
client
.send_value(json!({
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"arguments": {"text": "hello mesh"}}
}))
.await
.unwrap();
let call_resp = client.recv_value().await.unwrap().unwrap();
assert_eq!(call_resp["id"], 2);
assert_eq!(call_resp["result"]["content"][0]["text"], "hello mesh");
let observed_init = stub.await.unwrap();
let peer = &observed_init["params"]["_meta"]["mcpmesh/peer"];
assert_eq!(
peer["name"], "bob",
"forged 'attacker' name must be overwritten"
);
assert_eq!(
peer["groups"],
json!([]),
"forged groups ['admin'] must be dropped, not merged"
);
assert_eq!(
peer["user_id"],
json!(null),
"forged user_id 'root' must be dropped (pairing-mode identity is null)"
);
drop(client);
session
.await
.unwrap()
.expect("run_over returns Ok on transport EOF");
})
.await
.expect("socket backend test timed out");
}
#[tokio::test]
async fn socket_backend_builds_params_when_absent() {
timeout(Duration::from_secs(30), async {
let dir = tempfile::tempdir().unwrap();
let sock = dir.path().join("server.sock");
let listener = UnixListener::bind(&sock).unwrap();
let stub = tokio::spawn(stub_server(listener));
let (server_io, client_io) = duplex(64 * 1024);
let (sr, sw) = split(server_io);
let backend_transport = mcpmesh_net::transport::NdjsonTransport::new(sr, sw, MAX_FRAME);
let (cr, cw) = split(client_io);
let mut client = mcpmesh_net::transport::NdjsonTransport::new(cr, cw, MAX_FRAME);
let backend = SocketBackend {
path: sock.to_str().unwrap().to_string(),
service: "test".into(),
audit: crate::audit::AuditSink::disabled(),
limiter: crate::limits::RateLimiter::unlimited_shared(),
};
let identity = Some(PeerIdentity {
endpoint: [0u8; 32],
name: "bob".into(),
user_id: None,
groups: vec![],
});
let initialize = json!({
"jsonrpc": "2.0", "id": 1, "method": "initialize"
});
let session = tokio::spawn(async move {
backend
.run_over(identity, initialize, backend_transport)
.await
});
let init_resp = client.recv_value().await.unwrap().unwrap();
assert_eq!(init_resp["result"]["serverInfo"]["name"], "socket-stub");
client
.send_value(json!({
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"arguments": {"text": "built params"}}
}))
.await
.unwrap();
let call_resp = client.recv_value().await.unwrap().unwrap();
assert_eq!(call_resp["result"]["content"][0]["text"], "built params");
let observed_init = stub.await.unwrap();
assert_eq!(
observed_init["params"]["_meta"]["mcpmesh/peer"]["name"],
"bob"
);
drop(client);
session
.await
.unwrap()
.expect("run_over returns Ok on transport EOF");
})
.await
.expect("socket params-build test timed out");
}
#[tokio::test]
async fn socket_backend_guards_non_object_root() {
timeout(Duration::from_secs(30), async {
let dir = tempfile::tempdir().unwrap();
let sock = dir.path().join("server.sock");
let listener = UnixListener::bind(&sock).unwrap();
let stub = tokio::spawn(stub_server(listener));
let (server_io, client_io) = duplex(64 * 1024);
let (sr, sw) = split(server_io);
let backend_transport = mcpmesh_net::transport::NdjsonTransport::new(sr, sw, MAX_FRAME);
let (cr, cw) = split(client_io);
let mut client = mcpmesh_net::transport::NdjsonTransport::new(cr, cw, MAX_FRAME);
let backend = SocketBackend {
path: sock.to_str().unwrap().to_string(),
service: "test".into(),
audit: crate::audit::AuditSink::disabled(),
limiter: crate::limits::RateLimiter::unlimited_shared(),
};
let identity = Some(PeerIdentity {
endpoint: [0u8; 32],
name: "bob".into(),
user_id: None,
groups: vec![],
});
let initialize = json!([1, 2, 3]);
let session = tokio::spawn(async move {
backend
.run_over(identity, initialize, backend_transport)
.await
});
let init_resp = client.recv_value().await.unwrap().unwrap();
assert_eq!(init_resp["result"]["serverInfo"]["name"], "socket-stub");
client
.send_value(json!({
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"arguments": {"text": "fresh object"}}
}))
.await
.unwrap();
let call_resp = client.recv_value().await.unwrap().unwrap();
assert_eq!(call_resp["result"]["content"][0]["text"], "fresh object");
let observed_init = stub.await.unwrap();
assert_eq!(
observed_init["params"]["_meta"]["mcpmesh/peer"]["name"],
"bob"
);
drop(client);
session
.await
.unwrap()
.expect("run_over returns Ok on transport EOF");
})
.await
.expect("socket non-object-root test timed out");
}
#[tokio::test]
async fn socket_backend_no_identity_forwards_verbatim() {
timeout(Duration::from_secs(30), async {
let dir = tempfile::tempdir().unwrap();
let sock = dir.path().join("server.sock");
let listener = UnixListener::bind(&sock).unwrap();
let stub = tokio::spawn(stub_server(listener));
let (server_io, client_io) = duplex(64 * 1024);
let (sr, sw) = split(server_io);
let backend_transport = mcpmesh_net::transport::NdjsonTransport::new(sr, sw, MAX_FRAME);
let (cr, cw) = split(client_io);
let mut client = mcpmesh_net::transport::NdjsonTransport::new(cr, cw, MAX_FRAME);
let backend = SocketBackend {
path: sock.to_str().unwrap().to_string(),
service: "test".into(),
audit: crate::audit::AuditSink::disabled(),
limiter: crate::limits::RateLimiter::unlimited_shared(),
};
let identity: Option<PeerIdentity> = None;
let initialize = json!({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"capabilities": {}, "_meta": {"caller": "kept"}}
});
let session = tokio::spawn(async move {
backend
.run_over(identity, initialize, backend_transport)
.await
});
let init_resp = client.recv_value().await.unwrap().unwrap();
assert_eq!(init_resp["result"]["serverInfo"]["name"], "socket-stub");
client
.send_value(json!({
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"arguments": {"text": "no id"}}
}))
.await
.unwrap();
let call_resp = client.recv_value().await.unwrap().unwrap();
assert_eq!(call_resp["result"]["content"][0]["text"], "no id");
let observed_init = stub.await.unwrap();
assert!(
observed_init["params"]["_meta"]["mcpmesh/peer"].is_null(),
"identity None must inject nothing"
);
assert_eq!(observed_init["params"]["_meta"]["caller"], "kept");
drop(client);
session
.await
.unwrap()
.expect("run_over returns Ok on transport EOF");
})
.await
.expect("socket no-identity test timed out");
}
async fn deadlock_stub(listener: UnixListener, big: String) {
let (stream, _) = listener.accept().await.expect("stub accept");
let (read_half, mut write_half) = stream.into_split();
let mut reader = FrameReader::new(BufReader::new(read_half), MAX_FRAME);
let init = match reader.next().await.unwrap().unwrap() {
Inbound::Frame(v) => v,
Inbound::Violation(_) => panic!("violation on initialize"),
};
write_frame(
&mut write_half,
&json!({
"jsonrpc": "2.0", "id": init["id"].clone(),
"result": {"serverInfo": {"name": "socket-stub"}}
}),
)
.await
.unwrap();
write_frame(
&mut write_half,
&json!({
"jsonrpc": "2.0", "method": "notifications/message",
"params": {"data": big}
}),
)
.await
.unwrap();
let call = match reader.next().await.unwrap().unwrap() {
Inbound::Frame(v) => v,
Inbound::Violation(_) => panic!("violation on tools/call"),
};
write_frame(
&mut write_half,
&json!({
"jsonrpc": "2.0", "id": call["id"].clone(),
"result": {"content": [{"text": call["params"]["arguments"]["text"].clone()}]}
}),
)
.await
.unwrap();
}
#[tokio::test]
async fn pump_survives_simultaneous_large_bidirectional_traffic() {
timeout(Duration::from_secs(30), async {
let big = "x".repeat(2 * 1024 * 1024);
let dir = tempfile::tempdir().unwrap();
let sock = dir.path().join("server.sock");
let listener = UnixListener::bind(&sock).unwrap();
let stub = tokio::spawn(deadlock_stub(listener, big.clone()));
let (server_io, client_io) = duplex(64 * 1024);
let (sr, sw) = split(server_io);
let backend_transport = mcpmesh_net::transport::NdjsonTransport::new(sr, sw, MAX_FRAME);
let (cr, cw) = split(client_io);
let mut client = mcpmesh_net::transport::NdjsonTransport::new(cr, cw, MAX_FRAME);
let backend = SocketBackend {
path: sock.to_str().unwrap().to_string(),
service: "test".into(),
audit: crate::audit::AuditSink::disabled(),
limiter: crate::limits::RateLimiter::unlimited_shared(),
};
let identity = Some(PeerIdentity {
endpoint: [0u8; 32],
name: "bob".into(),
user_id: None,
groups: vec![],
});
let initialize = json!({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"capabilities": {}}
});
let session = tokio::spawn(async move {
backend
.run_over(identity, initialize, backend_transport)
.await
});
let client_writer = client.writer();
let big_arg = big.clone();
let sender = tokio::spawn(async move {
client_writer
.send_value(json!({
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"arguments": {"text": big_arg}}
}))
.await
});
let (mut saw_init, mut saw_notification, mut saw_echo) = (false, false, false);
while !(saw_init && saw_notification && saw_echo) {
let frame = client.recv_value().await.unwrap().unwrap();
if frame["method"] == "notifications/message" {
assert_eq!(frame["params"]["data"].as_str().unwrap().len(), big.len());
saw_notification = true;
} else if frame["id"] == 2 {
assert_eq!(
frame["result"]["content"][0]["text"]
.as_str()
.unwrap()
.len(),
big.len()
);
saw_echo = true;
} else if frame["id"] == 1 {
assert_eq!(frame["result"]["serverInfo"]["name"], "socket-stub");
saw_init = true;
}
}
sender.await.unwrap().unwrap();
stub.await.unwrap();
drop(client);
session
.await
.unwrap()
.expect("run_over returns Ok on transport EOF");
})
.await
.expect("large-frame pump deadlocked (the old single-loop pump would hang here)");
}
#[tokio::test]
async fn socket_backend_records_session_open_and_close() {
use crate::audit::{AuditLog, AuditSink};
timeout(Duration::from_secs(30), async {
let dir = tempfile::tempdir().unwrap();
let sock = dir.path().join("server.sock");
let listener = UnixListener::bind(&sock).unwrap();
let stub = tokio::spawn(stub_server(listener));
let audit_dir = dir.path().join("audit");
let sink = AuditSink::new(AuditLog::spawn(audit_dir.clone()));
let (server_io, client_io) = duplex(64 * 1024);
let (sr, sw) = split(server_io);
let backend_transport = mcpmesh_net::transport::NdjsonTransport::new(sr, sw, MAX_FRAME);
let (cr, cw) = split(client_io);
let mut client = mcpmesh_net::transport::NdjsonTransport::new(cr, cw, MAX_FRAME);
let backend = SocketBackend {
path: sock.to_str().unwrap().to_string(),
service: "notes".into(),
audit: sink,
limiter: crate::limits::RateLimiter::unlimited_shared(),
};
let identity = Some(PeerIdentity {
endpoint: [0u8; 32],
name: "bob".into(),
user_id: None,
groups: vec![],
});
let initialize = json!({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"capabilities": {}}
});
let session = tokio::spawn(async move {
backend
.run_over(identity, initialize, backend_transport)
.await
});
let init_resp = client.recv_value().await.unwrap().unwrap();
assert_eq!(init_resp["result"]["serverInfo"]["name"], "socket-stub");
client
.send_value(json!({
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"arguments": {"text": "lifecycle"}}
}))
.await
.unwrap();
let call_resp = client.recv_value().await.unwrap().unwrap();
assert_eq!(call_resp["result"]["content"][0]["text"], "lifecycle");
drop(client); session.await.unwrap().expect("run_over Ok on EOF");
stub.await.unwrap();
let month = &crate::audit::now_ts()[..7];
let file = audit_dir.join(format!("{month}.jsonl"));
let mut opens = 0;
let mut closes = 0;
for _ in 0..50 {
if let Ok(body) = std::fs::read_to_string(&file) {
opens = body.matches("\"kind\":\"session_open\"").count();
closes = body.matches("\"kind\":\"session_close\"").count();
if opens == 1 && closes == 1 {
break;
}
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
assert_eq!(
(opens, closes),
(1, 1),
"one open + one close for the session"
);
let body = std::fs::read_to_string(&file).unwrap();
assert!(
body.contains("\"peer\":\"bob\""),
"attributed to the resolved peer"
);
assert!(body.contains("\"service\":\"notes\""));
})
.await
.expect("session lifecycle audit test timed out");
}
#[tokio::test(flavor = "multi_thread")]
async fn socket_backend_populates_active_sessions_while_open() {
use crate::audit::{AuditLog, AuditSink};
timeout(Duration::from_secs(30), async {
let dir = tempfile::tempdir().unwrap();
let sock = dir.path().join("server.sock");
let listener = UnixListener::bind(&sock).unwrap();
let stub = tokio::spawn(stub_server(listener));
let sink = AuditSink::new(AuditLog::spawn(dir.path().join("audit")));
let (server_io, client_io) = duplex(64 * 1024);
let (sr, sw) = split(server_io);
let backend_transport = mcpmesh_net::transport::NdjsonTransport::new(sr, sw, MAX_FRAME);
let (cr, cw) = split(client_io);
let mut client = mcpmesh_net::transport::NdjsonTransport::new(cr, cw, MAX_FRAME);
let backend = SocketBackend {
path: sock.to_str().unwrap().to_string(),
service: "notes".into(),
audit: sink.clone(),
limiter: crate::limits::RateLimiter::unlimited_shared(),
};
let identity = Some(PeerIdentity {
endpoint: [0u8; 32],
name: "bob".into(),
user_id: None,
groups: vec![],
});
let initialize = json!({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"capabilities": {}}
});
assert!(
sink.active_sessions().is_empty(),
"no live session before the backend runs"
);
let session = tokio::spawn(async move {
backend
.run_over(identity, initialize, backend_transport)
.await
});
let init_resp = client.recv_value().await.unwrap().unwrap();
assert_eq!(init_resp["result"]["serverInfo"]["name"], "socket-stub");
let live = sink.active_sessions();
assert_eq!(live.len(), 1, "one live session while open");
assert_eq!(live[0].peer, "bob");
assert_eq!(live[0].service, "notes");
client
.send_value(json!({
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"arguments": {"text": "live"}}
}))
.await
.unwrap();
let call_resp = client.recv_value().await.unwrap().unwrap();
assert_eq!(call_resp["result"]["content"][0]["text"], "live");
drop(client); session.await.unwrap().expect("run_over Ok on EOF");
stub.await.unwrap();
assert!(
sink.active_sessions().is_empty(),
"the guard drop removed the live session"
);
})
.await
.expect("active-sessions telemetry test timed out");
}
}