use rmcp::model::ErrorData;
use serde_json::Value;
pub(crate) fn validate_arguments(schema: &Value, arguments: &Value) -> Result<(), ErrorData> {
let schema_obj = match schema.as_object() {
Some(obj) => obj,
None => return Ok(()),
};
if let Some(type_value) = schema_obj.get("type")
&& !check_type(type_value, arguments)
{
return Err(ErrorData::invalid_params(
format!(
"arguments type mismatch: schema requires type {}, got {}",
type_value,
json_type_name(arguments)
),
None,
));
}
if let Some(required) = schema_obj.get("required").and_then(|v| v.as_array())
&& let Some(args_obj) = arguments.as_object()
{
for req in required {
if let Some(field) = req.as_str()
&& !args_obj.contains_key(field)
{
return Err(ErrorData::invalid_params(
format!(
"missing required field: '{}' (required by tool input_schema)",
field
),
None,
));
}
}
}
if let Some(additional) = schema_obj.get("additionalProperties") {
if additional.as_bool() == Some(false) {
let properties = schema_obj.get("properties").and_then(|v| v.as_object());
if let Some(args_obj) = arguments.as_object() {
for key in args_obj.keys() {
let allowed = properties.map(|p| p.contains_key(key)).unwrap_or(false);
if !allowed {
return Err(ErrorData::invalid_params(
format!(
"unknown field: '{}' (tool input_schema has additionalProperties: false)",
key
),
None,
));
}
}
}
}
}
Ok(())
}
fn check_type(type_value: &Value, instance: &Value) -> bool {
match type_value {
Value::String(t) => matches_json_type(t, instance),
Value::Array(types) => types.iter().any(|t| {
t.as_str()
.map(|s| matches_json_type(s, instance))
.unwrap_or(false)
}),
_ => true,
}
}
fn matches_json_type(type_name: &str, instance: &Value) -> bool {
match type_name {
"object" => instance.is_object(),
"array" => instance.is_array(),
"string" => instance.is_string(),
"integer" => instance.is_i64() || instance.is_u64(),
"number" => instance.is_number(),
"boolean" => instance.is_boolean(),
"null" => instance.is_null(),
_ => true,
}
}
fn json_type_name(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => {
if value.is_i64() || value.is_u64() {
"integer"
} else {
"number"
}
}
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_type_object_matches_object() {
let schema = json!({"type": "object"});
let args = json!({"key": "value"});
assert!(validate_arguments(&schema, &args).is_ok());
}
#[test]
fn test_type_object_rejects_string() {
let schema = json!({"type": "object"});
let args = json!("not an object");
let err = validate_arguments(&schema, &args).unwrap_err();
assert!(err.message.contains("type mismatch"));
}
#[test]
fn test_type_string_matches_string() {
let schema = json!({"type": "string"});
assert!(validate_arguments(&schema, &json!("hello")).is_ok());
}
#[test]
fn test_type_integer_matches_integer() {
let schema = json!({"type": "integer"});
assert!(validate_arguments(&schema, &json!(42)).is_ok());
}
#[test]
fn test_type_integer_rejects_float() {
let schema = json!({"type": "integer"});
assert!(validate_arguments(&schema, &json!(42.5)).is_err());
}
#[test]
fn test_type_number_matches_float() {
let schema = json!({"type": "number"});
assert!(validate_arguments(&schema, &json!(42.5)).is_ok());
}
#[test]
fn test_type_array_matches_array() {
let schema = json!({"type": "array"});
assert!(validate_arguments(&schema, &json!([1, 2, 3])).is_ok());
}
#[test]
fn test_type_null_matches_null() {
let schema = json!({"type": "null"});
assert!(validate_arguments(&schema, &json!(null)).is_ok());
}
#[test]
fn test_type_boolean_matches_boolean() {
let schema = json!({"type": "boolean"});
assert!(validate_arguments(&schema, &json!(true)).is_ok());
}
#[test]
fn test_type_array_union() {
let schema = json!({"type": ["string", "null"]});
assert!(validate_arguments(&schema, &json!("hello")).is_ok());
assert!(validate_arguments(&schema, &json!(null)).is_ok());
assert!(validate_arguments(&schema, &json!(42)).is_err());
}
#[test]
fn test_required_all_present() {
let schema = json!({
"type": "object",
"required": ["name", "age"]
});
let args = json!({"name": "Alice", "age": 30});
assert!(validate_arguments(&schema, &args).is_ok());
}
#[test]
fn test_required_missing_field() {
let schema = json!({
"type": "object",
"required": ["name", "age"]
});
let args = json!({"name": "Alice"});
let err = validate_arguments(&schema, &args).unwrap_err();
assert!(err.message.contains("missing required field: 'age'"));
}
#[test]
fn test_required_not_checked_for_non_object() {
let schema = json!({
"type": "string",
"required": ["name"]
});
let args = json!("hello");
assert!(validate_arguments(&schema, &args).is_ok());
}
#[test]
fn test_additional_properties_false_rejects_unknown() {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"additionalProperties": false
});
let args = json!({"name": "Alice", "age": 30, "extra": "bad"});
let err = validate_arguments(&schema, &args).unwrap_err();
assert!(err.message.contains("unknown field: 'extra'"));
}
#[test]
fn test_additional_properties_false_allows_known() {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string"}
},
"additionalProperties": false
});
let args = json!({"name": "Alice"});
assert!(validate_arguments(&schema, &args).is_ok());
}
#[test]
fn test_additional_properties_true_allows_unknown() {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string"}
},
"additionalProperties": true
});
let args = json!({"name": "Alice", "extra": "ok"});
assert!(validate_arguments(&schema, &args).is_ok());
}
#[test]
fn test_additional_properties_absent_allows_unknown() {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string"}
}
});
let args = json!({"name": "Alice", "extra": "ok"});
assert!(validate_arguments(&schema, &args).is_ok());
}
#[test]
fn test_empty_schema_allows_anything() {
let schema = json!({});
assert!(validate_arguments(&schema, &json!("string")).is_ok());
assert!(validate_arguments(&schema, &json!(42)).is_ok());
assert!(validate_arguments(&schema, &json!({"key": "value"})).is_ok());
}
#[test]
fn test_non_object_schema_skips_validation() {
let schema = json!("not a schema");
assert!(validate_arguments(&schema, &json!("anything")).is_ok());
}
#[test]
fn test_combined_type_required_additional() {
let schema = json!({
"type": "object",
"required": ["name"],
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"additionalProperties": false
});
assert!(validate_arguments(&schema, &json!({"name": "Alice", "age": 30})).is_ok());
assert!(validate_arguments(&schema, &json!({"age": 30})).is_err());
assert!(validate_arguments(&schema, &json!({"name": "Alice", "extra": "bad"})).is_err());
assert!(validate_arguments(&schema, &json!("not an object")).is_err());
}
#[test]
fn test_null_arguments_with_object_schema() {
let schema = json!({"type": "object"});
assert!(validate_arguments(&schema, &json!(null)).is_err());
}
#[test]
fn test_null_arguments_with_null_schema() {
let schema = json!({"type": "null"});
assert!(validate_arguments(&schema, &json!(null)).is_ok());
}
#[test]
fn test_additional_properties_false_without_properties_rejects_non_empty() {
let schema = json!({
"type": "object",
"additionalProperties": false
});
let err = validate_arguments(&schema, &json!({"any_field": "value"})).unwrap_err();
assert!(err.message.contains("unknown field: 'any_field'"));
}
#[test]
fn test_additional_properties_false_without_properties_allows_empty() {
let schema = json!({
"type": "object",
"additionalProperties": false
});
assert!(validate_arguments(&schema, &json!({})).is_ok());
}
}