use std::io::{BufRead, Write};
use omgbase_surface::Surface;
use serde_json::{Value as Json, json};
pub const PROTOCOL_VERSION: &str = "2024-11-05";
const PARSE_ERROR: i64 = -32700;
const INVALID_REQUEST: i64 = -32600;
const METHOD_NOT_FOUND: i64 = -32601;
const INVALID_PARAMS: i64 = -32602;
fn response(id: &Json, result: Json) -> Json {
json!({ "jsonrpc": "2.0", "id": id, "result": result })
}
fn error(id: &Json, code: i64, message: &str) -> Json {
json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } })
}
pub fn handle(surface: &mut Surface, version: &str, req: &Json) -> Option<Json> {
let Some(obj) = req.as_object() else {
return Some(error(
&Json::Null,
INVALID_REQUEST,
"request must be an object",
));
};
let id = obj.get("id").cloned();
let method = obj.get("method").and_then(Json::as_str).unwrap_or("");
let params = obj.get("params").cloned().unwrap_or(Json::Null);
if method.starts_with("notifications/") {
return None;
}
let id = id?;
Some(match method {
"initialize" => response(
&id,
json!({
"protocolVersion": params.get("protocolVersion").cloned().unwrap_or_else(|| json!(PROTOCOL_VERSION)),
"capabilities": { "tools": {} },
"serverInfo": { "name": "omgbase", "version": version },
}),
),
"ping" => response(&id, json!({})),
"tools/list" => response(
&id,
json!({
"tools": surface.tools().iter().map(|t| json!({
"name": t.name,
"description": t.description,
"inputSchema": t.input_schema,
})).collect::<Vec<_>>(),
}),
),
"tools/call" => {
let Some(name) = params.get("name").and_then(Json::as_str) else {
return Some(error(&id, INVALID_PARAMS, "tools/call needs a tool `name`"));
};
if !surface.tools().iter().any(|t| t.name == name) {
return Some(error(&id, INVALID_PARAMS, &format!("unknown tool {name}")));
}
let args = params
.get("arguments")
.cloned()
.unwrap_or_else(|| json!({}));
let out = surface.call(name, args);
let mut result = json!({
"content": [{ "type": "text", "text": out.body.to_string() }],
});
if out.is_error {
result["isError"] = Json::Bool(true);
}
response(&id, result)
}
other => error(&id, METHOD_NOT_FOUND, &format!("method not found: {other}")),
})
}
pub fn serve(surface: &mut Surface, version: &str) -> std::io::Result<()> {
let stdin = std::io::stdin();
let stdout = std::io::stdout();
let mut out = stdout.lock();
for line in stdin.lock().lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
let reply = match serde_json::from_str::<Json>(&line) {
Ok(req) => handle(surface, version, &req),
Err(e) => Some(error(
&Json::Null,
PARSE_ERROR,
&format!("parse error: {e}"),
)),
};
if let Some(r) = reply {
writeln!(out, "{r}")?;
out.flush()?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use omgbase_store::Store;
fn surface() -> Surface {
let mut store = Store::open_in_memory().unwrap();
let repo = store.create_repo("r").unwrap();
Surface::new(store, &repo, None)
}
#[test]
fn initialize_list_call_ping() {
let mut s = surface();
let init = handle(&mut s, "0.1.0", &json!({ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-03-26" } })).unwrap();
assert_eq!(init["result"]["protocolVersion"], "2025-03-26");
assert_eq!(init["result"]["serverInfo"]["name"], "omgbase");
assert!(
handle(
&mut s,
"0.1.0",
&json!({ "jsonrpc": "2.0", "method": "notifications/initialized" })
)
.is_none()
);
let list = handle(
&mut s,
"0.1.0",
&json!({ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }),
)
.unwrap();
assert!(
list["result"]["tools"]
.as_array()
.unwrap()
.iter()
.any(|t| t["name"] == "query")
);
let call = handle(&mut s, "0.1.0", &json!({ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "docs_list", "arguments": {} } })).unwrap();
let text = call["result"]["content"][0]["text"].as_str().unwrap();
assert_eq!(
serde_json::from_str::<Json>(text).unwrap()["items"],
json!([])
);
assert!(call["result"].get("isError").is_none());
let bad = handle(&mut s, "0.1.0", &json!({ "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": { "name": "docs_read", "arguments": { "doc": "nope" } } })).unwrap();
assert_eq!(bad["result"]["isError"], true);
let ping = handle(
&mut s,
"0.1.0",
&json!({ "jsonrpc": "2.0", "id": 5, "method": "ping" }),
)
.unwrap();
assert_eq!(ping["result"], json!({}));
let nf = handle(
&mut s,
"0.1.0",
&json!({ "jsonrpc": "2.0", "id": 6, "method": "resources/list" }),
)
.unwrap();
assert_eq!(nf["error"]["code"], METHOD_NOT_FOUND);
}
}