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;
#[derive(Serialize)]
struct JobsResult {
jobs: Vec<JobRow>,
commands: Vec<CommandRow>,
schedules: Vec<ScheduleRow>,
}
#[derive(Serialize)]
struct JobRow {
module: String,
kind: String,
version: i16,
handler: String,
}
#[derive(Serialize)]
struct CommandRow {
module: String,
name: String,
function: String,
}
#[derive(Serialize)]
struct ScheduleRow {
module: String,
job: String,
version: i16,
cadence: arcature_build::uag::schema::CadenceEntry,
}
pub(crate) fn call(
_arguments: &Value,
_capabilities: &CapabilitySet,
context: &ToolContext,
) -> Result<Value, McpError> {
let mut jobs = Vec::new();
let mut commands = Vec::new();
let mut schedules = Vec::new();
for (module_name, module) in &context.uag.modules {
for job in &module.jobs {
jobs.push(JobRow {
module: module_name.clone(),
kind: job.kind.clone(),
version: job.version,
handler: job.handler.clone(),
});
}
for command in &module.commands {
commands.push(CommandRow {
module: module_name.clone(),
name: command.name.clone(),
function: command.function.clone(),
});
}
for schedule in &module.schedules {
schedules.push(ScheduleRow {
module: module_name.clone(),
job: schedule.job.clone(),
version: schedule.version,
cadence: schedule.cadence.clone(),
});
}
}
let result = JobsResult {
jobs,
commands,
schedules,
};
serde_json::to_value(result).map_err(McpError::from)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::inspection::tests::empty_module;
use arcature_build::uag::Uag;
use arcature_build::uag::schema::{CadenceEntry, CommandEntry, JobEntry, ScheduleEntry};
use std::collections::BTreeMap;
fn ctx_with_bindings() -> ToolContext {
let mut modules = BTreeMap::new();
let mut links = empty_module("Links");
links.jobs.push(JobEntry {
kind: "check_links".into(),
version: 1,
handler: "handle".into(),
});
links.commands.push(CommandEntry {
name: "links:prune".into(),
function: "prune".into(),
});
links.schedules.push(ScheduleEntry {
job: "cleanup".into(),
version: 1,
cadence: CadenceEntry::Every { seconds: 300 },
});
modules.insert("Links".into(), links);
modules.insert("Accounts".into(), empty_module("Accounts"));
ToolContext {
uag: Uag {
schema_version: 1,
application: "App".into(),
framework_version: "2026.1.0".into(),
modules,
routes: vec![],
services: vec![],
pages: vec![],
},
}
}
#[test]
fn aggregates_jobs_commands_schedules() {
let ctx = ctx_with_bindings();
let value = call(
&Value::Null,
&CapabilitySet::from_options(&crate::cli::McpOptions::default()),
&ctx,
)
.expect("ok");
assert_eq!(value["jobs"].as_array().expect("array").len(), 1);
assert_eq!(value["jobs"][0]["module"], "Links");
assert_eq!(value["jobs"][0]["kind"], "check_links");
assert_eq!(value["commands"].as_array().expect("array").len(), 1);
assert_eq!(value["commands"][0]["name"], "links:prune");
assert_eq!(value["schedules"].as_array().expect("array").len(), 1);
assert_eq!(value["schedules"][0]["cadence"]["kind"], "every");
assert_eq!(value["schedules"][0]["cadence"]["seconds"], 300);
}
#[test]
fn empty_uag_returns_empty_lists() {
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!(value["jobs"].as_array().expect("array").is_empty());
assert!(value["commands"].as_array().expect("array").is_empty());
assert!(value["schedules"].as_array().expect("array").is_empty());
}
}