use std::io::{self, BufRead, Write};
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::thread;
use plugmem_host::Database;
use serde_json::{Value, json};
use crate::tools::{ReaderShared, WorkspaceShared};
use crate::{messages, tools};
#[derive(Clone)]
pub enum Shared {
Writer(Database),
Reader(Arc<ReaderShared>),
Workspace(Arc<WorkspaceShared>),
}
pub fn serve(shared: Shared, workers: usize) {
let (tx, rx) = mpsc::channel::<String>();
let rx = Arc::new(Mutex::new(rx));
let out = Arc::new(Mutex::new(io::stdout()));
let mut handles = Vec::with_capacity(workers);
for _ in 0..workers.max(1) {
let rx = Arc::clone(&rx);
let out = Arc::clone(&out);
let shared = shared.clone();
handles.push(thread::spawn(move || worker(&shared, &rx, &out)));
}
let stdin = io::stdin();
for line in stdin.lock().lines() {
let Ok(line) = line else { break };
if line.trim().is_empty() {
continue;
}
if tx.send(line).is_err() {
break; }
}
drop(tx);
for h in handles {
let _ = h.join();
}
}
fn worker(shared: &Shared, rx: &Mutex<mpsc::Receiver<String>>, out: &Mutex<io::Stdout>) {
loop {
let line = {
let rx = rx.lock().expect("receiver lock");
match rx.recv() {
Ok(line) => line,
Err(_) => break, }
};
let Ok(req) = serde_json::from_str::<Value>(&line) else {
continue; };
if let Some(response) = handle(shared, &req) {
let mut out = out.lock().expect("stdout lock");
let _ = writeln!(out, "{response}");
let _ = out.flush();
}
}
}
fn handle(shared: &Shared, req: &Value) -> Option<Value> {
let method = req.get("method")?.as_str()?;
let id = req.get("id").cloned();
match method {
"initialize" => id.map(|id| {
result(
id,
json!({
"protocolVersion": messages::PROTOCOL_VERSION,
"capabilities": { "tools": {} },
"serverInfo": { "name": messages::SERVER_NAME, "version": env!("CARGO_PKG_VERSION") }
}),
)
}),
"notifications/initialized" => None,
"ping" => id.map(|id| result(id, json!({}))),
"tools/list" => id.map(|id| {
let tools = match shared {
Shared::Writer(_) => tools::definitions(),
Shared::Reader(_) => tools::definitions_ro(),
Shared::Workspace(ws) => tools::definitions_ws(ws.default_db()),
};
result(id, json!({ "tools": tools }))
}),
"tools/call" => id.map(|id| {
let params = req.get("params");
match shared {
Shared::Writer(db) => tools::call(db, id, params),
Shared::Reader(reader) => tools::call_ro(reader, id, params),
Shared::Workspace(ws) => tools::call_ws(ws, id, params),
}
}),
_ => id.map(|id| error(id, -32601, "method not found")),
}
}
pub fn result(id: Value, result: Value) -> Value {
json!({ "jsonrpc": "2.0", "id": id, "result": result })
}
pub fn error(id: Value, code: i64, message: &str) -> Value {
json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } })
}
pub fn tool_result(id: Value, text: String, is_error: bool) -> Value {
result(
id,
json!({ "content": [{ "type": "text", "text": text }], "isError": is_error }),
)
}
#[cfg(test)]
mod tests {
use super::*;
use plugmem_host::{Config, Database};
fn writer() -> Shared {
let dir = std::env::temp_dir().join(format!(
"plugmem-mcp-rpc-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
let (db, _) = Database::open(dir.join("m.plugmem"), Config::default()).unwrap();
Shared::Writer(db)
}
fn req(s: &str) -> Value {
serde_json::from_str(s).unwrap()
}
#[test]
fn handle_dispatches_the_protocol_methods() {
let s = writer();
let init = handle(&s, &req(r#"{"id":1,"method":"initialize"}"#)).unwrap();
assert_eq!(init["result"]["serverInfo"]["name"], "plugmem");
assert_eq!(init["result"]["protocolVersion"], "2024-11-05");
assert!(handle(&s, &req(r#"{"id":2,"method":"ping"}"#)).unwrap()["result"].is_object());
let list = handle(&s, &req(r#"{"id":3,"method":"tools/list"}"#)).unwrap();
assert_eq!(list["result"]["tools"][0]["name"], "plugmem_remember");
let call = handle(
&s,
&req(r#"{"id":4,"method":"tools/call","params":{"name":"plugmem_stats","arguments":{}}}"#),
)
.unwrap();
assert_eq!(call["result"]["isError"], false);
assert!(handle(&s, &req(r#"{"method":"notifications/initialized"}"#)).is_none());
assert_eq!(
handle(&s, &req(r#"{"id":5,"method":"nope"}"#)).unwrap()["error"]["code"],
-32601
);
assert!(handle(&s, &req(r#"{"method":"nope"}"#)).is_none());
assert!(handle(&s, &req(r#"{"id":6}"#)).is_none());
}
#[test]
fn envelopes_have_the_jsonrpc_shape() {
let ok = result(json!(1), json!({"a": 1}));
assert_eq!(ok["jsonrpc"], "2.0");
assert_eq!(ok["result"]["a"], 1);
let err = error(json!(2), -32602, "boom");
assert_eq!(err["error"]["code"], -32602);
assert_eq!(err["error"]["message"], "boom");
let tr = tool_result(json!(3), "hi".into(), true);
assert_eq!(tr["result"]["content"][0]["text"], "hi");
assert_eq!(tr["result"]["isError"], true);
}
}