use std::path::Path;
use systemprompt_loader::ExtensionRegistry as McpExtensionRegistry;
use systemprompt_models::mcp::McpServerType;
use systemprompt_models::{Config, ServicesConfig};
use systemprompt_traits::validation_report::ValidationIssue;
use systemprompt_traits::{StartupValidationReport, ValidationReport};
pub(super) fn validate_mcp_manifests(
config: &Config,
services_config: &ServicesConfig,
report: &mut StartupValidationReport,
) {
let registry = McpExtensionRegistry::build(
Path::new(&config.system_path),
config.is_cloud,
&config.bin_path,
);
let mcp_errors = collect_manifest_errors(services_config, config.is_cloud, |binary| {
registry
.get_path(binary)
.map(|_| ())
.map_err(|e| e.to_string())
});
merge_mcp_errors(report, mcp_errors);
}
pub fn collect_manifest_errors<F>(
services_config: &ServicesConfig,
is_cloud: bool,
resolve: F,
) -> Vec<ValidationIssue>
where
F: Fn(&str) -> Result<(), String>,
{
let mut mcp_errors: Vec<ValidationIssue> = Vec::new();
for (name, deployment) in &services_config.mcp_servers {
if !deployment.enabled {
continue;
}
if deployment.dev_only && is_cloud {
continue;
}
if deployment.tool_policy.is_none() {
mcp_errors.push(
ValidationIssue::new(
format!("mcp_servers.{}.tool_policy", name),
"No tool_policy declared; the server is withheld from the bridge manifest"
.to_owned(),
)
.with_suggestion(format!(
"Set tool_policy: allow | deny | prompt in services/mcp/{}.yaml",
name
)),
);
}
if !matches!(deployment.server_type, McpServerType::Internal) {
continue;
}
let Some(binary) = deployment.binary.as_deref() else {
mcp_errors.push(
ValidationIssue::new(
format!("mcp_servers.{}.binary", name),
"Internal server declares no binary".to_owned(),
)
.with_suggestion(format!(
"Set binary: <extension binary> in services/mcp/{}.yaml",
name
)),
);
continue;
};
if let Err(e) = resolve(binary) {
mcp_errors.push(
ValidationIssue::new(
format!("mcp_servers.{}.binary", name),
format!("Manifest not found for binary '{binary}': {e}"),
)
.with_suggestion(format!(
"Ensure manifest.yaml exists at extensions/mcp/{binary}/manifest.yaml"
)),
);
}
}
mcp_errors
}
pub fn merge_mcp_errors(report: &mut StartupValidationReport, mcp_errors: Vec<ValidationIssue>) {
if mcp_errors.is_empty() {
return;
}
if let Some(mcp_report) = report.domains.iter_mut().find(|d| d.domain == "mcp") {
for error in mcp_errors {
mcp_report.add_error(error);
}
} else {
let mut mcp_report = ValidationReport::new("mcp");
for error in mcp_errors {
mcp_report.add_error(error);
}
report.add_domain(mcp_report);
}
}