use crate::commands::mcp::capability::CapabilitySet;
use crate::commands::mcp::error::McpError;
use crate::commands::mcp::registry::ToolContext;
use crate::commands::mcp::tools::parse_bounded_string;
use crate::inspection::find_routes_by_name;
use serde::Serialize;
use serde_json::Value;
#[derive(Serialize)]
struct InspectRouteResult<'a> {
route_name: String,
routes: Vec<&'a arcature_build::uag::schema::RouteEntry>,
}
pub(crate) fn call(
arguments: &Value,
_capabilities: &CapabilitySet,
context: &ToolContext,
) -> Result<Value, McpError> {
let route_name = parse_bounded_string(arguments, "route_name")?;
let routes = find_routes_by_name(&context.uag, &route_name);
let result = InspectRouteResult { route_name, 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 route(name: &str, method: &str, path: &str) -> RouteEntry {
RouteEntry {
method: method.into(),
path: path.into(),
name: name.into(),
handler: format!("H::{name}"),
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(),
}
}
fn ctx_with(routes: Vec<RouteEntry>) -> ToolContext {
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 finds_a_named_route() {
let ctx = ctx_with(vec![
route("links.index", "get", "/links"),
route("links.show", "get", "/links/{link}"),
]);
let args = serde_json::json!({"route_name": "links.show"});
let value = call(
&args,
&CapabilitySet::from_options(&crate::cli::McpOptions::default()),
&ctx,
)
.expect("ok");
assert_eq!(value["route_name"], "links.show");
let arr = value["routes"].as_array().expect("array");
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["path"], "/links/{link}");
}
#[test]
fn finds_multiple_routes_sharing_a_name() {
let ctx = ctx_with(vec![route("dup", "get", "/a"), route("dup", "post", "/a")]);
let args = serde_json::json!({"route_name": "dup"});
let value = call(
&args,
&CapabilitySet::from_options(&crate::cli::McpOptions::default()),
&ctx,
)
.expect("ok");
assert_eq!(value["routes"].as_array().expect("array").len(), 2);
}
#[test]
fn unknown_route_returns_empty_not_an_error() {
let ctx = ctx_with(vec![route("links.index", "get", "/links")]);
let args = serde_json::json!({"route_name": "nope"});
let value = call(
&args,
&CapabilitySet::from_options(&crate::cli::McpOptions::default()),
&ctx,
)
.expect("ok");
assert_eq!(value["route_name"], "nope");
assert!(value["routes"].as_array().expect("array").is_empty());
}
#[test]
fn missing_argument_is_a_typed_error() {
let ctx = ctx_with(vec![]);
let args = serde_json::json!({});
let err = call(
&args,
&CapabilitySet::from_options(&crate::cli::McpOptions::default()),
&ctx,
)
.expect_err("missing");
assert!(matches!(err, McpError::InvalidArgument { .. }));
}
#[test]
fn non_string_argument_is_a_typed_error() {
let ctx = ctx_with(vec![]);
let args = serde_json::json!({"route_name": 42});
let err = call(
&args,
&CapabilitySet::from_options(&crate::cli::McpOptions::default()),
&ctx,
)
.expect_err("non-string");
assert!(matches!(err, McpError::InvalidArgument { .. }));
}
#[test]
fn oversized_route_name_is_a_typed_error() {
let ctx = ctx_with(vec![]);
let huge = "x".repeat(crate::commands::mcp::tools::parse::MAX_STRING_ARG + 1);
let args = serde_json::json!({"route_name": huge});
let err = call(
&args,
&CapabilitySet::from_options(&crate::cli::McpOptions::default()),
&ctx,
)
.expect_err("oversized");
assert!(matches!(
err,
McpError::ArgumentTooLarge {
field: "route_name",
..
}
));
}
}