use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use rmcp::model::{JsonObject, Tool};
use schemars::Schema;
use schemars::transform::{RecursiveTransform, Transform};
use serde_json::Value;
const SCHEMA_DESCRIPTION_CAP: DescriptionCap = DescriptionCap::truncating(100);
const SHARED_DEFINITION_DESCRIPTION_CAP: DescriptionCap = DescriptionCap::dropping(80);
const SHARED_NESTED_DESCRIPTION_CAP: DescriptionCap = DescriptionCap::dropping(24);
const DEFINITIONS_KEY: &str = "$defs";
const ABBREVIATIONS: [&str; 5] = ["e.g.", "i.e.", "etc.", "vs.", "cf."];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Overflow {
Truncate,
Drop,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct DescriptionCap {
bytes: usize,
overflow: Overflow,
}
impl DescriptionCap {
const fn truncating(bytes: usize) -> Self {
Self {
bytes,
overflow: Overflow::Truncate,
}
}
const fn dropping(bytes: usize) -> Self {
Self {
bytes,
overflow: Overflow::Drop,
}
}
fn shorten(self, text: &str) -> Option<String> {
let paragraph = first_paragraph(without_rustdoc_sections(text));
if paragraph.len() <= self.bytes {
return (!paragraph.is_empty()).then(|| paragraph.to_owned());
}
match self.overflow {
Overflow::Drop => None,
Overflow::Truncate => {
whole_sentences(paragraph, self.bytes).map(std::borrow::ToOwned::to_owned)
}
}
}
}
fn ends_sentence(text: &str) -> bool {
let text = text.trim_end();
let stop = text.strip_suffix(')').unwrap_or(text);
stop.ends_with(['.', '!', '?'])
&& !ABBREVIATIONS.iter().any(|abbreviation| {
stop.strip_suffix(abbreviation)
.is_some_and(|rest| !rest.ends_with(char::is_alphanumeric))
})
&& text.matches('`').count().is_multiple_of(2)
&& text.matches('(').count() == text.matches(')').count()
}
fn whole_sentences(paragraph: &str, cap: usize) -> Option<&str> {
let window = ¶graph[..paragraph.floor_char_boundary(cap)];
window
.rmatch_indices(['.', '!', '?'])
.map(|(index, mark)| index.saturating_add(mark.len()))
.map(|end| end.saturating_add(usize::from(window[end..].starts_with(')'))))
.find(|end| {
paragraph[*end..].starts_with(char::is_whitespace) && ends_sentence(¶graph[..*end])
})
.map(|end| ¶graph[..end])
}
pub(super) fn without_rustdoc_sections(text: &str) -> &str {
let mut offset = 0;
for line in text.split_inclusive('\n') {
if line.starts_with('#') || line.starts_with("```") {
return text[..offset].trim_end();
}
offset = offset.saturating_add(line.len());
}
text.trim_end()
}
fn first_paragraph(text: &str) -> &str {
text.split("\n\n").next().unwrap_or(text).trim()
}
struct ShortenDescriptions(DescriptionCap);
impl Transform for ShortenDescriptions {
fn transform(&mut self, schema: &mut Schema) {
let Some(object) = schema.as_object_mut() else {
return;
};
let Some(Value::String(description)) = object.get("description") else {
return;
};
match self.0.shorten(description) {
Some(shortened) => object.insert("description".to_owned(), Value::String(shortened)),
None => object.remove("description"),
};
}
}
struct DefinitionUsage(BTreeMap<String, usize>);
impl DefinitionUsage {
fn count(tools: &[&mut Tool]) -> Self {
let mut usage = BTreeMap::new();
for tool in tools {
let names: BTreeSet<&String> = schemas_of(tool)
.filter_map(|schema| schema.get(DEFINITIONS_KEY)?.as_object())
.flat_map(|definitions| definitions.keys())
.collect();
for name in names {
let tools = usage.entry(name.clone()).or_insert(0_usize);
*tools = tools.saturating_add(1);
}
}
Self(usage)
}
fn is_shared(&self, name: &str) -> bool {
self.0.get(name).is_some_and(|tools| *tools >= 2)
}
}
fn schemas_of(tool: &Tool) -> impl Iterator<Item = &JsonObject> {
std::iter::once(&*tool.input_schema).chain(tool.output_schema.as_deref())
}
pub(super) fn shape_tool_schemas<'a>(tools: impl IntoIterator<Item = &'a mut Tool>) {
let mut tools: Vec<&mut Tool> = tools.into_iter().collect();
let usage = DefinitionUsage::count(&tools);
for tool in &mut tools {
shape_schema(Arc::make_mut(&mut tool.input_schema), &usage);
if let Some(output) = tool.output_schema.as_mut() {
shape_schema(Arc::make_mut(output), &usage);
}
}
}
fn shape_schema(object: &mut JsonObject, usage: &DefinitionUsage) {
let definitions = object.remove(DEFINITIONS_KEY);
let mut root = Schema::from(std::mem::take(object));
RecursiveTransform(ShortenDescriptions(SCHEMA_DESCRIPTION_CAP)).transform(&mut root);
if let Value::Object(shaped) = Value::from(root) {
*object = shaped;
}
let Some(Value::Object(definitions)) = definitions else {
return;
};
let shaped = definitions
.into_iter()
.map(|(name, definition)| {
let Ok(mut schema) = Schema::try_from(definition.clone()) else {
return (name, definition);
};
if usage.is_shared(&name) {
shape_shared_definition(&mut schema);
} else {
RecursiveTransform(ShortenDescriptions(SCHEMA_DESCRIPTION_CAP))
.transform(&mut schema);
}
(name, Value::from(schema))
})
.collect();
object.insert(DEFINITIONS_KEY.to_owned(), Value::Object(shaped));
}
fn shape_shared_definition(definition: &mut Schema) {
let own = definition
.as_object()
.and_then(|object| object.get("description")?.as_str())
.and_then(|text| SHARED_DEFINITION_DESCRIPTION_CAP.shorten(text));
RecursiveTransform(ShortenDescriptions(SHARED_NESTED_DESCRIPTION_CAP)).transform(definition);
if let Some(object) = definition.as_object_mut() {
match own {
Some(text) => object.insert("description".to_owned(), Value::String(text)),
None => object.remove("description"),
};
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rustdoc_sections_and_fences_are_cut() {
let text = "Summary line.\n\nMore.\n\n# Examples\n\n```\nlet a = 1;\n```\n";
assert_eq!(without_rustdoc_sections(text), "Summary line.\n\nMore.");
assert_eq!(without_rustdoc_sections("a\n```\ncode\n```"), "a");
assert_eq!(without_rustdoc_sections("plain"), "plain");
}
#[test]
fn test_shorten_keeps_first_paragraph() {
let cap = DescriptionCap::truncating(100);
assert_eq!(
cap.shorten("First.\n\nSecond paragraph.").unwrap(),
"First."
);
assert_eq!(cap.shorten("Short").unwrap(), "Short");
assert_eq!(cap.shorten(""), None);
}
#[test]
fn test_shorten_cuts_at_a_sentence_end_within_the_cap() {
let cap = DescriptionCap::truncating(40);
let text = "One sentence here. Another sentence that runs well past the cap.";
assert_eq!(cap.shorten(text).unwrap(), "One sentence here.");
}
#[test]
fn test_shorten_keeps_the_longest_run_of_whole_sentences() {
let cap = DescriptionCap::truncating(30);
let text = "One. Two. Three is a long one. Four.";
assert_eq!(cap.shorten(text).unwrap(), "One. Two. Three is a long one.");
}
#[test]
fn test_shorten_drops_a_paragraph_without_a_sentence_end_within_the_cap() {
let cap = DescriptionCap::truncating(12);
assert_eq!(cap.shorten("alpha beta gamma delta"), None);
assert_eq!(
DescriptionCap::truncating(20).shorten("A long first sentence that overflows. Next."),
None
);
}
#[test]
fn test_shorten_never_cuts_after_an_abbreviation() {
let cap = DescriptionCap::truncating(40);
assert_eq!(
cap.shorten("A name (function, class, etc.) or a number that runs long."),
None
);
assert_eq!(
cap.shorten("Outside every root (e.g. the stdlib) and more text."),
None
);
assert_eq!(
cap.shorten("Takes a kind, vs. a name. Second sentence is long.")
.unwrap(),
"Takes a kind, vs. a name."
);
}
#[test]
fn test_ends_sentence_requires_closed_spans_and_a_real_stop() {
assert!(ends_sentence("A whole sentence."));
assert!(ends_sentence("A sentence (with an aside.)"));
assert!(ends_sentence("Says `x`!"));
assert!(!ends_sentence("Ends with a comma,"));
assert!(!ends_sentence("Ends with a colon:"));
assert!(!ends_sentence("Open `span."));
assert!(!ends_sentence("Open (paren."));
assert!(!ends_sentence("Lists kinds (function, class, etc.)"));
assert!(!ends_sentence("Outside a root (e.g."));
assert!(ends_sentence("Ends here, i.e. whole."));
}
#[test]
fn test_shorten_never_splits_a_multibyte_character() {
let cap = DescriptionCap::truncating(5);
assert_eq!(cap.shorten("ééééé ééééé"), None);
assert_eq!(cap.shorten("éé. ééééé").unwrap(), "éé.");
}
#[test]
fn test_shorten_is_idempotent() {
let cap = DescriptionCap::truncating(30);
let once = cap
.shorten("A fairly long description. With a second sentence in it.")
.unwrap();
assert_eq!(cap.shorten(&once).unwrap(), once);
}
#[test]
fn test_property_named_description_is_not_treated_as_text() {
let mut schema = Schema::try_from(serde_json::json!({
"properties": {"description": {"type": "string", "description": "x\n\nignored"}}
}))
.unwrap();
RecursiveTransform(ShortenDescriptions(SCHEMA_DESCRIPTION_CAP)).transform(&mut schema);
assert_eq!(
Value::from(schema),
serde_json::json!({
"properties": {"description": {"type": "string", "description": "x"}}
})
);
}
#[test]
fn test_shared_definitions_keep_a_short_description_whole_or_drop_it() {
let short = "A short summary.";
let long = "A definition summary that is longer than the shared one-line cap allows it to be, so it goes.";
let definition = |own: &str| {
serde_json::json!({
"type": "object",
"description": own,
"properties": {"a": {"type": "string", "description": "A nested field description that is long."}}
})
};
let tool = |name: &str, defs: Value| {
let mut object = JsonObject::new();
object.insert("$defs".into(), defs);
Tool::new(name.to_owned(), "d", Arc::new(object))
};
let mut tools = [
tool(
"a",
serde_json::json!({"Short": definition(short), "Long": definition(long), "Own": definition(long)}),
),
tool(
"b",
serde_json::json!({"Short": definition(short), "Long": definition(long)}),
),
];
shape_tool_schemas(tools.iter_mut());
let defs = &tools[0].input_schema["$defs"];
assert_eq!(defs["Short"]["description"], short);
assert!(defs["Long"].get("description").is_none());
assert!(
defs["Short"]["properties"]["a"]
.get("description")
.is_none()
);
assert!(defs["Own"]["description"].as_str().unwrap().len() <= SCHEMA_DESCRIPTION_CAP.bytes);
assert!(defs["Own"]["properties"]["a"].get("description").is_some());
}
use super::super::server::McplsServer;
const TOOLS_LIST_TOTAL_BUDGET_BYTES: usize = 136_000;
const TOOL_BUDGET_BYTES: usize = 9_000;
const SCHEMA_DESCRIPTION_BUDGET_BYTES: usize = 35_000;
fn compact_len(value: &impl serde::Serialize) -> usize {
serde_json::to_vec(value).unwrap().len()
}
fn for_each_description(value: &Value, path: &str, visit: &mut impl FnMut(&str, &str)) {
match value {
Value::Object(map) => {
for (key, child) in map {
let child_path = format!("{path}/{key}");
match (key.as_str(), child) {
("description", Value::String(text)) => visit(&child_path, text),
_ => for_each_description(child, &child_path, visit),
}
}
}
Value::Array(items) => {
for (index, item) in items.iter().enumerate() {
for_each_description(item, &format!("{path}/{index}"), visit);
}
}
_ => {}
}
}
fn schema_description_bytes(tools: &[Tool]) -> usize {
let mut total = 0;
for tool in tools {
for schema in schemas_of(tool) {
for_each_description(&Value::Object(schema.clone()), "", &mut |_, text| {
total += text.len();
});
}
}
total
}
#[test]
#[ignore = "run manually to measure the tools/list payload"]
fn measure_tools_list() {
for (label, tools) in [
("unshaped", McplsServer::unshaped_tool_router().list_all()),
("shaped", McplsServer::build_tool_router(None).list_all()),
] {
let mut rows: Vec<(String, usize)> = tools
.iter()
.map(|tool| (tool.name.to_string(), compact_len(tool)))
.collect();
rows.sort_by_key(|(_, bytes)| std::cmp::Reverse(*bytes));
println!("== {label}: total {} B", compact_len(&tools));
println!(
" schema descriptions {} B",
schema_description_bytes(&tools)
);
for (name, bytes) in rows.iter().take(6) {
println!(" {name}: {bytes} B");
}
}
}
fn shaped_tools() -> Vec<Tool> {
McplsServer::build_tool_router(None).list_all()
}
#[test]
fn test_tools_list_stays_within_the_size_budget() {
let tools = shaped_tools();
let total = compact_len(&tools);
let mut over: Vec<String> = tools
.iter()
.filter(|tool| compact_len(tool) > TOOL_BUDGET_BYTES)
.map(|tool| {
format!(
"{}: {} B (permitted {TOOL_BUDGET_BYTES} B)",
tool.name,
compact_len(tool)
)
})
.collect();
if total > TOOLS_LIST_TOTAL_BUDGET_BYTES {
over.push(format!(
"total: {total} B (permitted {TOOLS_LIST_TOTAL_BUDGET_BYTES} B)"
));
}
let descriptions = schema_description_bytes(&tools);
if descriptions > SCHEMA_DESCRIPTION_BUDGET_BYTES {
over.push(format!(
"schema descriptions: {descriptions} B (permitted {SCHEMA_DESCRIPTION_BUDGET_BYTES} B)"
));
}
assert!(
over.is_empty(),
"tools/list budget exceeded:\n{}",
over.join("\n")
);
}
#[test]
fn test_no_schema_description_embeds_rustdoc() {
for tool in shaped_tools() {
for schema in schemas_of(&tool) {
for_each_description(&Value::Object(schema.clone()), "", &mut |path, text| {
let kept = without_rustdoc_sections(text);
assert_eq!(
kept.len(),
text.trim_end().len(),
"{}{path}: rustdoc section or code fence in {text:?}",
tool.name
);
});
}
}
}
fn collect_refs(value: &Value, refs: &mut Vec<String>, ids: &mut usize) {
match value {
Value::Object(map) => {
for (key, child) in map {
match (key.as_str(), child) {
("$ref", Value::String(target)) => refs.push(target.clone()),
("$id", Value::String(_)) => *ids += 1,
_ => collect_refs(child, refs, ids),
}
}
}
Value::Array(items) => items.iter().for_each(|item| collect_refs(item, refs, ids)),
_ => {}
}
}
#[test]
fn test_every_ref_resolves_inside_its_own_schema_document() {
for tool in shaped_tools() {
for schema in schemas_of(&tool) {
let (mut refs, mut ids) = (Vec::new(), 0);
collect_refs(&Value::Object(schema.clone()), &mut refs, &mut ids);
assert_eq!(ids, 0, "{}: `$id` in a tool schema", tool.name);
let definitions = schema.get(DEFINITIONS_KEY).and_then(Value::as_object);
for target in refs {
let name = target.strip_prefix("#/$defs/").unwrap_or_else(|| {
panic!(
"{}: `$ref` {target} is not a same-document `$defs` pointer",
tool.name
)
});
assert!(
definitions.is_some_and(|defs| defs.contains_key(name)),
"{}: `$ref` {target} does not resolve",
tool.name
);
}
}
}
}
fn object_schemas_with_openness(
value: &Value,
path: &str,
want_closed: bool,
found: &mut Vec<String>,
) {
let Value::Object(map) = value else {
if let Value::Array(items) = value {
for (index, item) in items.iter().enumerate() {
object_schemas_with_openness(
item,
&format!("{path}/{index}"),
want_closed,
found,
);
}
}
return;
};
let is_object_schema =
map.contains_key("properties") || map.get("type").is_some_and(|t| t == "object");
let closed = map.get("additionalProperties") == Some(&Value::Bool(false));
if is_object_schema && closed != want_closed {
found.push(path.to_owned());
}
for (key, child) in map {
let child_path = format!("{path}/{key}");
match (key.as_str(), child) {
("properties" | DEFINITIONS_KEY, Value::Object(schemas)) => {
for (name, schema) in schemas {
object_schemas_with_openness(
schema,
&format!("{child_path}/{name}"),
want_closed,
found,
);
}
}
_ => object_schemas_with_openness(child, &child_path, want_closed, found),
}
}
}
#[test]
fn test_every_input_schema_object_is_closed_to_unknown_arguments() {
let mut violations = Vec::new();
for tool in shaped_tools() {
let mut found = Vec::new();
object_schemas_with_openness(
&Value::Object((*tool.input_schema).clone()),
"",
true,
&mut found,
);
violations.extend(
found
.into_iter()
.map(|path| format!("{}: {path}", tool.name)),
);
}
assert!(
violations.is_empty(),
"input schema objects without additionalProperties: false:\n{}",
violations.join("\n")
);
}
#[test]
fn test_no_output_schema_is_closed() {
let mut violations = Vec::new();
for tool in shaped_tools() {
let Some(output) = tool.output_schema.as_deref() else {
continue;
};
let mut found = Vec::new();
object_schemas_with_openness(&Value::Object(output.clone()), "", false, &mut found);
violations.extend(
found
.into_iter()
.map(|path| format!("{}: {path}", tool.name)),
);
}
assert!(
violations.is_empty(),
"output schema objects closed to additional properties:\n{}",
violations.join("\n")
);
}
fn without_annotations(value: &Value) -> Value {
match value {
Value::Object(map) => Value::Object(
map.iter()
.filter(|(key, child)| {
!(matches!(key.as_str(), "description" | "title") && child.is_string())
})
.map(|(key, child)| (key.clone(), without_annotations(child)))
.collect(),
),
Value::Array(items) => Value::Array(items.iter().map(without_annotations).collect()),
other => other.clone(),
}
}
#[test]
fn test_shaping_changes_only_description_annotations() {
let unshaped = McplsServer::unshaped_tool_router().list_all();
let shaped = shaped_tools();
assert_eq!(unshaped.len(), shaped.len());
for (before, after) in unshaped.iter().zip(&shaped) {
assert_eq!(before.name, after.name);
assert_eq!(before.description, after.description, "{}", before.name);
for (old, new) in schemas_of(before).zip(schemas_of(after)) {
assert_eq!(
without_annotations(&Value::Object(old.clone())),
without_annotations(&Value::Object(new.clone())),
"{}: structure changed",
before.name
);
}
assert_eq!(
before.output_schema.is_some(),
after.output_schema.is_some(),
"{}",
before.name
);
}
}
#[test]
fn test_every_published_description_is_a_whole_sentence() {
let mut broken = Vec::new();
for tool in shaped_tools() {
for schema in schemas_of(&tool) {
for_each_description(&Value::Object(schema.clone()), "", &mut |path, text| {
if !ends_sentence(text) {
broken.push(format!("{}{path}: {text:?}", tool.name));
}
});
}
}
assert!(
broken.is_empty(),
"descriptions that are not whole sentences:\n{}",
broken.join("\n")
);
}
#[test]
fn test_the_truncating_cap_removes_no_description() {
let unshaped = McplsServer::unshaped_tool_router().list_all();
let shaped = shaped_tools();
let refs: Vec<&Tool> = unshaped.iter().collect();
let usage = {
let mut tools: Vec<Tool> = refs.iter().map(|tool| (*tool).clone()).collect();
DefinitionUsage::count(&tools.iter_mut().collect::<Vec<_>>())
};
let mut lost = Vec::new();
for (before, after) in unshaped.iter().zip(&shaped) {
for (old, new) in schemas_of(before).zip(schemas_of(after)) {
let mut kept = BTreeSet::new();
for_each_description(&Value::Object(new.clone()), "", &mut |path, _| {
kept.insert(path.to_owned());
});
for_each_description(&Value::Object(old.clone()), "", &mut |path, text| {
let in_shared = path
.strip_prefix("/$defs/")
.and_then(|rest| rest.split('/').next())
.is_some_and(|name| usage.is_shared(name));
let has_text = !first_paragraph(without_rustdoc_sections(text)).is_empty();
if has_text && !in_shared && !kept.contains(path) {
lost.push(format!("{}{path}", before.name));
}
});
}
}
assert!(
lost.is_empty(),
"descriptions removed by the truncating cap:\n{}",
lost.join("\n")
);
}
#[test]
fn test_published_descriptions_keep_their_key_facts() {
const FACTS: [(&str, &str); 9] = [
(
"keep only symbols inside a container",
"type, impl, class, module",
),
("Extra context:", "one request per file"),
("symbol kind filter", "function, class"),
("`pending` or `evicted`", "not a clean file"),
("workspace root (stdlib", "not provably inside"),
("`character` offsets are inexact", "non-UTF-16"),
("capped below", "response"),
("Refused before", "keeps serving"),
("Another restart of this server", "none was repeated"),
];
let tools = shaped_tools();
let mut published = Vec::new();
for tool in &tools {
for schema in schemas_of(tool) {
for_each_description(&Value::Object(schema.clone()), "", &mut |_, text| {
published.push(text.to_owned());
});
}
}
for (marker, fact) in FACTS {
let carrying: Vec<&String> = published.iter().filter(|t| t.contains(marker)).collect();
assert!(!carrying.is_empty(), "no description carries {marker:?}");
for text in carrying {
assert!(text.contains(fact), "{marker:?} lost {fact:?}: {text:?}");
}
}
let restart = tools
.iter()
.find(|tool| tool.name == "restart_server")
.unwrap();
let text = restart.description.as_deref().unwrap();
assert!(
text.contains("`refused` means nothing was stopped"),
"{text}"
);
}
}