use serde::Deserialize; use serde::Serialize; use serde_json::Value as JsonValue; use serde_json::json; use std::collections::BTreeMap; /// Generic JSON-Schema subset needed for our tool definitions. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(tag = "type", rename_all = "lowercase")] pub enum JsonSchema { Boolean { #[serde(skip_serializing_if = "Option::is_none")] description: Option, }, String { #[serde(skip_serializing_if = "Option::is_none")] description: Option, }, /// MCP schema allows "number" | "integer" for Number. #[serde(alias = "integer")] Number { #[serde(skip_serializing_if = "Option::is_none")] description: Option, }, Array { items: Box, #[serde(skip_serializing_if = "Option::is_none")] description: Option, }, Object { properties: BTreeMap, #[serde(skip_serializing_if = "Option::is_none")] required: Option>, #[serde( rename = "additionalProperties", skip_serializing_if = "Option::is_none" )] additional_properties: Option, }, } /// Whether additional properties are allowed, and if so, any required schema. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(untagged)] pub enum AdditionalProperties { Boolean(bool), Schema(Box), } impl From for AdditionalProperties { fn from(value: bool) -> Self { Self::Boolean(value) } } impl From for AdditionalProperties { fn from(value: JsonSchema) -> Self { Self::Schema(Box::new(value)) } } /// Parse the tool `input_schema` or return an error for invalid schema. pub fn parse_tool_input_schema(input_schema: &JsonValue) -> Result { let mut input_schema = input_schema.clone(); sanitize_json_schema(&mut input_schema); serde_json::from_value::(input_schema) } /// Sanitize a JSON Schema (as serde_json::Value) so it can fit our limited /// JsonSchema enum. This function: /// - Ensures every schema object has a "type". If missing, infers it from /// common keywords (properties => object, items => array, enum/const/format => string) /// and otherwise defaults to "string". /// - Fills required child fields (e.g. array items, object properties) with /// permissive defaults when absent. fn sanitize_json_schema(value: &mut JsonValue) { match value { JsonValue::Bool(_) => { // JSON Schema boolean form: true/false. Coerce to an accept-all string. *value = json!({ "type": "string" }); } JsonValue::Array(values) => { for value in values { sanitize_json_schema(value); } } JsonValue::Object(map) => { if let Some(properties) = map.get_mut("properties") && let Some(properties_map) = properties.as_object_mut() { for value in properties_map.values_mut() { sanitize_json_schema(value); } } if let Some(items) = map.get_mut("items") { sanitize_json_schema(items); } for combiner in ["oneOf", "anyOf", "allOf", "prefixItems"] { if let Some(value) = map.get_mut(combiner) { sanitize_json_schema(value); } } let mut schema_type = map .get("type") .and_then(|value| value.as_str()) .map(str::to_string); if schema_type.is_none() && let Some(JsonValue::Array(types)) = map.get("type") { for candidate in types { if let Some(candidate_type) = candidate.as_str() && matches!( candidate_type, "object" | "array" | "string" | "number" | "integer" | "boolean" ) { schema_type = Some(candidate_type.to_string()); break; } } } if schema_type.is_none() { if map.contains_key("properties") || map.contains_key("required") || map.contains_key("additionalProperties") { schema_type = Some("object".to_string()); } else if map.contains_key("items") || map.contains_key("prefixItems") { schema_type = Some("array".to_string()); } else if map.contains_key("enum") || map.contains_key("const") || map.contains_key("format") { schema_type = Some("string".to_string()); } else if map.contains_key("minimum") || map.contains_key("maximum") || map.contains_key("exclusiveMinimum") || map.contains_key("exclusiveMaximum") || map.contains_key("multipleOf") { schema_type = Some("number".to_string()); } } let schema_type = schema_type.unwrap_or_else(|| "string".to_string()); map.insert("type".to_string(), JsonValue::String(schema_type.clone())); if schema_type == "object" { if !map.contains_key("properties") { map.insert( "properties".to_string(), JsonValue::Object(serde_json::Map::new()), ); } if let Some(additional_properties) = map.get_mut("additionalProperties") && !matches!(additional_properties, JsonValue::Bool(_)) { sanitize_json_schema(additional_properties); } } if schema_type == "array" && !map.contains_key("items") { map.insert("items".to_string(), json!({ "type": "string" })); } } _ => {} } } #[cfg(test)] #[path = "json_schema_tests.rs"] mod tests;