use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
pub mod daemon_bridge;
pub mod openrpc;
pub mod service;
pub use daemon_bridge::{DaemonBridgeConfig, ensure_daemon_up};
pub use service::ServiceDescriptor;
pub mod error_codes {
pub const PARSE_ERROR: i32 = -32700;
pub const INVALID_REQUEST: i32 = -32600;
pub const METHOD_NOT_FOUND: i32 = -32601;
pub const INVALID_PARAMS: i32 = -32602;
pub const INTERNAL_ERROR: i32 = -32603;
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Request {
#[serde(default)]
pub jsonrpc: Option<String>,
#[serde(default)]
pub id: Option<Value>,
pub method: String,
#[serde(default)]
pub params: Option<Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Response {
pub jsonrpc: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<JsonRpcError>,
#[serde(skip)]
pub suppress: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct JsonRpcError {
pub code: i32,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
impl Response {
pub fn ok(id: Option<Value>, result: Value) -> Self {
Self {
jsonrpc: "2.0".into(),
id,
result: Some(result),
error: None,
suppress: false,
}
}
pub fn err(id: Option<Value>, code: i32, message: impl Into<String>) -> Self {
Self {
jsonrpc: "2.0".into(),
id,
result: None,
error: Some(JsonRpcError {
code,
message: message.into(),
data: None,
}),
suppress: false,
}
}
pub fn suppressed() -> Self {
Self {
jsonrpc: "2.0".into(),
id: None,
result: None,
error: None,
suppress: true,
}
}
}
pub fn initialize_response(server_name: &str, version: &str, extra: Option<Value>) -> Value {
let mut server_info = json!({
"name": server_name,
"version": version,
});
if let Some(Value::Object(map)) = extra
&& let Some(obj) = server_info.as_object_mut()
{
for (k, v) in map {
obj.insert(k, v);
}
}
json!({
"protocolVersion": "2024-11-05",
"capabilities": { "tools": {} },
"serverInfo": server_info,
})
}
pub async fn run_stdio_loop<F, Fut>(dispatcher: F) -> anyhow::Result<()>
where
F: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Response> + Send,
{
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
let stdin = tokio::io::stdin();
let mut stdout = tokio::io::stdout();
let mut reader = BufReader::new(stdin).lines();
while let Some(line) = reader.next_line().await? {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let response = match serde_json::from_str::<Request>(trimmed) {
Ok(req) => dispatcher(req).await,
Err(e) => Response::err(
None,
error_codes::PARSE_ERROR,
format!("invalid JSON-RPC: {e}"),
),
};
if response.suppress {
continue;
}
let serialised = serde_json::to_string(&response)?;
stdout.write_all(serialised.as_bytes()).await?;
stdout.write_all(b"\n").await?;
stdout.flush().await?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_codes_are_spec_values() {
assert_eq!(error_codes::PARSE_ERROR, -32700);
assert_eq!(error_codes::INVALID_REQUEST, -32600);
assert_eq!(error_codes::METHOD_NOT_FOUND, -32601);
assert_eq!(error_codes::INVALID_PARAMS, -32602);
assert_eq!(error_codes::INTERNAL_ERROR, -32603);
}
#[test]
fn request_deserialises_without_params() {
let r: Request =
serde_json::from_str(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#).unwrap();
assert_eq!(r.method, "ping");
assert!(r.params.is_none());
}
#[test]
fn ok_response_round_trips() {
let r = Response::ok(Some(json!(7)), json!({"ok": true}));
let s = serde_json::to_string(&r).unwrap();
assert!(s.contains("\"jsonrpc\":\"2.0\""));
assert!(s.contains("\"id\":7"));
assert!(s.contains("\"ok\":true"));
assert!(!s.contains("\"error\""));
}
#[test]
fn err_response_carries_code_and_message() {
let r = Response::err(Some(json!(1)), error_codes::METHOD_NOT_FOUND, "boom");
let err = r.error.unwrap();
assert_eq!(err.code, error_codes::METHOD_NOT_FOUND);
assert_eq!(err.message, "boom");
}
#[test]
fn suppressed_response_marks_flag() {
let r = Response::suppressed();
assert!(r.suppress);
}
#[test]
fn initialize_response_has_required_fields() {
let v = initialize_response("trusty-x", "9.9.9", None);
assert_eq!(v["protocolVersion"], "2024-11-05");
assert!(v["capabilities"]["tools"].is_object());
assert_eq!(v["serverInfo"]["name"], "trusty-x");
assert_eq!(v["serverInfo"]["version"], "9.9.9");
}
#[test]
fn initialize_response_merges_extra_server_info() {
let extra = json!({ "default_palace": "myproj" });
let v = initialize_response("trusty-memory", "1.0", Some(extra));
assert_eq!(v["serverInfo"]["default_palace"], "myproj");
assert_eq!(v["serverInfo"]["name"], "trusty-memory");
}
#[tokio::test]
async fn stdio_loop_dispatches_and_suppresses_notifications() {
use tokio::io::AsyncWriteExt;
let (mut client_tx, server_rx) = tokio::io::duplex(4096);
let (server_tx, client_rx) = tokio::io::duplex(4096);
let normal = r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#;
let notification = r#"{"jsonrpc":"2.0","method":"ping"}"#;
client_tx
.write_all(format!("{normal}\n{notification}\n").as_bytes())
.await
.unwrap();
drop(client_tx);
let fut = run_stdio_loop_with_io(
|req| async move {
if req.id.is_none() {
Response::suppressed()
} else {
Response::ok(req.id, json!({"method": req.method}))
}
},
server_rx,
server_tx,
);
fut.await.expect("loop should return Ok on EOF");
drop(client_rx); }
#[tokio::test]
async fn stdio_loop_exits_on_eof() {
use tokio::io;
let stdin = io::empty();
let stdout = io::sink();
let result = run_stdio_loop_with_io(
|_req| async { Response::ok(None, json!(null)) },
stdin,
stdout,
)
.await;
assert!(
result.is_ok(),
"run_stdio_loop must return Ok on EOF, got: {result:?}"
);
}
async fn run_stdio_loop_with_io<F, Fut, R, W>(
dispatcher: F,
reader: R,
mut writer: W,
) -> anyhow::Result<()>
where
F: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Response> + Send,
R: tokio::io::AsyncRead + Unpin,
W: tokio::io::AsyncWrite + Unpin,
{
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
let mut lines = BufReader::new(reader).lines();
while let Some(line) = lines.next_line().await? {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let response = match serde_json::from_str::<Request>(trimmed) {
Ok(req) => dispatcher(req).await,
Err(e) => Response::err(None, error_codes::PARSE_ERROR, format!("{e}")),
};
if response.suppress {
continue;
}
let serialised = serde_json::to_string(&response)?;
writer.write_all(serialised.as_bytes()).await?;
writer.write_all(b"\n").await?;
writer.flush().await?;
}
Ok(())
}
}