use crate::commands::mcp::capability::CapabilitySet;
use crate::commands::mcp::error::McpError;
use crate::commands::mcp::registry::ToolContext;
use crate::commands::mcp::tools::parse_optional_bounded_string;
use crate::stack::load as load_contract;
use serde::Serialize;
use serde_json::Value;
#[derive(Serialize)]
struct DocsSearchResult {
arcature_version: String,
arcature_client_version: String,
cross_stack_protocol_version: String,
uag_schema_version: u32,
components: Vec<ComponentDoc>,
}
#[derive(Serialize)]
struct ComponentDoc {
name: String,
version: String,
role: String,
}
pub(crate) fn call(
arguments: &Value,
_capabilities: &CapabilitySet,
context: &ToolContext,
) -> Result<Value, McpError> {
let query = parse_optional_bounded_string(arguments, "query")?;
let contract = load_contract();
let q = query.as_deref().map(str::to_lowercase);
let mut components = Vec::new();
for (name, component) in &contract.components {
let role = component.role.clone();
let needle = q.as_deref();
let matches = match needle {
None => true,
Some(q) => name.to_lowercase().contains(q) || role.to_lowercase().contains(q),
};
if matches {
components.push(ComponentDoc {
name: name.clone(),
version: component.version.clone(),
role,
});
}
}
let result = DocsSearchResult {
arcature_version: contract.snapshot.arcature_version.clone(),
arcature_client_version: contract.snapshot.arcature_client_version.clone(),
cross_stack_protocol_version: context.uag.framework_version.clone(),
uag_schema_version: context.uag.schema_version,
components,
};
serde_json::to_value(result).map_err(McpError::from)
}
#[cfg(test)]
mod tests {
use super::*;
use arcature_build::uag::Uag;
use std::collections::BTreeMap;
fn ctx() -> ToolContext {
ToolContext {
uag: Uag {
schema_version: 1,
application: "App".into(),
framework_version: "2026.1.0".into(),
modules: BTreeMap::new(),
routes: vec![],
services: vec![],
pages: vec![],
},
}
}
fn caps() -> CapabilitySet {
CapabilitySet::from_options(&crate::cli::McpOptions::default())
}
#[test]
fn returns_arcature_version_and_components_without_query() {
let value = call(&Value::Null, &caps(), &ctx()).expect("ok");
let components = value["components"].as_array().expect("array");
assert!(!components.is_empty(), "the certified stack has components");
assert!(value["arcature_version"].as_str().is_some());
assert_eq!(value["arcature_version"], "2026.1.0");
}
#[test]
fn cross_stack_protocol_version_comes_from_uag() {
let value = call(&Value::Null, &caps(), &ctx()).expect("ok");
assert_eq!(value["cross_stack_protocol_version"], "2026.1.0");
assert_eq!(value["uag_schema_version"], 1);
}
#[test]
fn query_filters_components_case_insensitively() {
let args = serde_json::json!({"query": "HTTP"});
let value = call(&args, &caps(), &ctx()).expect("ok");
let components = value["components"].as_array().expect("array");
assert!(!components.is_empty());
for c in components {
let role = c["role"].as_str().expect("role");
let name = c["name"].as_str().expect("name");
let hits = role.to_lowercase().contains("http") || name.to_lowercase().contains("http");
assert!(hits, "non-matching component returned: {name} / {role}");
}
}
#[test]
fn query_by_component_name() {
let args = serde_json::json!({"query": "axum"});
let value = call(&args, &caps(), &ctx()).expect("ok");
let components = value["components"].as_array().expect("array");
let axum = components
.iter()
.find(|c| c["name"] == "axum")
.expect("axum component matched by name");
assert_eq!(axum["version"], "0.8.9");
assert!(!components.is_empty());
}
#[test]
fn no_match_query_returns_empty_components() {
let args = serde_json::json!({"query": "zzznope"});
let value = call(&args, &caps(), &ctx()).expect("ok");
assert!(value["components"].as_array().expect("array").is_empty());
}
#[test]
fn oversized_query_is_a_typed_error() {
let huge = "x".repeat(crate::commands::mcp::tools::parse::MAX_STRING_ARG + 1);
let args = serde_json::json!({"query": huge});
let err = call(&args, &caps(), &ctx()).expect_err("oversized");
assert!(matches!(
err,
McpError::ArgumentTooLarge { field: "query", .. }
));
}
#[test]
fn non_string_query_is_a_typed_error() {
let args = serde_json::json!({"query": 42});
let err = call(&args, &caps(), &ctx()).expect_err("non-string");
assert!(matches!(err, McpError::InvalidArgument { .. }));
}
#[test]
fn client_version_is_reported_honestly() {
let value = call(&Value::Null, &caps(), &ctx()).expect("ok");
assert!(value["arcature_client_version"].as_str().is_some());
}
}