use super::{ArgsCheck, ConstraintRule, McpPolicy};
use serde_json::{json, Value};
use std::collections::HashMap;
use std::sync::Arc;
pub(super) fn check_tool_args(policy: &McpPolicy, tool_name: &str, args: &Value) -> ArgsCheck {
if tool_name == "$defs" || !policy.schemas.contains_key(tool_name) {
return ArgsCheck::NoSchema;
}
let schemas = Value::Object(policy.schemas.clone().into_iter().collect());
let schema_to_compile = match crate::policy_engine::prepare_tool_schema(&schemas, tool_name) {
Ok(schema) => schema,
Err(_) => return ArgsCheck::Malformed,
};
match crate::policy_engine::compile_schema(&schema_to_compile) {
Ok(validator) => {
if validator.is_valid(args) {
ArgsCheck::Valid
} else {
ArgsCheck::Invalid
}
}
Err(_) => ArgsCheck::Malformed,
}
}
pub(super) fn migrate_constraints_to_schemas(policy: &mut McpPolicy) {
for constraint in std::mem::take(&mut policy.constraints) {
let schema = constraint_to_schema(&constraint);
policy.schemas.insert(constraint.tool.clone(), schema);
}
if policy.version.is_empty() || policy.version == "1.0" {
policy.version = "2.0".to_string();
}
}
pub(super) fn compile_all_schemas(policy: &McpPolicy) -> super::types::CompiledSchemas {
let schemas = Value::Object(policy.schemas.clone().into_iter().collect());
let mut compiled = HashMap::new();
for tool_name in policy.schemas.keys() {
if tool_name == "$defs" {
continue;
}
let result = crate::policy_engine::prepare_tool_schema(&schemas, tool_name)
.and_then(|schema| crate::policy_engine::compile_schema(&schema))
.map(Arc::new);
if let Err(error) = &result {
tracing::error!(
"Schema for tool '{}' failed to prepare or compile; its calls will be denied \
(E_SCHEMA_COMPILE): {}",
tool_name,
error
);
}
compiled.insert(tool_name.clone(), result);
}
compiled
}
fn constraint_to_schema(constraint: &ConstraintRule) -> Value {
let mut properties = json!({});
let mut required = vec![];
for (param_name, param_constraint) in &constraint.params {
if let Some(pattern) = ¶m_constraint.matches {
properties[param_name] = json!({
"type": "string",
"pattern": pattern,
"minLength": 1
});
required.push(param_name.clone());
}
}
json!({
"type": "object",
"additionalProperties": true,
"properties": properties,
"required": required,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mcp_compilation_merges_shared_and_local_definitions() {
let mut policy = McpPolicy::default();
policy
.schemas
.insert("$defs".to_string(), json!({"shared": {"type": "string"}}));
policy.schemas.insert(
"lookup".to_string(),
json!({
"$defs": {"local": {"type": "integer"}},
"type": "object",
"properties": {
"name": {"$ref": "#/$defs/shared"},
"count": {"$ref": "#/$defs/local"}
},
"required": ["name", "count"]
}),
);
assert_eq!(
check_tool_args(&policy, "lookup", &json!({"name": "item", "count": 1})),
ArgsCheck::Valid
);
assert!(policy
.try_compile_all_schemas()
.expect("merged schemas compile")
.contains_key("lookup"));
}
#[test]
fn mcp_compilation_records_collision_as_error_instead_of_panicking() {
let mut policy = McpPolicy::default();
policy
.schemas
.insert("$defs".to_string(), json!({"id": {"type": "string"}}));
policy.schemas.insert(
"lookup".to_string(),
json!({"$defs": {"id": {"type": "integer"}}, "$ref": "#/$defs/id"}),
);
policy
.schemas
.insert("healthy".to_string(), json!({"type": "object"}));
let compiled = compile_all_schemas(&policy);
assert!(compiled["lookup"].is_err(), "collision is a per-tool error");
assert!(compiled["healthy"].is_ok(), "healthy tools still compile");
assert!(!compiled.contains_key("$defs"), "$defs is never a tool");
let error = policy
.try_compile_all_schemas()
.expect_err("load-time surface names the broken tool");
assert!(error.contains("lookup"), "{error}");
assert!(error.contains("overlap"), "{error}");
}
#[test]
fn dollar_prefixed_tool_names_are_tools_on_both_paths() {
let mut policy = McpPolicy::default();
policy
.schemas
.insert("$weird".to_string(), json!({"type": "object"}));
assert_eq!(
check_tool_args(&policy, "$weird", &json!({})),
ArgsCheck::Valid
);
policy
.schemas
.insert("$defs".to_string(), json!({"shared": {"type": "string"}}));
assert_eq!(
check_tool_args(&policy, "$defs", &json!({})),
ArgsCheck::NoSchema,
"$defs is consumed by preparation and is never itself a checkable tool"
);
let compiled = compile_all_schemas(&policy);
assert!(
compiled.contains_key("$weird"),
"load-time compiler sees the same tool set as check_tool_args"
);
assert!(compiled["$weird"].is_ok());
}
#[test]
fn mcp_compilation_rejects_shared_local_definition_collisions() {
let mut policy = McpPolicy::default();
policy
.schemas
.insert("$defs".to_string(), json!({"id": {"type": "string"}}));
policy.schemas.insert(
"lookup".to_string(),
json!({"$defs": {"id": {"type": "integer"}}, "$ref": "#/$defs/id"}),
);
assert_eq!(
check_tool_args(&policy, "lookup", &json!(1)),
ArgsCheck::Malformed
);
}
#[test]
fn mcp_compilation_never_retrieves_external_references() {
let mut policy = McpPolicy::default();
policy.schemas.insert(
"lookup".to_string(),
json!({"$ref": "https://example.invalid/external-schema"}),
);
assert_eq!(
check_tool_args(&policy, "lookup", &json!({})),
ArgsCheck::Malformed
);
}
}