use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
pub mod openrpc;
pub mod service;
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");
}
}