use serde_json::Value;
use crate::commands::mcp::capability::{Capability, CapabilitySet};
use crate::commands::mcp::error::McpError;
use crate::commands::mcp::tools;
use arcature_build::uag::Uag;
#[derive(Debug, Clone)]
pub(crate) struct ToolContext {
pub(crate) uag: Uag,
}
impl ToolContext {
#[cfg(test)]
#[allow(dead_code)] pub(crate) fn from_uag(uag: Uag) -> Self {
Self { uag }
}
}
pub(crate) struct Tool {
pub(crate) name: &'static str,
pub(crate) description: &'static str,
pub(crate) required_capabilities: &'static [Capability],
pub(crate) handler:
for<'a> fn(&'a Value, &'a CapabilitySet, &'a ToolContext) -> Result<Value, McpError>,
}
pub(crate) fn all_tools() -> &'static [Tool] {
&[
Tool {
name: "application_info",
description: "Summary of the application graph: module/route/page/service counts, application name, framework version.",
required_capabilities: &[],
handler: tools::application_info::call,
},
Tool {
name: "routes",
description: "List all routes in the application graph (method, path, name, handler, pages).",
required_capabilities: &[],
handler: tools::routes::call,
},
Tool {
name: "inspect_route",
description: "Inspect a single route by its dotted name (e.g. links.show).",
required_capabilities: &[],
handler: tools::inspect_route::call,
},
Tool {
name: "pages",
description: "List page-contract identities in the application graph.",
required_capabilities: &[],
handler: tools::pages::call,
},
Tool {
name: "services",
description: "List services and their typed dependencies.",
required_capabilities: &[],
handler: tools::services::call,
},
Tool {
name: "jobs",
description: "List job handlers, commands, and scheduled jobs across modules.",
required_capabilities: &[],
handler: tools::jobs::call,
},
Tool {
name: "config_schema",
description: "Describe the UAG schema: schema version and the fields each UAG entry exposes. (App-config schema is deferred to AP2.1-6.)",
required_capabilities: &[],
handler: tools::config_schema::call,
},
Tool {
name: "docs_search",
description: "Version-aware docs retrieval: the certified stack components, their pinned versions, and the cross-stack protocol coordinates for the installed stack.",
required_capabilities: &[],
handler: tools::docs_search::call,
},
]
}
pub(crate) fn find(name: &str) -> Option<&'static Tool> {
all_tools().iter().find(|tool| tool.name == name)
}
#[cfg(test)]
mod tests {
use super::*;
use arcature_build::uag::SCHEMA_VERSION;
use std::collections::BTreeMap;
fn empty_context() -> ToolContext {
ToolContext {
uag: Uag {
schema_version: SCHEMA_VERSION,
application: String::new(),
framework_version: String::new(),
modules: BTreeMap::new(),
routes: vec![],
services: vec![],
pages: vec![],
},
}
}
#[test]
fn every_tool_has_a_unique_nonempty_name() {
let tools = all_tools();
let names: Vec<&str> = tools.iter().map(|t| t.name).collect();
let mut sorted = names.clone();
sorted.sort();
sorted.dedup();
assert_eq!(names.len(), sorted.len(), "duplicate tool names");
assert!(names.iter().all(|n| !n.is_empty()), "empty tool name");
}
#[test]
fn every_tool_has_a_description_and_handler() {
for tool in all_tools() {
assert!(!tool.description.is_empty(), "{}", tool.name);
let noop: fn(&Value, &CapabilitySet, &ToolContext) -> Result<Value, McpError> =
|_, _, _| Ok(Value::Null);
let _ = noop; }
}
#[test]
fn find_returns_known_tools() {
assert!(find("application_info").is_some());
assert!(find("inspect_route").is_some());
assert!(find("docs_search").is_some());
assert!(find("nope").is_none());
}
#[test]
fn shipped_tools_require_no_capabilities() {
for tool in all_tools() {
assert!(
tool.required_capabilities.is_empty(),
"{} requires capabilities but is a read-only tool",
tool.name
);
}
}
#[test]
fn empty_context_is_usable() {
let ctx = empty_context();
assert_eq!(ctx.uag.schema_version, SCHEMA_VERSION);
assert!(ctx.uag.routes.is_empty());
}
}