liminal-cli 0.2.1

The liminal command: serve the network, maintain its stores, or serve its discovery MCP over stdio
Documentation
//! The negotiated tools and the session that owns their subscriptions.

use super::framing::{self, INVALID_PARAMS, METHOD_NOT_FOUND};
use super::negotiate::{Era, Refusal, Session, shaped};
use super::tools::{TOOLS_TTL_MS, ToolResult, object, string, tool_table};
use crate::network::{Rail, call, directory, flow};
use serde_json::{Map, Value, json};

/// The ticket's server: one rail, one session and its service subscriptions.
#[derive(Debug)]
pub struct Server<R> {
    rail: R,
    session: Session,
    pub(super) frame_bytes: usize,
    pub(super) subscriptions: std::sync::Arc<super::subscriptions::Subscriptions>,
    pub(super) warning_only: std::sync::Arc<std::sync::atomic::AtomicBool>,
}

impl<R: Rail> Server<R> {
    /// A server over `rail`, reading lines of at most `frame_bytes`.
    #[must_use]
    pub fn new(rail: R, frame_bytes: usize) -> Self {
        Self {
            rail,
            session: Session::new(),
            frame_bytes,
            subscriptions: std::sync::Arc::default(),
            warning_only: std::sync::Arc::default(),
        }
    }

    /// The rail behind the tools.
    #[must_use]
    pub const fn rail(&self) -> &R {
        &self.rail
    }

    /// One request's answer.
    pub fn answer(&mut self, id: &Value, method: &str, params: &Map<String, Value>) -> Value {
        match self.respond(method, params) {
            Ok(result) => framing::result(id, &result),
            Err(refusal) => framing::error(id, refusal.code, &refusal.message, refusal.data),
        }
    }

    fn respond(&mut self, method: &str, params: &Map<String, Value>) -> Result<Value, Refusal> {
        match method {
            "initialize" => return self.session.initialize(params),
            "server/discover" => {
                self.session.era_of(params)?;
                return Ok(Session::discover());
            }
            _ => {}
        }
        let era = self.session.era_of(params)?;
        match method {
            "ping" => Ok(shaped(era, Map::new())),
            "logging/setLevel" => {
                super::notifications::set_level(params, &self.warning_only)?;
                Ok(shaped(era, Map::new()))
            }
            "tools/list" => {
                let mut result = Map::new();
                result.insert("tools".to_owned(), tool_table());
                if era == Era::Modern {
                    // 2026-07-28 list results carry their cache terms. The
                    // table is the same for every pass (what a call may reach
                    // is decided per call), so any client may share it; a
                    // minute lets a replaced binary's tools show promptly.
                    result.insert("ttlMs".to_owned(), json!(TOOLS_TTL_MS));
                    result.insert("cacheScope".to_owned(), json!("public"));
                }
                Ok(shaped(era, result))
            }
            "tools/call" => {
                let name = params
                    .get("name")
                    .and_then(Value::as_str)
                    .ok_or_else(|| Refusal {
                        code: INVALID_PARAMS,
                        message: "the request is missing name".to_owned(),
                        data: None,
                    })?;
                let arguments = params
                    .get("arguments")
                    .and_then(Value::as_object)
                    .cloned()
                    .unwrap_or_default();
                let outcome = self.tool(name, &arguments)?;
                Ok(outcome.into_result(era))
            }
            other => Err(Refusal {
                code: METHOD_NOT_FOUND,
                message: format!("the ticket has no method {other}"),
                data: None,
            }),
        }
    }

    /// One tool's outcome; an unknown tool is method-not-found.
    ///
    /// # Errors
    /// Returns the method-not-found or invalid-params refusal.
    pub fn tool(&self, name: &str, arguments: &Map<String, Value>) -> Result<ToolResult, Refusal> {
        if matches!(name, "subscribe" | "unsubscribe") {
            let service = super::subscriptions::service_argument(arguments)?;
            return Ok(self.subscriptions.tool(&self.rail, name, service));
        }
        if let Some(outcome) = crate::network::lifecycle::tool(&self.rail, name, arguments) {
            return Ok(outcome);
        }
        let outcome = match name {
            "list" => directory::list(&self.rail),
            "describe" => {
                let subject = string(arguments, "subject")?;
                directory::describe(&self.rail, subject)
            }
            "call" => {
                let subject = string(arguments, "subject")?;
                let input = object(arguments, "input")?;
                let reply = arguments
                    .get("reply")
                    .and_then(Value::as_bool)
                    .unwrap_or(true);
                let pass_for = match arguments.get("pass_for") {
                    None => None,
                    Some(Value::String(audience)) => Some(audience.as_str()),
                    Some(_) => {
                        return Err(Refusal {
                            code: INVALID_PARAMS,
                            message: "the tool call's pass_for is not a string".to_owned(),
                            data: None,
                        });
                    }
                };
                call::call(&self.rail, subject, input, reply, pass_for)
            }
            "flow" => {
                let steps = arguments
                    .get("steps")
                    .and_then(Value::as_array)
                    .ok_or_else(|| Refusal {
                        code: INVALID_PARAMS,
                        message: "the flow is missing steps".to_owned(),
                        data: None,
                    })?;
                flow::flow(&self.rail, steps)
            }
            other => {
                return Err(Refusal {
                    code: METHOD_NOT_FOUND,
                    message: format!("the ticket has no tool {other}"),
                    data: None,
                });
            }
        };
        Ok(outcome)
    }
}