use serde_json::{json, Value};
const PROTOCOL_VERSION: &str = "2024-11-05";
fn tool_defs() -> Value {
json!([
{
"name": "identify",
"description": "Identify a timestamp VALUE across every format — ranked, scored, cited readings (a raw value is usually underdetermined, so this returns all plausible readings, not one verdict).",
"inputSchema": { "type": "object", "properties": { "value": { "type": "string", "description": "the raw value (integer, float, or hex/string form)" } }, "required": ["value"] }
},
{
"name": "decode",
"description": "Decode a value under ONE known format id (see the `explain` tool or the format list).",
"inputSchema": { "type": "object", "properties": { "format": { "type": "string" }, "value": { "type": "string" } }, "required": ["format", "value"] }
},
{
"name": "explain",
"description": "Explain a format: a spec card (epoch, tick unit, tz/leap semantics, valid range, known sentinels, citation).",
"inputSchema": { "type": "object", "properties": { "format": { "type": "string" } }, "required": ["format"] }
}
])
}
fn call_tool(params: Option<&Value>) -> Result<Value, (i64, &'static str)> {
let params = params.ok_or((-32602, "missing params"))?;
let name = params
.get("name")
.and_then(Value::as_str)
.ok_or((-32602, "missing tool name"))?;
let args = params
.get("arguments")
.cloned()
.unwrap_or_default();
let str_arg = |k: &str| args.get(k).and_then(Value::as_str).map(str::to_owned);
let text = match name {
"identify" => {
let value = str_arg("value").ok_or((-32602, "missing 'value'"))?;
crate::interpret::identify_json(&value)
}
"explain" => {
let format = str_arg("format").ok_or((-32602, "missing 'format'"))?;
crate::interpret::explain(&format).ok_or((-32602, "unknown format id"))?
}
"decode" => {
let format = str_arg("format").ok_or((-32602, "missing 'format'"))?;
let value = str_arg("value").ok_or((-32602, "missing 'value'"))?;
let f = crate::format(&format).map_err(|_| (-32602, "unknown format id"))?;
let inst = if let Ok(v) = value.parse::<i64>() {
f.decode_int(v)
} else if let Ok(v) = value.parse::<f64>() {
f.decode_float(v)
} else {
return Err((-32602, "value is not numeric"));
};
inst.ok()
.and_then(crate::PosixNs::to_rfc3339)
.ok_or((-32602, "value out of decodable range"))?
}
_ => return Err((-32601, "unknown tool")),
};
Ok(json!({ "content": [ { "type": "text", "text": text } ] }))
}
#[must_use]
pub fn handle(request: &str) -> Option<String> {
let req: Value = serde_json::from_str(request).ok()?;
let id = req.get("id").cloned()?;
let method = req.get("method").and_then(Value::as_str).unwrap_or("");
let outcome: Result<Value, (i64, &'static str)> = match method {
"initialize" => Ok(json!({
"protocolVersion": PROTOCOL_VERSION,
"capabilities": { "tools": {} },
"serverInfo": { "name": "timeglyph", "version": env!("CARGO_PKG_VERSION") },
})),
"tools/list" => Ok(json!({ "tools": tool_defs() })),
"tools/call" => call_tool(req.get("params")),
_ => Err((-32601, "method not found")),
};
Some(match outcome {
Ok(result) => json!({ "jsonrpc": "2.0", "id": id, "result": result }).to_string(),
Err((code, message)) => {
json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } })
.to_string()
}
})
}