- 1
use serde_json::Value; - 2
- 3
/// Validate the portable JSON-Schema subset used by MCP tool declarations. - 4
/// This deliberately runs before `tools/call`, turning provider mistakes into - 5
/// actionable broker errors without adding a schema dependency to the client. - 6
pub fn validate_arguments(arguments: &Value, schema: &Value) -> Result<(), String> { - 7
validate(arguments, schema, "$") - 8
} - 9
- 10
fn validate(value: &Value, schema: &Value, path: &str) -> Result<(), String> { - 11
if let Some(types) = schema.get("type") { - 12
let valid = match types { - 13
Value::String(kind) => matches_type(value, kind), - 14
Value::Array(kinds) => kinds - 15
.iter() - 16
.filter_map(Value::as_str) - 17
.any(|kind| matches_type(value, kind)), - 18
_ => true, - 19
}; - 20
if !valid { - 21
return Err(format!("invalid MCP arguments: {path} must be {}", types)); - 22
} - 23
} - 24
if let Some(allowed) = schema.get("enum").and_then(Value::as_array) - 25
&& !allowed.iter().any(|candidate| candidate == value) - 26
{ - 27
return Err(format!( - 28
"invalid MCP arguments: {path} is not an allowed enum value" - 29
)); - 30
} - 31
if let Some(required) = schema.get("required").and_then(Value::as_array) - 32
&& let Some(object) = value.as_object() - 33
{ - 34
for name in required.iter().filter_map(Value::as_str) { - 35
if !object.contains_key(name) { - 36
return Err(format!("invalid MCP arguments: {path}.{name} is required")); - 37
} - 38
} - 39
} - 40
if let Some(one_of) = schema.get("oneOf").and_then(Value::as_array) { - 41
let mut matched = 0usize; - 42
let mut last_err = String::new(); - 43
for sub in one_of { - 44
match validate(value, sub, &format!("{path} (oneOf)")) { - 45
Ok(()) => matched += 1, - 46
Err(e) => last_err = e, - 47
} - 48
} - 49
if matched != 1 { - 50
return Err(if matched == 0 { - 51
last_err - 52
} else { - 53
format!( - 54
"invalid MCP arguments: {path} matched {matched} of {} oneOf branches", - 55
one_of.len() - 56
) - 57
}); - 58
} - 59
return Ok(()); - 60
} - 61
if let (Some(properties), Some(object)) = ( - 62
schema.get("properties").and_then(Value::as_object), - 63
value.as_object(), - 64
) { - 65
for (name, child_schema) in properties { - 66
if let Some(child) = object.get(name) { - 67
validate(child, child_schema, &format!("{path}.{name}"))?; - 68
} - 69
} - 70
} - 71
if schema.get("additionalProperties").and_then(Value::as_bool) == Some(false) - 72
&& let Some(object) = value.as_object() - 73
{ - 74
let known: std::collections::HashSet<&str> = schema - 75
.get("properties") - 76
.and_then(Value::as_object) - 77
.map(|props| props.keys().map(|k| k.as_str()).collect()) - 78
.unwrap_or_default(); - 79
for name in object.keys() { - 80
if !known.contains(name.as_str()) { - 81
return Err(format!( - 82
"invalid MCP arguments: unexpected parameter `{name}` for {path}" - 83
)); - 84
} - 85
} - 86
} - 87
if let Some(items) = schema.get("items") - 88
&& let Some(array) = value.as_array() - 89
{ - 90
for (index, child) in array.iter().enumerate() { - 91
validate(child, items, &format!("{path}[{index}]"))?; - 92
} - 93
} - 94
Ok(()) - 95
} - 96
- 97
fn matches_type(value: &Value, kind: &str) -> bool { - 98
match kind { - 99
"object" => value.is_object(), - 100
"array" => value.is_array(), - 101
"string" => value.is_string(), - 102
"number" => value.is_number(), - 103
"integer" => value.as_i64().is_some() || value.as_u64().is_some(), - 104
"boolean" => value.is_boolean(), - 105
"null" => value.is_null(), - 106
_ => true, - 107
} - 108
} - 109
- 110
#[cfg(test)] - 111
mod tests { - 112
use super::validate_arguments; - 113
use serde_json::json; - 114
- 115
#[test] - 116
fn validates_required_and_nested_types() { - 117
let schema = json!({ - 118
"type": "object", - 119
"required": ["input"], - 120
"properties": {"input": {"type": "string"}} - 121
}); - 122
assert!(validate_arguments(&json!({"input": "query"}), &schema).is_ok()); - 123
assert!(validate_arguments(&json!({}), &schema).is_err()); - 124
assert!(validate_arguments(&json!({"input": {"query": "query"}}), &schema).is_err()); - 125
} - 126
- 127
#[test] - 128
fn validates_array_items() { - 129
let schema = json!({"type": "array", "items": {"type": "string"}}); - 130
assert!(validate_arguments(&json!(["one", "two"]), &schema).is_ok()); - 131
assert!(validate_arguments(&json!(["one", 2]), &schema).is_err()); - 132
} - 133
- 134
#[test] - 135
fn validates_server_declared_enum_options_before_call() { - 136
let schema = json!({ - 137
"type": "object", - 138
"properties": { - 139
"query": {"type": "string"}, - 140
"depth": {"type": "string", "enum": ["basic", "advanced"]} - 141
}, - 142
"required": ["query"] - 143
}); - 144
assert!(validate_arguments(&json!({"query": "weather"}), &schema).is_ok()); - 145
assert!( - 146
validate_arguments(&json!({"query": "weather", "depth": "advanced"}), &schema).is_ok() - 147
); - 148
assert!( - 149
validate_arguments( - 150
&json!({"query": "weather", "depth": "unsupported"}), - 151
&schema - 152
) - 153
.is_err() - 154
); - 155
} - 156
- 157
#[test] - 158
fn validates_one_of_and_additional_properties() { - 159
let schema = json!({ - 160
"oneOf": [ - 161
{"properties": {"q": {"type": "string"}}, "required": ["q"], "additionalProperties": false}, - 162
{"properties": {"id": {"type": "integer"}}, "required": ["id"], "additionalProperties": false} - 163
] - 164
}); - 165
assert!(validate_arguments(&json!({"q": "hello"}), &schema).is_ok()); - 166
assert!(validate_arguments(&json!({"id": 7}), &schema).is_ok()); - 167
// neither branch - 168
assert!(validate_arguments(&json!({"nope": 1}), &schema).is_err()); - 169
// unexpected key under a matching branch - 170
assert!(validate_arguments(&json!({"q": "x", "extra": 1}), &schema).is_err()); - 171
} - 172
} - 173
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.