use std::future::Future;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context, Result};
use mcpmesh_local_api::transport::{LocalListener, LocalStream};
use mcpmesh_local_api::{
API_NAME, API_VERSION, BlobFetchParams, BlobGrantParams, BlobPublishParams, Hello,
InviteParams, OpenSessionParams, OrgJoinParams, PairParams, RosterInstallParams,
SetRosterUrlParams, StatusResult, method_of,
};
use mcpmesh_net::framing::{FrameReader, Inbound, write_frame};
use serde_json::{Value, json};
use tokio::sync::Notify;
use crate::daemon::MeshState;
use crate::ipc::{self, MAX_FRAME_BYTES};
pub struct DaemonState {
pub stack_version: String,
pub(crate) mesh: Option<Arc<MeshState>>,
shutdown: Notify,
}
impl DaemonState {
pub fn new(stack_version: impl Into<String>) -> Self {
Self {
stack_version: stack_version.into(),
mesh: None,
shutdown: Notify::new(),
}
}
pub fn with_mesh(stack_version: impl Into<String>, mesh: Arc<MeshState>) -> Self {
Self {
stack_version: stack_version.into(),
mesh: Some(mesh),
shutdown: Notify::new(),
}
}
pub(crate) async fn shutdown_requested(&self) {
self.shutdown.notified().await;
}
pub(crate) fn request_shutdown(&self) {
self.shutdown.notify_one();
}
pub(crate) fn mesh(&self) -> Option<&Arc<MeshState>> {
self.mesh.as_ref()
}
pub(crate) fn mesh_required(&self) -> Result<&Arc<MeshState>> {
self.mesh()
.context("daemon has no mesh (control-only mode)")
}
}
pub async fn serve_control(mut listener: LocalListener, state: Arc<DaemonState>) -> Result<()> {
loop {
tokio::select! {
() = state.shutdown.notified() => {
tracing::info!("shutdown requested; control server stopping");
return Ok(());
}
accepted = listener.accept() => {
let stream = match accepted {
Ok(s) => s,
Err(e) => {
tracing::warn!(%e, "control accept failed; backing off");
tokio::time::sleep(Duration::from_millis(50)).await;
continue;
}
};
let state = state.clone();
tokio::spawn(async move {
if let Err(e) = handle_conn(stream, state).await {
tracing::debug!(%e, "control connection ended");
}
});
}
}
}
}
async fn handle_conn(stream: LocalStream, state: Arc<DaemonState>) -> Result<()> {
if let Err(e) = ipc::check_peer(&stream) {
tracing::warn!(%e, "refused unauthorized control connection");
return Ok(());
}
let (read_half, write_half) = mcpmesh_local_api::transport::split_local(stream);
serve_control_io(read_half, write_half, state).await
}
pub async fn serve_control_io(
read_half: impl tokio::io::AsyncRead + Unpin + Send + 'static,
mut write_half: impl tokio::io::AsyncWrite + Unpin + Send + 'static,
state: Arc<DaemonState>,
) -> Result<()> {
let hello = Hello {
api: API_NAME.into(),
api_version: API_VERSION.into(),
api_minor: mcpmesh_local_api::API_MINOR,
stack_version: state.stack_version.clone(),
};
write_frame(&mut write_half, &serde_json::to_value(&hello)?).await?;
let reader = FrameReader::new(tokio::io::BufReader::new(read_half), MAX_FRAME_BYTES);
let ephemeral_registered = Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
let loop_state = state.clone();
let eph = ephemeral_registered.clone();
let outcome: Result<()> = async move {
let mut reader = reader;
let mut write_half = write_half;
loop {
match reader.next().await? {
None => return Ok(()), Some(Inbound::Violation(v)) => {
let resp = error(Value::Null, -32700, format!("invalid request frame: {v:?}"));
write_frame(&mut write_half, &resp).await?;
}
Some(Inbound::Frame(req)) => {
if method_of(&req) == Some("shutdown") {
loop_state.shutdown.notify_one();
let resp = dispatch(&req, &loop_state);
let _ = write_frame(&mut write_half, &resp).await;
return Ok(());
}
if method_of(&req) == Some("open_session") {
let params = req.get("params").cloned().unwrap_or(Value::Null);
let p: OpenSessionParams = match params_of(¶ms) {
Ok(p) => p,
Err(e) => {
let id = req.get("id").cloned().unwrap_or(Value::Null);
let resp = error(id, -32602, format!("open_session failed: {e}"));
write_frame(&mut write_half, &resp).await?;
continue;
}
};
return crate::daemon::open_session(
&loop_state,
&p.peer,
&p.service,
reader,
write_half,
)
.await;
}
if method_of(&req) == Some("subscribe") {
return run_subscription(&loop_state, write_half).await;
}
let resp = handle_request(&req, &loop_state).await;
if method_of(&req) == Some("register_service")
&& resp.get("result").is_some()
&& req
.get("params")
.and_then(|p| p.get("ephemeral"))
.and_then(|v| v.as_bool())
.unwrap_or(false)
&& let Some(name) = req
.get("params")
.and_then(|p| p.get("name"))
.and_then(|v| v.as_str())
{
eph.lock()
.expect("ephemeral_registered lock not poisoned")
.push(name.to_string());
}
write_frame(&mut write_half, &resp).await?;
}
}
}
}
.await;
if let Some(mesh) = state.mesh() {
let names = ephemeral_registered
.lock()
.expect("ephemeral_registered lock not poisoned")
.clone();
crate::daemon::unregister_ephemeral(mesh, &names).await;
}
outcome
}
async fn run_subscription(
state: &Arc<DaemonState>,
mut w: impl tokio::io::AsyncWrite + Unpin,
) -> Result<()> {
use crate::stream::StreamFrame;
let (audit, mesh) = match state.mesh() {
Some(mesh) => (mesh.audit(), Some(mesh)),
None => (crate::audit::AuditSink::disabled(), None),
};
let rx = audit.subscribe();
let snapshot = StreamFrame::Snapshot {
active_sessions: audit.active_sessions(),
reachability: mesh.map(crate::daemon::reachability_of).unwrap_or_default(),
};
write_frame(&mut w, &serde_json::to_value(&snapshot)?).await?;
let Some(mut rx) = rx else {
return Ok(());
};
use tokio::sync::broadcast::error::RecvError;
loop {
let frame = match rx.recv().await {
Ok(record) => StreamFrame::Event {
record: Box::new(record),
},
Err(RecvError::Lagged(n)) => StreamFrame::Lagged { dropped: n },
Err(RecvError::Closed) => return Ok(()),
};
if write_frame(&mut w, &serde_json::to_value(&frame)?)
.await
.is_err()
{
return Ok(()); }
}
}
async fn handle_request(req: &Value, state: &DaemonState) -> Value {
let id = req.get("id").cloned().unwrap_or(Value::Null);
let params = req.get("params").cloned().unwrap_or(Value::Null);
match method_of(req) {
Some("register_service") => respond(
id,
"register_service",
with_params(¶ms, |p| crate::daemon::register_service(state, p))
.await
.map(unit),
),
Some("peer_add") => respond(
id,
"peer_add",
with_params(¶ms, |p| crate::daemon::add_peer(state, p))
.await
.map(unit),
),
Some("peer_remove") => respond(
id,
"peer_remove",
with_params(¶ms, |p| crate::daemon::remove_peer(state, p))
.await
.map(unit),
),
Some("peer_rename") => respond(
id,
"peer_rename",
with_params(¶ms, |p| crate::daemon::rename_peer(state, p))
.await
.map(unit),
),
Some("invite") => {
let mesh = match state.mesh_required() {
Ok(mesh) => mesh,
Err(e) => return error(id, -32000, e.to_string()),
};
respond(
id,
"invite",
with_params(¶ms, |p: InviteParams| {
crate::daemon::mint_invite(p.services, p.app_label, mesh)
})
.await,
)
}
Some("pair") => respond(
id,
"pair",
with_params(¶ms, |p: PairParams| {
crate::daemon::redeem(state, p.invite_line)
})
.await,
),
Some("roster_install") => respond(
id,
"roster_install",
with_params(¶ms, |p: RosterInstallParams| {
crate::daemon::install_roster(state, p.path, p.org_root_pk)
})
.await,
),
Some("org_join") => respond(
id,
"org_join",
with_params(¶ms, |p: OrgJoinParams| {
crate::daemon::org_join(state, p.org_id, p.org_root_pk, p.user_id, p.user_key)
})
.await,
),
Some("set_roster_url") => respond(
id,
"set_roster_url",
with_params(¶ms, |p: SetRosterUrlParams| {
crate::daemon::set_roster_url(state, p.url)
})
.await
.map(unit),
),
Some("blob_publish") => respond(
id,
"blob_publish",
with_params(¶ms, |p: BlobPublishParams| {
crate::daemon::blob_publish(state, p.scope, p.path)
})
.await,
),
Some("blob_grant") => respond(
id,
"blob_grant",
with_params(¶ms, |p: BlobGrantParams| {
crate::daemon::blob_grant(state, p.scope, p.principal)
})
.await
.map(unit),
),
Some("blob_list") => respond(id, "blob_list", crate::daemon::blob_list(state).await),
Some("blob_fetch") => respond(
id,
"blob_fetch",
with_params(¶ms, |p: BlobFetchParams| {
crate::daemon::blob_fetch(state, p.ticket, p.dest_path)
})
.await,
),
Some("audit_summary") => {
let sink_dir = state
.mesh
.as_ref()
.and_then(|m| m.audit().dir().map(std::path::Path::to_path_buf));
match tokio::task::spawn_blocking(move || {
let dir = match sink_dir {
Some(d) => d,
None => mcpmesh_trust::paths::default_audit_dir()?,
};
crate::audit::read_all_records(&dir)
.map(|recs| crate::audit::summarize_sessions(&recs))
})
.await
{
Ok(r) => respond(id, "audit_summary", r.map_err(anyhow::Error::from)),
Err(e) => error(id, -32000, format!("audit_summary task failed: {e}")),
}
}
_ => dispatch(req, state),
}
}
#[derive(Debug)]
struct InvalidParams(String);
impl std::fmt::Display for InvalidParams {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for InvalidParams {}
fn respond<T: serde::Serialize>(id: Value, method: &str, r: anyhow::Result<T>) -> Value {
match r {
Ok(v) => ok(
id,
serde_json::to_value(v).expect("control result serializes"),
),
Err(e) if e.downcast_ref::<InvalidParams>().is_some() => {
error(id, -32602, format!("{method} failed: {e}"))
}
Err(e) => error(id, -32000, format!("{method} failed: {e}")),
}
}
fn unit((): ()) -> Value {
json!({})
}
fn dispatch(req: &Value, state: &DaemonState) -> Value {
let id = req.get("id").cloned().unwrap_or(Value::Null);
match method_of(req) {
Some("status") => respond(id, "status", status_result(state)),
Some("shutdown") => ok(id, json!({})),
Some(other) => error(id, -32601, format!("unknown method: {other}")),
None => error(id, -32600, "request is missing a `method`"),
}
}
pub(crate) fn status_result(state: &DaemonState) -> Result<StatusResult> {
let (services, peers, roster) = match state.mesh() {
Some(mesh) => {
let cfg = crate::config::Config::load(&mesh.config_path).map_err(|e| {
anyhow::anyhow!("config unreadable at {}: {e}", mesh.config_path.display())
})?;
let roster = crate::daemon::roster_status(mesh, Some(&cfg));
let ephemeral = mesh
.ephemeral_services
.lock()
.expect("ephemeral_services lock not poisoned")
.clone();
(
crate::daemon::service_infos(&cfg, &ephemeral),
crate::daemon::peer_infos(&mesh.store),
roster,
)
}
None => (Vec::new(), Vec::new(), None),
};
let presence = state
.mesh()
.map(crate::daemon::presence_peers)
.unwrap_or_default();
let self_user_id = state
.mesh()
.and_then(|mesh| mesh.self_binding())
.map(|binding| binding.user_pk);
let recent_pairings = state
.mesh()
.map(|mesh| mesh.recent_pairings())
.unwrap_or_default();
let reachability = state
.mesh()
.map(crate::daemon::reachability_of)
.unwrap_or_default();
Ok(StatusResult {
stack_version: state.stack_version.clone(),
services,
peers,
roster,
presence,
self_user_id,
recent_pairings,
reachability,
})
}
fn params_of<T: serde::de::DeserializeOwned>(params: &Value) -> anyhow::Result<T> {
let v = match params {
Value::Null => json!({}),
p => p.clone(),
};
serde_json::from_value(v)
.map_err(|e| anyhow::Error::new(InvalidParams(format!("invalid params: {e}"))))
}
async fn with_params<P, R, F>(params: &Value, f: impl FnOnce(P) -> F) -> anyhow::Result<R>
where
P: serde::de::DeserializeOwned,
F: Future<Output = anyhow::Result<R>>,
{
f(params_of(params)?).await
}
fn ok(id: Value, result: Value) -> Value {
json!({ "jsonrpc": "2.0", "id": id, "result": result })
}
fn error(id: Value, code: i64, message: impl Into<String>) -> Value {
json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message.into() } })
}
#[cfg(test)]
mod tests {
use super::*;
fn control_only() -> Arc<DaemonState> {
Arc::new(DaemonState::new("0.1.0-test"))
}
fn req(method: &str, params: Value) -> Value {
json!({ "jsonrpc": "2.0", "id": 1, "method": method, "params": params })
}
#[tokio::test]
async fn serve_control_io_speaks_the_protocol_over_a_duplex() {
let state = control_only();
let (client_io, server_io) = tokio::io::duplex(64 * 1024);
let (sr, sw) = tokio::io::split(server_io);
tokio::spawn(serve_control_io(sr, sw, state));
let (cr, cw) = tokio::io::split(client_io);
let mut client = mcpmesh_local_api::connect_control_io(cr, cw)
.await
.expect("hello handshake");
assert_eq!(client.hello().stack_version, "0.1.0-test");
let status = client.status().await.expect("status");
assert_eq!(status.stack_version, "0.1.0-test");
assert!(status.services.is_empty());
}
#[test]
fn dispatch_status_answers_empty_lists_without_a_mesh() {
let st = control_only();
let r = dispatch(&req("status", json!({})), &st);
assert_eq!(r["result"]["stack_version"], "0.1.0-test");
assert!(r["result"]["peers"].as_array().unwrap().is_empty());
assert!(r["result"]["services"].as_array().unwrap().is_empty());
assert!(r["result"]["roster"].is_null());
}
#[test]
fn dispatch_status_tolerates_any_params_shape() {
let st = control_only();
for p in [json!({}), Value::Null, json!({ "junk": true })] {
assert!(dispatch(&req("status", p), &st).get("result").is_some());
}
let omitted = json!({ "jsonrpc": "2.0", "id": 1, "method": "status" });
assert!(dispatch(&omitted, &st).get("result").is_some());
}
#[test]
fn dispatch_shutdown_acks_and_unknown_methods_error() {
let st = control_only();
assert_eq!(
dispatch(&req("shutdown", json!({})), &st)["result"],
json!({})
);
assert_eq!(
dispatch(&req("frobnicate", json!({})), &st)["error"]["code"],
-32601
);
let no_method = json!({ "jsonrpc": "2.0", "id": 1 });
assert_eq!(dispatch(&no_method, &st)["error"]["code"], -32600);
}
#[tokio::test]
async fn mesh_methods_error_gracefully_without_a_mesh() {
let st = control_only();
for method in [
"register_service",
"peer_add",
"peer_remove",
"peer_rename",
"invite",
"pair",
"roster_install",
"org_join",
"set_roster_url",
"blob_publish",
"blob_grant",
"blob_list",
"blob_fetch",
] {
let r = handle_request(&req(method, json!({})), &st).await;
let code = r["error"]["code"].as_i64();
assert!(
matches!(code, Some(-32000) | Some(-32602)),
"method {method} should error gracefully in control-only mode, got {r}"
);
assert!(
r.get("result").is_none(),
"method {method} must not succeed: {r}"
);
}
}
#[tokio::test]
async fn malformed_params_answer_an_invalid_params_error() {
let st = control_only();
let r = handle_request(&req("peer_remove", json!({ "nickname": 42 })), &st).await;
assert_eq!(r["error"]["code"], -32602);
assert!(
r["error"]["message"]
.as_str()
.unwrap()
.contains("invalid params"),
"message names the params problem: {r}"
);
let r = handle_request(&req("peer_rename", json!({ "user_id": "u" })), &st).await;
assert_eq!(r["error"]["code"], -32602);
let r = handle_request(
&req("peer_remove", json!({ "nickname": "a", "extra": true })),
&st,
)
.await;
assert_eq!(
r["error"]["code"], -32602,
"unknown params field is rejected: {r}"
);
}
#[tokio::test]
async fn audit_summary_works_in_control_only_mode() {
let st = control_only();
let r = handle_request(&req("audit_summary", json!({})), &st).await;
assert!(
r.get("result").is_some(),
"audit_summary should succeed: {r}"
);
}
#[tokio::test]
async fn handle_request_delegates_status_to_dispatch() {
let st = control_only();
let r = handle_request(&req("status", json!({})), &st).await;
assert_eq!(r["result"]["stack_version"], "0.1.0-test");
}
}