use schemars::Schema;
use serde_json::{Map, Value};
pub(crate) fn strip_int_formats(schema: &mut Schema) {
if let Some(object) = schema.as_object_mut() {
strip_in_map(object);
}
}
fn strip_in_map(map: &mut Map<String, Value>) {
let drop_format = matches!(map.get("format"), Some(Value::String(f)) if is_rust_int_format(f));
if drop_format {
map.remove("format");
}
for value in map.values_mut() {
strip_in_value(value);
}
}
fn strip_in_value(value: &mut Value) {
match value {
Value::Object(map) => strip_in_map(map),
Value::Array(items) => items.iter_mut().for_each(strip_in_value),
_ => {}
}
}
#[cfg(feature = "mcp")]
pub(crate) fn widen_id_properties(map: &mut Map<String, Value>, keys: &[&str]) {
if let Some(Value::Object(properties)) = map.get_mut("properties") {
for (name, subschema) in properties.iter_mut() {
if keys.contains(&name.as_str()) {
widen_id_schema(subschema);
}
}
}
for value in map.values_mut() {
widen_in_value(value, keys);
}
}
#[cfg(feature = "mcp")]
fn widen_in_value(value: &mut Value, keys: &[&str]) {
match value {
Value::Object(map) => widen_id_properties(map, keys),
Value::Array(items) => items.iter_mut().for_each(|item| widen_in_value(item, keys)),
_ => {}
}
}
#[cfg(feature = "mcp")]
fn widen_id_schema(schema: &mut Value) {
let Value::Object(map) = schema else {
return;
};
match map.get("type").cloned() {
Some(Value::String(kind)) if kind == "integer" => {
map.insert(
"type".to_owned(),
Value::Array(vec![
Value::String("integer".to_owned()),
Value::String("string".to_owned()),
]),
);
}
Some(Value::String(kind)) if kind == "array" => {
if let Some(items) = map.get_mut("items") {
widen_id_schema(items);
}
}
Some(Value::Array(mut kinds)) => {
let has_integer = kinds.iter().any(|kind| kind == "integer");
let has_string = kinds.iter().any(|kind| kind == "string");
if has_integer && !has_string {
let after = kinds
.iter()
.position(|kind| kind == "integer")
.map_or(kinds.len(), |position| position + 1);
kinds.insert(after, Value::String("string".to_owned()));
map.insert("type".to_owned(), Value::Array(kinds));
}
}
_ => {}
}
}
#[cfg(feature = "mcp")]
const MAX_INLINE_DEPTH: usize = 8;
#[cfg(feature = "mcp")]
type InlineChain = Vec<String>;
#[cfg(feature = "mcp")]
pub(crate) fn inline_ref_only_properties(map: &mut Map<String, Value>) {
let Some(Value::Object(defs)) = map.get("$defs").cloned() else {
return;
};
let mut chain = InlineChain::new();
inline_in_map(map, &defs, &mut chain, 0);
prune_unreferenced_defs(map);
}
#[cfg(feature = "mcp")]
fn prune_unreferenced_defs(map: &mut Map<String, Value>) {
let Some(Value::Object(defs)) = map.get("$defs") else {
return;
};
let names: Vec<String> = defs.keys().cloned().collect();
let defs = defs.clone();
let mut live: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
let mut outside = map.clone();
outside.remove("$defs");
collect_refs(&Value::Object(outside), &mut live);
loop {
let mut grown = false;
for name in &names {
if !live.contains(name) {
continue;
}
if let Some(def) = defs.get(name) {
let before = live.len();
collect_refs(def, &mut live);
grown |= live.len() != before;
}
}
if !grown {
break;
}
}
if let Some(Value::Object(defs)) = map.get_mut("$defs") {
defs.retain(|name, _| live.contains(name));
if defs.is_empty() {
map.remove("$defs");
}
}
}
#[cfg(feature = "mcp")]
fn collect_refs(value: &Value, out: &mut std::collections::BTreeSet<String>) {
match value {
Value::Object(map) => {
if let Some(Value::String(target)) = map.get("$ref") {
if let Some(name) = target.strip_prefix("#/$defs/") {
out.insert(name.to_owned());
}
}
for sub in map.values() {
collect_refs(sub, out);
}
}
Value::Array(entries) => {
for sub in entries {
collect_refs(sub, out);
}
}
_ => {}
}
}
#[cfg(feature = "mcp")]
fn inline_in_map(
map: &mut Map<String, Value>,
defs: &Map<String, Value>,
chain: &mut InlineChain,
depth: usize,
) {
if depth >= MAX_INLINE_DEPTH {
return;
}
inline_property_slots(map, defs, chain, depth);
inline_item_slots(map, defs, chain, depth);
inline_union_branches(map, defs, chain, depth);
inline_remaining_keywords(map, defs, chain, depth);
}
#[cfg(feature = "mcp")]
const SLOT_KEYWORDS: [&str; 6] = [
"$defs",
"properties",
"items",
"anyOf",
"oneOf",
"prefixItems",
];
#[cfg(feature = "mcp")]
fn inline_property_slots(
map: &mut Map<String, Value>,
defs: &Map<String, Value>,
chain: &mut InlineChain,
depth: usize,
) {
if let Some(Value::Object(properties)) = map.get_mut("properties") {
for slot in properties.values_mut() {
inline_slot(slot, defs, chain, depth);
}
}
}
#[cfg(feature = "mcp")]
fn inline_item_slots(
map: &mut Map<String, Value>,
defs: &Map<String, Value>,
chain: &mut InlineChain,
depth: usize,
) {
match map.get_mut("items") {
Some(Value::Array(entries)) => {
for entry in entries {
inline_slot(entry, defs, chain, depth);
}
}
Some(single) => inline_slot(single, defs, chain, depth),
None => {}
}
}
#[cfg(feature = "mcp")]
fn inline_union_branches(
map: &mut Map<String, Value>,
defs: &Map<String, Value>,
chain: &mut InlineChain,
depth: usize,
) {
for keyword in ["anyOf", "oneOf", "prefixItems"] {
if let Some(Value::Array(branches)) = map.get_mut(keyword) {
for branch in branches {
inline_slot(branch, defs, chain, depth);
}
}
}
}
#[cfg(feature = "mcp")]
fn inline_remaining_keywords(
map: &mut Map<String, Value>,
defs: &Map<String, Value>,
chain: &mut InlineChain,
depth: usize,
) {
for (key, value) in map.iter_mut() {
if !SLOT_KEYWORDS.contains(&key.as_str()) {
inline_in_value(value, defs, chain, depth);
}
}
}
#[cfg(feature = "mcp")]
fn inline_in_value(
value: &mut Value,
defs: &Map<String, Value>,
chain: &mut InlineChain,
depth: usize,
) {
match value {
Value::Object(map) => inline_in_map(map, defs, chain, depth),
Value::Array(items) => items
.iter_mut()
.for_each(|item| inline_in_value(item, defs, chain, depth)),
_ => {}
}
}
#[cfg(feature = "mcp")]
fn inline_slot(slot: &mut Value, defs: &Map<String, Value>, chain: &mut InlineChain, depth: usize) {
let Value::Object(prop) = slot else {
return;
};
if !prop.contains_key("type") {
if let Some(name) = ref_only_target(prop) {
if !chain.contains(&name) {
if let Some(Value::Object(definition)) = defs.get(&name) {
let mut merged = definition.clone();
for (key, value) in prop.iter() {
if key != "$ref" && key != "allOf" {
merged.insert(key.clone(), value.clone());
}
}
*prop = merged;
chain.push(name);
inline_in_map(prop, defs, chain, depth + 1);
chain.pop();
return;
}
}
}
}
inline_in_map(prop, defs, chain, depth + 1);
}
#[cfg(feature = "mcp")]
fn ref_only_target(prop: &Map<String, Value>) -> Option<String> {
let reference = match (prop.get("$ref"), prop.get("allOf")) {
(Some(Value::String(r)), _) => r.clone(),
(None, Some(Value::Array(items))) if items.len() == 1 => match &items[0] {
Value::Object(inner) => match inner.get("$ref") {
Some(Value::String(r)) => r.clone(),
_ => return None,
},
_ => return None,
},
_ => return None,
};
reference.strip_prefix("#/$defs/").map(str::to_owned)
}
fn is_rust_int_format(format: &str) -> bool {
matches!(
format,
"uint"
| "uint8"
| "uint16"
| "uint32"
| "uint64"
| "uint128"
| "int"
| "int8"
| "int16"
| "int32"
| "int64"
| "int128"
)
}
#[cfg(test)]
#[path = "schema_tests.rs"]
mod tests;
#[cfg(feature = "mcp")]
pub(crate) const WIRE_ID_KEYS: &[&str] =
&["fragment_id", "content_hash", "memory_id", "fragment_ids"];
#[cfg(feature = "mcp")]
pub(crate) fn wire_safe_output_schema<T: schemars::JsonSchema + std::any::Any>(
) -> std::sync::Arc<rmcp::model::JsonObject> {
let schema = rmcp::handler::server::tool::schema_for_output::<T>().unwrap_or_else(|e| {
panic!(
"Invalid output schema for {}: {e}",
std::any::type_name::<T>()
)
});
let mut map = (*schema).clone();
widen_id_properties(&mut map, WIRE_ID_KEYS);
inline_ref_only_properties(&mut map);
std::sync::Arc::new(map)
}