use anyhow::Result;
use std::path::PathBuf;
use crate::transport::rpc::{dispatch, JsonRpcRequest};
use crate::AppState;
fn is_notification(req: &trusty_common::mcp::Request) -> bool {
req.id.is_none() || req.method.starts_with("notifications/")
}
pub async fn run_stdio(data_root: PathBuf, palace: Option<String>) -> Result<()> {
let state = AppState::new(data_root).with_default_palace(palace);
let warmup_state = state.clone();
let warmup_handle = tokio::spawn(async move {
match trusty_common::memory_core::retrieval::shared_embedder().await {
Ok(_) => warmup_state.set_ready(),
Err(e) => tracing::warn!(
"stdio serve: background embedder warm-up failed \
(memory ops will return a bounded error on first request): {e:#}"
),
}
});
let state = std::sync::Arc::new(state);
let result = trusty_common::mcp::run_stdio_loop(move |req| {
let state = state.clone();
async move {
if is_notification(&req) {
return trusty_common::mcp::Response::suppressed();
}
let rpc_req = rpc_request_from_mcp(req);
let rpc_resp = dispatch(&state, rpc_req).await;
mcp_response_from_rpc(rpc_resp)
}
})
.await;
warmup_handle.abort();
result
}
fn rpc_request_from_mcp(req: trusty_common::mcp::Request) -> JsonRpcRequest {
JsonRpcRequest {
jsonrpc: req.jsonrpc,
id: req.id,
method: req.method,
params: req.params,
}
}
fn mcp_response_from_rpc(
resp: crate::transport::rpc::JsonRpcResponse,
) -> trusty_common::mcp::Response {
use serde_json::Value;
let id = if resp.id == Value::Null {
None
} else {
Some(resp.id)
};
match (resp.result, resp.error) {
(Some(result), _) => trusty_common::mcp::Response::ok(id, result),
(None, Some(err)) => trusty_common::mcp::Response::err(id, err.code, err.message),
(None, None) => {
trusty_common::mcp::Response::err(
id,
trusty_common::mcp::error_codes::INTERNAL_ERROR,
"dispatch returned a response with no result and no error",
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn rpc_request_adapter_preserves_fields() {
let mcp_req = trusty_common::mcp::Request {
jsonrpc: Some("2.0".to_string()),
id: Some(json!(42)),
method: "palace_list".to_string(),
params: Some(json!({"palace": "test"})),
};
let rpc_req = rpc_request_from_mcp(mcp_req);
assert_eq!(rpc_req.id, Some(json!(42)));
assert_eq!(rpc_req.method, "palace_list");
assert_eq!(rpc_req.params, Some(json!({"palace": "test"})));
}
#[test]
fn notification_requests_are_detected() {
let normal = trusty_common::mcp::Request {
jsonrpc: Some("2.0".to_string()),
id: Some(json!(1)),
method: "tools/list".to_string(),
params: None,
};
assert!(
!is_notification(&normal),
"request with id must not be classified as notification"
);
let notif_no_id = trusty_common::mcp::Request {
jsonrpc: Some("2.0".to_string()),
id: None,
method: "notifications/initialized".to_string(),
params: None,
};
assert!(
is_notification(¬if_no_id),
"request with no id must be classified as notification"
);
let notif_prefix = trusty_common::mcp::Request {
jsonrpc: Some("2.0".to_string()),
id: None,
method: "notifications/cancelled".to_string(),
params: None,
};
assert!(
is_notification(¬if_prefix),
"notifications/* method with no id must be classified as notification"
);
let notif_prefix_with_id = trusty_common::mcp::Request {
jsonrpc: Some("2.0".to_string()),
id: Some(json!(99)),
method: "notifications/initialized".to_string(),
params: None,
};
assert!(
is_notification(¬if_prefix_with_id),
"notifications/* method must be classified as notification even with id"
);
}
#[test]
fn normal_response_is_not_suppressed() {
let rpc_resp = crate::transport::rpc::JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id: json!(7),
result: Some(json!({"tools": []})),
error: None,
};
let mcp_resp = mcp_response_from_rpc(rpc_resp);
assert!(
!mcp_resp.suppress,
"non-notification response must not be suppressed"
);
assert_eq!(mcp_resp.id, Some(json!(7)));
}
}