Files
mesh-sdk/rust/src/lib.rs
T

81 lines
3.8 KiB
Rust

//! 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
//! `<seat>.<verb>` 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<String, Value>) -> Result<Value, String>,
}
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<Value> = 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(())
}