Skip to main content

headless_lms_mcp/tools/
schema.rs

1//! Input schema generation: draft 2020-12, no `oneOf`/`anyOf` and no nullable type arrays, so the
2//! schemas survive Codex's schema sanitiser.
3
4use schemars::{Schema, SchemaGenerator, json_schema};
5
6/// Schema for patch `value`: deliberately untyped, since block schemas would push tool schemas
7/// past client size limits.
8pub fn untyped_value(_: &mut SchemaGenerator) -> Schema {
9    json_schema!({
10        "description": "Value to write: a block or list of blocks, an entity, a field value or null."
11    })
12}
13
14/// The input schema of an args struct, with every subschema inlined so no `$ref` or `$defs`
15/// depends on client support.
16pub fn input_schema<T: schemars::JsonSchema>() -> serde_json::Map<String, serde_json::Value> {
17    let settings =
18        schemars::generate::SchemaSettings::draft2020_12().with(|s| s.inline_subschemas = true);
19    let schema = settings.into_generator().into_root_schema_for::<T>();
20    let mut value = schema.to_value();
21    flatten_unions(&mut value);
22    let mut object = match value {
23        serde_json::Value::Object(object) => object,
24        _ => serde_json::Map::new(),
25    };
26    object.remove("title");
27    object.remove("description");
28    object
29}
30
31/// Removes the unions schemars emits (nullable types, `null` alternatives, string enums with
32/// documented variants), which Codex would flatten. Optional fields stay out of `required` instead.
33fn flatten_unions(value: &mut serde_json::Value) {
34    use serde_json::Value;
35    match value {
36        Value::Object(object) => {
37            if let Some(Value::Array(types)) = object.get("type") {
38                let non_null: Vec<Value> = types
39                    .iter()
40                    .filter(|t| t.as_str() != Some("null"))
41                    .cloned()
42                    .collect();
43                if non_null.len() == 1 {
44                    object.insert("type".into(), non_null[0].clone());
45                }
46            }
47            for key in ["anyOf", "oneOf"] {
48                if let Some(Value::Array(alternatives)) = object.get(key) {
49                    let non_null: Vec<Value> = alternatives
50                        .iter()
51                        .filter(|a| a.get("type").and_then(Value::as_str) != Some("null"))
52                        .cloned()
53                        .collect();
54                    if non_null.len() == 1 {
55                        object.remove(key);
56                        if let Value::Object(inner) = &non_null[0] {
57                            for (k, v) in inner {
58                                object.entry(k.clone()).or_insert_with(|| v.clone());
59                            }
60                        }
61                    }
62                }
63            }
64            if let Some(Value::Array(alternatives)) = object.get("oneOf") {
65                let constants: Option<Vec<Value>> = alternatives
66                    .iter()
67                    .map(|a| a.get("const").cloned())
68                    .collect();
69                if let Some(constants) = constants.filter(|c| c.iter().all(Value::is_string)) {
70                    let described: Vec<String> = alternatives
71                        .iter()
72                        .filter_map(|a| {
73                            Some(format!(
74                                "{}: {}",
75                                a.get("const")?.as_str()?,
76                                a.get("description")?.as_str()?.trim_end_matches('.')
77                            ))
78                        })
79                        .collect();
80                    object.remove("oneOf");
81                    object.insert("type".into(), Value::String("string".into()));
82                    object.insert("enum".into(), Value::Array(constants));
83                    if !described.is_empty() && !object.contains_key("description") {
84                        object.insert("description".into(), Value::String(described.join("; ")));
85                    }
86                }
87            }
88            if object.get("default").is_some_and(Value::is_null) {
89                object.remove("default");
90            }
91            if let Some(Value::Array(options)) = object.get_mut("enum") {
92                options.retain(|o| !o.is_null());
93            }
94            object.values_mut().for_each(flatten_unions);
95        }
96        Value::Array(items) => items.iter_mut().for_each(flatten_unions),
97        _ => {}
98    }
99}