1use super::*;
2
3pub fn schema_for_input(input: McpInput) -> McpSchema {
4 match input.input_shape() {
5 McpInputShape::Unsupported => McpSchema::impossible(),
6 McpInputShape::Scalar(kind) => schema_for_primitive(kind),
7 McpInputShape::List(kind) => array_schema(schema_for_primitive(kind)),
8 McpInputShape::Set(kind) => unique_array_schema(schema_for_primitive(kind)),
9 McpInputShape::Range(kind) => range_schema(schema_for_range_bound(kind)),
10 McpInputShape::Object => McpSchema::object(),
11 }
12}
13
14pub fn mcp_input_descriptor_value(input: McpInput) -> Value {
20 match input.input_shape() {
21 McpInputShape::Unsupported => json!({
22 "supported": false,
23 "shape": "unsupported",
24 }),
25 McpInputShape::Scalar(kind) => json!({
26 "supported": true,
27 "shape": "scalar",
28 "primitive": kind.as_str(),
29 }),
30 McpInputShape::List(kind) => json!({
31 "supported": true,
32 "shape": "list",
33 "items": kind.as_str(),
34 }),
35 McpInputShape::Set(kind) => json!({
36 "supported": true,
37 "shape": "set",
38 "items": kind.as_str(),
39 }),
40 McpInputShape::Range(kind) => json!({
41 "supported": true,
42 "shape": "range",
43 "bound": kind.as_str(),
44 }),
45 McpInputShape::Object => json!({
46 "supported": true,
47 "shape": "object",
48 }),
49 }
50}
51
52pub fn apply_validation_schema_metadata(
54 object: &mut Map<String, Value>,
55 extension_key: &str,
56 rules: &[McpValidationRule],
57) {
58 if !rules.is_empty() {
59 object.insert(
60 extension_key.to_string(),
61 Value::Array(rules.iter().map(|rule| rule.to_value()).collect()),
62 );
63 }
64 apply_validation_schema_hints(object, rules);
65}
66
67pub fn apply_validation_schema_hints(object: &mut Map<String, Value>, rules: &[McpValidationRule]) {
69 for rule in rules {
70 match rule.validator() {
71 "LenValidation" => apply_len_validation_schema_hint(*rule, object),
72 "RangeValidation" => apply_range_validation_schema_hint(*rule, object),
73 "NonEmptyValidation" => apply_non_empty_validation_schema_hint(object),
74 _ => {},
75 }
76 }
77}
78
79fn apply_len_validation_schema_hint(rule: McpValidationRule, object: &mut Map<String, Value>) {
80 let Some(schema_type) = primary_schema_type(object) else {
81 return;
82 };
83 let (min_keyword, max_keyword) = match schema_type {
84 "string" => ("minLength", "maxLength"),
85 "array" => ("minItems", "maxItems"),
86 _ => return,
87 };
88 if let Some(min) = rule
89 .params()
90 .iter()
91 .find(|param| param.name() == "min")
92 .and_then(|param| param.literal_value())
93 .and_then(literal_to_u64)
94 {
95 object.insert(min_keyword.to_string(), Value::Number(min.into()));
96 }
97 if let Some(max) = rule
98 .params()
99 .iter()
100 .find(|param| param.name() == "max")
101 .and_then(|param| param.literal_value())
102 .and_then(literal_to_u64)
103 {
104 object.insert(max_keyword.to_string(), Value::Number(max.into()));
105 }
106}
107
108fn apply_range_validation_schema_hint(rule: McpValidationRule, object: &mut Map<String, Value>) {
109 if !matches!(primary_schema_type(object), Some("integer" | "number")) {
110 return;
111 }
112 let exclusive_min = rule
113 .params()
114 .iter()
115 .find(|param| param.name() == "exclusive_min")
116 .and_then(|param| param.literal_value())
117 .and_then(literal_to_bool)
118 .unwrap_or(false);
119 let exclusive_max = rule
120 .params()
121 .iter()
122 .find(|param| param.name() == "exclusive_max")
123 .and_then(|param| param.literal_value())
124 .and_then(literal_to_bool)
125 .unwrap_or(false);
126 if let Some(min) = rule
127 .params()
128 .iter()
129 .find(|param| param.name() == "min")
130 .and_then(|param| param.literal_value())
131 .and_then(literal_to_number_value)
132 {
133 let keyword = if exclusive_min {
134 "exclusiveMinimum"
135 } else {
136 "minimum"
137 };
138 object.insert(keyword.to_string(), min);
139 }
140 if let Some(max) = rule
141 .params()
142 .iter()
143 .find(|param| param.name() == "max")
144 .and_then(|param| param.literal_value())
145 .and_then(literal_to_number_value)
146 {
147 let keyword = if exclusive_max {
148 "exclusiveMaximum"
149 } else {
150 "maximum"
151 };
152 object.insert(keyword.to_string(), max);
153 }
154}
155
156fn apply_non_empty_validation_schema_hint(object: &mut Map<String, Value>) {
157 match primary_schema_type(object) {
158 Some("string") => {
159 object
160 .entry("minLength")
161 .or_insert(Value::Number(1_u64.into()));
162 },
163 Some("array") => {
164 object
165 .entry("minItems")
166 .or_insert(Value::Number(1_u64.into()));
167 },
168 _ => {},
169 }
170}
171
172fn primary_schema_type(object: &Map<String, Value>) -> Option<&str> {
173 object.get("type").and_then(Value::as_str).or_else(|| {
174 object
175 .get("anyOf")
176 .and_then(Value::as_array)
177 .and_then(|schemas| schemas.iter().find_map(schema_type_from_value))
178 })
179}
180
181fn schema_type_from_value(value: &Value) -> Option<&str> {
182 match value {
183 Value::Object(object) => object.get("type").and_then(Value::as_str),
184 _ => None,
185 }
186}
187
188fn literal_to_u64(literal: &str) -> Option<u64> {
189 literal.parse::<u64>().ok()
190}
191
192fn literal_to_bool(literal: &str) -> Option<bool> {
193 match literal {
194 "true" => Some(true),
195 "false" => Some(false),
196 _ => None,
197 }
198}
199
200fn literal_to_number_value(literal: &str) -> Option<Value> {
201 if let Ok(value) = literal.parse::<i64>() {
202 return Some(Value::Number(value.into()));
203 }
204 if let Ok(value) = literal.parse::<u64>() {
205 return Some(Value::Number(value.into()));
206 }
207 literal
208 .parse::<f64>()
209 .ok()
210 .and_then(serde_json::Number::from_f64)
211 .map(Value::Number)
212}
213
214pub fn range_schema(bound_schema: McpSchema) -> McpSchema {
215 McpSchema::object()
216 .with_properties(McpSchemaProperties::from([
217 ("min".to_string(), nullable_schema(bound_schema.clone())),
218 ("max".to_string(), nullable_schema(bound_schema)),
219 ]))
220 .with_additional_properties(false)
221}
222
223pub fn schema_for_primitive(kind: McpPrimitiveKind) -> McpSchema {
224 match kind {
225 McpPrimitiveKind::Any => McpSchema::any(),
226 McpPrimitiveKind::Boolean => McpSchema::boolean(),
227 McpPrimitiveKind::Integer => McpSchema::integer(),
228 McpPrimitiveKind::Number => McpSchema::number(),
229 McpPrimitiveKind::Decimal => McpSchema::any_of([McpSchema::number(), McpSchema::string()]),
230 McpPrimitiveKind::String => McpSchema::string(),
231 McpPrimitiveKind::Date => McpSchema::string().with_format(McpStringFormat::Date),
232 McpPrimitiveKind::DateTime => McpSchema::string().with_format(McpStringFormat::DateTime),
233 }
234}
235
236pub fn schema_for_range_bound(kind: McpRangeBoundKind) -> McpSchema {
237 schema_for_primitive(kind.primitive_kind())
238}
239
240pub fn nullable_schema(schema: McpSchema) -> McpSchema {
241 McpSchema::any_of([schema, McpSchema::null()])
242}
243
244pub fn schema_allows_null(schema: &McpSchema) -> bool {
245 value_schema_allows_null(schema.as_value())
246}
247
248pub(crate) fn value_schema_allows_null(schema: &Value) -> bool {
249 match schema {
250 Value::Bool(value) => *value,
251 Value::Object(object) if object.is_empty() => true,
252 Value::Object(object) => {
253 object.get("type").is_some_and(type_allows_null)
254 || object.get("const").is_some_and(Value::is_null)
255 || object
256 .get("enum")
257 .and_then(Value::as_array)
258 .is_some_and(|values| values.iter().any(Value::is_null))
259 || object
260 .get("anyOf")
261 .and_then(Value::as_array)
262 .is_some_and(|schemas| schemas.iter().any(value_schema_allows_null))
263 || object
264 .get("oneOf")
265 .and_then(Value::as_array)
266 .is_some_and(|schemas| schemas.iter().any(value_schema_allows_null))
267 || object
268 .get("allOf")
269 .and_then(Value::as_array)
270 .is_some_and(|schemas| schemas.iter().all(value_schema_allows_null))
271 },
272 _ => false,
273 }
274}
275
276fn type_allows_null(value: &Value) -> bool {
277 match value {
278 Value::String(value) => value == "null",
279 Value::Array(values) => values
280 .iter()
281 .any(|value| matches!(value, Value::String(value) if value == "null")),
282 _ => false,
283 }
284}