use serde_json::{Map, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct McpSchemaLimits {
pub max_bytes: usize,
pub max_nodes: usize,
}
impl McpSchemaLimits {
pub const DEFAULT_MAX_BYTES: usize = 256 * 1024;
pub const DEFAULT_MAX_NODES: usize = 10_000;
pub fn with_max_bytes(mut self, max_bytes: usize) -> Self {
self.max_bytes = max_bytes;
self
}
pub fn with_max_nodes(mut self, max_nodes: usize) -> Self {
self.max_nodes = max_nodes;
self
}
pub(crate) fn measure(
&self,
schema: &Map<String, Value>,
) -> std::result::Result<SchemaSize, SchemaSize> {
enum Members<'a> {
Object(serde_json::map::Iter<'a>),
Array(std::slice::Iter<'a, Value>),
}
let container_bytes = |len: usize| 2 + len.saturating_sub(1);
let mut size = SchemaSize { bytes: container_bytes(schema.len()), nodes: 1 };
let mut open = vec![Members::Object(schema.iter())];
while size.bytes <= self.max_bytes && size.nodes <= self.max_nodes {
let next = match open.last_mut() {
None => return Ok(size),
Some(Members::Object(entries)) => {
entries.next().map(|(key, value)| (Some(key), value))
}
Some(Members::Array(items)) => items.next().map(|value| (None, value)),
};
let Some((key, value)) = next else {
open.pop();
continue;
};
size.nodes += 1;
if let Some(key) = key {
size.bytes += key.len() + 3;
}
size.bytes += match value {
Value::Null | Value::Bool(true) => 4,
Value::Bool(false) => 5,
Value::Number(number) => number.to_string().len(),
Value::String(text) => text.len() + 2,
Value::Array(items) => {
open.push(Members::Array(items.iter()));
container_bytes(items.len())
}
Value::Object(entries) => {
open.push(Members::Object(entries.iter()));
container_bytes(entries.len())
}
};
}
Err(size)
}
}
impl Default for McpSchemaLimits {
fn default() -> Self {
Self { max_bytes: Self::DEFAULT_MAX_BYTES, max_nodes: Self::DEFAULT_MAX_NODES }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct SchemaSize {
pub(crate) bytes: usize,
pub(crate) nodes: usize,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn object(value: Value) -> Map<String, Value> {
match value {
Value::Object(map) => map,
other => panic!("expected a JSON object, got {other}"),
}
}
fn unlimited() -> McpSchemaLimits {
McpSchemaLimits { max_bytes: usize::MAX, max_nodes: usize::MAX }
}
fn realistic_schema() -> Map<String, Value> {
object(json!({
"type": "object",
"properties": {
"repository": { "type": "string", "description": "Repository as owner/name." },
"title": { "type": "string", "minLength": 1, "maxLength": 256 },
"labels": { "type": "array", "items": { "type": "string" }, "maxItems": 20 },
"priority": { "type": "string", "enum": ["low", "normal", "high"] },
"weight": { "type": "number", "minimum": 0.5 },
"draft": { "type": "boolean", "default": false },
"assignee": { "type": ["string", "null"], "default": null }
},
"required": ["repository", "title"],
"additionalProperties": false
}))
}
#[test]
fn defaults_are_256_kib_and_10_000_nodes() {
assert_eq!(
McpSchemaLimits::default(),
McpSchemaLimits { max_bytes: 262_144, max_nodes: 10_000 }
);
}
#[test]
fn the_byte_measure_equals_compact_serialization_without_escapes() {
let schema = realistic_schema();
let compact = serde_json::to_string(&schema).unwrap().len();
let expected = SchemaSize { bytes: compact, nodes: 36 };
assert_eq!(unlimited().measure(&schema), Ok(expected));
}
#[test]
fn the_default_limits_accept_a_realistic_schema() {
let schema = realistic_schema();
assert_eq!(McpSchemaLimits::default().measure(&schema), unlimited().measure(&schema));
}
#[test]
fn a_schema_exactly_at_the_byte_limit_is_accepted_and_one_byte_over_is_rejected() {
let schema = realistic_schema();
let full = SchemaSize { bytes: serde_json::to_string(&schema).unwrap().len(), nodes: 36 };
let at_limit = unlimited().with_max_bytes(full.bytes);
assert_eq!(at_limit.measure(&schema), Ok(full));
let below = unlimited().with_max_bytes(full.bytes - 1);
assert_eq!(below.measure(&schema), Err(full));
}
#[test]
fn a_schema_exactly_at_the_node_limit_is_accepted_and_one_node_over_is_rejected() {
let schema = realistic_schema();
let full = SchemaSize { bytes: serde_json::to_string(&schema).unwrap().len(), nodes: 36 };
assert_eq!(unlimited().with_max_nodes(36).measure(&schema), Ok(full));
assert_eq!(unlimited().with_max_nodes(35).measure(&schema), Err(full));
}
#[test]
fn a_wide_shallow_schema_stops_at_the_node_limit() {
let properties: Map<String, Value> =
(0..5_000).map(|index| (format!("p{index}"), json!({ "type": "string" }))).collect();
let schema = object(json!({ "type": "object", "properties": properties }));
let full = SchemaSize {
bytes: serde_json::to_string(&schema).unwrap().len(),
nodes: 3 + 5_000 * 2,
};
assert!(full.bytes < McpSchemaLimits::DEFAULT_MAX_BYTES, "only the node limit applies");
let stopped = McpSchemaLimits::default().measure(&schema).unwrap_err();
assert_eq!(stopped.nodes, McpSchemaLimits::DEFAULT_MAX_NODES + 1);
assert!(stopped.bytes < full.bytes, "the walk stops before the last property");
let raised = McpSchemaLimits::default().with_max_nodes(full.nodes);
assert_eq!(raised.measure(&schema), Ok(full));
}
#[test]
fn a_single_long_string_exceeds_the_byte_limit_on_its_own() {
let description = "x".repeat(1024 * 1024);
let schema = object(json!({ "description": description, "type": "string" }));
let stopped = SchemaSize { bytes: 3 + 14 + (1024 * 1024 + 2), nodes: 2 };
assert_eq!(McpSchemaLimits::default().measure(&schema), Err(stopped));
}
#[test]
fn a_long_key_counts_toward_the_byte_limit() {
let schema = Map::from_iter([("k".repeat(300 * 1024), Value::Bool(true))]);
let stopped = SchemaSize { bytes: 2 + (300 * 1024 + 3) + 4, nodes: 2 };
assert_eq!(McpSchemaLimits::default().measure(&schema), Err(stopped));
}
#[test]
fn the_root_object_counts_as_a_node() {
let empty = Map::new();
assert_eq!(unlimited().measure(&empty), Ok(SchemaSize { bytes: 2, nodes: 1 }));
assert_eq!(
unlimited().with_max_nodes(0).measure(&empty),
Err(SchemaSize { bytes: 2, nodes: 1 })
);
}
}