1use std::fmt::Write as _;
8use std::io::Read;
9
10use serde_json::{Map, Value, json};
11
12use super::Rail;
13use super::call::{Outcome, request};
14use crate::mcp::tools::ToolResult;
15
16#[must_use]
18pub fn tool_entries() -> Vec<Value> {
19 let id = json!({
20 "type": "object",
21 "required": ["id"],
22 "properties": {
23 "id": { "type": "string", "description": "the service id, svc-..." },
24 "operation": { "type": "string", "description": "an operation id, so a request sent again does nothing twice" }
25 },
26 "additionalProperties": false
27 });
28 vec![
29 json!({
30 "name": "services",
31 "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.",
32 "inputSchema": { "type": "object", "properties": {}, "additionalProperties": false }
33 }),
34 json!({
35 "name": "register",
36 "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.",
37 "inputSchema": {
38 "type": "object",
39 "required": ["template", "name"],
40 "properties": {
41 "template": { "type": "string" },
42 "name": { "type": "string", "description": "one segment: letters, digits, '-' or '_'" },
43 "operation": { "type": "string" }
44 },
45 "additionalProperties": false
46 }
47 }),
48 json!({
49 "name": "start",
50 "description": "Start an asleep or held service's process; it is live once its worker registers. Lys decides (deploy_service on the service).",
51 "inputSchema": id
52 }),
53 json!({
54 "name": "stop",
55 "description": "Stop a running service and hold it; it can be started again. Lys decides (deploy_service on the service).",
56 "inputSchema": id
57 }),
58 json!({
59 "name": "retire",
60 "description": "Stop a service for good. It is kept with its history and never started again. Lys decides (deploy_service on the service).",
61 "inputSchema": id
62 }),
63 ]
64}
65
66pub fn tool<R: Rail>(rail: &R, name: &str, arguments: &Map<String, Value>) -> Option<ToolResult> {
68 let text = |field: &str| arguments.get(field).and_then(Value::as_str);
69 let operation =
70 match text("operation").map_or_else(fresh_operation, |given| Ok(given.to_owned())) {
71 Ok(operation) => operation,
72 Err(words) => return Some(ToolResult::failed(&words, json!({ "tool": name }))),
73 };
74 let answer = match name {
75 "services" => send(rail, "list", &operation, &Value::Null),
76 "register" => match (text("template"), text("name")) {
77 (Some(template), Some(service)) => send(
78 rail,
79 "create",
80 &operation,
81 &json!({ "template": template, "name": service }),
82 ),
83 _ => Err("register needs template and name".to_owned()),
84 },
85 "start" | "stop" | "retire" => text("id").map_or_else(
86 || Err(format!("{name} needs id")),
87 |id| {
88 let body = json!({ "id": id });
89 match name {
90 "start" => send(rail, "deploy", &operation, &body).or_else(|refusal| {
91 if refusal.contains("not_now") && refusal.contains("Held") {
92 send(rail, "wake", &operation, &body)
93 } else {
94 Err(refusal)
95 }
96 }),
97 "stop" => send(rail, "hold", &operation, &body),
98 _ => send(rail, "retire", &operation, &body),
99 }
100 },
101 ),
102 _ => return None,
103 };
104 Some(match answer {
105 Ok(reply) => ToolResult::ok(reply),
106 Err(words) => ToolResult::failed(&words, json!({ "tool": name })),
107 })
108}
109
110fn send<R: Rail>(rail: &R, verb: &str, operation: &str, body: &Value) -> Result<Value, String> {
112 let input = json!({ "operation": operation, "payload": body });
113 match request(rail, &format!("services.{verb}"), &input, true, None) {
114 Outcome::Reply(reply) => Ok(reply),
115 Outcome::Sent => Err("the server sent no reply".to_owned()),
116 Outcome::Refused(words) => Err(words),
117 }
118}
119
120fn fresh_operation() -> Result<String, String> {
123 let mut bytes = [0_u8; 16];
124 std::fs::File::open("/dev/urandom")
125 .and_then(|mut source| source.read_exact(&mut bytes))
126 .map_err(|error| format!("no operation id could be made: {}", error.kind()))?;
127 Ok(bytes.iter().fold(String::from("op-"), |mut out, byte| {
128 let _infallible = write!(out, "{byte:02x}");
129 out
130 }))
131}
132
133#[cfg(test)]
134#[path = "lifecycle_tests.rs"]
135mod tests;