use std::fmt::Write as _;
use std::io::Read;
use serde_json::{Map, Value, json};
use super::Rail;
use super::call::{Outcome, request};
use crate::mcp::tools::ToolResult;
#[must_use]
pub fn tool_entries() -> Vec<Value> {
let id = json!({
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "string", "description": "the service id, svc-..." },
"operation": { "type": "string", "description": "an operation id, so a request sent again does nothing twice" }
},
"additionalProperties": false
});
vec![
json!({
"name": "services",
"description": "The templates services can be made from, and every service this seat created or may call, with its state (asleep, live, held, retired), subjects, whether its process runs, and its history.",
"inputSchema": { "type": "object", "properties": {}, "additionalProperties": false }
}),
json!({
"name": "register",
"description": "Create a service from one of the server's templates, named once per template. Lys decides whether this seat may (deploy_service on the network). The service starts asleep and serves <template subject>.<name> once started.",
"inputSchema": {
"type": "object",
"required": ["template", "name"],
"properties": {
"template": { "type": "string" },
"name": { "type": "string", "description": "one segment: letters, digits, '-' or '_'" },
"operation": { "type": "string" }
},
"additionalProperties": false
}
}),
json!({
"name": "start",
"description": "Start an asleep or held service's process; it is live once its worker registers. Lys decides (deploy_service on the service).",
"inputSchema": id
}),
json!({
"name": "stop",
"description": "Stop a running service and hold it; it can be started again. Lys decides (deploy_service on the service).",
"inputSchema": id
}),
json!({
"name": "retire",
"description": "Stop a service for good. It is kept with its history and never started again. Lys decides (deploy_service on the service).",
"inputSchema": id
}),
]
}
pub fn tool<R: Rail>(rail: &R, name: &str, arguments: &Map<String, Value>) -> Option<ToolResult> {
let text = |field: &str| arguments.get(field).and_then(Value::as_str);
let operation =
match text("operation").map_or_else(fresh_operation, |given| Ok(given.to_owned())) {
Ok(operation) => operation,
Err(words) => return Some(ToolResult::failed(&words, json!({ "tool": name }))),
};
let answer = match name {
"services" => send(rail, "list", &operation, &Value::Null),
"register" => match (text("template"), text("name")) {
(Some(template), Some(service)) => send(
rail,
"create",
&operation,
&json!({ "template": template, "name": service }),
),
_ => Err("register needs template and name".to_owned()),
},
"start" | "stop" | "retire" => text("id").map_or_else(
|| Err(format!("{name} needs id")),
|id| {
let body = json!({ "id": id });
match name {
"start" => send(rail, "deploy", &operation, &body).or_else(|refusal| {
if refusal.contains("not_now") && refusal.contains("Held") {
send(rail, "wake", &operation, &body)
} else {
Err(refusal)
}
}),
"stop" => send(rail, "hold", &operation, &body),
_ => send(rail, "retire", &operation, &body),
}
},
),
_ => return None,
};
Some(match answer {
Ok(reply) => ToolResult::ok(reply),
Err(words) => ToolResult::failed(&words, json!({ "tool": name })),
})
}
fn send<R: Rail>(rail: &R, verb: &str, operation: &str, body: &Value) -> Result<Value, String> {
let input = json!({ "operation": operation, "payload": body });
match request(rail, &format!("services.{verb}"), &input, true, None) {
Outcome::Reply(reply) => Ok(reply),
Outcome::Sent => Err("the server sent no reply".to_owned()),
Outcome::Refused(words) => Err(words),
}
}
fn fresh_operation() -> Result<String, String> {
let mut bytes = [0_u8; 16];
std::fs::File::open("/dev/urandom")
.and_then(|mut source| source.read_exact(&mut bytes))
.map_err(|error| format!("no operation id could be made: {}", error.kind()))?;
Ok(bytes.iter().fold(String::from("op-"), |mut out, byte| {
let _infallible = write!(out, "{byte:02x}");
out
}))
}
#[cfg(test)]
#[path = "lifecycle_tests.rs"]
mod tests;