1use super::*;
2
3#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
5pub enum McpToolError {
6 #[error("tool arguments must be a JSON object")]
8 ArgumentsMustBeObject,
9 #[error("missing required field `{field}`")]
11 MissingField { field: String },
12 #[error("field `{field}` does not accept null")]
14 UnexpectedNull { field: String },
15 #[error("field `{field}` was provided more than once")]
17 DuplicateField { field: String },
18 #[error("failed to decode field `{field}`: {message}")]
20 DecodeField { field: String, message: String },
21 #[error("unknown field `{field}`")]
23 UnknownField { field: String },
24 #[error("unknown value `{value}` for field `{field}`")]
26 InvalidFieldValue { field: String, value: String },
27 #[error("validation failed: {message}")]
29 Validation {
30 message: String,
31 details: Vec<Value>,
32 },
33 #[error("tool `{name}` returned invalid structured content: {message}")]
35 InvalidToolOutput { name: String, message: String },
36 #[error("conversion failed: {message}")]
38 Conversion { message: String },
39 #[error("handler failed: {message}")]
41 Handler { message: String },
42 #[error("invalid {label}: {message}")]
44 InvalidSchema { label: String, message: String },
45 #[error("tool `{name}` is already registered")]
47 DuplicateTool { name: String },
48 #[error("unknown tool `{name}`")]
50 UnknownTool { name: String },
51 #[error("resource `{uri}` is already registered")]
53 DuplicateResource { uri: String },
54 #[error("unknown resource `{uri}`")]
56 UnknownResource { uri: String },
57 #[error("prompt `{name}` is already registered")]
59 DuplicatePrompt { name: String },
60 #[error("unknown prompt `{name}`")]
62 UnknownPrompt { name: String },
63}
64
65impl McpToolError {
66 pub const fn kind(&self) -> &'static str {
68 match self {
69 Self::ArgumentsMustBeObject => "arguments_must_be_object",
70 Self::MissingField { .. } => "missing_field",
71 Self::UnexpectedNull { .. } => "unexpected_null",
72 Self::DuplicateField { .. } => "duplicate_field",
73 Self::DecodeField { .. } => "decode_field",
74 Self::UnknownField { .. } => "unknown_field",
75 Self::InvalidFieldValue { .. } => "invalid_field_value",
76 Self::Validation { .. } => "validation",
77 Self::InvalidToolOutput { .. } => "invalid_tool_output",
78 Self::Conversion { .. } => "conversion",
79 Self::Handler { .. } => "handler",
80 Self::InvalidSchema { .. } => "invalid_schema",
81 Self::DuplicateTool { .. } => "duplicate_tool",
82 Self::UnknownTool { .. } => "unknown_tool",
83 Self::DuplicateResource { .. } => "duplicate_resource",
84 Self::UnknownResource { .. } => "unknown_resource",
85 Self::DuplicatePrompt { .. } => "duplicate_prompt",
86 Self::UnknownPrompt { .. } => "unknown_prompt",
87 }
88 }
89
90 pub fn to_structured_value(&self) -> Value {
92 let mut object = Map::new();
93 object.insert("kind".to_string(), json!(self.kind()));
94 object.insert("message".to_string(), json!(self.to_string()));
95
96 match self {
97 Self::ArgumentsMustBeObject => {},
98 Self::MissingField { field }
99 | Self::UnexpectedNull { field }
100 | Self::DuplicateField { field }
101 | Self::UnknownField { field } => {
102 object.insert("field".to_string(), json!(field));
103 },
104 Self::DecodeField { field, message } => {
105 object.insert("field".to_string(), json!(field));
106 object.insert("detail".to_string(), json!(message));
107 },
108 Self::InvalidFieldValue { field, value } => {
109 object.insert("field".to_string(), json!(field));
110 object.insert("value".to_string(), json!(value));
111 },
112 Self::Validation { message, details } => {
113 object.insert("detail".to_string(), json!(message));
114 if !details.is_empty() {
115 object.insert("details".to_string(), json!(details));
116 }
117 },
118 Self::InvalidToolOutput { name, message } => {
119 object.insert("name".to_string(), json!(name));
120 object.insert("detail".to_string(), json!(message));
121 },
122 Self::Conversion { message } | Self::Handler { message } => {
123 object.insert("detail".to_string(), json!(message));
124 },
125 Self::InvalidSchema { label, message } => {
126 object.insert("label".to_string(), json!(label));
127 object.insert("detail".to_string(), json!(message));
128 },
129 Self::DuplicateTool { name } | Self::UnknownTool { name } => {
130 object.insert("name".to_string(), json!(name));
131 },
132 Self::DuplicateResource { uri } | Self::UnknownResource { uri } => {
133 object.insert("uri".to_string(), json!(uri));
134 },
135 Self::DuplicatePrompt { name } | Self::UnknownPrompt { name } => {
136 object.insert("name".to_string(), json!(name));
137 },
138 }
139
140 Value::Object(object)
141 }
142
143 pub fn missing_field(field: impl Into<String>) -> Self {
145 Self::MissingField {
146 field: field.into(),
147 }
148 }
149
150 pub fn decode(field: impl Into<String>, message: impl Into<String>) -> Self {
152 Self::DecodeField {
153 field: field.into(),
154 message: message.into(),
155 }
156 }
157
158 pub fn invalid_field_value(field: impl Into<String>, value: impl Into<String>) -> Self {
160 Self::InvalidFieldValue {
161 field: field.into(),
162 value: value.into(),
163 }
164 }
165
166 pub fn validation(message: impl Into<String>) -> Self {
168 Self::Validation {
169 message: message.into(),
170 details: Vec::new(),
171 }
172 }
173
174 pub fn validation_details(details: impl IntoIterator<Item = impl Into<String>>) -> Self {
176 let details = details.into_iter().map(Into::into).collect::<Vec<_>>();
177 Self::Validation {
178 message: details.join("; "),
179 details: details.into_iter().map(Value::String).collect(),
180 }
181 }
182
183 pub fn validation_structured_details(
185 message: impl Into<String>,
186 details: impl IntoIterator<Item = Value>,
187 ) -> Self {
188 Self::Validation {
189 message: message.into(),
190 details: details.into_iter().collect(),
191 }
192 }
193
194 pub fn conversion(message: impl Into<String>) -> Self {
196 Self::Conversion {
197 message: message.into(),
198 }
199 }
200
201 pub fn invalid_tool_output(name: impl Into<String>, message: impl Into<String>) -> Self {
203 Self::InvalidToolOutput {
204 name: name.into(),
205 message: message.into(),
206 }
207 }
208
209 pub fn handler(message: impl Into<String>) -> Self {
211 Self::Handler {
212 message: message.into(),
213 }
214 }
215
216 pub fn invalid_schema(label: impl Into<String>, message: impl Into<String>) -> Self {
218 Self::InvalidSchema {
219 label: label.into(),
220 message: message.into(),
221 }
222 }
223
224 pub fn duplicate_tool(name: impl Into<String>) -> Self {
226 Self::DuplicateTool { name: name.into() }
227 }
228
229 pub fn duplicate_resource(uri: impl Into<String>) -> Self {
231 Self::DuplicateResource { uri: uri.into() }
232 }
233
234 pub fn unknown_resource(uri: impl Into<String>) -> Self {
236 Self::UnknownResource { uri: uri.into() }
237 }
238
239 pub fn duplicate_prompt(name: impl Into<String>) -> Self {
241 Self::DuplicatePrompt { name: name.into() }
242 }
243
244 pub fn unknown_prompt(name: impl Into<String>) -> Self {
246 Self::UnknownPrompt { name: name.into() }
247 }
248}
249
250pub(crate) fn reject_unknown_arguments(arguments: McpToolArguments) -> Result<(), McpToolError> {
251 if let Some(field) = arguments.keys().next() {
252 return Err(McpToolError::UnknownField {
253 field: field.clone(),
254 });
255 }
256 Ok(())
257}
258
259pub(crate) fn validate_value_against_closed_schema(
260 field: &str,
261 schema: &Value,
262 value: &Value,
263) -> Result<(), McpToolError> {
264 if value.is_null() {
265 return Ok(());
266 }
267
268 let Value::Object(schema) = schema else {
269 return Ok(());
270 };
271
272 if let Some(object_value) = value.as_object() {
273 if let Some(schemas) = schema.get("anyOf").and_then(Value::as_array) {
274 validate_value_against_any_closed_schema(field, schemas, value)?;
275 }
276 if let Some(schemas) = schema.get("oneOf").and_then(Value::as_array) {
277 validate_value_against_any_closed_schema(field, schemas, value)?;
278 }
279 if let Some(schemas) = schema.get("allOf").and_then(Value::as_array) {
280 for schema in applicable_closed_schemas(schemas, value) {
281 validate_value_against_closed_schema(field, schema, value)?;
282 }
283 }
284 validate_closed_object_fields(field, schema, object_value)?;
285 } else if let Some(array_value) = value.as_array() {
286 if let Some(schemas) = schema.get("anyOf").and_then(Value::as_array) {
287 validate_value_against_any_closed_schema(field, schemas, value)?;
288 }
289 if let Some(items) = schema.get("items") {
290 for (index, item) in array_value.iter().enumerate() {
291 validate_value_against_closed_schema(&format!("{field}[{index}]"), items, item)?;
292 }
293 }
294 if let Some(items) = schema.get("prefixItems").and_then(Value::as_array) {
295 for (index, (schema, item)) in items.iter().zip(array_value).enumerate() {
296 validate_value_against_closed_schema(&format!("{field}[{index}]"), schema, item)?;
297 }
298 }
299 }
300
301 Ok(())
302}
303
304fn validate_value_against_any_closed_schema(
305 field: &str,
306 schemas: &[Value],
307 value: &Value,
308) -> Result<(), McpToolError> {
309 let mut first_error = None;
310 for schema in applicable_closed_schemas(schemas, value) {
311 match validate_value_against_closed_schema(field, schema, value) {
312 Ok(()) => return Ok(()),
313 Err(error) if first_error.is_none() => first_error = Some(error),
314 Err(_) => {},
315 }
316 }
317
318 first_error.map_or(Ok(()), Err)
319}
320
321fn applicable_closed_schemas<'a>(
322 schemas: &'a [Value],
323 value: &Value,
324) -> impl Iterator<Item = &'a Value> {
325 schemas
326 .iter()
327 .filter(move |schema| closed_schema_applies_to_value(schema, value))
328}
329
330fn closed_schema_applies_to_value(schema: &Value, value: &Value) -> bool {
331 let Value::Object(schema) = schema else {
332 return false;
333 };
334
335 if schema.contains_key("anyOf") || schema.contains_key("oneOf") || schema.contains_key("allOf")
336 {
337 return true;
338 }
339
340 match value {
341 Value::Object(_) => {
342 type_includes(schema, "object")
343 || schema.contains_key("properties")
344 || schema.contains_key("required")
345 || schema.contains_key("additionalProperties")
346 },
347 Value::Array(_) => {
348 type_includes(schema, "array")
349 || schema.contains_key("items")
350 || schema.contains_key("prefixItems")
351 },
352 _ => false,
353 }
354}
355
356pub(crate) fn type_includes(schema: &Map<String, Value>, expected: &str) -> bool {
357 match schema.get("type") {
358 Some(Value::String(value)) => value == expected,
359 Some(Value::Array(values)) => values
360 .iter()
361 .any(|value| matches!(value, Value::String(value) if value == expected)),
362 _ => false,
363 }
364}
365
366fn validate_closed_object_fields(
367 field: &str,
368 schema: &Map<String, Value>,
369 value: &Map<String, Value>,
370) -> Result<(), McpToolError> {
371 let mut wire_names = BTreeMap::new();
372 let properties = schema.get("properties").and_then(Value::as_object);
373 if let Some(properties) = properties {
374 for (property, property_schema) in properties {
375 wire_names.insert(property.as_str(), property.as_str());
376 if let Some(aliases) = property_schema
377 .get("x-mcpAliases")
378 .and_then(Value::as_array)
379 {
380 for alias in aliases.iter().filter_map(Value::as_str) {
381 wire_names.insert(alias, property.as_str());
382 }
383 }
384 }
385 }
386
387 if let Some(required) = schema.get("required").and_then(Value::as_array) {
388 for required in required.iter().filter_map(Value::as_str) {
389 let present = if wire_names.is_empty() {
390 value.contains_key(required)
391 } else {
392 value.keys().any(|name| {
393 wire_names
394 .get(name.as_str())
395 .is_some_and(|property| *property == required)
396 })
397 };
398 if !present {
399 return Err(McpToolError::MissingField {
400 field: nested_field_name(field, required),
401 });
402 }
403 }
404 }
405
406 let rejects_additional = rejects_additional_properties(schema);
407 let additional_schema = additional_properties_schema(schema);
408 let mut seen = BTreeSet::new();
409 for (name, nested_value) in value {
410 let Some(property) = wire_names.get(name.as_str()).copied() else {
411 if rejects_additional {
412 return Err(McpToolError::UnknownField {
413 field: nested_field_name(field, name),
414 });
415 }
416 if let Some(additional_schema) = additional_schema {
417 validate_value_against_closed_schema(
418 &nested_field_name(field, name),
419 additional_schema,
420 nested_value,
421 )?;
422 }
423 continue;
424 };
425 if !seen.insert(property) {
426 return Err(McpToolError::DuplicateField {
427 field: nested_field_name(field, property),
428 });
429 }
430 if let Some(property_schema) = properties.and_then(|properties| properties.get(property)) {
431 validate_value_against_closed_schema(
432 &nested_field_name(field, property),
433 property_schema,
434 nested_value,
435 )?;
436 }
437 }
438
439 Ok(())
440}
441
442fn rejects_additional_properties(schema: &Map<String, Value>) -> bool {
443 schema
444 .get("additionalProperties")
445 .is_some_and(|value| matches!(value, Value::Bool(false)))
446}
447
448fn additional_properties_schema(schema: &Map<String, Value>) -> Option<&Value> {
449 match schema.get("additionalProperties") {
450 Some(Value::Bool(_)) | None => None,
451 Some(schema) => Some(schema),
452 }
453}
454
455fn nested_field_name(parent: &str, child: &str) -> String {
456 if parent.is_empty() {
457 child.to_string()
458 } else {
459 format!("{parent}.{child}")
460 }
461}
462
463pub(crate) fn normalize_value_against_schema(schema: &Value, value: Value) -> Value {
464 if schema_has_composite_keywords(schema) {
465 return normalize_with_composite_schema(schema, value);
466 }
467
468 let Value::Object(schema) = schema else {
469 return value;
470 };
471
472 match value {
473 Value::Object(value) => normalize_object_value_against_schema(schema, value),
474 Value::Array(value) => normalize_array_value_against_schema(schema, value),
475 Value::String(value) => normalize_string_value_against_schema(schema, value),
476 value => value,
477 }
478}
479
480fn schema_has_composite_keywords(schema: &Value) -> bool {
481 let Value::Object(schema) = schema else {
482 return false;
483 };
484 schema.contains_key("anyOf") || schema.contains_key("oneOf") || schema.contains_key("allOf")
485}
486
487fn normalize_with_composite_schema(schema: &Value, value: Value) -> Value {
488 let Value::Object(schema) = schema else {
489 return value;
490 };
491
492 for keyword in ["anyOf", "oneOf"] {
493 let Some(schemas) = schema.get(keyword).and_then(Value::as_array) else {
494 continue;
495 };
496 let Some(schema) = schemas
497 .iter()
498 .find(|schema| schema_applies_to_value_for_normalization(schema, &value))
499 else {
500 continue;
501 };
502 return normalize_value_against_schema(schema, value);
503 }
504
505 let Some(schemas) = schema.get("allOf").and_then(Value::as_array) else {
506 return value;
507 };
508 let mut value = value;
509 for schema in schemas {
510 if schema_applies_to_value_for_normalization(schema, &value) {
511 value = normalize_value_against_schema(schema, value);
512 }
513 }
514 value
515}
516
517fn schema_applies_to_value_for_normalization(schema: &Value, value: &Value) -> bool {
518 let Value::Object(schema_object) = schema else {
519 return matches!(schema, Value::Bool(true));
520 };
521
522 if schema_object.is_empty() {
523 return true;
524 }
525 if schema_object.contains_key("anyOf")
526 || schema_object.contains_key("oneOf")
527 || schema_object.contains_key("allOf")
528 {
529 return true;
530 }
531
532 match value {
533 Value::Null => value_schema_allows_null(schema),
534 Value::Bool(_) => type_includes(schema_object, "boolean"),
535 Value::Number(number) if number.is_i64() || number.is_u64() => {
536 type_includes(schema_object, "integer") || type_includes(schema_object, "number")
537 },
538 Value::Number(_) => type_includes(schema_object, "number"),
539 Value::String(value) => {
540 type_includes(schema_object, "string")
541 || schema_object
542 .get("enum")
543 .and_then(Value::as_array)
544 .is_some_and(|values| {
545 values
546 .iter()
547 .any(|candidate| matches!(candidate, Value::String(candidate) if candidate == value))
548 })
549 || enum_decode_alias_target(schema_object, value).is_some()
550 },
551 Value::Array(_) | Value::Object(_) => closed_schema_applies_to_value(schema, value),
552 }
553}
554
555fn normalize_object_value_against_schema(
556 schema: &Map<String, Value>,
557 value: Map<String, Value>,
558) -> Value {
559 let mut wire_names = BTreeMap::new();
560 let properties = schema.get("properties").and_then(Value::as_object);
561 if let Some(properties) = properties {
562 for (property, property_schema) in properties {
563 wire_names.insert(property.as_str(), property.as_str());
564 if let Some(aliases) = property_schema
565 .get("x-mcpAliases")
566 .and_then(Value::as_array)
567 {
568 for alias in aliases.iter().filter_map(Value::as_str) {
569 wire_names.insert(alias, property.as_str());
570 }
571 }
572 }
573 }
574
575 let additional_schema = additional_properties_schema(schema);
576 let mut normalized = Map::new();
577 for (name, nested_value) in value {
578 let Some(property) = wire_names.get(name.as_str()).copied() else {
579 let nested_value = match additional_schema {
580 Some(schema) => normalize_value_against_schema(schema, nested_value),
581 None => nested_value,
582 };
583 normalized.insert(name, nested_value);
584 continue;
585 };
586
587 let property_schema = properties
588 .and_then(|properties| properties.get(property))
589 .expect("wire name should refer to an existing schema property");
590 let decode_name = property_schema
591 .get("x-mcpDecodeName")
592 .and_then(Value::as_str)
593 .unwrap_or(property);
594 normalized.insert(
595 decode_name.to_string(),
596 normalize_value_against_schema(property_schema, nested_value),
597 );
598 }
599
600 Value::Object(normalized)
601}
602
603fn normalize_array_value_against_schema(schema: &Map<String, Value>, value: Vec<Value>) -> Value {
604 if let Some(items) = schema.get("items") {
605 return Value::Array(
606 value
607 .into_iter()
608 .map(|item| normalize_value_against_schema(items, item))
609 .collect(),
610 );
611 }
612
613 let Some(prefix_items) = schema.get("prefixItems").and_then(Value::as_array) else {
614 return Value::Array(value);
615 };
616
617 Value::Array(
618 value
619 .into_iter()
620 .enumerate()
621 .map(|(index, item)| match prefix_items.get(index) {
622 Some(schema) => normalize_value_against_schema(schema, item),
623 None => item,
624 })
625 .collect(),
626 )
627}
628
629fn normalize_string_value_against_schema(schema: &Map<String, Value>, value: String) -> Value {
630 match enum_decode_alias_target(schema, &value) {
631 Some(target) => Value::String(target.to_string()),
632 None => Value::String(value),
633 }
634}
635
636fn enum_decode_alias_target<'a>(schema: &'a Map<String, Value>, value: &str) -> Option<&'a str> {
637 schema
638 .get("x-mcpEnumDecodeAliases")
639 .and_then(Value::as_object)
640 .and_then(|aliases| aliases.get(value))
641 .and_then(Value::as_str)
642}