//! The Novox Mesh SDK for Rust — the skeleton (novox/hq ADR 0188). //! //! A tools bundle is a process the node's runtime launches and speaks MCP over stdio to: //! `initialize`, `tools/list` once, `tools/call` per call, newline-framed JSON-RPC on stdin and //! stdout. This crate is the loop, so a bundle writes its tools and nothing else. A tool named //! `.` is the module's implementation of that seat's verb; any other name is the //! module's own. //! //! No transport here: the bus, the subjects and the memberships are the runtime's (ADR 0039, //! 0175). stdout is the protocol; log to stderr. use serde_json::{json, Map, Value}; use std::io::{self, BufRead, Write}; /// The MCP version this speaks; the runtime says the same. pub const PROTOCOL: &str = "2025-03-26"; /// One tool a bundle serves. `input` is the JSON schema of the arguments, or the bare property /// map; `run` answers with any JSON or an error message. pub struct Tool { pub name: &'static str, pub description: &'static str, pub input: Value, pub run: fn(&Map) -> Result, } fn as_schema(given: &Value) -> Value { match given.as_object() { None => json!({"type": "object", "properties": {}}), Some(m) if m.is_empty() => json!({"type": "object", "properties": {}}), Some(m) if m.contains_key("type") || m.contains_key("properties") => given.clone(), Some(_) => json!({"type": "object", "properties": given}), } } /// Serve these tools to the runtime over stdio until stdin closes. pub fn serve(name: &str, tools: &[Tool]) -> io::Result<()> { let stdin = io::stdin(); let mut out = io::stdout(); let mut say = |m: Value| -> io::Result<()> { let mut m = m; m["jsonrpc"] = json!("2.0"); writeln!(out, "{}", m)?; out.flush() }; for line in stdin.lock().lines() { let line = line?; let req: Value = match serde_json::from_str(line.trim()) { Ok(v) => v, Err(_) => continue, }; let id = req.get("id").cloned().unwrap_or(Value::Null); let method = req.get("method").and_then(Value::as_str).unwrap_or(""); let params = req.get("params").cloned().unwrap_or(json!({})); match method { "initialize" => say(json!({"id": id, "result": {"protocolVersion": PROTOCOL, "capabilities": {"tools": {}}, "serverInfo": {"name": name, "version": "1"}}}))?, "notifications/initialized" => {} "ping" => { if !id.is_null() { say(json!({"id": id, "result": {}}))? } } "tools/list" => { let listed: Vec = tools.iter().map(|t| json!({ "name": t.name, "description": t.description, "inputSchema": as_schema(&t.input)})).collect(); say(json!({"id": id, "result": {"tools": listed}}))? } "tools/call" => { let wanted = params.get("name").and_then(Value::as_str).unwrap_or(""); let empty = Map::new(); let args = params.get("arguments").and_then(Value::as_object).unwrap_or(&empty); match tools.iter().find(|t| t.name == wanted) { None => say(json!({"id": id, "error": {"code": -32602, "message": format!("{} has no tool {}", name, wanted)}}))?, Some(t) => match (t.run)(args) { Ok(v) => say(json!({"id": id, "result": {"content": [{"type": "text", "text": v.to_string()}]}}))?, Err(e) => say(json!({"id": id, "result": {"content": [{"type": "text", "text": e}], "isError": true}}))?, }, } } _ => { if !id.is_null() { say(json!({"id": id, "error": {"code": -32601, "message": format!("no {}", method)}}))? } } } } Ok(()) }