use std::time::Duration;
use anyhow::{Context, Result, anyhow};
use trusty_common::mcp::{self, DaemonBridgeConfig, Request, Response};
const REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
fn build_rpc_client() -> Result<reqwest::Client> {
reqwest::Client::builder()
.timeout(REQUEST_TIMEOUT)
.connect_timeout(REQUEST_TIMEOUT)
.build()
.context("build reqwest client for trusty-mpm stdio bridge")
}
fn build_bridge_config() -> DaemonBridgeConfig {
DaemonBridgeConfig {
service_name: "trusty-mpm".to_string(),
spawn_args: vec!["daemon".to_string()],
health_path: "/health".to_string(),
base_url_fn: Box::new(|| trusty_mpm::core::resolve_daemon_url(None)),
startup_timeout: None, poll_interval: None, no_spawn: false,
}
}
fn is_notification(req: &Request) -> bool {
req.id.is_none() || req.method.starts_with("notifications/")
}
fn req_to_value(req: &Request) -> serde_json::Value {
serde_json::to_value(req).unwrap_or_else(|_| serde_json::json!({}))
}
fn value_to_mcp_response(v: serde_json::Value) -> Response {
let id = v
.get("id")
.cloned()
.and_then(|id| if id.is_null() { None } else { Some(id) });
if let Some(result) = v.get("result").cloned() {
return Response::ok(id, result);
}
if let Some(err) = v.get("error") {
let code = err
.get("code")
.and_then(|c| c.as_i64())
.map(|c| c as i32)
.unwrap_or(mcp::error_codes::INTERNAL_ERROR);
let message = err
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("unknown daemon error")
.to_string();
return Response::err(id, code, message);
}
Response::err(
id,
mcp::error_codes::INTERNAL_ERROR,
"daemon returned a response with neither result nor error",
)
}
async fn forward_rpc(
client: &reqwest::Client,
base_url: &str,
req: serde_json::Value,
) -> Result<serde_json::Value> {
let url = format!("{base_url}/rpc");
let resp = client
.post(&url)
.json(&req)
.send()
.await
.with_context(|| format!("POST {url}: connection to trusty-mpm daemon failed"))?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(anyhow!(
"daemon returned HTTP {status} for POST /rpc: {body}"
));
}
resp.json::<serde_json::Value>()
.await
.context("deserialise JSON-RPC response from trusty-mpm daemon")
}
pub(crate) async fn run_stdio_bridge() -> Result<()> {
let config = build_bridge_config();
let _ = mcp::ensure_daemon_up(&config).await?;
let client = build_rpc_client()?;
mcp::run_stdio_loop(move |req| {
let client = client.clone();
async move {
if is_notification(&req) {
return Response::suppressed();
}
let base_url = trusty_mpm::core::resolve_daemon_url(None);
let req_value = req_to_value(&req);
match forward_rpc(&client, &base_url, req_value).await {
Ok(resp_value) => value_to_mcp_response(resp_value),
Err(e) => {
tracing::warn!("trusty-mpm stdio bridge: transport error: {e:#}");
Response::err(
req.id.clone(),
mcp::error_codes::INTERNAL_ERROR,
format!("trusty-mpm daemon unreachable: {e:#}"),
)
}
}
}
})
.await
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn build_rpc_client_succeeds() {
assert!(build_rpc_client().is_ok());
}
#[test]
fn is_notification_detects_missing_id_and_prefix() {
let normal = Request {
jsonrpc: Some("2.0".into()),
id: Some(json!(1)),
method: "tools/list".into(),
params: None,
};
assert!(!is_notification(&normal));
let no_id = Request {
jsonrpc: Some("2.0".into()),
id: None,
method: "tools/list".into(),
params: None,
};
assert!(is_notification(&no_id));
let prefixed = Request {
jsonrpc: Some("2.0".into()),
id: Some(json!(9)),
method: "notifications/initialized".into(),
params: None,
};
assert!(is_notification(&prefixed));
}
#[test]
fn value_to_mcp_response_maps_ok_err_and_malformed() {
let ok = value_to_mcp_response(json!({"jsonrpc":"2.0","id":1,"result":{"tools":[]}}));
assert!(ok.error.is_none());
assert_eq!(ok.id, Some(json!(1)));
let err = value_to_mcp_response(
json!({"jsonrpc":"2.0","id":2,"error":{"code":-32601,"message":"x"}}),
);
assert_eq!(err.error.unwrap().code, -32601);
let bad = value_to_mcp_response(json!({"jsonrpc":"2.0","id":3}));
assert_eq!(bad.error.unwrap().code, mcp::error_codes::INTERNAL_ERROR);
let null_id = value_to_mcp_response(json!({"jsonrpc":"2.0","id":null,"result":{}}));
assert_eq!(null_id.id, None);
}
#[test]
fn req_to_value_round_trips_method() {
let req = Request {
jsonrpc: Some("2.0".into()),
id: Some(json!(5)),
method: "session_list".into(),
params: None,
};
let v = req_to_value(&req);
assert_eq!(v["method"], "session_list");
assert_eq!(v["id"], 5);
}
#[tokio::test]
async fn forward_rpc_errors_on_refused() {
let client = build_rpc_client().expect("client");
let result = forward_rpc(&client, "http://127.0.0.1:65534", json!({"method":"ping"})).await;
assert!(result.is_err(), "must error when no daemon is listening");
}
}