Skip to main content

vtcode_core/mcp/
schema.rs

1/// JSON Schema generation and validation for MCP tools
2///
3/// This module provides JSON Schema 2020-12 validation using jsonschema library.
4/// Phase 1 provided basic type checking; Phase 2 adds full schema validation.
5use anyhow::Result;
6use serde_json::{Value, json};
7
8/// Validate input against a JSON Schema (Phase 2 - Full JSON Schema 2020-12)
9///
10/// # Arguments
11/// * `schema` - The JSON Schema to validate against (JSON Schema 2020-12)
12/// * `input` - The input value to validate
13///
14/// # Returns
15/// * `Ok(())` if validation succeeds
16/// * `Err` with detailed error message if validation fails
17///
18/// # Validation Coverage (Phase 2)
19/// - Type validation (string, number, integer, boolean, object, array)
20/// - Required properties
21/// - Min/max constraints (minLength, maxLength, minimum, maximum)
22/// - Pattern matching (regex)
23/// - Enum validation
24/// - Nested objects and arrays
25/// - Complex schemas (oneOf, anyOf, allOf, not)
26pub fn validate_against_schema(schema: &Value, input: &Value) -> Result<()> {
27    if input.is_null() {
28        anyhow::bail!("Input cannot be null");
29    }
30
31    // Use jsonschema for full JSON Schema 2020-12 validation
32    jsonschema::validate(schema, input)
33        .map_err(|err| anyhow::anyhow!("Schema validation failed: {}", err))
34}
35
36/// Validate tool input parameters (Phase 2 - Full validation)
37///
38/// Validates tool input against the tool's schema using full JSON Schema support.
39pub fn validate_tool_input(input_schema: Option<&Value>, input: &Value) -> Result<()> {
40    if input.is_null() {
41        anyhow::bail!("Tool input cannot be null");
42    }
43
44    // If schema is provided, validate against it
45    if let Some(schema) = input_schema {
46        validate_against_schema(schema, input)?;
47    }
48
49    Ok(())
50}
51
52/// Build a simple JSON Schema for a tool with no specific input requirements
53pub fn simple_schema() -> Value {
54    json!({
55        "type": "object",
56        "properties": {},
57        "$schema": "https://json-schema.org/draft/2020-12/schema"
58    })
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn test_validate_simple_object() {
67        let schema = json!({
68            "type": "object",
69            "properties": {
70                "name": { "type": "string" }
71            }
72        });
73
74        let valid_input = json!({"name": "test"});
75        validate_against_schema(&schema, &valid_input).unwrap();
76
77        let invalid_input = json!({"name": 123});
78        assert!(validate_against_schema(&schema, &invalid_input).is_err());
79    }
80
81    #[test]
82    fn test_validate_required_properties() {
83        let schema = json!({
84            "type": "object",
85            "properties": {
86                "name": { "type": "string" },
87                "age": { "type": "integer" }
88            },
89            "required": ["name"]
90        });
91
92        let valid = json!({"name": "John"});
93        validate_against_schema(&schema, &valid).unwrap();
94
95        let invalid = json!({"age": 30});
96        assert!(validate_against_schema(&schema, &invalid).is_err());
97    }
98
99    #[test]
100    fn test_validate_string_length_constraints() {
101        let schema = json!({
102            "type": "object",
103            "properties": {
104                "name": {
105                    "type": "string",
106                    "minLength": 1,
107                    "maxLength": 50
108                }
109            }
110        });
111
112        let valid = json!({"name": "John"});
113        validate_against_schema(&schema, &valid).unwrap();
114
115        let invalid_empty = json!({"name": ""});
116        assert!(validate_against_schema(&schema, &invalid_empty).is_err());
117
118        let invalid_long = json!({"name": "x".repeat(51)});
119        assert!(validate_against_schema(&schema, &invalid_long).is_err());
120    }
121
122    #[test]
123    fn test_validate_enum_values() {
124        let schema = json!({
125            "type": "object",
126            "properties": {
127                "status": {
128                    "type": "string",
129                    "enum": ["active", "inactive", "pending"]
130                }
131            }
132        });
133
134        let valid = json!({"status": "active"});
135        validate_against_schema(&schema, &valid).unwrap();
136
137        let invalid = json!({"status": "unknown"});
138        assert!(validate_against_schema(&schema, &invalid).is_err());
139    }
140
141    #[test]
142    fn test_validate_array_items() {
143        let schema = json!({
144            "type": "object",
145            "properties": {
146                "tags": {
147                    "type": "array",
148                    "items": { "type": "string" }
149                }
150            }
151        });
152
153        let valid = json!({"tags": ["rust", "mcp"]});
154        validate_against_schema(&schema, &valid).unwrap();
155
156        let invalid = json!({"tags": ["rust", 123]});
157        assert!(validate_against_schema(&schema, &invalid).is_err());
158    }
159
160    #[test]
161    fn test_validate_nested_objects() {
162        let schema = json!({
163            "type": "object",
164            "properties": {
165                "user": {
166                    "type": "object",
167                    "properties": {
168                        "name": { "type": "string" },
169                        "age": { "type": "integer" }
170                    },
171                    "required": ["name"]
172                }
173            }
174        });
175
176        let valid = json!({"user": {"name": "John", "age": 30}});
177        validate_against_schema(&schema, &valid).unwrap();
178
179        let invalid = json!({"user": {"age": 30}});
180        assert!(validate_against_schema(&schema, &invalid).is_err());
181    }
182
183    #[test]
184    fn test_validate_tool_input_with_no_schema() {
185        let input = json!({"any": "value"});
186        validate_tool_input(None, &input).unwrap();
187    }
188
189    #[test]
190    fn test_validate_tool_input_with_schema() {
191        let schema = json!({
192            "type": "object",
193            "properties": {
194                "path": { "type": "string" }
195            },
196            "required": ["path"]
197        });
198
199        let valid = json!({"path": "/home"});
200        validate_tool_input(Some(&schema), &valid).unwrap();
201
202        let invalid = json!({});
203        assert!(validate_tool_input(Some(&schema), &invalid).is_err());
204    }
205
206    #[test]
207    fn test_simple_schema() {
208        let schema = simple_schema();
209        let input = json!({});
210        validate_against_schema(&schema, &input).unwrap();
211    }
212
213    #[test]
214    fn test_null_input_rejection() {
215        let schema = json!({"type": "object"});
216        let null_input = json!(null);
217        assert!(validate_against_schema(&schema, &null_input).is_err());
218    }
219}