use anyhow::{Context, Result};
use serde_json::{Value, json};
use std::sync::Arc;
use crate::rpc::transport::Transport;
pub fn new_id() -> String {
uuid::Uuid::new_v4().to_string()
}
pub fn extract_result(resp: Value) -> Result<Value> {
if let Some(error) = resp.get("error") {
let code = error.get("code").and_then(|c| c.as_i64()).unwrap_or(-1);
let message = error
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("unknown error");
anyhow::bail!("JSON-RPC error {code}: {message}");
}
Ok(resp.get("result").cloned().unwrap_or(resp))
}
pub struct RpcClient {
transport: Arc<dyn Transport>,
}
impl RpcClient {
pub fn new(transport: Arc<dyn Transport>) -> Self {
Self { transport }
}
pub async fn initialize(&self) -> Result<Value> {
let req = json!({
"jsonrpc": "2.0",
"id": new_id(),
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {
"name": "trpc",
"version": env!("CARGO_PKG_VERSION"),
}
}
});
let resp = self
.transport
.send(req)
.await
.context("sending initialize")?;
let result = extract_result(resp)?;
let notif = json!({
"jsonrpc": "2.0",
"method": "notifications/initialized",
"params": {}
});
let _ = self.transport.send(notif).await;
Ok(result)
}
pub async fn tools_list(&self) -> Result<Value> {
self.request("tools/list", Some(json!({}))).await
}
pub async fn tools_call(&self, name: &str, args: Value) -> Result<Value> {
self.request("tools/call", Some(json!({"name": name, "arguments": args})))
.await
}
pub async fn request(&self, method: &str, params: Option<Value>) -> Result<Value> {
let req = json!({
"jsonrpc": "2.0",
"id": new_id(),
"method": method,
"params": params.unwrap_or_else(|| json!({})),
});
let resp = self
.transport
.send(req)
.await
.with_context(|| format!("sending {method}"))?;
extract_result(resp)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_result_returns_inner_result() {
let resp = json!({"jsonrpc": "2.0", "id": 1, "result": {"ok": true}});
let out = extract_result(resp).unwrap();
assert_eq!(out, json!({"ok": true}));
}
#[test]
fn extract_result_bails_on_error_object() {
let resp = json!({
"jsonrpc": "2.0",
"id": 1,
"error": {"code": -32601, "message": "Method not found"}
});
let err = extract_result(resp).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("-32601"), "msg = {msg}");
assert!(msg.contains("Method not found"), "msg = {msg}");
}
#[test]
fn extract_result_passthrough_when_no_result_or_error() {
let resp = json!({"jsonrpc": "2.0", "id": 1});
let out = extract_result(resp.clone()).unwrap();
assert_eq!(out, resp);
}
#[test]
fn new_id_is_unique() {
let a = new_id();
let b = new_id();
assert_ne!(a, b);
}
}