liminal-cli 0.2.1

The liminal command: serve the network, maintain its stores, or serve its discovery MCP over stdio
Documentation
//! Tool schemas and result shaping for the ticket.

use serde_json::{Map, Value, json};

use super::framing::INVALID_PARAMS;
use super::negotiate::{Era, Refusal, shaped};
pub use super::server::Server;

/// How long a modern client may keep the tool table (`ttlMs`).
pub(super) const TOOLS_TTL_MS: u64 = 60_000;

/// The tool table as `tools/list` answers it.
#[must_use]
pub fn tool_table() -> Value {
    let mut table = json!([
        {
            "name": "list",
            "description": "The stations on the network's map right now: each with its id, name, device, kind, state and, for a worker, the subjects it serves and the names of its activities. A fresh read every time.",
            "inputSchema": { "type": "object", "properties": {}, "additionalProperties": false }
        },
        {
            "name": "describe",
            "description": "One subject's station and its activities with the exact input and output JSON schemas the worker registered.",
            "inputSchema": {
                "type": "object",
                "required": ["subject"],
                "properties": { "subject": { "type": "string", "description": "the subject a station serves" } },
                "additionalProperties": false
            }
        },
        {
            "name": "call",
            "description": "Send a JSON request on a subject and return the station's reply, or the server's refusal in its own words. With reply false the request is sent and nothing is waited for. With reply true, an unserved subject returns no_server carrying the subject with isError true and never returns the request as a reply.",
            "inputSchema": {
                "type": "object",
                "required": ["subject", "input"],
                "properties": {
                    "subject": { "type": "string" },
                    "input": { "type": "object", "description": "the request body the station's input schema describes" },
                    "reply": { "type": "boolean", "default": true },
                    "pass_for": { "type": "string", "description": "an audience, such as haematite: the ticket asks Lys for this seat's own pass for it and puts it in the input as pass" }
                },
                "additionalProperties": false
            }
        },
        {
            "name": "flow",
            "description": "Fire a list of calls in order; a step's input may draw on the previous reply with {\"$prev\": \"<JSON pointer>\"} objects (\"\" is the whole reply). Stops at the first failure, naming the step.",
            "inputSchema": {
                "type": "object",
                "required": ["steps"],
                "properties": {
                    "steps": {
                        "type": "array",
                        "minItems": 1,
                        "items": {
                            "type": "object",
                            "required": ["subject", "input"],
                            "properties": {
                                "subject": { "type": "string" },
                                "input": { "type": "object" },
                                "reply": { "type": "boolean", "default": true },
                                "pass_for": { "type": "string" }
                            },
                            "additionalProperties": false
                        }
                    }
                },
                "additionalProperties": false
            }
        }
    ]);
    if let Some(entries) = table.as_array_mut() {
        entries.extend(crate::network::lifecycle::tool_entries());
        entries.extend(super::subscriptions::tool_entries());
    }
    table
}

/// A tool's outcome as `tools/call` answers it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ToolResult {
    /// The result's structured content.
    pub structured: Value,
    /// Whether the result is a failure.
    pub is_error: bool,
}

impl ToolResult {
    /// A success carrying `structured`.
    #[must_use]
    pub const fn ok(structured: Value) -> Self {
        Self {
            structured,
            is_error: false,
        }
    }

    /// A failure in `words`, carrying `structured` beside them.
    #[must_use]
    pub fn failed(words: &str, structured: Value) -> Self {
        let mut structured = structured;
        if let Some(object) = structured.as_object_mut() {
            object.insert("error".to_owned(), json!(words));
        }
        Self {
            structured,
            is_error: true,
        }
    }

    pub(super) fn into_result(self, era: Era) -> Value {
        let text = self
            .structured
            .get("error")
            .and_then(Value::as_str)
            .map_or_else(
                || serde_json::to_string(&self.structured).unwrap_or_default(),
                str::to_owned,
            );
        let mut result = Map::new();
        result.insert(
            "content".to_owned(),
            json!([{ "type": "text", "text": text }]),
        );
        result.insert("structuredContent".to_owned(), self.structured);
        result.insert("isError".to_owned(), json!(self.is_error));
        shaped(era, result)
    }
}

pub(super) fn string<'a>(
    arguments: &'a Map<String, Value>,
    field: &str,
) -> Result<&'a str, Refusal> {
    arguments
        .get(field)
        .and_then(Value::as_str)
        .ok_or_else(|| Refusal {
            code: INVALID_PARAMS,
            message: format!("the tool call is missing {field}"),
            data: None,
        })
}

pub(super) fn object<'a>(
    arguments: &'a Map<String, Value>,
    field: &str,
) -> Result<&'a Value, Refusal> {
    match arguments.get(field) {
        Some(value @ Value::Object(_)) => Ok(value),
        _ => Err(Refusal {
            code: INVALID_PARAMS,
            message: format!("the tool call's {field} is missing or not an object"),
            data: None,
        }),
    }
}