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 INERT_NUMERIC_KEYWORDS: [&str; 6] = [
"minimum",
"maximum",
"exclusiveMinimum",
"exclusiveMaximum",
"multipleOf",
"format",
];
#[cfg(feature = "mcp")]
const UNION_KEYWORDS: [&str; 4] = ["anyOf", "oneOf", "allOf", "$ref"];
#[cfg(feature = "mcp")]
pub(crate) fn stringify_id_properties(map: &mut Map<String, Value>, keys: &[&str]) {
if keys.is_empty() {
return;
}
if let Some(Value::Object(properties)) = map.get_mut("properties") {
for (name, subschema) in properties.iter_mut() {
if keys.contains(&name.as_str()) {
stringify_id_schema(subschema);
}
}
}
for value in map.values_mut() {
stringify_in_value(value, keys);
}
}
#[cfg(feature = "mcp")]
fn stringify_in_value(value: &mut Value, keys: &[&str]) {
match value {
Value::Object(map) => stringify_id_properties(map, keys),
Value::Array(items) => items
.iter_mut()
.for_each(|item| stringify_in_value(item, keys)),
_ => {}
}
}
#[cfg(feature = "mcp")]
fn stringify_id_schema(schema: &mut Value) {
let Value::Object(map) = schema else {
return;
};
if declares_array(map) {
if let Some(items) = map.get_mut("items") {
stringify_id_schema(items);
}
return;
}
map.insert("type".to_owned(), Value::String("string".to_owned()));
for keyword in INERT_NUMERIC_KEYWORDS.iter().chain(UNION_KEYWORDS.iter()) {
map.remove(*keyword);
}
}
#[cfg(feature = "mcp")]
fn declares_array(map: &Map<String, Value>) -> bool {
match map.get("type") {
Some(Value::String(kind)) => kind == "array",
Some(Value::Array(kinds)) => kinds.iter().any(|kind| kind == "array"),
_ => false,
}
}
#[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_additional_property_slot(map, defs, chain, depth);
inline_remaining_keywords(map, defs, chain, depth);
}
#[cfg(feature = "mcp")]
const SLOT_KEYWORDS: [&str; 7] = [
"$defs",
"properties",
"items",
"anyOf",
"oneOf",
"prefixItems",
"additionalProperties",
];
#[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_additional_property_slot(
map: &mut Map<String, Value>,
defs: &Map<String, Value>,
chain: &mut InlineChain,
depth: usize,
) {
if let Some(extra @ Value::Object(_)) = map.get_mut("additionalProperties") {
inline_slot(extra, 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)
}
#[cfg(feature = "mcp")]
pub(crate) fn scalarize_slot_types(schema: &mut WireInputSchema) {
scalarize_in_map(&mut schema.0);
}
#[cfg(feature = "mcp")]
const SCALARIZE_SLOT_KEYWORDS: [&str; 6] = [
"properties",
"items",
"anyOf",
"oneOf",
"prefixItems",
"additionalProperties",
];
#[cfg(feature = "mcp")]
const DATA_KEYWORDS: [&str; 4] = ["enum", "const", "default", "examples"];
#[cfg(feature = "mcp")]
fn scalarize_in_map(map: &mut Map<String, Value>) {
scalarize_property_slots(map);
scalarize_item_slots(map);
scalarize_union_branches(map);
scalarize_additional_properties(map);
scalarize_remaining_keywords(map);
}
#[cfg(feature = "mcp")]
fn scalarize_property_slots(map: &mut Map<String, Value>) {
if let Some(Value::Object(properties)) = map.get_mut("properties") {
for slot in properties.values_mut() {
scalarize_slot(slot);
}
}
}
#[cfg(feature = "mcp")]
fn scalarize_item_slots(map: &mut Map<String, Value>) {
match map.get_mut("items") {
Some(Value::Array(entries)) => entries.iter_mut().for_each(scalarize_slot),
Some(single) => scalarize_slot(single),
None => {}
}
}
#[cfg(feature = "mcp")]
fn scalarize_union_branches(map: &mut Map<String, Value>) {
for keyword in ["anyOf", "oneOf", "prefixItems"] {
if let Some(Value::Array(branches)) = map.get_mut(keyword) {
branches.iter_mut().for_each(scalarize_slot);
}
}
}
#[cfg(feature = "mcp")]
fn scalarize_additional_properties(map: &mut Map<String, Value>) {
if let Some(extra @ Value::Object(_)) = map.get_mut("additionalProperties") {
scalarize_slot(extra);
}
}
#[cfg(feature = "mcp")]
fn scalarize_remaining_keywords(map: &mut Map<String, Value>) {
for (key, value) in map.iter_mut() {
let name = key.as_str();
if !SCALARIZE_SLOT_KEYWORDS.contains(&name) && !DATA_KEYWORDS.contains(&name) {
scalarize_in_value(value);
}
}
}
#[cfg(feature = "mcp")]
fn scalarize_in_value(value: &mut Value) {
match value {
Value::Object(map) => scalarize_in_map(map),
Value::Array(items) => items.iter_mut().for_each(scalarize_in_value),
_ => {}
}
}
#[cfg(feature = "mcp")]
fn scalarize_slot(slot: &mut Value) {
let Value::Object(map) = slot else {
return;
};
scalarize_in_map(map);
collapse_nullable_union(map);
collapse_const_union(map);
collapse_nullable_type_list(map);
drop_inert_null_default(map);
}
#[cfg(feature = "mcp")]
fn collapse_nullable_union(map: &mut Map<String, Value>) {
for keyword in ["anyOf", "oneOf"] {
if let Some(promoted) = nullable_union_branch(map, keyword) {
promote_branch(map, keyword, promoted);
return;
}
}
}
#[cfg(feature = "mcp")]
fn nullable_union_branch(map: &Map<String, Value>, keyword: &str) -> Option<Map<String, Value>> {
let Value::Array(branches) = map.get(keyword)? else {
return None;
};
if branches.len() != 2 {
return None;
}
let kept = match (is_null_branch(&branches[0]), is_null_branch(&branches[1])) {
(false, true) => &branches[0],
(true, false) => &branches[1],
_ => return None,
};
let Value::Object(kept) = kept else {
return None;
};
matches!(kept.get("type"), Some(Value::String(_))).then(|| kept.clone())
}
#[cfg(feature = "mcp")]
fn is_null_branch(branch: &Value) -> bool {
let Value::Object(map) = branch else {
return false;
};
matches!(map.get("type"), Some(Value::String(kind)) if kind == "null")
}
#[cfg(feature = "mcp")]
fn promote_branch(map: &mut Map<String, Value>, keyword: &str, mut promoted: Map<String, Value>) {
for (key, value) in map.iter() {
if key != keyword {
promoted.insert(key.clone(), value.clone());
}
}
*map = promoted;
}
#[cfg(feature = "mcp")]
fn collapse_const_union(map: &mut Map<String, Value>) {
for keyword in ["oneOf", "anyOf"] {
let Some(branches) = const_branches(map, keyword) else {
continue;
};
let description = folded_description(map.get("description"), &branches);
let values: Vec<Value> = branches.into_iter().map(|(value, _)| value).collect();
map.remove(keyword);
map.insert("type".to_owned(), Value::String("string".to_owned()));
map.insert("enum".to_owned(), Value::Array(values));
if let Some(text) = description {
map.insert("description".to_owned(), Value::String(text));
}
return;
}
}
#[cfg(feature = "mcp")]
fn const_branches(map: &Map<String, Value>, keyword: &str) -> Option<Vec<(Value, Option<String>)>> {
let Value::Array(branches) = map.get(keyword)? else {
return None;
};
if branches.is_empty() {
return None;
}
let mut collected = Vec::with_capacity(branches.len());
for branch in branches {
let Some(Value::String(literal)) = branch.get("const") else {
return None;
};
collected.push((
Value::String(literal.clone()),
text_keyword(branch, "description"),
));
}
Some(collected)
}
#[cfg(feature = "mcp")]
fn text_keyword(node: &Value, keyword: &str) -> Option<String> {
match node.get(keyword) {
Some(Value::String(text)) => Some(text.clone()),
_ => None,
}
}
#[cfg(feature = "mcp")]
fn folded_description(own: Option<&Value>, branches: &[(Value, Option<String>)]) -> Option<String> {
let mut lines: Vec<String> = branches
.iter()
.filter_map(|(value, text)| text.as_ref().map(|text| format!("- {value}: {text}")))
.collect();
let own = match own {
Some(Value::String(text)) => Some(text.clone()),
_ => None,
};
if lines.is_empty() {
return own;
}
if let Some(text) = own {
lines.insert(0, text);
}
Some(lines.join("\n"))
}
#[cfg(feature = "mcp")]
fn collapse_nullable_type_list(map: &mut Map<String, Value>) {
if let Some(kept) = nullable_type_pair(map) {
map.insert("type".to_owned(), Value::String(kept));
}
}
#[cfg(feature = "mcp")]
fn nullable_type_pair(map: &Map<String, Value>) -> Option<String> {
let Value::Array(kinds) = map.get("type")? else {
return None;
};
let named: Vec<&str> = kinds.iter().filter_map(Value::as_str).collect();
if named.len() != 2 || !named.contains(&"null") {
return None;
}
named
.iter()
.find(|kind| **kind != "null")
.map(|kind| (*kind).to_owned())
}
#[cfg(feature = "mcp")]
fn drop_inert_null_default(map: &mut Map<String, Value>) {
let scalar = matches!(map.get("type"), Some(Value::String(kind)) if kind != "null");
if scalar && map.get("default") == Some(&Value::Null) {
map.remove("default");
}
}
#[cfg(feature = "mcp")]
pub(crate) fn untyped_input_slots(map: &Map<String, Value>) -> Vec<String> {
let mut found = Vec::new();
collect_untyped(&Value::Object(map.clone()), "$", &mut found);
found
}
#[cfg(feature = "mcp")]
fn collect_untyped(node: &Value, path: &str, found: &mut Vec<String>) {
let Value::Object(map) = node else {
return;
};
if let Some(Value::Object(properties)) = map.get("properties") {
for (name, slot) in properties {
check_untyped_slot(slot, &format!("{path}.{name}"), found);
}
}
collect_untyped_items(map, path, found);
collect_untyped_branches(map, path, found);
if let Some(extra @ Value::Object(_)) = map.get("additionalProperties") {
check_untyped_slot(extra, &format!("{path}.additionalProperties"), found);
}
}
#[cfg(feature = "mcp")]
fn collect_untyped_items(map: &Map<String, Value>, path: &str, found: &mut Vec<String>) {
match map.get("items") {
Some(Value::Array(entries)) => {
for (index, entry) in entries.iter().enumerate() {
check_untyped_slot(entry, &format!("{path}.items[{index}]"), found);
}
}
Some(single) => check_untyped_slot(single, &format!("{path}.items"), found),
None => {}
}
}
#[cfg(feature = "mcp")]
fn collect_untyped_branches(map: &Map<String, Value>, path: &str, found: &mut Vec<String>) {
for keyword in ["anyOf", "oneOf", "allOf", "prefixItems"] {
let Some(Value::Array(entries)) = map.get(keyword) else {
continue;
};
for (index, entry) in entries.iter().enumerate() {
check_untyped_slot(entry, &format!("{path}.{keyword}[{index}]"), found);
}
}
}
#[cfg(feature = "mcp")]
fn check_untyped_slot(slot: &Value, path: &str, found: &mut Vec<String>) {
let typed = match slot {
Value::Object(map) => {
map.contains_key("type") || map.contains_key("enum") || map.contains_key("const")
}
_ => false,
};
if !typed {
found.push(format!("{path} = {slot}"));
}
collect_untyped(slot, path, found);
}
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(any(feature = "mcp", feature = "context"))]
pub const WIRE_ID_KEYS: &[&str] = &["fragment_id", "content_hash", "memory_id", "fragment_ids"];
#[cfg(feature = "mcp")]
pub(crate) struct WireInputSchema(Map<String, Value>);
#[cfg(feature = "mcp")]
pub(crate) struct WireOutputSchema(Map<String, Value>);
#[cfg(feature = "mcp")]
impl WireInputSchema {
fn derived<T: schemars::JsonSchema + std::any::Any>() -> Self {
let schema = rmcp::handler::server::tool::schema_for_input::<
rmcp::handler::server::wrapper::Parameters<T>,
>()
.unwrap_or_else(|e| {
panic!(
"Invalid input schema for {}: {e}",
std::any::type_name::<T>()
)
});
Self((*schema).clone())
}
fn adopt(map: Map<String, Value>) -> Self {
Self(map)
}
fn harden(mut self, id_keys: &[&str]) -> Self {
stringify_id_properties(&mut self.0, id_keys);
inline_ref_only_properties(&mut self.0);
scalarize_slot_types(&mut self);
self
}
fn publish(self) -> std::sync::Arc<rmcp::model::JsonObject> {
std::sync::Arc::new(self.0)
}
}
#[cfg(feature = "mcp")]
impl WireOutputSchema {
fn derived<T: schemars::JsonSchema + std::any::Any>() -> Self {
let schema = rmcp::handler::server::tool::schema_for_output::<T>();
Self((*schema).clone())
}
fn harden(mut self) -> Self {
widen_id_properties(&mut self.0, WIRE_ID_KEYS);
inline_ref_only_properties(&mut self.0);
self
}
fn publish(self) -> std::sync::Arc<rmcp::model::JsonObject> {
std::sync::Arc::new(self.0)
}
}
#[cfg(feature = "mcp")]
pub(crate) fn wire_safe_input_schema<T: schemars::JsonSchema + std::any::Any>(
id_keys: &[&str],
) -> std::sync::Arc<rmcp::model::JsonObject> {
WireInputSchema::derived::<T>().harden(id_keys).publish()
}
#[cfg(feature = "mcp")]
pub(crate) fn reharden_tool_input(tool: &mut rmcp::model::Tool) {
tool.input_schema = WireInputSchema::adopt((*tool.input_schema).clone())
.harden(&[])
.publish();
}
#[cfg(feature = "mcp")]
pub(crate) fn wire_safe_output_schema<T: schemars::JsonSchema + std::any::Any>(
) -> std::sync::Arc<rmcp::model::JsonObject> {
WireOutputSchema::derived::<T>().harden().publish()
}