turul_mcp_protocol_2025_06_18/
schema.rs

1//! JSON Schema Support for MCP
2//!
3//! This module provides JSON Schema types used throughout the MCP protocol.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// Trait for generating JSON schemas from Rust types
9pub trait JsonSchemaGenerator {
10    /// Generate a ToolSchema for this type
11    fn json_schema() -> crate::tools::ToolSchema;
12}
13
14/// A JSON Schema definition
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(tag = "type", rename_all = "lowercase")]
17pub enum JsonSchema {
18    /// String type
19    String {
20        #[serde(skip_serializing_if = "Option::is_none")]
21        description: Option<String>,
22        #[serde(skip_serializing_if = "Option::is_none")]
23        pattern: Option<String>,
24        #[serde(rename = "minLength", skip_serializing_if = "Option::is_none")]
25        min_length: Option<u64>,
26        #[serde(rename = "maxLength", skip_serializing_if = "Option::is_none")]
27        max_length: Option<u64>,
28        #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
29        enum_values: Option<Vec<String>>,
30    },
31    /// Number type
32    Number {
33        #[serde(skip_serializing_if = "Option::is_none")]
34        description: Option<String>,
35        #[serde(skip_serializing_if = "Option::is_none")]
36        minimum: Option<f64>,
37        #[serde(skip_serializing_if = "Option::is_none")]
38        maximum: Option<f64>,
39    },
40    /// Integer type
41    Integer {
42        #[serde(skip_serializing_if = "Option::is_none")]
43        description: Option<String>,
44        #[serde(skip_serializing_if = "Option::is_none")]
45        minimum: Option<i64>,
46        #[serde(skip_serializing_if = "Option::is_none")]
47        maximum: Option<i64>,
48    },
49    /// Boolean type
50    Boolean {
51        #[serde(skip_serializing_if = "Option::is_none")]
52        description: Option<String>,
53    },
54    /// Array type
55    Array {
56        #[serde(skip_serializing_if = "Option::is_none")]
57        description: Option<String>,
58        #[serde(skip_serializing_if = "Option::is_none")]
59        items: Option<Box<JsonSchema>>,
60        #[serde(rename = "minItems", skip_serializing_if = "Option::is_none")]
61        min_items: Option<u64>,
62        #[serde(rename = "maxItems", skip_serializing_if = "Option::is_none")]
63        max_items: Option<u64>,
64    },
65    /// Object type
66    Object {
67        #[serde(skip_serializing_if = "Option::is_none")]
68        description: Option<String>,
69        #[serde(skip_serializing_if = "Option::is_none")]
70        properties: Option<HashMap<String, JsonSchema>>,
71        #[serde(skip_serializing_if = "Option::is_none")]
72        required: Option<Vec<String>>,
73        #[serde(
74            rename = "additionalProperties",
75            skip_serializing_if = "Option::is_none"
76        )]
77        additional_properties: Option<bool>,
78    },
79}
80
81impl JsonSchema {
82    /// Create a string schema
83    pub fn string() -> Self {
84        Self::String {
85            description: None,
86            pattern: None,
87            min_length: None,
88            max_length: None,
89            enum_values: None,
90        }
91    }
92
93    /// Create a string schema with description
94    pub fn string_with_description(description: impl Into<String>) -> Self {
95        Self::String {
96            description: Some(description.into()),
97            pattern: None,
98            min_length: None,
99            max_length: None,
100            enum_values: None,
101        }
102    }
103
104    /// Create a string enum schema
105    pub fn string_enum(values: Vec<String>) -> Self {
106        Self::String {
107            description: None,
108            pattern: None,
109            min_length: None,
110            max_length: None,
111            enum_values: Some(values),
112        }
113    }
114
115    /// Create a number schema
116    pub fn number() -> Self {
117        Self::Number {
118            description: None,
119            minimum: None,
120            maximum: None,
121        }
122    }
123
124    /// Create a number schema with description
125    pub fn number_with_description(description: impl Into<String>) -> Self {
126        Self::Number {
127            description: Some(description.into()),
128            minimum: None,
129            maximum: None,
130        }
131    }
132
133    /// Create an integer schema
134    pub fn integer() -> Self {
135        Self::Integer {
136            description: None,
137            minimum: None,
138            maximum: None,
139        }
140    }
141
142    /// Create an integer schema with description
143    pub fn integer_with_description(description: impl Into<String>) -> Self {
144        Self::Integer {
145            description: Some(description.into()),
146            minimum: None,
147            maximum: None,
148        }
149    }
150
151    /// Create a boolean schema
152    pub fn boolean() -> Self {
153        Self::Boolean { description: None }
154    }
155
156    /// Create a boolean schema with description
157    pub fn boolean_with_description(description: impl Into<String>) -> Self {
158        Self::Boolean {
159            description: Some(description.into()),
160        }
161    }
162
163    /// Create an array schema
164    pub fn array(items: JsonSchema) -> Self {
165        Self::Array {
166            description: None,
167            items: Some(Box::new(items)),
168            min_items: None,
169            max_items: None,
170        }
171    }
172
173    /// Create an array schema with description
174    pub fn array_with_description(items: JsonSchema, description: impl Into<String>) -> Self {
175        Self::Array {
176            description: Some(description.into()),
177            items: Some(Box::new(items)),
178            min_items: None,
179            max_items: None,
180        }
181    }
182
183    /// Create an object schema
184    pub fn object() -> Self {
185        Self::Object {
186            description: None,
187            properties: None,
188            required: None,
189            additional_properties: None,
190        }
191    }
192
193    /// Create an object schema with properties
194    pub fn object_with_properties(properties: HashMap<String, JsonSchema>) -> Self {
195        Self::Object {
196            description: None,
197            properties: Some(properties),
198            required: None,
199            additional_properties: None,
200        }
201    }
202
203    /// Create an object schema with properties and required fields
204    pub fn object_with_required(
205        properties: HashMap<String, JsonSchema>,
206        required: Vec<String>,
207    ) -> Self {
208        Self::Object {
209            description: None,
210            properties: Some(properties),
211            required: Some(required),
212            additional_properties: None,
213        }
214    }
215
216    /// Add description to any schema
217    pub fn with_description(mut self, description: impl Into<String>) -> Self {
218        match &mut self {
219            JsonSchema::String { description: d, .. } => *d = Some(description.into()),
220            JsonSchema::Number { description: d, .. } => *d = Some(description.into()),
221            JsonSchema::Integer { description: d, .. } => *d = Some(description.into()),
222            JsonSchema::Boolean { description: d, .. } => *d = Some(description.into()),
223            JsonSchema::Array { description: d, .. } => *d = Some(description.into()),
224            JsonSchema::Object { description: d, .. } => *d = Some(description.into()),
225        }
226        self
227    }
228
229    /// Add minimum constraint to number schema
230    pub fn with_minimum(mut self, minimum: f64) -> Self {
231        match &mut self {
232            JsonSchema::Number { minimum: m, .. } => *m = Some(minimum),
233            JsonSchema::Integer { minimum: m, .. } => *m = Some(minimum as i64),
234            _ => {} // Ignore for non-numeric types
235        }
236        self
237    }
238
239    /// Add maximum constraint to number schema
240    pub fn with_maximum(mut self, maximum: f64) -> Self {
241        match &mut self {
242            JsonSchema::Number { maximum: m, .. } => *m = Some(maximum),
243            JsonSchema::Integer { maximum: m, .. } => *m = Some(maximum as i64),
244            _ => {} // Ignore for non-numeric types
245        }
246        self
247    }
248
249    /// Add properties to object schema
250    pub fn with_properties(mut self, properties: HashMap<String, JsonSchema>) -> Self {
251        if let JsonSchema::Object { properties: p, .. } = &mut self {
252            *p = Some(properties);
253        }
254        self
255    }
256
257    /// Add required fields to object schema
258    pub fn with_required(mut self, required: Vec<String>) -> Self {
259        if let JsonSchema::Object { required: r, .. } = &mut self {
260            *r = Some(required);
261        }
262        self
263    }
264}
265
266/// Converts common Rust types to JsonSchema
267pub trait ToJsonSchema {
268    fn to_json_schema() -> JsonSchema;
269}
270
271impl ToJsonSchema for String {
272    fn to_json_schema() -> JsonSchema {
273        JsonSchema::string()
274    }
275}
276
277impl ToJsonSchema for &str {
278    fn to_json_schema() -> JsonSchema {
279        JsonSchema::string()
280    }
281}
282
283impl ToJsonSchema for i32 {
284    fn to_json_schema() -> JsonSchema {
285        JsonSchema::integer()
286    }
287}
288
289impl ToJsonSchema for i64 {
290    fn to_json_schema() -> JsonSchema {
291        JsonSchema::integer()
292    }
293}
294
295impl ToJsonSchema for f32 {
296    fn to_json_schema() -> JsonSchema {
297        JsonSchema::number()
298    }
299}
300
301impl ToJsonSchema for f64 {
302    fn to_json_schema() -> JsonSchema {
303        JsonSchema::number()
304    }
305}
306
307impl ToJsonSchema for bool {
308    fn to_json_schema() -> JsonSchema {
309        JsonSchema::boolean()
310    }
311}
312
313impl<T: ToJsonSchema> ToJsonSchema for Vec<T> {
314    fn to_json_schema() -> JsonSchema {
315        JsonSchema::array(T::to_json_schema())
316    }
317}
318
319impl<T: ToJsonSchema> ToJsonSchema for Option<T> {
320    fn to_json_schema() -> JsonSchema {
321        T::to_json_schema()
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    #[test]
330    fn test_string_schema() {
331        let schema = JsonSchema::string_with_description("A test string");
332        let json = serde_json::to_string(&schema).unwrap();
333        assert!(json.contains("string"));
334        assert!(json.contains("A test string"));
335    }
336
337    #[test]
338    fn test_object_schema() {
339        let mut properties = HashMap::new();
340        properties.insert("name".to_string(), JsonSchema::string());
341        properties.insert("age".to_string(), JsonSchema::integer());
342
343        let schema = JsonSchema::object_with_required(properties, vec!["name".to_string()]);
344
345        let json = serde_json::to_string(&schema).unwrap();
346        assert!(json.contains("object"));
347        assert!(json.contains("name"));
348        assert!(json.contains("age"));
349    }
350
351    #[test]
352    fn test_array_schema() {
353        let schema = JsonSchema::array(JsonSchema::string());
354        let json = serde_json::to_string(&schema).unwrap();
355        assert!(json.contains("array"));
356    }
357
358    #[test]
359    fn test_enum_schema() {
360        let schema = JsonSchema::string_enum(vec!["option1".to_string(), "option2".to_string()]);
361        let json = serde_json::to_string(&schema).unwrap();
362        assert!(json.contains("option1"));
363        assert!(json.contains("option2"));
364    }
365
366    #[test]
367    fn test_to_json_schema_trait() {
368        assert!(matches!(
369            String::to_json_schema(),
370            JsonSchema::String { .. }
371        ));
372        assert!(matches!(i32::to_json_schema(), JsonSchema::Integer { .. }));
373        assert!(matches!(f64::to_json_schema(), JsonSchema::Number { .. }));
374        assert!(matches!(bool::to_json_schema(), JsonSchema::Boolean { .. }));
375    }
376}