MCP-Relational-Data/src/daemon.rs

227 lines
8.7 KiB
Rust

//! Daemon mode — stdin/stdout JSON API.
//!
//! Pipe-friendly protocol: one JSON object per line.
//! Suitable for MCP server wrappers, shell scripts, and inter-process calls.
//!
//! # Protocol
//!
//! ```text
//! Request: {"tool":"ohm","current":2,"resistance":4}
//! Response: {"tool":"ohm","power":"16 Watts(W)","current":"2 Amps(A)",...}
//!
//! {"tool":"list"} → available tools
//! {"tool":"help","name":"ohm"} → parameter docs
//! {"tool":"ohm","current":2,"resistance":4} → calculate
//! ```
use crate::calc::*;
use serde_json::{json, Value};
use std::collections::BTreeMap;
use std::io::{self, BufRead, Write};
/// Start the daemon event loop. Blocks until stdin closes.
pub fn run() {
eprintln!("MCP-Relational-Data daemon v2.1.0 — stdin/stdout JSON");
eprintln!(" {{\"tool\":\"list\"}} available tools");
eprintln!(" {{\"tool\":\"help\",\"name\":\"ohm\"}} parameter docs");
eprintln!(" {{\"tool\":\"ohm\",...}} run a calculation");
let stdin = io::stdin();
let mut stdout = io::stdout();
for line in stdin.lock().lines() {
let input = match line {
Ok(l) => l,
Err(e) => {
eprintln!("stdin error: {}", e);
break;
}
};
let trimmed = input.trim();
if trimmed.is_empty() {
continue;
}
let response = handle(trimmed);
let output = serde_json::to_string(&response).unwrap_or_else(|e| {
json!({"error": e.to_string()}).to_string()
});
let _ = writeln!(stdout, "{}", output);
let _ = stdout.flush();
}
}
// ── dispatch ─────────────────────────────────────────────────────────
fn handle(input: &str) -> Value {
let req: Value = match serde_json::from_str(input) {
Ok(v) => v,
Err(e) => return json!({"error": format!("invalid JSON: {}", e)}),
};
let tool = match req.get("tool").and_then(|v| v.as_str()) {
Some(t) => t,
None => return json!({"error": "missing \"tool\" field"}),
};
match tool {
"list" => json!({"tools": [
"ohm", "margin", "roi",
"token_cost", "electricity", "break_even"
]}),
"help" => tool_help(
req.get("name").and_then(|v| v.as_str()).unwrap_or(""),
),
"ohm" => do_ohm(&req),
"margin" => do_margin(&req),
"roi" => do_roi(&req),
"token_cost" => do_token(&req),
"electricity" => do_electricity(&req),
"break_even" => do_break_even(&req),
_ => json!({"error": format!(
"unknown tool: \"{}\". Send {{\"tool\":\"list\"}} for options.", tool
)}),
}
}
// ── per-tool handlers ────────────────────────────────────────────────
fn get_f64(req: &Value, key: &str) -> Option<f64> {
req.get(key).and_then(|v| v.as_f64())
}
fn do_ohm(req: &Value) -> Value {
let p = get_f64(req, "power").unwrap_or(0.0);
let i = get_f64(req, "current").unwrap_or(0.0);
let r = get_f64(req, "resistance").unwrap_or(0.0);
let v = get_f64(req, "voltage").unwrap_or(0.0);
if p < 0.0 || r < 0.0 {
return json!({"error": "power and resistance must be non-negative"});
}
let (p, i, r, v) = ohm_calculate(p, i, r, v);
let mut m: BTreeMap<String, Value> = BTreeMap::new();
m.insert("tool".into(), json!("ohm"));
m.insert("power".into(), json!(fmt_eng(p, "Watts(W)")));
m.insert("current".into(), json!(fmt_eng(i, "Amps(A)")));
m.insert("resistance".into(), json!(fmt_eng(r, "Ohms(\u{03A9})")));
m.insert("voltage".into(), json!(fmt_eng(v, "Volts(V)")));
json!(m)
}
fn do_margin(req: &Value) -> Value {
let cost = get_f64(req, "cost").unwrap_or(0.0);
let sell = get_f64(req, "sell").unwrap_or(0.0);
let (profit, margin, markup) = margin_calculate(cost, sell);
let mut m: BTreeMap<String, Value> = BTreeMap::new();
m.insert("tool".into(), json!("margin"));
m.insert("profit".into(), json!(fmt_num(profit)));
m.insert("margin".into(), json!(fmt_pct(margin)));
m.insert("markup".into(), json!(fmt_pct(markup)));
json!(m)
}
fn do_roi(req: &Value) -> Value {
let inv = get_f64(req, "investment").unwrap_or(0.0);
let rev = get_f64(req, "revenue").unwrap_or(0.0);
let (profit, roi) = roi_calculate(inv, rev);
let mut m: BTreeMap<String, Value> = BTreeMap::new();
m.insert("tool".into(), json!("roi"));
m.insert("profit".into(), json!(fmt_num(profit)));
m.insert("roi".into(), json!(fmt_pct(roi)));
json!(m)
}
fn do_token(req: &Value) -> Value {
let model_name = req.get("model").and_then(|v| v.as_str()).unwrap_or("gpt-4o");
let model = parse_model(model_name).unwrap_or(AiModel::Gpt4o);
let in_tok = get_f64(req, "input_tokens").unwrap_or(0.0);
let out_tok = get_f64(req, "output_tokens").unwrap_or(0.0);
let (in_cost, out_cost, total) = token_cost_calculate(model, in_tok, out_tok);
let mut m: BTreeMap<String, Value> = BTreeMap::new();
m.insert("tool".into(), json!("token_cost"));
m.insert("model".into(), json!(model.label()));
m.insert("input_cost".into(), json!(fmt_dollar(in_cost)));
m.insert("output_cost".into(), json!(fmt_dollar(out_cost)));
m.insert("total_cost".into(), json!(fmt_dollar(total)));
json!(m)
}
fn do_electricity(req: &Value) -> Value {
let watts = get_f64(req, "watts").unwrap_or(0.0);
let hours = get_f64(req, "hours").unwrap_or(0.0);
let days = get_f64(req, "days").unwrap_or(30.0);
let rate = get_f64(req, "rate").unwrap_or(0.0);
let (daily, monthly_kwh, monthly_cost, yearly) =
electricity_calculate(watts, hours, days, rate);
let mut m: BTreeMap<String, Value> = BTreeMap::new();
m.insert("tool".into(), json!("electricity"));
m.insert("daily_kwh".into(), json!(fmt_eng(daily, "kWh")));
m.insert("monthly_kwh".into(), json!(fmt_eng(monthly_kwh, "kWh")));
m.insert("monthly_cost".into(), json!(fmt_dollar(monthly_cost)));
m.insert("yearly_cost".into(), json!(fmt_dollar(yearly)));
json!(m)
}
fn do_break_even(req: &Value) -> Value {
let fixed = get_f64(req, "fixed").unwrap_or(0.0);
let price = get_f64(req, "price").unwrap_or(0.0);
let variable = get_f64(req, "variable").unwrap_or(0.0);
let (units, revenue) = break_even_calculate(fixed, price, variable);
let mut m: BTreeMap<String, Value> = BTreeMap::new();
m.insert("tool".into(), json!("break_even"));
m.insert("units".into(), json!(fmt_num(units)));
m.insert("revenue".into(), json!(fmt_dollar(revenue)));
json!(m)
}
// ── help metadata ────────────────────────────────────────────────────
fn tool_help(name: &str) -> Value {
let (desc, params) = match name {
"ohm" => (
"Ohm's Law & Watts Law — enter any 2 of 4 values to solve for the other 2. \
Applies to circuit design, audio impedance matching, MOSFET PSU design, \
and LED driver sizing.",
vec!["power", "current", "resistance", "voltage"],
),
"margin" => (
"Margin & Markup — calculate profit, margin %, and markup % \
from cost and selling price.",
vec!["cost", "sell"],
),
"roi" => (
"Return on Investment — measure profitability of ad spend, \
marketing campaigns, and capital expenditures.",
vec!["investment", "revenue"],
),
"token_cost" => (
"AI Token Cost — estimate LLM API costs per million tokens \
(GPT-4o, Claude 3.5 Sonnet, etc.).",
vec!["model", "input_tokens", "output_tokens"],
),
"electricity" => (
"Electricity Cost — project monthly and yearly power costs \
for GPU servers, datacenter racks, and mining rigs.",
vec!["watts", "hours", "days", "rate"],
),
"break_even" => (
"Break-Even Analysis — how many units must you sell to cover \
all fixed and variable costs?",
vec!["fixed", "price", "variable"],
),
_ => return json!({"error": format!(
"unknown tool: \"{}\". Send {{\"tool\":\"list\"}} for options.", name
)}),
};
json!({ "name": name, "description": desc, "params": params })
}