81 lines
3.8 KiB
Rust
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(())
|
|
}
|