1use crate::error::{MCPResult, ToolError};
2use serde_json::{Value, json};
3use std::borrow::Cow;
4use std::collections::HashSet;
5
6pub fn validate_against_schema(data: &Value, schema: &Value) -> MCPResult<()> {
8 if let Some(all_of) = schema.get("allOf") {
10 if let Some(schemas) = all_of.as_array() {
11 for schema_item in schemas {
12 if let Err(e) = validate_against_schema(data, schema_item) {
13 return Err(ToolError::SchemaValidation(format!(
14 "allOf validation failed: {e}"
15 ))
16 .into());
17 }
18 }
19 }
20 if let Some(mut parent) = schema.as_object().cloned() {
22 parent.remove("allOf");
23 let parent_schema = Value::Object(parent);
24 return validate_against_schema(data, &parent_schema);
25 }
26 }
27 if schema.get("anyOf").is_some() || schema.get("oneOf").is_some() {
29 validate_combined_schemas(data, schema)?;
30 return Ok(());
31 }
32
33 if data.is_null() {
35 if let Some(nullable) = schema.get("nullable") {
36 if nullable.as_bool().unwrap_or(false) {
37 return Ok(());
38 }
39 }
40 if let Some(schema_type) = schema.get("type").and_then(|t| t.as_str()) {
41 if schema_type == "null" {
42 return Ok(());
43 }
44 }
45 return Err(ToolError::SchemaValidation("Value cannot be null".to_string()).into());
46 }
47
48 if let Some(schema_type) = schema.get("type").and_then(|t| t.as_str()) {
50 match schema_type {
51 "string" => validate_string(data, schema)?,
52 "number" => validate_number(data, schema)?,
53 "integer" => validate_integer(data, schema)?,
54 "boolean" => validate_boolean(data, schema)?,
55 "array" => validate_array(data, schema)?,
56 "object" => validate_object(data, schema)?,
57 "null" => {
58 if !data.is_null() {
59 return Err(ToolError::SchemaValidation(format!(
60 "Expected null, got {}",
61 type_name(data)
62 ))
63 .into());
64 }
65 }
66 _ => {
67 }
69 }
70 }
71
72 if data.is_string() {
75 let string_value = data.as_str().unwrap();
76 if let Some(min_length) = schema.get("minLength").and_then(|v| v.as_u64()) {
77 if string_value.len() < min_length as usize {
78 return Err(ToolError::SchemaValidation(format!(
79 "String length {} is less than minimum {}",
80 string_value.len(),
81 min_length
82 ))
83 .into());
84 }
85 }
86 if let Some(max_length) = schema.get("maxLength").and_then(|v| v.as_u64()) {
87 if string_value.len() > max_length as usize {
88 return Err(ToolError::SchemaValidation(format!(
89 "String length {} is greater than maximum {}",
90 string_value.len(),
91 max_length
92 ))
93 .into());
94 }
95 }
96 if let Some(pattern) = schema.get("pattern").and_then(|v| v.as_str()) {
97 if let Ok(regex) = regex::Regex::new(pattern) {
98 if !regex.is_match(string_value) {
99 return Err(ToolError::SchemaValidation(format!(
100 "String '{string_value}' does not match pattern '{pattern}'"
101 ))
102 .into());
103 }
104 }
105 }
106 if let Some(format) = schema.get("format").and_then(|v| v.as_str()) {
107 validate_string_format(string_value, format)?;
108 }
109 }
110 if data.is_number() {
112 let number_value = data.as_f64().unwrap();
113 if let Some(minimum) = schema.get("minimum").and_then(|v| v.as_f64()) {
114 let exclusive = schema
115 .get("exclusiveMinimum")
116 .and_then(|v| v.as_bool())
117 .unwrap_or(false);
118 if exclusive {
119 if number_value <= minimum {
120 return Err(ToolError::SchemaValidation(format!(
121 "Number {number_value} must be greater than {minimum}"
122 ))
123 .into());
124 }
125 } else if number_value < minimum {
126 return Err(ToolError::SchemaValidation(format!(
127 "Number {number_value} must be greater than or equal to {minimum}"
128 ))
129 .into());
130 }
131 }
132 if let Some(maximum) = schema.get("maximum").and_then(|v| v.as_f64()) {
133 let exclusive = schema
134 .get("exclusiveMaximum")
135 .and_then(|v| v.as_bool())
136 .unwrap_or(false);
137 if exclusive {
138 if number_value >= maximum {
139 return Err(ToolError::SchemaValidation(format!(
140 "Number {number_value} must be less than {maximum}"
141 ))
142 .into());
143 }
144 } else if number_value > maximum {
145 return Err(ToolError::SchemaValidation(format!(
146 "Number {number_value} must be less than or equal to {maximum}"
147 ))
148 .into());
149 }
150 }
151 if let Some(multiple_of) = schema.get("multipleOf").and_then(|v| v.as_f64()) {
152 if multiple_of != 0.0 && (number_value % multiple_of).abs() > f64::EPSILON {
153 return Err(ToolError::SchemaValidation(format!(
154 "Number {number_value} must be a multiple of {multiple_of}"
155 ))
156 .into());
157 }
158 }
159 }
160 if data.is_i64() || data.is_u64() {
162 let integer_value = data
163 .as_i64()
164 .unwrap_or_else(|| data.as_u64().unwrap() as i64);
165 if let Some(minimum) = schema.get("minimum").and_then(|v| v.as_i64()) {
166 let exclusive = schema
167 .get("exclusiveMinimum")
168 .and_then(|v| v.as_bool())
169 .unwrap_or(false);
170 if exclusive {
171 if integer_value <= minimum {
172 return Err(ToolError::SchemaValidation(format!(
173 "Integer {integer_value} must be greater than {minimum}"
174 ))
175 .into());
176 }
177 } else if integer_value < minimum {
178 return Err(ToolError::SchemaValidation(format!(
179 "Integer {integer_value} must be greater than or equal to {minimum}"
180 ))
181 .into());
182 }
183 }
184 if let Some(maximum) = schema.get("maximum").and_then(|v| v.as_i64()) {
185 let exclusive = schema
186 .get("exclusiveMaximum")
187 .and_then(|v| v.as_bool())
188 .unwrap_or(false);
189 if exclusive {
190 if integer_value >= maximum {
191 return Err(ToolError::SchemaValidation(format!(
192 "Integer {integer_value} must be less than {maximum}"
193 ))
194 .into());
195 }
196 } else if integer_value > maximum {
197 return Err(ToolError::SchemaValidation(format!(
198 "Integer {integer_value} must be less than or equal to {maximum}"
199 ))
200 .into());
201 }
202 }
203 if let Some(multiple_of) = schema.get("multipleOf").and_then(|v| v.as_i64()) {
204 if multiple_of != 0 && integer_value % multiple_of != 0 {
205 return Err(ToolError::SchemaValidation(format!(
206 "Integer {integer_value} must be a multiple of {multiple_of}"
207 ))
208 .into());
209 }
210 }
211 }
212
213 if let Some(enum_values) = schema.get("enum") {
215 if let Some(enum_array) = enum_values.as_array() {
216 if !enum_array.contains(data) {
217 return Err(ToolError::SchemaValidation(format!(
218 "Value must be one of: {enum_array:?}"
219 ))
220 .into());
221 }
222 }
223 }
224
225 if let Some(const_value) = schema.get("const") {
227 if data != const_value {
228 return Err(ToolError::SchemaValidation(format!(
229 "Value must be exactly: {const_value:?}"
230 ))
231 .into());
232 }
233 }
234
235 Ok(())
236}
237
238fn validate_string(data: &Value, schema: &Value) -> MCPResult<()> {
239 if !data.is_string() {
240 return Err(ToolError::SchemaValidation(format!(
241 "Expected string, got {}",
242 type_name(data)
243 ))
244 .into());
245 }
246
247 let string_value = data.as_str().unwrap();
248
249 if let Some(min_length) = schema.get("minLength").and_then(|v| v.as_u64()) {
251 if string_value.len() < min_length as usize {
252 return Err(ToolError::SchemaValidation(format!(
253 "String length {} is less than minimum {}",
254 string_value.len(),
255 min_length
256 ))
257 .into());
258 }
259 }
260
261 if let Some(max_length) = schema.get("maxLength").and_then(|v| v.as_u64()) {
262 if string_value.len() > max_length as usize {
263 return Err(ToolError::SchemaValidation(format!(
264 "String length {} is greater than maximum {}",
265 string_value.len(),
266 max_length
267 ))
268 .into());
269 }
270 }
271
272 if let Some(pattern) = schema.get("pattern").and_then(|v| v.as_str()) {
274 if let Ok(regex) = regex::Regex::new(pattern) {
275 if !regex.is_match(string_value) {
276 return Err(ToolError::SchemaValidation(format!(
277 "String '{string_value}' does not match pattern '{pattern}'"
278 ))
279 .into());
280 }
281 }
282 }
283
284 if let Some(format) = schema.get("format").and_then(|v| v.as_str()) {
286 validate_string_format(string_value, format)?;
287 }
288
289 Ok(())
290}
291
292fn validate_number(data: &Value, schema: &Value) -> MCPResult<()> {
293 if !data.is_number() {
294 return Err(ToolError::SchemaValidation(format!(
295 "Expected number, got {}",
296 type_name(data)
297 ))
298 .into());
299 }
300
301 let number_value = data.as_f64().unwrap();
302
303 if let Some(minimum) = schema.get("minimum").and_then(|v| v.as_f64()) {
305 let exclusive = schema
306 .get("exclusiveMinimum")
307 .and_then(|v| v.as_bool())
308 .unwrap_or(false);
309 if exclusive {
310 if number_value <= minimum {
311 return Err(ToolError::SchemaValidation(format!(
312 "Number {number_value} must be greater than {minimum}"
313 ))
314 .into());
315 }
316 } else if number_value < minimum {
317 return Err(ToolError::SchemaValidation(format!(
318 "Number {number_value} must be greater than or equal to {minimum}"
319 ))
320 .into());
321 }
322 }
323
324 if let Some(maximum) = schema.get("maximum").and_then(|v| v.as_f64()) {
325 let exclusive = schema
326 .get("exclusiveMaximum")
327 .and_then(|v| v.as_bool())
328 .unwrap_or(false);
329 if exclusive {
330 if number_value >= maximum {
331 return Err(ToolError::SchemaValidation(format!(
332 "Number {number_value} must be less than {maximum}"
333 ))
334 .into());
335 }
336 } else if number_value > maximum {
337 return Err(ToolError::SchemaValidation(format!(
338 "Number {number_value} must be less than or equal to {maximum}"
339 ))
340 .into());
341 }
342 }
343
344 if let Some(multiple_of) = schema.get("multipleOf").and_then(|v| v.as_f64()) {
346 if multiple_of != 0.0 && (number_value % multiple_of).abs() > f64::EPSILON {
347 return Err(ToolError::SchemaValidation(format!(
348 "Number {number_value} must be a multiple of {multiple_of}"
349 ))
350 .into());
351 }
352 }
353
354 Ok(())
355}
356
357fn validate_integer(data: &Value, schema: &Value) -> MCPResult<()> {
358 if !data.is_i64() && !data.is_u64() {
359 return Err(ToolError::SchemaValidation(format!(
360 "Expected integer, got {}",
361 type_name(data)
362 ))
363 .into());
364 }
365
366 let integer_value = data
367 .as_i64()
368 .unwrap_or_else(|| data.as_u64().unwrap() as i64);
369
370 if let Some(minimum) = schema.get("minimum").and_then(|v| v.as_i64()) {
372 let exclusive = schema
373 .get("exclusiveMinimum")
374 .and_then(|v| v.as_bool())
375 .unwrap_or(false);
376 if exclusive {
377 if integer_value <= minimum {
378 return Err(ToolError::SchemaValidation(format!(
379 "Integer {integer_value} must be greater than {minimum}"
380 ))
381 .into());
382 }
383 } else if integer_value < minimum {
384 return Err(ToolError::SchemaValidation(format!(
385 "Integer {integer_value} must be greater than or equal to {minimum}"
386 ))
387 .into());
388 }
389 }
390
391 if let Some(maximum) = schema.get("maximum").and_then(|v| v.as_i64()) {
392 let exclusive = schema
393 .get("exclusiveMaximum")
394 .and_then(|v| v.as_bool())
395 .unwrap_or(false);
396 if exclusive {
397 if integer_value >= maximum {
398 return Err(ToolError::SchemaValidation(format!(
399 "Integer {integer_value} must be less than {maximum}"
400 ))
401 .into());
402 }
403 } else if integer_value > maximum {
404 return Err(ToolError::SchemaValidation(format!(
405 "Integer {integer_value} must be less than or equal to {maximum}"
406 ))
407 .into());
408 }
409 }
410
411 if let Some(multiple_of) = schema.get("multipleOf").and_then(|v| v.as_i64()) {
413 if multiple_of != 0 && integer_value % multiple_of != 0 {
414 return Err(ToolError::SchemaValidation(format!(
415 "Integer {integer_value} must be a multiple of {multiple_of}"
416 ))
417 .into());
418 }
419 }
420
421 Ok(())
422}
423
424fn validate_boolean(data: &Value, _schema: &Value) -> MCPResult<()> {
425 if !data.is_boolean() {
426 return Err(ToolError::SchemaValidation(format!(
427 "Expected boolean, got {}",
428 type_name(data)
429 ))
430 .into());
431 }
432 Ok(())
433}
434
435fn validate_array(data: &Value, schema: &Value) -> MCPResult<()> {
436 if !data.is_array() {
437 return Err(ToolError::SchemaValidation(format!(
438 "Expected array, got {}",
439 type_name(data)
440 ))
441 .into());
442 }
443
444 let array = data.as_array().unwrap();
445
446 if let Some(min_items) = schema.get("minItems").and_then(|v| v.as_u64()) {
448 if array.len() < min_items as usize {
449 return Err(ToolError::SchemaValidation(format!(
450 "Array has {} items, minimum required is {}",
451 array.len(),
452 min_items
453 ))
454 .into());
455 }
456 }
457
458 if let Some(max_items) = schema.get("maxItems").and_then(|v| v.as_u64()) {
459 if array.len() > max_items as usize {
460 return Err(ToolError::SchemaValidation(format!(
461 "Array has {} items, maximum allowed is {}",
462 array.len(),
463 max_items
464 ))
465 .into());
466 }
467 }
468
469 if let Some(unique_items) = schema.get("uniqueItems").and_then(|v| v.as_bool()) {
471 if unique_items {
472 let mut seen = HashSet::new();
473 for item in array {
474 if !seen.insert(item) {
475 return Err(ToolError::SchemaValidation(
476 "Array items must be unique".to_string(),
477 )
478 .into());
479 }
480 }
481 }
482 }
483
484 if let Some(items_schema) = schema.get("items") {
486 for (i, item) in array.iter().enumerate() {
487 validate_against_schema(item, items_schema).map_err(|e| {
488 ToolError::SchemaValidation(format!("Array item {i} validation failed: {e}"))
489 })?;
490 }
491 }
492
493 if let Some(additional_items) = schema.get("additionalItems") {
495 if let Some(items_schema) = schema.get("items") {
496 if let Some(items_array) = items_schema.as_array() {
497 let max_defined_items = items_array.len();
498 if array.len() > max_defined_items {
499 if let Some(additional_schema) = additional_items.as_object() {
500 if additional_schema.is_empty() {
501 return Err(ToolError::SchemaValidation(
502 "Additional items not allowed".to_string(),
503 )
504 .into());
505 }
506 } else if !additional_items.as_bool().unwrap_or(true) {
507 return Err(ToolError::SchemaValidation(
508 "Additional items not allowed".to_string(),
509 )
510 .into());
511 } else {
512 for item in &array[max_defined_items..] {
513 validate_against_schema(item, additional_items)?;
514 }
515 }
516 }
517 }
518 }
519 }
520
521 Ok(())
522}
523
524fn validate_object(data: &Value, schema: &Value) -> MCPResult<()> {
525 if !data.is_object() {
526 return Err(ToolError::SchemaValidation(format!(
527 "Expected object, got {}",
528 type_name(data)
529 ))
530 .into());
531 }
532
533 let obj = data.as_object().unwrap();
534
535 if let Some(min_properties) = schema.get("minProperties").and_then(|v| v.as_u64()) {
537 if obj.len() < min_properties as usize {
538 return Err(ToolError::SchemaValidation(format!(
539 "Object has {} properties, minimum required is {}",
540 obj.len(),
541 min_properties
542 ))
543 .into());
544 }
545 }
546
547 if let Some(max_properties) = schema.get("maxProperties").and_then(|v| v.as_u64()) {
548 if obj.len() > max_properties as usize {
549 return Err(ToolError::SchemaValidation(format!(
550 "Object has {} properties, maximum allowed is {}",
551 obj.len(),
552 max_properties
553 ))
554 .into());
555 }
556 }
557
558 if let Some(required) = schema.get("required") {
560 if let Some(required_array) = required.as_array() {
561 for req in required_array {
562 if let Some(prop_name) = req.as_str() {
563 if !obj.contains_key(prop_name) {
564 return Err(ToolError::SchemaValidation(format!(
565 "Missing required property: {prop_name}"
566 ))
567 .into());
568 }
569 }
570 }
571 }
572 }
573
574 if let Some(properties_schema) = schema.get("properties") {
576 if let Some(props) = properties_schema.as_object() {
577 for (key, value) in obj {
578 if let Some(prop_schema) = props.get(key) {
579 validate_against_schema(value, prop_schema).map_err(|e| {
580 ToolError::SchemaValidation(format!(
581 "Property '{key}' validation failed: {e}"
582 ))
583 })?;
584 }
585 }
586 }
587 }
588
589 if let Some(additional_properties) = schema.get("additionalProperties") {
591 if let Some(properties_schema) = schema.get("properties") {
592 if let Some(props) = properties_schema.as_object() {
593 for (key, value) in obj {
594 if !props.contains_key(key) {
595 if let Some(additional_schema) = additional_properties.as_object() {
596 if additional_schema.is_empty() {
597 return Err(ToolError::SchemaValidation(format!(
598 "Additional property '{key}' not allowed"
599 ))
600 .into());
601 }
602 } else if !additional_properties.as_bool().unwrap_or(true) {
603 return Err(ToolError::SchemaValidation(format!(
604 "Additional property '{key}' not allowed"
605 ))
606 .into());
607 } else {
608 validate_against_schema(value, additional_properties)?;
609 }
610 }
611 }
612 }
613 }
614 }
615
616 if let Some(property_names) = schema.get("propertyNames") {
618 for key in obj.keys() {
619 let key_value = Value::String(key.clone());
620 validate_against_schema(&key_value, property_names).map_err(|e| {
621 ToolError::SchemaValidation(format!("Property name '{key}' validation failed: {e}"))
622 })?;
623 }
624 }
625
626 if let Some(dependencies) = schema.get("dependencies") {
628 if let Some(deps) = dependencies.as_object() {
629 for (property, dependency) in deps {
630 if obj.contains_key(property) {
631 match dependency {
632 Value::Array(required_props) => {
633 for req_prop in required_props {
634 if let Some(prop_name) = req_prop.as_str() {
635 if !obj.contains_key(prop_name) {
636 return Err(ToolError::SchemaValidation(format!(
637 "Property '{property}' requires property '{prop_name}'"
638 ))
639 .into());
640 }
641 }
642 }
643 }
644 Value::Object(schema_dep) => {
645 let schema_value = Value::Object(schema_dep.clone());
646 validate_against_schema(data, &schema_value)?;
647 }
648 _ => {}
649 }
650 }
651 }
652 }
653 }
654
655 Ok(())
656}
657
658fn validate_combined_schemas(data: &Value, schema: &Value) -> MCPResult<()> {
659 if let Some(one_of) = schema.get("oneOf") {
661 if let Some(schemas) = one_of.as_array() {
662 let mut matches = 0;
663 for schema_item in schemas {
664 if validate_against_schema(data, schema_item).is_ok() {
665 matches += 1;
666 }
667 }
668 if matches != 1 {
669 return Err(ToolError::SchemaValidation(
670 "Value must match exactly one schema from oneOf".to_string(),
671 )
672 .into());
673 }
674 }
675 }
676
677 if let Some(any_of) = schema.get("anyOf") {
679 if let Some(schemas) = any_of.as_array() {
680 let mut has_match = false;
681 for schema_item in schemas {
682 if validate_against_schema(data, schema_item).is_ok() {
683 has_match = true;
684 break;
685 }
686 }
687 if !has_match {
688 return Err(ToolError::SchemaValidation(
689 "Value must match at least one schema from anyOf".to_string(),
690 )
691 .into());
692 }
693 }
694 }
695
696 if let Some(all_of) = schema.get("allOf") {
698 if let Some(schemas) = all_of.as_array() {
699 for schema_item in schemas {
700 if let Err(e) = validate_against_schema(data, schema_item) {
701 return Err(ToolError::SchemaValidation(format!(
702 "allOf validation failed: {e}"
703 ))
704 .into());
705 }
706 }
707 }
708 }
709
710 Ok(())
711}
712
713fn validate_string_format(value: &str, format: &str) -> MCPResult<()> {
714 match format {
715 "date-time" => {
716 if !value.contains('T') && !value.contains(' ') {
718 return Err(
719 ToolError::SchemaValidation("Invalid date-time format".to_string()).into(),
720 );
721 }
722 }
723 "date" => {
724 if value.matches(r"^\d{4}-\d{2}-\d{2}$").next().is_none() {
726 return Err(ToolError::SchemaValidation("Invalid date format".to_string()).into());
727 }
728 }
729 "time" => {
730 if value.matches(r"^\d{2}:\d{2}:\d{2}").next().is_none() {
732 return Err(ToolError::SchemaValidation("Invalid time format".to_string()).into());
733 }
734 }
735 "email" => {
736 if !value.contains('@') || !value.contains('.') {
738 return Err(ToolError::SchemaValidation("Invalid email format".to_string()).into());
739 }
740 }
741 "uri" => {
742 if !value.contains("://") {
744 return Err(ToolError::SchemaValidation("Invalid URI format".to_string()).into());
745 }
746 }
747 "uuid" => {
748 if value
750 .matches(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
751 .next()
752 .is_none()
753 {
754 return Err(ToolError::SchemaValidation("Invalid UUID format".to_string()).into());
755 }
756 }
757 _ => {
758 }
760 }
761 Ok(())
762}
763
764fn type_name(value: &Value) -> &'static str {
766 match value {
767 Value::Null => "null",
768 Value::Bool(_) => "boolean",
769 Value::Number(_) => "number",
770 Value::String(_) => "string",
771 Value::Array(_) => "array",
772 Value::Object(_) => "object",
773 }
774}
775
776pub fn validate_tool_input(data: &Value, schema: &Value) -> MCPResult<()> {
778 validate_against_schema(data, schema)
779}
780
781pub fn validate_tool_output(data: &Value, schema: &Value) -> MCPResult<()> {
783 validate_against_schema(data, schema)
784}
785
786pub fn validate_tool_schema(schema: &Value) -> MCPResult<()> {
788 if !schema.is_object() {
790 return Err(
791 ToolError::SchemaValidation("Tool schema must be an object".to_string()).into(),
792 );
793 }
794
795 if let Some(obj) = schema.as_object() {
797 if !obj.contains_key("type") {
799 return Err(ToolError::SchemaValidation(
800 "Tool schema must have 'type' property".to_string(),
801 )
802 .into());
803 }
804
805 if let Some(schema_type) = obj.get("type").and_then(|t| t.as_str()) {
807 if schema_type != "object" {
808 return Err(ToolError::SchemaValidation(
809 "Tool schema type must be 'object'".to_string(),
810 )
811 .into());
812 }
813 }
814
815 if let Some(properties) = obj.get("properties") {
817 validate_against_schema(properties, &json!({"type": "object"}))?;
818 }
819
820 if let Some(required) = obj.get("required") {
822 if let Some(required_array) = required.as_array() {
823 for req in required_array {
824 if !req.is_string() {
825 return Err(ToolError::SchemaValidation(
826 "Required properties must be strings".to_string(),
827 )
828 .into());
829 }
830 }
831 } else {
832 return Err(ToolError::SchemaValidation(
833 "Required property must be an array".to_string(),
834 )
835 .into());
836 }
837 }
838 }
839
840 Ok(())
841}
842
843pub fn validate_tool_input_with_context(
845 data: &Value,
846 schema: &Value,
847 tool_name: &str,
848) -> MCPResult<ValidationContext> {
849 let mut context = ValidationContext::new(tool_name.to_string());
850
851 validate_tool_schema(schema)?;
853
854 validate_with_context(data, schema, &mut context, "".to_string())?;
856
857 Ok(context)
858}
859
860pub fn validate_tool_output_with_context(
862 data: &Value,
863 schema: &Value,
864 tool_name: &str,
865) -> MCPResult<ValidationContext> {
866 let mut context = ValidationContext::new(tool_name.to_string());
867
868 if schema.as_object().is_some() {
870 validate_tool_schema(schema)?;
871 }
872
873 validate_with_context(data, schema, &mut context, "".to_string())?;
875
876 Ok(context)
877}
878
879pub fn validate_tool_definition_comprehensive(
881 tool: &crate::types::tools::Tool,
882) -> MCPResult<ValidationReport> {
883 let mut report = ValidationReport::new(tool.name.clone());
884
885 if let Err(e) = tool.validate() {
887 report.add_error(ValidationError::new(
888 "basic_validation".to_string(),
889 format!("Basic tool validation failed: {e}"),
890 ErrorSeverity::High,
891 ));
892 return Ok(report);
893 }
894
895 validate_schema_complexity(&tool.input_schema, &mut report, "input_schema")?;
897
898 if let Some(ref output_schema) = tool.output_schema {
899 validate_schema_complexity(output_schema, &mut report, "output_schema")?;
900 }
901
902 validate_tool_security(tool, &mut report)?;
904
905 validate_tool_performance(tool, &mut report)?;
907
908 Ok(report)
909}
910
911fn validate_schema_complexity(
913 schema: &Value,
914 report: &mut ValidationReport,
915 context: &str,
916) -> MCPResult<()> {
917 let complexity = calculate_schema_complexity(schema, 0);
918
919 if complexity > MAX_SCHEMA_COMPLEXITY {
921 report.add_error(ValidationError::new(
922 format!("{context}_complexity"),
923 format!("Schema complexity {complexity} exceeds maximum {MAX_SCHEMA_COMPLEXITY}"),
924 ErrorSeverity::High,
925 ));
926 } else if complexity > WARN_SCHEMA_COMPLEXITY {
927 report.add_warning(ValidationWarning::new(
928 format!("{context}_complexity"),
929 format!("Schema complexity {complexity} is high, consider simplifying"),
930 ));
931 }
932
933 let depth = calculate_schema_depth(schema, 0);
935 if depth > MAX_SCHEMA_DEPTH {
936 report.add_error(ValidationError::new(
937 format!("{context}_depth"),
938 format!("Schema nesting depth {depth} exceeds maximum {MAX_SCHEMA_DEPTH}"),
939 ErrorSeverity::High,
940 ));
941 }
942
943 Ok(())
944}
945
946fn calculate_schema_complexity(schema: &Value, current_depth: usize) -> usize {
948 if current_depth > 20 {
949 return 1000; }
952
953 match schema {
954 Value::Object(obj) => {
955 let mut complexity = 1;
956
957 if let Some(properties) = obj.get("properties").and_then(|p| p.as_object()) {
959 complexity += properties.len();
960 for prop_schema in properties.values() {
961 complexity += calculate_schema_complexity(prop_schema, current_depth + 1);
962 }
963 }
964
965 if let Some(items) = obj.get("items") {
967 complexity += calculate_schema_complexity(items, current_depth + 1);
968 }
969
970 for key in &["allOf", "anyOf", "oneOf"] {
972 if let Some(schemas) = obj.get(*key).and_then(|s| s.as_array()) {
973 complexity += schemas.len() * 2; for sub_schema in schemas {
975 complexity += calculate_schema_complexity(sub_schema, current_depth + 1);
976 }
977 }
978 }
979
980 complexity
981 }
982 Value::Array(arr) => arr
983 .iter()
984 .map(|item| calculate_schema_complexity(item, current_depth + 1))
985 .sum(),
986 _ => 1,
987 }
988}
989
990fn calculate_schema_depth(schema: &Value, current_depth: usize) -> usize {
992 match schema {
993 Value::Object(obj) => {
994 let mut max_depth = current_depth;
995
996 if let Some(properties) = obj.get("properties").and_then(|p| p.as_object()) {
998 for prop_schema in properties.values() {
999 let prop_depth = calculate_schema_depth(prop_schema, current_depth + 1);
1000 max_depth = max_depth.max(prop_depth);
1001 }
1002 }
1003
1004 if let Some(items) = obj.get("items") {
1006 let items_depth = calculate_schema_depth(items, current_depth + 1);
1007 max_depth = max_depth.max(items_depth);
1008 }
1009
1010 for key in &["allOf", "anyOf", "oneOf"] {
1012 if let Some(schemas) = obj.get(*key).and_then(|s| s.as_array()) {
1013 for sub_schema in schemas {
1014 let sub_depth = calculate_schema_depth(sub_schema, current_depth + 1);
1015 max_depth = max_depth.max(sub_depth);
1016 }
1017 }
1018 }
1019
1020 max_depth
1021 }
1022 _ => current_depth,
1023 }
1024}
1025
1026fn validate_tool_security(
1028 tool: &crate::types::tools::Tool,
1029 report: &mut ValidationReport,
1030) -> MCPResult<()> {
1031 if tool.name.contains("..") || tool.name.contains("/") || tool.name.contains("\\") {
1033 report.add_error(ValidationError::new(
1034 "tool_name_security".to_string(),
1035 "Tool name contains potentially unsafe characters".to_string(),
1036 ErrorSeverity::High,
1037 ));
1038 }
1039
1040 if let Some(properties) = tool
1042 .input_schema
1043 .get("properties")
1044 .and_then(|p| p.as_object())
1045 {
1046 for prop_name in properties.keys() {
1047 if is_sensitive_parameter_name(prop_name) {
1048 report.add_warning(ValidationWarning::new(
1049 "sensitive_parameter".to_string(),
1050 format!(
1051 "Parameter '{prop_name}' may contain sensitive data, ensure proper handling"
1052 ),
1053 ));
1054 }
1055 }
1056 }
1057
1058 if is_overly_permissive_schema(&tool.input_schema) {
1060 report.add_warning(ValidationWarning::new(
1061 "permissive_schema".to_string(),
1062 "Input schema is very permissive, consider adding more constraints".to_string(),
1063 ));
1064 }
1065
1066 Ok(())
1067}
1068
1069fn validate_tool_performance(
1071 tool: &crate::types::tools::Tool,
1072 report: &mut ValidationReport,
1073) -> MCPResult<()> {
1074 if has_performance_antipatterns(&tool.input_schema) {
1076 report.add_warning(ValidationWarning::new(
1077 "performance_concern".to_string(),
1078 "Schema contains patterns that may impact validation performance".to_string(),
1079 ));
1080 }
1081
1082 if tool.description.len() > 1000 {
1084 report.add_warning(ValidationWarning::new(
1085 "long_description".to_string(),
1086 format!(
1087 "Tool description is {} characters, consider shortening for better UX",
1088 tool.description.len()
1089 ),
1090 ));
1091 }
1092
1093 Ok(())
1094}
1095
1096fn is_sensitive_parameter_name(name: &str) -> bool {
1098 let sensitive_patterns = [
1099 "password",
1100 "passwd",
1101 "secret",
1102 "key",
1103 "token",
1104 "credential",
1105 "auth",
1106 "api_key",
1107 "private",
1108 "confidential",
1109 "sensitive",
1110 ];
1111
1112 let name_lower = name.to_lowercase();
1113 sensitive_patterns
1114 .iter()
1115 .any(|pattern| name_lower.contains(pattern))
1116}
1117
1118fn is_overly_permissive_schema(schema: &Value) -> bool {
1120 if let Some(obj) = schema.as_object() {
1122 if !obj.contains_key("type")
1124 && !obj.contains_key("properties")
1125 && !obj.contains_key("enum")
1126 && !obj.contains_key("const")
1127 && !obj.contains_key("pattern")
1128 {
1129 return true;
1130 }
1131
1132 if obj.get("type").and_then(|t| t.as_str()) == Some("object")
1134 && obj.get("additionalProperties").and_then(|ap| ap.as_bool()) == Some(true)
1135 && !obj.contains_key("properties")
1136 {
1137 return true;
1138 }
1139
1140 if let Some(properties) = obj.get("properties").and_then(|p| p.as_object()) {
1142 for (_, prop_schema) in properties {
1143 if let Some(prop_obj) = prop_schema.as_object() {
1144 if prop_obj.is_empty() {
1145 return true; }
1147 }
1148 }
1149 }
1150 } else if schema.as_object().is_some_and(|o| o.is_empty()) {
1151 return true; }
1153
1154 false
1155}
1156
1157fn has_performance_antipatterns(schema: &Value) -> bool {
1159 if let Some(pattern) = schema.get("pattern").and_then(|p| p.as_str()) {
1161 if pattern.len() > 200 || pattern.contains(".*.*") || pattern.contains("(.+)+") {
1162 return true;
1163 }
1164 }
1165
1166 if let Some(enum_values) = schema.get("enum").and_then(|e| e.as_array()) {
1168 if enum_values.len() > 100 {
1169 return true;
1170 }
1171 }
1172
1173 false
1174}
1175
1176fn validate_with_context(
1178 data: &Value,
1179 schema: &Value,
1180 context: &mut ValidationContext,
1181 path: String,
1182) -> MCPResult<()> {
1183 context.push_path(path.clone());
1185
1186 if let Err(e) = validate_against_schema(data, schema) {
1188 context.add_error(ValidationError::new(
1189 path.clone(),
1190 format!("{e}"),
1191 ErrorSeverity::High,
1192 ));
1193 return Err(e);
1194 }
1195
1196 validate_data_size_limits(data, context, &path)?;
1198 validate_data_security(data, context, &path)?;
1199
1200 context.pop_path();
1201 Ok(())
1202}
1203
1204fn validate_data_size_limits(
1206 data: &Value,
1207 context: &mut ValidationContext,
1208 path: &str,
1209) -> MCPResult<()> {
1210 match data {
1211 Value::String(s) => {
1212 if s.len() > MAX_STRING_LENGTH {
1213 context.add_warning(ValidationWarning::new(
1214 path.to_string(),
1215 format!(
1216 "String length {} exceeds recommended maximum {}",
1217 s.len(),
1218 MAX_STRING_LENGTH
1219 ),
1220 ));
1221 }
1222 }
1223 Value::Array(arr) => {
1224 if arr.len() > MAX_ARRAY_LENGTH {
1225 context.add_warning(ValidationWarning::new(
1226 path.to_string(),
1227 format!(
1228 "Array length {} exceeds recommended maximum {}",
1229 arr.len(),
1230 MAX_ARRAY_LENGTH
1231 ),
1232 ));
1233 }
1234 }
1235 Value::Object(obj) => {
1236 if obj.len() > MAX_OBJECT_PROPERTIES {
1237 context.add_warning(ValidationWarning::new(
1238 path.to_string(),
1239 format!(
1240 "Object has {} properties, exceeds recommended maximum {}",
1241 obj.len(),
1242 MAX_OBJECT_PROPERTIES
1243 ),
1244 ));
1245 }
1246 }
1247 _ => {}
1248 }
1249 Ok(())
1250}
1251
1252fn validate_data_security(
1254 data: &Value,
1255 context: &mut ValidationContext,
1256 path: &str,
1257) -> MCPResult<()> {
1258 if let Value::String(s) = data {
1259 if contains_script_patterns(s) {
1261 context.add_warning(ValidationWarning::new(
1262 path.to_string(),
1263 "String contains potential script injection patterns".to_string(),
1264 ));
1265 }
1266
1267 if s.contains("../") || s.contains("..\\") {
1269 context.add_warning(ValidationWarning::new(
1270 path.to_string(),
1271 "String contains potential path traversal patterns".to_string(),
1272 ));
1273 }
1274
1275 if s.len() > SECURITY_STRING_LENGTH_LIMIT {
1277 context.add_warning(ValidationWarning::new(
1278 path.to_string(),
1279 format!("String length {} may indicate malicious input", s.len()),
1280 ));
1281 }
1282 }
1283 Ok(())
1284}
1285
1286fn contains_script_patterns(s: &str) -> bool {
1288 let patterns = [
1289 "<script",
1290 "javascript:",
1291 "data:text/html",
1292 "eval(",
1293 "setTimeout(",
1294 "setInterval(",
1295 ];
1296 let s_lower = s.to_lowercase();
1297 patterns.iter().any(|pattern| s_lower.contains(pattern))
1298}
1299
1300const MAX_SCHEMA_COMPLEXITY: usize = 1000;
1302const WARN_SCHEMA_COMPLEXITY: usize = 500;
1303const MAX_SCHEMA_DEPTH: usize = 20;
1304const MAX_STRING_LENGTH: usize = 100_000;
1305const MAX_ARRAY_LENGTH: usize = 10_000;
1306const MAX_OBJECT_PROPERTIES: usize = 1_000;
1307const SECURITY_STRING_LENGTH_LIMIT: usize = 1_000_000;
1308
1309#[derive(Debug, Clone)]
1311pub struct ValidationContext {
1312 pub tool_name: String,
1313 pub path_stack: Vec<String>,
1314 pub errors: Vec<ValidationError>,
1315 pub warnings: Vec<ValidationWarning>,
1316}
1317
1318impl ValidationContext {
1319 pub fn new(tool_name: String) -> Self {
1320 Self {
1321 tool_name,
1322 path_stack: Vec::new(),
1323 errors: Vec::new(),
1324 warnings: Vec::new(),
1325 }
1326 }
1327
1328 pub fn push_path(&mut self, path: String) {
1329 self.path_stack.push(path);
1330 }
1331
1332 pub fn pop_path(&mut self) {
1333 self.path_stack.pop();
1334 }
1335
1336 pub fn add_error(&mut self, error: ValidationError) {
1337 self.errors.push(error);
1338 }
1339
1340 pub fn add_warning(&mut self, warning: ValidationWarning) {
1341 self.warnings.push(warning);
1342 }
1343
1344 pub fn has_errors(&self) -> bool {
1345 !self.errors.is_empty()
1346 }
1347
1348 pub fn has_warnings(&self) -> bool {
1349 !self.warnings.is_empty()
1350 }
1351}
1352
1353#[derive(Debug, Clone)]
1355pub struct ValidationReport {
1356 pub tool_name: String,
1357 pub errors: Vec<ValidationError>,
1358 pub warnings: Vec<ValidationWarning>,
1359 pub performance_metrics: PerformanceMetrics,
1360}
1361
1362impl ValidationReport {
1363 pub fn new(tool_name: String) -> Self {
1364 Self {
1365 tool_name,
1366 errors: Vec::new(),
1367 warnings: Vec::new(),
1368 performance_metrics: PerformanceMetrics::default(),
1369 }
1370 }
1371
1372 pub fn add_error(&mut self, error: ValidationError) {
1373 self.errors.push(error);
1374 }
1375
1376 pub fn add_warning(&mut self, warning: ValidationWarning) {
1377 self.warnings.push(warning);
1378 }
1379
1380 pub fn is_valid(&self) -> bool {
1381 self.errors.is_empty()
1382 }
1383
1384 pub fn severity_level(&self) -> ErrorSeverity {
1385 self.errors
1386 .iter()
1387 .map(|e| &e.severity)
1388 .max()
1389 .cloned()
1390 .unwrap_or(ErrorSeverity::Low)
1391 }
1392}
1393
1394#[derive(Debug, Clone)]
1396pub struct ValidationError {
1397 pub path: String,
1398 pub message: String,
1399 pub severity: ErrorSeverity,
1400}
1401
1402impl ValidationError {
1403 pub fn new(path: String, message: String, severity: ErrorSeverity) -> Self {
1404 Self {
1405 path,
1406 message,
1407 severity,
1408 }
1409 }
1410}
1411
1412#[derive(Debug, Clone)]
1414pub struct ValidationWarning {
1415 pub path: String,
1416 pub message: String,
1417}
1418
1419impl ValidationWarning {
1420 pub fn new(path: String, message: String) -> Self {
1421 Self { path, message }
1422 }
1423}
1424
1425#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
1427pub enum ErrorSeverity {
1428 Low,
1429 Medium,
1430 High,
1431 Critical,
1432}
1433
1434#[derive(Debug, Clone, Default)]
1436pub struct PerformanceMetrics {
1437 pub schema_complexity: usize,
1438 pub validation_time_ms: Option<u64>,
1439 pub memory_usage_bytes: Option<usize>,
1440}
1441
1442pub struct MCPMessageValidator {
1444 max_string_length: usize,
1446 max_array_length: usize,
1448 max_object_properties: usize,
1450 max_depth: usize,
1452 allow_dangerous_content: bool,
1454 blocked_patterns: Vec<regex::Regex>,
1456}
1457
1458impl Default for MCPMessageValidator {
1459 fn default() -> Self {
1460 let blocked_patterns = vec![
1461 regex::Regex::new(r"(?i)<script[^>]*>").unwrap(),
1463 regex::Regex::new(r"(?i)javascript:").unwrap(),
1464 regex::Regex::new(r"(?i)data:text/html").unwrap(),
1465 regex::Regex::new(r"(?i)eval\s*\(").unwrap(),
1466 regex::Regex::new(r"(?i)(union|select|insert|update|delete|drop|create|alter)\s+")
1468 .unwrap(),
1469 regex::Regex::new(r"(?i)(or|and)\s+\d+\s*=\s*\d+").unwrap(),
1470 regex::Regex::new(r"[;&|`$(){}\[\]\\]").unwrap(),
1472 regex::Regex::new(r"\.\.[\\/]").unwrap(),
1474 regex::Regex::new(r"(?i)<!entity").unwrap(),
1476 regex::Regex::new(r"(?i)<!doctype").unwrap(),
1477 ];
1478
1479 Self {
1480 max_string_length: 1_000_000, max_array_length: 50_000,
1482 max_object_properties: 5_000,
1483 max_depth: 50,
1484 allow_dangerous_content: false,
1485 blocked_patterns,
1486 }
1487 }
1488}
1489
1490impl MCPMessageValidator {
1491 pub fn new(
1493 max_string_length: usize,
1494 max_array_length: usize,
1495 max_object_properties: usize,
1496 max_depth: usize,
1497 ) -> Self {
1498 Self {
1499 max_string_length,
1500 max_array_length,
1501 max_object_properties,
1502 max_depth,
1503 ..Default::default()
1504 }
1505 }
1506
1507 pub fn with_dangerous_content(mut self, allow: bool) -> Self {
1509 self.allow_dangerous_content = allow;
1510 self
1511 }
1512
1513 pub fn add_blocked_pattern(&mut self, pattern: &str) -> Result<(), regex::Error> {
1515 let regex = regex::Regex::new(pattern)?;
1516 self.blocked_patterns.push(regex);
1517 Ok(())
1518 }
1519
1520 pub fn validate_message(
1522 &self,
1523 message: &crate::protocol::jsonrpc::JsonRpcMessage,
1524 ) -> MCPResult<ValidationReport> {
1525 let mut report = ValidationReport::new("message".to_string());
1526
1527 match message {
1528 crate::protocol::jsonrpc::JsonRpcMessage::Request(req) => {
1529 self.validate_request(req, &mut report)?;
1530 }
1531 crate::protocol::jsonrpc::JsonRpcMessage::Response(resp) => {
1532 self.validate_response(resp, &mut report)?;
1533 }
1534 crate::protocol::jsonrpc::JsonRpcMessage::Notification(notif) => {
1535 self.validate_notification(notif, &mut report)?;
1536 }
1537 }
1538
1539 Ok(report)
1540 }
1541
1542 fn validate_request(
1544 &self,
1545 request: &crate::protocol::jsonrpc::JsonRpcRequest,
1546 report: &mut ValidationReport,
1547 ) -> MCPResult<()> {
1548 if request.jsonrpc != Cow::Borrowed("2.0") {
1550 report.add_error(ValidationError::new(
1551 "jsonrpc".to_string(),
1552 "Invalid JSON-RPC version, must be '2.0'".to_string(),
1553 ErrorSeverity::High,
1554 ));
1555 }
1556
1557 self.validate_method_name(&request.method, report)?;
1559
1560 if let Some(ref id) = request.id {
1562 self.validate_request_id(id, report)?;
1563 }
1564
1565 if let Some(ref params) = request.params {
1567 self.validate_value(params, report, "params".to_string(), 0)?;
1568 self.validate_method_specific_params(&request.method, params, report)?;
1569 }
1570
1571 Ok(())
1572 }
1573
1574 fn validate_response(
1576 &self,
1577 response: &crate::protocol::jsonrpc::JsonRpcResponse,
1578 report: &mut ValidationReport,
1579 ) -> MCPResult<()> {
1580 if response.jsonrpc != Cow::Borrowed("2.0") {
1582 report.add_error(ValidationError::new(
1583 "jsonrpc".to_string(),
1584 "Invalid JSON-RPC version, must be '2.0'".to_string(),
1585 ErrorSeverity::High,
1586 ));
1587 }
1588
1589 if let Some(ref id) = response.id {
1591 self.validate_request_id(id, report)?;
1592 }
1593
1594 match (&response.result, &response.error) {
1596 (Some(_), Some(_)) => {
1597 report.add_error(ValidationError::new(
1598 "response".to_string(),
1599 "Response cannot have both result and error".to_string(),
1600 ErrorSeverity::High,
1601 ));
1602 }
1603 (None, None) => {
1604 report.add_error(ValidationError::new(
1605 "response".to_string(),
1606 "Response must have either result or error".to_string(),
1607 ErrorSeverity::High,
1608 ));
1609 }
1610 _ => {}
1611 }
1612
1613 if let Some(ref result) = response.result {
1615 self.validate_value(result, report, "result".to_string(), 0)?;
1616 }
1617
1618 if let Some(ref error) = response.error {
1620 self.validate_error(error, report)?;
1621 }
1622
1623 Ok(())
1624 }
1625
1626 fn validate_notification(
1628 &self,
1629 notification: &crate::protocol::jsonrpc::JsonRpcRequest,
1630 report: &mut ValidationReport,
1631 ) -> MCPResult<()> {
1632 if notification.jsonrpc != Cow::Borrowed("2.0") {
1634 report.add_error(ValidationError::new(
1635 "jsonrpc".to_string(),
1636 "Invalid JSON-RPC version, must be '2.0'".to_string(),
1637 ErrorSeverity::High,
1638 ));
1639 }
1640
1641 self.validate_method_name(¬ification.method, report)?;
1643
1644 if let Some(ref params) = notification.params {
1646 self.validate_value(params, report, "params".to_string(), 0)?;
1647 self.validate_method_specific_params(¬ification.method, params, report)?;
1648 }
1649
1650 Ok(())
1651 }
1652
1653 fn validate_method_name(&self, method: &str, report: &mut ValidationReport) -> MCPResult<()> {
1655 if method.is_empty() {
1657 report.add_error(ValidationError::new(
1658 "method".to_string(),
1659 "Method name cannot be empty".to_string(),
1660 ErrorSeverity::High,
1661 ));
1662 return Ok(());
1663 }
1664
1665 if method.len() > 100 {
1667 report.add_error(ValidationError::new(
1668 "method".to_string(),
1669 format!(
1670 "Method name too long: {} characters (max 100)",
1671 method.len()
1672 ),
1673 ErrorSeverity::High,
1674 ));
1675 }
1676
1677 if !method
1679 .chars()
1680 .all(|c| c.is_alphanumeric() || c == '/' || c == '_' || c == '-' || c == '.')
1681 {
1682 report.add_error(ValidationError::new(
1683 "method".to_string(),
1684 "Method name contains invalid characters".to_string(),
1685 ErrorSeverity::High,
1686 ));
1687 }
1688
1689 if method.contains("..") || method.starts_with('/') || method.ends_with('/') {
1691 report.add_error(ValidationError::new(
1692 "method".to_string(),
1693 "Method name contains potentially unsafe patterns".to_string(),
1694 ErrorSeverity::High,
1695 ));
1696 }
1697
1698 if method.starts_with('_') || method.contains("internal") || method.contains("private") {
1700 report.add_warning(ValidationWarning::new(
1701 "method".to_string(),
1702 "Method name suggests internal/private usage".to_string(),
1703 ));
1704 }
1705
1706 Ok(())
1707 }
1708
1709 fn validate_request_id(
1711 &self,
1712 id: &crate::protocol::jsonrpc::RequestId,
1713 report: &mut ValidationReport,
1714 ) -> MCPResult<()> {
1715 match id {
1716 crate::protocol::jsonrpc::RequestId::String(s) => {
1717 if s.is_empty() {
1718 report.add_error(ValidationError::new(
1719 "id".to_string(),
1720 "Request ID string cannot be empty".to_string(),
1721 ErrorSeverity::Medium,
1722 ));
1723 }
1724
1725 if s.len() > 200 {
1726 report.add_error(ValidationError::new(
1727 "id".to_string(),
1728 format!(
1729 "Request ID string too long: {} characters (max 200)",
1730 s.len()
1731 ),
1732 ErrorSeverity::Medium,
1733 ));
1734 }
1735
1736 if s.contains('\0') || s.contains('\n') || s.contains('\r') || s.contains('\t') {
1738 report.add_error(ValidationError::new(
1739 "id".to_string(),
1740 "Request ID contains control characters".to_string(),
1741 ErrorSeverity::Medium,
1742 ));
1743 }
1744
1745 self.validate_string_security(s, report, "id".to_string())?;
1747 }
1748 crate::protocol::jsonrpc::RequestId::Number(n) => {
1749 if *n < -9_223_372_036_854_775_807 {
1751 report.add_error(ValidationError::new(
1752 "id".to_string(),
1753 "Request ID number out of reasonable range".to_string(),
1754 ErrorSeverity::Low,
1755 ));
1756 }
1757 }
1758 }
1759
1760 Ok(())
1761 }
1762
1763 fn validate_error(
1765 &self,
1766 error: &crate::protocol::jsonrpc::JsonRpcError,
1767 report: &mut ValidationReport,
1768 ) -> MCPResult<()> {
1769 if (error.code < -32999 || error.code > -32000)
1771 && (error.code < -32700 || error.code > -32600)
1772 {
1773 report.add_warning(ValidationWarning::new(
1774 "error.code".to_string(),
1775 format!(
1776 "Error code {} is outside standard JSON-RPC ranges",
1777 error.code
1778 ),
1779 ));
1780 }
1781
1782 if error.message.is_empty() {
1784 report.add_error(ValidationError::new(
1785 "error.message".to_string(),
1786 "Error message cannot be empty".to_string(),
1787 ErrorSeverity::Medium,
1788 ));
1789 }
1790
1791 if error.message.len() > 1000 {
1792 report.add_warning(ValidationWarning::new(
1793 "error.message".to_string(),
1794 format!(
1795 "Error message very long: {} characters",
1796 error.message.len()
1797 ),
1798 ));
1799 }
1800
1801 self.validate_string_security(&error.message, report, "error.message".to_string())?;
1802
1803 if let Some(ref data) = error.data {
1805 self.validate_value(data, report, "error.data".to_string(), 0)?;
1806 }
1807
1808 Ok(())
1809 }
1810
1811 fn validate_method_specific_params(
1813 &self,
1814 method: &str,
1815 params: &serde_json::Value,
1816 report: &mut ValidationReport,
1817 ) -> MCPResult<()> {
1818 match method {
1819 "initialize" => self.validate_initialize_params(params, report)?,
1820 "tools/call" => self.validate_tool_call_params(params, report)?,
1821 "resources/read" => self.validate_resource_read_params(params, report)?,
1822 "resources/subscribe" => self.validate_resource_subscribe_params(params, report)?,
1823 "resources/unsubscribe" => self.validate_resource_unsubscribe_params(params, report)?,
1824 "prompts/get" => self.validate_prompt_get_params(params, report)?,
1825 "sampling/createMessage" => self.validate_sampling_params(params, report)?,
1826 "elicitation/request" => self.validate_elicitation_params(params, report)?,
1827 "logging/log" => self.validate_logging_params(params, report)?,
1828 _ => {
1829 if let Some(obj) = params.as_object() {
1831 if obj.len() > 50 {
1832 report.add_warning(ValidationWarning::new(
1833 "params".to_string(),
1834 format!(
1835 "Large parameter object for method '{}': {} properties",
1836 method,
1837 obj.len()
1838 ),
1839 ));
1840 }
1841 }
1842 }
1843 }
1844
1845 Ok(())
1846 }
1847
1848 fn validate_initialize_params(
1850 &self,
1851 params: &serde_json::Value,
1852 report: &mut ValidationReport,
1853 ) -> MCPResult<()> {
1854 let obj = match params.as_object() {
1855 Some(obj) => obj,
1856 None => {
1857 report.add_error(ValidationError::new(
1858 "initialize.params".to_string(),
1859 "Initialize parameters must be an object".to_string(),
1860 ErrorSeverity::High,
1861 ));
1862 return Ok(());
1863 }
1864 };
1865
1866 if let Some(version) = obj.get("protocolVersion") {
1868 if let Some(version_str) = version.as_str() {
1869 if version_str != "2025-06-18"
1870 && version_str != "2025-03-26"
1871 && version_str != "2024-11-05"
1872 {
1873 report.add_error(ValidationError::new(
1874 "initialize.protocolVersion".to_string(),
1875 format!("Unsupported protocol version: {version_str}"),
1876 ErrorSeverity::High,
1877 ));
1878 }
1879 } else {
1880 report.add_error(ValidationError::new(
1881 "initialize.protocolVersion".to_string(),
1882 "Protocol version must be a string".to_string(),
1883 ErrorSeverity::High,
1884 ));
1885 }
1886 }
1887
1888 if let Some(client_info) = obj.get("clientInfo") {
1890 self.validate_client_info(client_info, report)?;
1891 }
1892
1893 if let Some(capabilities) = obj.get("capabilities") {
1895 self.validate_capabilities(
1896 capabilities,
1897 report,
1898 "initialize.capabilities".to_string(),
1899 )?;
1900 }
1901
1902 Ok(())
1903 }
1904
1905 fn validate_tool_call_params(
1907 &self,
1908 params: &serde_json::Value,
1909 report: &mut ValidationReport,
1910 ) -> MCPResult<()> {
1911 let obj = match params.as_object() {
1912 Some(obj) => obj,
1913 None => {
1914 report.add_error(ValidationError::new(
1915 "tools/call.params".to_string(),
1916 "Tool call parameters must be an object".to_string(),
1917 ErrorSeverity::High,
1918 ));
1919 return Ok(());
1920 }
1921 };
1922
1923 if let Some(name) = obj.get("name") {
1925 if let Some(name_str) = name.as_str() {
1926 if name_str.is_empty() {
1927 report.add_error(ValidationError::new(
1928 "tools/call.name".to_string(),
1929 "Tool name cannot be empty".to_string(),
1930 ErrorSeverity::High,
1931 ));
1932 }
1933
1934 if name_str.len() > 200 {
1935 report.add_error(ValidationError::new(
1936 "tools/call.name".to_string(),
1937 format!(
1938 "Tool name too long: {} characters (max 200)",
1939 name_str.len()
1940 ),
1941 ErrorSeverity::High,
1942 ));
1943 }
1944
1945 if name_str.starts_with('_') || name_str.contains("..") || name_str.contains('/') {
1947 report.add_error(ValidationError::new(
1948 "tools/call.name".to_string(),
1949 "Tool name contains potentially unsafe characters".to_string(),
1950 ErrorSeverity::High,
1951 ));
1952 }
1953
1954 self.validate_string_security(name_str, report, "tools/call.name".to_string())?;
1955 } else {
1956 report.add_error(ValidationError::new(
1957 "tools/call.name".to_string(),
1958 "Tool name must be a string".to_string(),
1959 ErrorSeverity::High,
1960 ));
1961 }
1962 } else {
1963 report.add_error(ValidationError::new(
1964 "tools/call.name".to_string(),
1965 "Tool name is required".to_string(),
1966 ErrorSeverity::High,
1967 ));
1968 }
1969
1970 if let Some(arguments) = obj.get("arguments") {
1972 self.validate_value(arguments, report, "tools/call.arguments".to_string(), 0)?;
1973 }
1974
1975 Ok(())
1976 }
1977
1978 fn validate_resource_read_params(
1980 &self,
1981 params: &serde_json::Value,
1982 report: &mut ValidationReport,
1983 ) -> MCPResult<()> {
1984 let obj = match params.as_object() {
1985 Some(obj) => obj,
1986 None => {
1987 report.add_error(ValidationError::new(
1988 "resources/read.params".to_string(),
1989 "Resource read parameters must be an object".to_string(),
1990 ErrorSeverity::High,
1991 ));
1992 return Ok(());
1993 }
1994 };
1995
1996 if let Some(uri) = obj.get("uri") {
1998 if let Some(uri_str) = uri.as_str() {
1999 self.validate_uri(uri_str, report)?;
2000 } else {
2001 report.add_error(ValidationError::new(
2002 "resources/read.uri".to_string(),
2003 "URI must be a string".to_string(),
2004 ErrorSeverity::High,
2005 ));
2006 }
2007 } else {
2008 report.add_error(ValidationError::new(
2009 "resources/read.uri".to_string(),
2010 "URI is required".to_string(),
2011 ErrorSeverity::High,
2012 ));
2013 }
2014
2015 Ok(())
2016 }
2017
2018 fn validate_resource_subscribe_params(
2020 &self,
2021 params: &serde_json::Value,
2022 report: &mut ValidationReport,
2023 ) -> MCPResult<()> {
2024 let obj = match params.as_object() {
2025 Some(obj) => obj,
2026 None => {
2027 report.add_error(ValidationError::new(
2028 "resources/subscribe.params".to_string(),
2029 "Resource subscribe parameters must be an object".to_string(),
2030 ErrorSeverity::High,
2031 ));
2032 return Ok(());
2033 }
2034 };
2035
2036 if let Some(uri) = obj.get("uri") {
2038 if let Some(uri_str) = uri.as_str() {
2039 self.validate_uri(uri_str, report)?;
2040 } else {
2041 report.add_error(ValidationError::new(
2042 "resources/subscribe.uri".to_string(),
2043 "URI must be a string".to_string(),
2044 ErrorSeverity::High,
2045 ));
2046 }
2047 } else {
2048 report.add_error(ValidationError::new(
2049 "resources/subscribe.uri".to_string(),
2050 "URI is required".to_string(),
2051 ErrorSeverity::High,
2052 ));
2053 }
2054
2055 Ok(())
2056 }
2057
2058 fn validate_resource_unsubscribe_params(
2060 &self,
2061 params: &serde_json::Value,
2062 report: &mut ValidationReport,
2063 ) -> MCPResult<()> {
2064 let obj = match params.as_object() {
2065 Some(obj) => obj,
2066 None => {
2067 report.add_error(ValidationError::new(
2068 "resources/unsubscribe.params".to_string(),
2069 "Resource unsubscribe parameters must be an object".to_string(),
2070 ErrorSeverity::High,
2071 ));
2072 return Ok(());
2073 }
2074 };
2075
2076 if let Some(uri) = obj.get("uri") {
2078 if let Some(uri_str) = uri.as_str() {
2079 self.validate_uri(uri_str, report)?;
2080 } else {
2081 report.add_error(ValidationError::new(
2082 "resources/unsubscribe.uri".to_string(),
2083 "URI must be a string".to_string(),
2084 ErrorSeverity::High,
2085 ));
2086 }
2087 } else {
2088 report.add_error(ValidationError::new(
2089 "resources/unsubscribe.uri".to_string(),
2090 "URI is required".to_string(),
2091 ErrorSeverity::High,
2092 ));
2093 }
2094
2095 Ok(())
2096 }
2097
2098 fn validate_prompt_get_params(
2100 &self,
2101 params: &serde_json::Value,
2102 report: &mut ValidationReport,
2103 ) -> MCPResult<()> {
2104 let obj = match params.as_object() {
2105 Some(obj) => obj,
2106 None => {
2107 report.add_error(ValidationError::new(
2108 "prompts/get.params".to_string(),
2109 "Prompt get parameters must be an object".to_string(),
2110 ErrorSeverity::High,
2111 ));
2112 return Ok(());
2113 }
2114 };
2115
2116 if let Some(name) = obj.get("name") {
2118 if let Some(name_str) = name.as_str() {
2119 if name_str.is_empty() {
2120 report.add_error(ValidationError::new(
2121 "prompts/get.name".to_string(),
2122 "Prompt name cannot be empty".to_string(),
2123 ErrorSeverity::High,
2124 ));
2125 }
2126
2127 if name_str.len() > 200 {
2128 report.add_error(ValidationError::new(
2129 "prompts/get.name".to_string(),
2130 format!(
2131 "Prompt name too long: {} characters (max 200)",
2132 name_str.len()
2133 ),
2134 ErrorSeverity::High,
2135 ));
2136 }
2137
2138 self.validate_string_security(name_str, report, "prompts/get.name".to_string())?;
2139 } else {
2140 report.add_error(ValidationError::new(
2141 "prompts/get.name".to_string(),
2142 "Prompt name must be a string".to_string(),
2143 ErrorSeverity::High,
2144 ));
2145 }
2146 } else {
2147 report.add_error(ValidationError::new(
2148 "prompts/get.name".to_string(),
2149 "Prompt name is required".to_string(),
2150 ErrorSeverity::High,
2151 ));
2152 }
2153
2154 if let Some(arguments) = obj.get("arguments") {
2156 self.validate_value(arguments, report, "prompts/get.arguments".to_string(), 0)?;
2157 }
2158
2159 Ok(())
2160 }
2161
2162 fn validate_sampling_params(
2164 &self,
2165 params: &serde_json::Value,
2166 report: &mut ValidationReport,
2167 ) -> MCPResult<()> {
2168 let obj = match params.as_object() {
2169 Some(obj) => obj,
2170 None => {
2171 report.add_error(ValidationError::new(
2172 "sampling/createMessage.params".to_string(),
2173 "Sampling parameters must be an object".to_string(),
2174 ErrorSeverity::High,
2175 ));
2176 return Ok(());
2177 }
2178 };
2179
2180 if let Some(messages) = obj.get("messages") {
2182 if let Some(messages_array) = messages.as_array() {
2183 if messages_array.len() > 1000 {
2184 report.add_error(ValidationError::new(
2185 "sampling/createMessage.messages".to_string(),
2186 format!("Too many messages: {} (max 1000)", messages_array.len()),
2187 ErrorSeverity::High,
2188 ));
2189 }
2190
2191 for (i, message) in messages_array.iter().enumerate() {
2192 self.validate_value(
2193 message,
2194 report,
2195 format!("sampling/createMessage.messages[{i}]"),
2196 0,
2197 )?;
2198 }
2199 } else {
2200 report.add_error(ValidationError::new(
2201 "sampling/createMessage.messages".to_string(),
2202 "Messages must be an array".to_string(),
2203 ErrorSeverity::High,
2204 ));
2205 }
2206 }
2207
2208 if let Some(model_prefs) = obj.get("modelPreferences") {
2210 self.validate_value(
2211 model_prefs,
2212 report,
2213 "sampling/createMessage.modelPreferences".to_string(),
2214 0,
2215 )?;
2216 }
2217
2218 Ok(())
2219 }
2220
2221 fn validate_elicitation_params(
2223 &self,
2224 params: &serde_json::Value,
2225 report: &mut ValidationReport,
2226 ) -> MCPResult<()> {
2227 let obj = match params.as_object() {
2228 Some(obj) => obj,
2229 None => {
2230 report.add_error(ValidationError::new(
2231 "elicitation/request.params".to_string(),
2232 "Elicitation parameters must be an object".to_string(),
2233 ErrorSeverity::High,
2234 ));
2235 return Ok(());
2236 }
2237 };
2238
2239 if let Some(prompt) = obj.get("prompt") {
2241 if let Some(prompt_str) = prompt.as_str() {
2242 if prompt_str.len() > 10000 {
2243 report.add_warning(ValidationWarning::new(
2244 "elicitation/request.prompt".to_string(),
2245 format!("Very long prompt: {} characters", prompt_str.len()),
2246 ));
2247 }
2248
2249 self.validate_string_security(
2250 prompt_str,
2251 report,
2252 "elicitation/request.prompt".to_string(),
2253 )?;
2254 }
2255 }
2256
2257 Ok(())
2258 }
2259
2260 fn validate_logging_params(
2262 &self,
2263 params: &serde_json::Value,
2264 report: &mut ValidationReport,
2265 ) -> MCPResult<()> {
2266 let obj = match params.as_object() {
2267 Some(obj) => obj,
2268 None => {
2269 report.add_error(ValidationError::new(
2270 "logging/log.params".to_string(),
2271 "Logging parameters must be an object".to_string(),
2272 ErrorSeverity::High,
2273 ));
2274 return Ok(());
2275 }
2276 };
2277
2278 if let Some(level) = obj.get("level") {
2280 if let Some(level_str) = level.as_str() {
2281 let valid_levels = ["error", "warning", "info", "debug"];
2282 if !valid_levels.contains(&level_str) {
2283 report.add_error(ValidationError::new(
2284 "logging/log.level".to_string(),
2285 format!(
2286 "Invalid log level: {level_str} (must be one of: {valid_levels:?})"
2287 ),
2288 ErrorSeverity::Medium,
2289 ));
2290 }
2291 } else {
2292 report.add_error(ValidationError::new(
2293 "logging/log.level".to_string(),
2294 "Log level must be a string".to_string(),
2295 ErrorSeverity::Medium,
2296 ));
2297 }
2298 }
2299
2300 if let Some(message) = obj.get("message") {
2302 if let Some(message_str) = message.as_str() {
2303 if message_str.len() > 10000 {
2304 report.add_warning(ValidationWarning::new(
2305 "logging/log.message".to_string(),
2306 format!("Very long log message: {} characters", message_str.len()),
2307 ));
2308 }
2309
2310 self.validate_string_security(
2311 message_str,
2312 report,
2313 "logging/log.message".to_string(),
2314 )?;
2315 }
2316 }
2317
2318 Ok(())
2319 }
2320
2321 fn validate_client_info(
2323 &self,
2324 client_info: &serde_json::Value,
2325 report: &mut ValidationReport,
2326 ) -> MCPResult<()> {
2327 if let Some(obj) = client_info.as_object() {
2328 if let Some(name) = obj.get("name") {
2330 if let Some(name_str) = name.as_str() {
2331 if name_str.is_empty() {
2332 report.add_error(ValidationError::new(
2333 "clientInfo.name".to_string(),
2334 "Client name cannot be empty".to_string(),
2335 ErrorSeverity::Medium,
2336 ));
2337 }
2338
2339 if name_str.len() > 200 {
2340 report.add_error(ValidationError::new(
2341 "clientInfo.name".to_string(),
2342 format!(
2343 "Client name too long: {} characters (max 200)",
2344 name_str.len()
2345 ),
2346 ErrorSeverity::Medium,
2347 ));
2348 }
2349
2350 self.validate_string_security(name_str, report, "clientInfo.name".to_string())?;
2351 }
2352 }
2353
2354 if let Some(version) = obj.get("version") {
2356 if let Some(version_str) = version.as_str() {
2357 if version_str.len() > 100 {
2358 report.add_warning(ValidationWarning::new(
2359 "clientInfo.version".to_string(),
2360 format!(
2361 "Client version string very long: {} characters",
2362 version_str.len()
2363 ),
2364 ));
2365 }
2366 }
2367 }
2368 } else {
2369 report.add_error(ValidationError::new(
2370 "clientInfo".to_string(),
2371 "Client info must be an object".to_string(),
2372 ErrorSeverity::Medium,
2373 ));
2374 }
2375
2376 Ok(())
2377 }
2378
2379 fn validate_capabilities(
2381 &self,
2382 capabilities: &serde_json::Value,
2383 report: &mut ValidationReport,
2384 path: String,
2385 ) -> MCPResult<()> {
2386 if let Some(obj) = capabilities.as_object() {
2387 if obj.len() > 100 {
2389 report.add_warning(ValidationWarning::new(
2390 path.clone(),
2391 format!("Large capabilities object: {} properties", obj.len()),
2392 ));
2393 }
2394
2395 for (key, value) in obj {
2397 self.validate_value(value, report, format!("{path}.{key}"), 0)?;
2398 }
2399 } else {
2400 report.add_error(ValidationError::new(
2401 path,
2402 "Capabilities must be an object".to_string(),
2403 ErrorSeverity::Medium,
2404 ));
2405 }
2406
2407 Ok(())
2408 }
2409
2410 fn validate_uri(&self, uri: &str, report: &mut ValidationReport) -> MCPResult<()> {
2412 if uri.len() > 2048 {
2414 report.add_error(ValidationError::new(
2415 "uri".to_string(),
2416 format!("URI too long: {} characters (max 2048)", uri.len()),
2417 ErrorSeverity::High,
2418 ));
2419 }
2420
2421 if !uri.contains("://") && !uri.starts_with("file://") && !uri.starts_with("data:") {
2423 report.add_error(ValidationError::new(
2424 "uri".to_string(),
2425 "URI must contain a valid scheme".to_string(),
2426 ErrorSeverity::High,
2427 ));
2428 }
2429
2430 let dangerous_schemes = [
2432 "javascript:",
2433 "data:text/html",
2434 "vbscript:",
2435 "file:///proc",
2436 "file:///sys",
2437 ];
2438 let uri_lower = uri.to_lowercase();
2439
2440 for scheme in &dangerous_schemes {
2441 if uri_lower.starts_with(scheme) {
2442 if !self.allow_dangerous_content {
2443 report.add_error(ValidationError::new(
2444 "uri".to_string(),
2445 format!("Potentially dangerous URI scheme: {scheme}"),
2446 ErrorSeverity::High,
2447 ));
2448 } else {
2449 report.add_warning(ValidationWarning::new(
2450 "uri".to_string(),
2451 format!("Dangerous URI scheme detected: {scheme}"),
2452 ));
2453 }
2454 }
2455 }
2456
2457 if uri.contains("../") || uri.contains("..\\") {
2459 report.add_error(ValidationError::new(
2460 "uri".to_string(),
2461 "URI contains path traversal patterns".to_string(),
2462 ErrorSeverity::High,
2463 ));
2464 }
2465
2466 self.validate_string_security(uri, report, "uri".to_string())?;
2468
2469 Ok(())
2470 }
2471
2472 fn validate_value(
2474 &self,
2475 value: &serde_json::Value,
2476 report: &mut ValidationReport,
2477 path: String,
2478 depth: usize,
2479 ) -> MCPResult<()> {
2480 if depth > self.max_depth {
2482 report.add_error(ValidationError::new(
2483 path.clone(),
2484 format!("Value depth {} exceeds maximum {}", depth, self.max_depth),
2485 ErrorSeverity::High,
2486 ));
2487 return Ok(());
2488 }
2489
2490 match value {
2491 serde_json::Value::String(s) => {
2492 self.validate_string_value(s, report, path)?;
2493 }
2494 serde_json::Value::Array(arr) => {
2495 self.validate_array_value(arr, report, path, depth)?;
2496 }
2497 serde_json::Value::Object(obj) => {
2498 self.validate_object_value(obj, report, path, depth)?;
2499 }
2500 serde_json::Value::Number(n) => {
2501 self.validate_number_value(n, report, path)?;
2502 }
2503 _ => {}
2505 }
2506
2507 Ok(())
2508 }
2509
2510 fn validate_string_value(
2512 &self,
2513 s: &str,
2514 report: &mut ValidationReport,
2515 path: String,
2516 ) -> MCPResult<()> {
2517 if s.len() > self.max_string_length {
2519 report.add_error(ValidationError::new(
2520 path.clone(),
2521 format!(
2522 "String too long: {} characters (max {})",
2523 s.len(),
2524 self.max_string_length
2525 ),
2526 ErrorSeverity::High,
2527 ));
2528 }
2529
2530 if s.len() > 100_000 {
2532 report.add_warning(ValidationWarning::new(
2533 path.clone(),
2534 format!("Very large string: {} characters", s.len()),
2535 ));
2536 }
2537
2538 self.validate_string_security(s, report, path)?;
2540
2541 Ok(())
2542 }
2543
2544 fn validate_string_security(
2546 &self,
2547 s: &str,
2548 report: &mut ValidationReport,
2549 path: String,
2550 ) -> MCPResult<()> {
2551 if s.contains('\0') {
2553 report.add_error(ValidationError::new(
2554 path.clone(),
2555 "String contains null bytes".to_string(),
2556 ErrorSeverity::High,
2557 ));
2558 }
2559
2560 for pattern in &self.blocked_patterns {
2562 if pattern.is_match(s) {
2563 if !self.allow_dangerous_content {
2564 report.add_error(ValidationError::new(
2565 path.clone(),
2566 format!("String matches blocked pattern: {}", pattern.as_str()),
2567 ErrorSeverity::High,
2568 ));
2569 } else {
2570 report.add_warning(ValidationWarning::new(
2571 path.clone(),
2572 format!(
2573 "String matches potentially dangerous pattern: {}",
2574 pattern.as_str()
2575 ),
2576 ));
2577 }
2578 }
2579 }
2580
2581 let suspicious_patterns = [
2583 ("\r\n\r\n", "HTTP header injection"),
2584 ("\n\n", "Potential header injection"),
2585 ("%%", "URL encoding attack"),
2586 ("%00", "Null byte encoding"),
2587 ("${", "Variable substitution attack"),
2588 ("#{", "Expression language injection"),
2589 ];
2590
2591 for (pattern, description) in &suspicious_patterns {
2592 if s.contains(pattern) {
2593 report.add_warning(ValidationWarning::new(
2594 path.clone(),
2595 format!("String contains suspicious pattern '{pattern}': {description}"),
2596 ));
2597 }
2598 }
2599
2600 Ok(())
2601 }
2602
2603 fn validate_array_value(
2605 &self,
2606 arr: &[serde_json::Value],
2607 report: &mut ValidationReport,
2608 path: String,
2609 depth: usize,
2610 ) -> MCPResult<()> {
2611 if arr.len() > self.max_array_length {
2613 report.add_error(ValidationError::new(
2614 path.clone(),
2615 format!(
2616 "Array too large: {} elements (max {})",
2617 arr.len(),
2618 self.max_array_length
2619 ),
2620 ErrorSeverity::High,
2621 ));
2622 }
2623
2624 for (i, item) in arr.iter().enumerate() {
2626 self.validate_value(item, report, format!("{path}[{i}]"), depth + 1)?;
2627 }
2628
2629 Ok(())
2630 }
2631
2632 fn validate_object_value(
2634 &self,
2635 obj: &serde_json::Map<String, serde_json::Value>,
2636 report: &mut ValidationReport,
2637 path: String,
2638 depth: usize,
2639 ) -> MCPResult<()> {
2640 if obj.len() > self.max_object_properties {
2642 report.add_error(ValidationError::new(
2643 path.clone(),
2644 format!(
2645 "Object too large: {} properties (max {})",
2646 obj.len(),
2647 self.max_object_properties
2648 ),
2649 ErrorSeverity::High,
2650 ));
2651 }
2652
2653 for (key, value) in obj {
2655 if key.is_empty() {
2657 report.add_error(ValidationError::new(
2658 path.clone(),
2659 "Object key cannot be empty".to_string(),
2660 ErrorSeverity::Medium,
2661 ));
2662 }
2663
2664 if key.len() > 200 {
2665 report.add_error(ValidationError::new(
2666 path.clone(),
2667 format!("Object key too long: {} characters (max 200)", key.len()),
2668 ErrorSeverity::Medium,
2669 ));
2670 }
2671
2672 if key.starts_with('_') && key != "_meta" {
2674 report.add_warning(ValidationWarning::new(
2675 path.clone(),
2676 format!("Private key detected: {key}"),
2677 ));
2678 }
2679
2680 self.validate_string_security(key, report, format!("{path}.{key}"))?;
2682
2683 self.validate_value(value, report, format!("{path}.{key}"), depth + 1)?;
2685 }
2686
2687 Ok(())
2688 }
2689
2690 fn validate_number_value(
2692 &self,
2693 n: &serde_json::Number,
2694 report: &mut ValidationReport,
2695 path: String,
2696 ) -> MCPResult<()> {
2697 if let Some(f) = n.as_f64() {
2699 if f.is_infinite() || f.is_nan() {
2700 report.add_error(ValidationError::new(
2701 path,
2702 "Number cannot be infinite or NaN".to_string(),
2703 ErrorSeverity::Medium,
2704 ));
2705 }
2706 }
2707
2708 Ok(())
2709 }
2710}
2711
2712#[cfg(test)]
2713mod mcp_message_validator_tests {
2714 use super::*;
2715 use crate::protocol::jsonrpc::{
2716 JsonRpcError, JsonRpcMessage, JsonRpcRequest, JsonRpcResponse, RequestId,
2717 };
2718 use serde_json::json;
2719
2720 fn create_test_validator() -> MCPMessageValidator {
2721 MCPMessageValidator::default()
2722 }
2723
2724 #[test]
2725 fn test_validate_valid_request() {
2726 let validator = create_test_validator();
2727 let request = JsonRpcRequest {
2728 jsonrpc: Cow::Borrowed("2.0"),
2729 method: "tools/call".to_string(),
2730 params: Some(json!({
2731 "name": "test_tool",
2732 "arguments": {"input": "hello"}
2733 })),
2734 id: Some(RequestId::String("1".to_string())),
2735 meta: std::collections::HashMap::new(),
2736 };
2737 let message = JsonRpcMessage::Request(request);
2738
2739 let result = validator.validate_message(&message);
2740 assert!(result.is_ok());
2741 let report = result.unwrap();
2742 assert!(
2743 report.is_valid(),
2744 "Expected no validation errors: {:?}",
2745 report.errors
2746 );
2747 }
2748
2749 #[test]
2750 fn test_validate_invalid_jsonrpc_version() {
2751 let validator = create_test_validator();
2752 let request = JsonRpcRequest {
2753 jsonrpc: Cow::Borrowed("1.0"),
2754 method: "test".to_string(),
2755 params: None,
2756 id: Some(RequestId::String("1".to_string())),
2757 meta: std::collections::HashMap::new(),
2758 };
2759 let message = JsonRpcMessage::Request(request);
2760
2761 let result = validator.validate_message(&message);
2762 assert!(result.is_ok());
2763 let report = result.unwrap();
2764 assert!(!report.is_valid());
2765 assert_eq!(report.errors.len(), 1);
2766 assert!(
2767 report.errors[0]
2768 .message
2769 .contains("Invalid JSON-RPC version")
2770 );
2771 }
2772
2773 #[test]
2774 fn test_validate_dangerous_method_name() {
2775 let validator = create_test_validator();
2776 let request = JsonRpcRequest {
2777 jsonrpc: Cow::Borrowed("2.0"),
2778 method: "../system/admin".to_string(),
2779 params: None,
2780 id: Some(RequestId::String("1".to_string())),
2781 meta: std::collections::HashMap::new(),
2782 };
2783 let message = JsonRpcMessage::Request(request);
2784
2785 let result = validator.validate_message(&message);
2786 assert!(result.is_ok());
2787 let report = result.unwrap();
2788 assert!(!report.is_valid());
2789 assert!(
2790 report
2791 .errors
2792 .iter()
2793 .any(|e| e.message.contains("unsafe patterns"))
2794 );
2795 }
2796
2797 #[test]
2798 fn test_validate_script_injection_in_params() {
2799 let validator = create_test_validator();
2800 let request = JsonRpcRequest {
2801 jsonrpc: Cow::Borrowed("2.0"),
2802 method: "tools/call".to_string(),
2803 params: Some(json!({
2804 "name": "test_tool",
2805 "arguments": {"input": "<script>alert('xss')</script>"}
2806 })),
2807 id: Some(RequestId::String("1".to_string())),
2808 meta: std::collections::HashMap::new(),
2809 };
2810 let message = JsonRpcMessage::Request(request);
2811
2812 let result = validator.validate_message(&message);
2813 assert!(result.is_ok());
2814 let report = result.unwrap();
2815 assert!(!report.is_valid());
2816 assert!(
2817 report
2818 .errors
2819 .iter()
2820 .any(|e| e.message.contains("blocked pattern"))
2821 );
2822 }
2823
2824 #[test]
2825 fn test_validate_tool_call_params() {
2826 let validator = create_test_validator();
2827 let request = JsonRpcRequest {
2828 jsonrpc: Cow::Borrowed("2.0"),
2829 method: "tools/call".to_string(),
2830 params: Some(json!({
2831 "name": "_private_tool",
2832 "arguments": {"input": "test"}
2833 })),
2834 id: Some(RequestId::String("1".to_string())),
2835 meta: std::collections::HashMap::new(),
2836 };
2837 let message = JsonRpcMessage::Request(request);
2838
2839 let result = validator.validate_message(&message);
2840 assert!(result.is_ok());
2841 let report = result.unwrap();
2842 assert!(!report.is_valid());
2843 assert!(
2844 report
2845 .errors
2846 .iter()
2847 .any(|e| e.message.contains("unsafe characters"))
2848 );
2849 }
2850
2851 #[test]
2852 fn test_validate_initialize_params() {
2853 let validator = create_test_validator();
2854 let request = JsonRpcRequest {
2855 jsonrpc: Cow::Borrowed("2.0"),
2856 method: "initialize".to_string(),
2857 params: Some(json!({
2858 "protocolVersion": "1.0.0",
2859 "clientInfo": {"name": "test", "version": "1.0"}
2860 })),
2861 id: Some(RequestId::String("1".to_string())),
2862 meta: std::collections::HashMap::new(),
2863 };
2864 let message = JsonRpcMessage::Request(request);
2865
2866 let result = validator.validate_message(&message);
2867 assert!(result.is_ok());
2868 let report = result.unwrap();
2869 assert!(!report.is_valid());
2870 assert!(
2871 report
2872 .errors
2873 .iter()
2874 .any(|e| e.message.contains("Unsupported protocol version"))
2875 );
2876 }
2877
2878 #[test]
2879 fn test_validate_response_with_both_result_and_error() {
2880 let validator = create_test_validator();
2881 let response = JsonRpcResponse {
2882 jsonrpc: Cow::Borrowed("2.0"),
2883 result: Some(json!({"status": "ok"})),
2884 error: Some(JsonRpcError::new(-32600, "Invalid request".to_string())),
2885 id: Some(RequestId::String("1".to_string())),
2886 meta: std::collections::HashMap::new(),
2887 };
2888 let message = JsonRpcMessage::Response(response);
2889
2890 let result = validator.validate_message(&message);
2891 assert!(result.is_ok());
2892 let report = result.unwrap();
2893 assert!(!report.is_valid());
2894 assert!(
2895 report
2896 .errors
2897 .iter()
2898 .any(|e| e.message.contains("cannot have both result and error"))
2899 );
2900 }
2901
2902 #[test]
2903 fn test_validate_large_array() {
2904 let validator = create_test_validator();
2905 let large_array: Vec<serde_json::Value> = (0..60000).map(|i| json!(i)).collect();
2906 let request = JsonRpcRequest {
2907 jsonrpc: Cow::Borrowed("2.0"),
2908 method: "test".to_string(),
2909 params: Some(json!({"items": large_array})),
2910 id: Some(RequestId::String("1".to_string())),
2911 meta: std::collections::HashMap::new(),
2912 };
2913 let message = JsonRpcMessage::Request(request);
2914
2915 let result = validator.validate_message(&message);
2916 assert!(result.is_ok());
2917 let report = result.unwrap();
2918 assert!(!report.is_valid());
2919 assert!(
2920 report
2921 .errors
2922 .iter()
2923 .any(|e| e.message.contains("Array too large"))
2924 );
2925 }
2926
2927 #[test]
2928 fn test_validate_deep_nested_object() {
2929 let validator = create_test_validator();
2930
2931 let mut nested = json!("deep_value");
2933 for _ in 0..60 {
2934 nested = json!({"nested": nested});
2935 }
2936
2937 let request = JsonRpcRequest {
2938 jsonrpc: Cow::Borrowed("2.0"),
2939 method: "test".to_string(),
2940 params: Some(nested),
2941 id: Some(RequestId::String("1".to_string())),
2942 meta: std::collections::HashMap::new(),
2943 };
2944 let message = JsonRpcMessage::Request(request);
2945
2946 let result = validator.validate_message(&message);
2947 assert!(result.is_ok());
2948 let report = result.unwrap();
2949 assert!(!report.is_valid());
2950 assert!(
2951 report
2952 .errors
2953 .iter()
2954 .any(|e| e.message.contains("depth") && e.message.contains("exceeds maximum"))
2955 );
2956 }
2957
2958 #[test]
2959 fn test_validate_uri_security() {
2960 let validator = create_test_validator();
2961 let request = JsonRpcRequest {
2962 jsonrpc: Cow::Borrowed("2.0"),
2963 method: "resources/read".to_string(),
2964 params: Some(json!({
2965 "uri": "file:///../../etc/passwd"
2966 })),
2967 id: Some(RequestId::String("1".to_string())),
2968 meta: std::collections::HashMap::new(),
2969 };
2970 let message = JsonRpcMessage::Request(request);
2971
2972 let result = validator.validate_message(&message);
2973 assert!(result.is_ok());
2974 let report = result.unwrap();
2975 assert!(!report.is_valid());
2976 assert!(
2977 report
2978 .errors
2979 .iter()
2980 .any(|e| e.message.contains("path traversal"))
2981 );
2982 }
2983
2984 #[test]
2985 fn test_validate_long_strings() {
2986 let validator = create_test_validator();
2987 let long_string = "a".repeat(2_000_000); let request = JsonRpcRequest {
2989 jsonrpc: Cow::Borrowed("2.0"),
2990 method: "test".to_string(),
2991 params: Some(json!({"data": long_string})),
2992 id: Some(RequestId::String("1".to_string())),
2993 meta: std::collections::HashMap::new(),
2994 };
2995 let message = JsonRpcMessage::Request(request);
2996
2997 let result = validator.validate_message(&message);
2998 assert!(result.is_ok());
2999 let report = result.unwrap();
3000 assert!(!report.is_valid());
3001 assert!(
3002 report
3003 .errors
3004 .iter()
3005 .any(|e| e.message.contains("String too long"))
3006 );
3007 }
3008
3009 #[test]
3010 fn test_validate_custom_blocked_pattern() {
3011 let mut validator = create_test_validator();
3012 validator.add_blocked_pattern(r"SECRET_\w+").unwrap();
3013
3014 let request = JsonRpcRequest {
3015 jsonrpc: Cow::Borrowed("2.0"),
3016 method: "test".to_string(),
3017 params: Some(json!({"token": "SECRET_API_KEY_12345"})),
3018 id: Some(RequestId::String("1".to_string())),
3019 meta: std::collections::HashMap::new(),
3020 };
3021 let message = JsonRpcMessage::Request(request);
3022
3023 let result = validator.validate_message(&message);
3024 assert!(result.is_ok());
3025 let report = result.unwrap();
3026 assert!(!report.is_valid());
3027 assert!(
3028 report
3029 .errors
3030 .iter()
3031 .any(|e| e.message.contains("blocked pattern"))
3032 );
3033 }
3034
3035 #[test]
3036 fn test_validate_dangerous_content_allowed() {
3037 let validator = MCPMessageValidator::default().with_dangerous_content(true);
3038 let request = JsonRpcRequest {
3039 jsonrpc: Cow::Borrowed("2.0"),
3040 method: "test".to_string(),
3041 params: Some(json!({"script": "<script>alert('test')</script>"})),
3042 id: Some(RequestId::String("1".to_string())),
3043 meta: std::collections::HashMap::new(),
3044 };
3045 let message = JsonRpcMessage::Request(request);
3046
3047 let result = validator.validate_message(&message);
3048 assert!(result.is_ok());
3049 let report = result.unwrap();
3050 assert!(report.is_valid());
3052 assert!(!report.warnings.is_empty());
3053 }
3054
3055 #[test]
3056 fn test_validate_notification() {
3057 let validator = create_test_validator();
3058 let notification = JsonRpcRequest {
3059 jsonrpc: Cow::Borrowed("2.0"),
3060 method: "logging/log".to_string(),
3061 params: Some(json!({
3062 "level": "info",
3063 "message": "Test message"
3064 })),
3065 id: None, meta: std::collections::HashMap::new(),
3067 };
3068 let message = JsonRpcMessage::Notification(notification);
3069
3070 let result = validator.validate_message(&message);
3071 assert!(result.is_ok());
3072 let report = result.unwrap();
3073 assert!(
3074 report.is_valid(),
3075 "Expected valid notification: {:?}",
3076 report.errors
3077 );
3078 }
3079}
3080
3081#[cfg(test)]
3082mod tests {
3083 use super::*;
3084
3085 #[test]
3086 fn test_validate_resource_unsubscribe_params() {
3087 let validator = MCPMessageValidator::new(1000, 100, 50, 10);
3088 let mut report = ValidationReport::new("test".to_string());
3089
3090 let valid_params = serde_json::json!({
3092 "uri": "test://static/resource/0"
3093 });
3094 let result = validator.validate_resource_unsubscribe_params(&valid_params, &mut report);
3095 assert!(result.is_ok());
3096 assert!(report.errors.is_empty());
3097
3098 let mut report = ValidationReport::new("test".to_string());
3100 let invalid_params = serde_json::json!({});
3101 let result = validator.validate_resource_unsubscribe_params(&invalid_params, &mut report);
3102 assert!(result.is_ok());
3103 assert!(!report.errors.is_empty());
3104 assert!(
3105 report
3106 .errors
3107 .iter()
3108 .any(|e| e.path == "resources/unsubscribe.uri")
3109 );
3110
3111 let mut report = ValidationReport::new("test".to_string());
3113 let invalid_params = serde_json::json!({
3114 "uri": 123
3115 });
3116 let result = validator.validate_resource_unsubscribe_params(&invalid_params, &mut report);
3117 assert!(result.is_ok());
3118 assert!(!report.errors.is_empty());
3119 assert!(
3120 report
3121 .errors
3122 .iter()
3123 .any(|e| e.path == "resources/unsubscribe.uri")
3124 );
3125 }
3126}