//! 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 { 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 = 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 = 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 = 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 = 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 = 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 = 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 }) }