arcature-cli 2026.1.1

Developer lifecycle CLI for Arcature applications.
Documentation
//! `routes` tool — the flattened route table from the UAG.
//!
//! Returns every route the UAG carries (method, path, name, handler, pages)
//! in the UAG's deterministic order. The UAG content is developer-authored
//! (trusted), so returning all routes is not an abuse vector; the bound is
//! on the client's *arguments*, not the app's graph. Takes no arguments.

use crate::commands::mcp::capability::CapabilitySet;
use crate::commands::mcp::error::McpError;
use crate::commands::mcp::registry::ToolContext;
use serde::Serialize;
use serde_json::Value;

/// The `routes` tool result: the whole route table, serialized. Wraps the
/// UAG's owned `RouteEntry` mirror so the JSON shape is stable and does not
/// leak the full UAG.
#[derive(Serialize)]
struct RoutesResult<'a> {
    routes: &'a [arcature_build::uag::schema::RouteEntry],
}

pub(crate) fn call(
    _arguments: &Value,
    _capabilities: &CapabilitySet,
    context: &ToolContext,
) -> Result<Value, McpError> {
    let result = RoutesResult {
        routes: &context.uag.routes,
    };
    serde_json::to_value(result).map_err(McpError::from)
}

#[cfg(test)]
mod tests {
    use super::*;
    use arcature_build::uag::Uag;
    use arcature_build::uag::schema::RouteEntry;
    use std::collections::BTreeMap;

    fn ctx_with_routes(count: usize) -> ToolContext {
        let routes: Vec<RouteEntry> = (0..count)
            .map(|i| RouteEntry {
                method: if i % 2 == 0 { "get" } else { "post" }.into(),
                path: format!("/r{i}"),
                name: format!("r{i}"),
                handler: format!("H::r{i}"),
                pages: vec![],
                action_fields: vec![],
                action_type: String::new(),
                query_fields: vec![],
                query_type: String::new(),
                query_array: false,
                query_string_fields: vec![],
                query_string_type: String::new(),
            })
            .collect();
        ToolContext {
            uag: Uag {
                schema_version: 1,
                application: "App".into(),
                framework_version: "2026.1.0".into(),
                modules: BTreeMap::new(),
                routes,
                services: vec![],
                pages: vec![],
            },
        }
    }

    #[test]
    fn returns_all_routes() {
        let ctx = ctx_with_routes(3);
        let value = call(
            &Value::Null,
            &CapabilitySet::from_options(&crate::cli::McpOptions::default()),
            &ctx,
        )
        .expect("ok");
        let arr = value["routes"].as_array().expect("array");
        assert_eq!(arr.len(), 3);
        assert_eq!(arr[0]["method"], "get");
        assert_eq!(arr[1]["method"], "post");
    }

    #[test]
    fn empty_uag_returns_empty_routes() {
        let ctx = ToolContext {
            uag: Uag {
                schema_version: 1,
                application: String::new(),
                framework_version: String::new(),
                modules: BTreeMap::new(),
                routes: vec![],
                services: vec![],
                pages: vec![],
            },
        };
        let value = call(
            &Value::Null,
            &CapabilitySet::from_options(&crate::cli::McpOptions::default()),
            &ctx,
        )
        .expect("ok");
        assert_eq!(value["routes"].as_array().expect("array").len(), 0);
    }
}