use std::borrow::Cow;
use std::collections::HashSet;
use rmcp::ErrorData as McpError;
use rmcp::handler::server::common::FromContextPart;
use rmcp::handler::server::tool::ToolCallContext;
use rmcp::model::JsonObject;
use schemars::JsonSchema;
use serde::de::DeserializeOwned;
use serde_json::Value;
use crate::util::{MAX_ECHOED_NAME_BYTES, MAX_ERROR_MESSAGE_CALLER_BYTES, escape_bounded};
const DESERIALIZE_ERROR_PREFIX: &str = "failed to deserialize parameters:";
#[derive(Debug)]
pub struct Parameters<P>(pub P);
impl<P: JsonSchema> JsonSchema for Parameters<P> {
fn schema_name() -> Cow<'static, str> {
P::schema_name()
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
P::json_schema(generator)
}
}
impl<S, P> FromContextPart<ToolCallContext<'_, S>> for Parameters<P>
where
P: DeserializeOwned,
{
fn from_context_part(context: &mut ToolCallContext<'_, S>) -> Result<Self, McpError> {
parse_arguments(context.arguments.take()).map(Self)
}
}
fn parse_arguments<P: DeserializeOwned>(arguments: Option<JsonObject>) -> Result<P, McpError> {
let mut value = Value::Object(arguments.unwrap_or_default());
let original = match P::deserialize(&value) {
Ok(parsed) => return Ok(parsed),
Err(error) => error,
};
let error = if bound_keys(&mut value) {
P::deserialize(&value).err().unwrap_or(original)
} else {
original
};
Err(McpError::invalid_params(rejection_message(&error), None))
}
fn rejection_message(error: &serde_json::Error) -> String {
escape_bounded(
&format!("{DESERIALIZE_ERROR_PREFIX} {error}"),
MAX_ERROR_MESSAGE_CALLER_BYTES,
)
}
fn bound_keys(value: &mut Value) -> bool {
match value {
Value::Object(map) => {
let mut changed = map
.values_mut()
.fold(false, |acc, child| bound_keys(child) | acc);
if map.keys().any(|key| key.len() > MAX_ECHOED_NAME_BYTES) {
changed = true;
let entries = std::mem::take(map);
let kept: HashSet<String> = entries
.keys()
.filter(|key| key.len() <= MAX_ECHOED_NAME_BYTES)
.cloned()
.collect();
for (key, child) in entries {
let key = if key.len() > MAX_ECHOED_NAME_BYTES {
unused_name(
|name| map.contains_key(name) || kept.contains(name),
key.len(),
)
} else {
key
};
map.insert(key, child);
}
}
changed
}
Value::Array(items) => items
.iter_mut()
.fold(false, |acc, item| bound_keys(item) | acc),
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => false,
}
}
fn unused_name(is_taken: impl Fn(&str) -> bool, len: usize) -> String {
let mut attempt = 1_usize;
loop {
let name = if attempt == 1 {
format!("<{len}-byte name>")
} else {
format!("<{len}-byte name #{attempt}>")
};
if !is_taken(&name) {
return name;
}
attempt = attempt.saturating_add(1);
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
use crate::mcp::tools::PositionParams;
fn object(value: Value) -> JsonObject {
match value {
Value::Object(map) => map,
other => panic!("not an object: {other}"),
}
}
fn parse_error(value: Value) -> String {
let error = parse_arguments::<PositionParams>(Some(object(value)))
.expect_err("arguments must be rejected");
assert_eq!(error.code, rmcp::model::ErrorCode::INVALID_PARAMS);
error.message.into_owned()
}
#[test]
fn test_valid_arguments_parse() {
let parsed = parse_arguments::<PositionParams>(Some(object(
json!({"file_path": "/a.rs", "line": 1, "character": 2}),
)))
.unwrap();
assert_eq!(parsed.character, 2);
}
#[test]
fn test_error_keeps_the_rmcp_prefix() {
let message = parse_error(json!({"file_path": "/a.rs", "line": 1, "character": 2, "x": 1}));
assert!(message.starts_with(DESERIALIZE_ERROR_PREFIX), "{message}");
assert!(message.contains("`x`"), "{message}");
assert!(message.contains("`file_path`"), "{message}");
}
#[test]
fn test_long_unknown_key_is_replaced_by_its_length() {
let long = "k".repeat(2048);
let message = parse_error(json!({
"file_path": "/a.rs", "line": 1, "character": 2, long.clone(): 1
}));
assert!(message.contains("<2048-byte name>"), "{message}");
assert!(!message.contains(&long), "{message}");
assert!(message.contains("`file_path`"), "{message}");
}
#[test]
fn test_key_at_the_bound_is_kept() {
let key = "k".repeat(MAX_ECHOED_NAME_BYTES);
let message = parse_error(json!({
"file_path": "/a.rs", "line": 1, "character": 2, key.clone(): 1
}));
assert!(message.contains(&key), "{message}");
}
#[test]
fn test_long_string_value_is_capped() {
let message = parse_error(json!({
"file_path": "/a.rs", "line": "z".repeat(1 << 20), "character": 2
}));
assert!(
message.len() <= MAX_ERROR_MESSAGE_CALLER_BYTES + 64,
"{}",
message.len()
);
assert!(message.starts_with(DESERIALIZE_ERROR_PREFIX));
}
#[test]
fn test_nested_long_keys_are_bounded() {
let mut value = json!({"a": [{"b": 1}]});
let long = "n".repeat(MAX_ECHOED_NAME_BYTES + 1);
value["a"][0][long.as_str()] = json!(1);
bound_keys(&mut value);
let text = value.to_string();
assert!(!text.contains(&long));
assert!(text.contains(&format!("<{}-byte name>", long.len())));
}
#[test]
fn test_equal_length_long_keys_stay_distinct() {
let mut value = json!({});
for c in ['a', 'b', 'c'] {
value[c.to_string().repeat(MAX_ECHOED_NAME_BYTES + 1)] = json!(1);
}
assert!(bound_keys(&mut value));
let len = MAX_ECHOED_NAME_BYTES + 1;
let keys: Vec<_> = value.as_object().unwrap().keys().cloned().collect();
assert_eq!(keys.len(), 3, "{keys:?}");
assert!(keys.contains(&format!("<{len}-byte name>")));
assert!(keys.contains(&format!("<{len}-byte name #2>")));
}
#[test]
fn test_long_key_never_replaces_a_literal_key_of_the_placeholder_name() {
let len = MAX_ECHOED_NAME_BYTES + 1;
let placeholder = format!("<{len}-byte name>");
let mut value = json!({});
value["!".repeat(len)] = json!("long");
value[placeholder.as_str()] = json!("literal");
assert!(bound_keys(&mut value));
let map = value.as_object().unwrap();
assert_eq!(map.len(), 2, "{map:?}");
assert_eq!(map[&placeholder], json!("literal"));
assert_eq!(map[&format!("<{len}-byte name #2>")], json!("long"));
}
#[test]
fn test_object_without_long_keys_is_untouched() {
let mut value = json!({"a": [{"b": 1}]});
assert!(!bound_keys(&mut value));
assert_eq!(value, json!({"a": [{"b": 1}]}));
}
}