227 lines
8.7 KiB
Rust
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 })
|
|
} |