mcp_protocol_sdk/protocol/
validation.rs1use crate::core::error::{McpError, McpResult};
8use crate::protocol::{messages::*, methods, types::*};
9use serde_json::Value;
10
11pub fn validate_jsonrpc_message(message: &Value) -> McpResult<()> {
13 let obj = message
14 .as_object()
15 .ok_or_else(|| McpError::Validation("Message must be a JSON object".to_string()))?;
16
17 let jsonrpc = obj
19 .get("jsonrpc")
20 .and_then(|v| v.as_str())
21 .ok_or_else(|| McpError::Validation("Missing or invalid 'jsonrpc' field".to_string()))?;
22
23 if jsonrpc != "2.0" {
24 return Err(McpError::Validation("jsonrpc must be '2.0'".to_string()));
25 }
26
27 let has_method = obj.contains_key("method");
29 let has_result = obj.contains_key("result");
30 let has_error = obj.contains_key("error");
31 let has_id = obj.contains_key("id");
32
33 if has_method {
34 if has_result || has_error {
36 return Err(McpError::Validation(
37 "Request/notification cannot have 'result' or 'error' fields".to_string(),
38 ));
39 }
40
41 } else if has_result || has_error {
44 if !has_id {
46 return Err(McpError::Validation(
47 "Response must have an 'id' field".to_string(),
48 ));
49 }
50
51 if has_result && has_error {
52 return Err(McpError::Validation(
53 "Response cannot have both 'result' and 'error' fields".to_string(),
54 ));
55 }
56 } else {
57 return Err(McpError::Validation(
58 "Message must be a request, response, or notification".to_string(),
59 ));
60 }
61
62 Ok(())
63}
64
65pub fn validate_jsonrpc_request(request: &JsonRpcRequest) -> McpResult<()> {
67 if request.jsonrpc != "2.0" {
68 return Err(McpError::Validation("jsonrpc must be '2.0'".to_string()));
69 }
70
71 if request.method.is_empty() {
72 return Err(McpError::Validation(
73 "Method name cannot be empty".to_string(),
74 ));
75 }
76
77 if request.method.starts_with("rpc.") && !request.method.starts_with("rpc.discover") {
79 return Err(McpError::Validation(
80 "Method names starting with 'rpc.' are reserved".to_string(),
81 ));
82 }
83
84 Ok(())
85}
86
87pub fn validate_jsonrpc_response(response: &JsonRpcResponse) -> McpResult<()> {
89 if response.jsonrpc != "2.0" {
90 return Err(McpError::Validation("jsonrpc must be '2.0'".to_string()));
91 }
92
93 Ok(())
96}
97
98pub fn validate_jsonrpc_notification(notification: &JsonRpcNotification) -> McpResult<()> {
100 if notification.jsonrpc != "2.0" {
101 return Err(McpError::Validation("jsonrpc must be '2.0'".to_string()));
102 }
103
104 if notification.method.is_empty() {
105 return Err(McpError::Validation(
106 "Method name cannot be empty".to_string(),
107 ));
108 }
109
110 Ok(())
111}
112
113pub fn validate_initialize_params(params: &InitializeParams) -> McpResult<()> {
115 if params.client_info.name.is_empty() {
116 return Err(McpError::Validation(
117 "Client name cannot be empty".to_string(),
118 ));
119 }
120
121 if params.client_info.version.is_empty() {
122 return Err(McpError::Validation(
123 "Client version cannot be empty".to_string(),
124 ));
125 }
126
127 if params.protocol_version.is_empty() {
128 return Err(McpError::Validation(
129 "Protocol version cannot be empty".to_string(),
130 ));
131 }
132
133 Ok(())
134}
135
136pub fn validate_tool_info(tool: &Tool) -> McpResult<()> {
138 if tool.name.is_empty() {
139 return Err(McpError::Validation(
140 "Tool name cannot be empty".to_string(),
141 ));
142 }
143
144 if tool.input_schema.schema_type != "object" {
146 return Err(McpError::Validation(
147 "Tool input_schema type must be 'object'".to_string(),
148 ));
149 }
150
151 if let Some(annotations) = &tool.annotations {
153 validate_tool_annotations(annotations)?;
154 }
155
156 Ok(())
157}
158
159pub fn validate_call_tool_params(params: &CallToolParams) -> McpResult<()> {
161 if params.name.is_empty() {
162 return Err(McpError::Validation(
163 "Tool name cannot be empty".to_string(),
164 ));
165 }
166
167 Ok(())
168}
169
170pub fn validate_resource_info(resource: &Resource) -> McpResult<()> {
172 if resource.uri.is_empty() {
173 return Err(McpError::Validation(
174 "Resource URI cannot be empty".to_string(),
175 ));
176 }
177
178 if resource.name.as_ref().map_or(true, |name| name.is_empty()) {
179 return Err(McpError::Validation(
180 "Resource name cannot be empty".to_string(),
181 ));
182 }
183
184 validate_uri(&resource.uri)?;
186
187 if let Some(annotations) = &resource.annotations {
189 validate_annotations(annotations)?;
190 }
191
192 Ok(())
193}
194
195pub fn validate_read_resource_params(params: &ReadResourceParams) -> McpResult<()> {
197 if params.uri.is_empty() {
198 return Err(McpError::Validation(
199 "Resource URI cannot be empty".to_string(),
200 ));
201 }
202
203 validate_uri(¶ms.uri)?;
204
205 Ok(())
206}
207
208pub fn validate_resource_content(content: &ResourceContents) -> McpResult<()> {
210 match content {
211 ResourceContents::Text { uri, text, .. } => {
212 if uri.is_empty() {
213 return Err(McpError::Validation(
214 "Resource content URI cannot be empty".to_string(),
215 ));
216 }
217 if text.is_empty() {
218 return Err(McpError::Validation(
219 "Text resource content cannot be empty".to_string(),
220 ));
221 }
222 }
223 ResourceContents::Blob { uri, blob, .. } => {
224 if uri.is_empty() {
225 return Err(McpError::Validation(
226 "Resource content URI cannot be empty".to_string(),
227 ));
228 }
229 if blob.is_empty() {
230 return Err(McpError::Validation(
231 "Blob resource content cannot be empty".to_string(),
232 ));
233 }
234 }
235 }
236
237 Ok(())
238}
239
240pub fn validate_prompt_info(prompt: &Prompt) -> McpResult<()> {
242 if prompt.name.is_empty() {
243 return Err(McpError::Validation(
244 "Prompt name cannot be empty".to_string(),
245 ));
246 }
247
248 if let Some(args) = &prompt.arguments {
249 for arg in args {
250 if arg.name.is_empty() {
251 return Err(McpError::Validation(
252 "Prompt argument name cannot be empty".to_string(),
253 ));
254 }
255 }
256 }
257
258 Ok(())
259}
260
261pub fn validate_get_prompt_params(params: &GetPromptParams) -> McpResult<()> {
263 if params.name.is_empty() {
264 return Err(McpError::Validation(
265 "Prompt name cannot be empty".to_string(),
266 ));
267 }
268
269 Ok(())
270}
271
272pub fn validate_prompt_messages(messages: &[PromptMessage]) -> McpResult<()> {
274 if messages.is_empty() {
275 return Err(McpError::Validation(
276 "Prompt must have at least one message".to_string(),
277 ));
278 }
279
280 for message in messages {
281 validate_content(&message.content)?;
283 }
284
285 Ok(())
286}
287
288pub fn validate_sampling_messages(messages: &[SamplingMessage]) -> McpResult<()> {
290 if messages.is_empty() {
291 return Err(McpError::Validation(
292 "Sampling request must have at least one message".to_string(),
293 ));
294 }
295
296 for message in messages {
297 validate_content(&message.content)?;
299 }
300
301 Ok(())
302}
303
304pub fn validate_create_message_params(params: &CreateMessageParams) -> McpResult<()> {
306 validate_sampling_messages(¶ms.messages)?;
307
308 if params.max_tokens == 0 {
310 return Err(McpError::Validation(
311 "max_tokens must be greater than 0".to_string(),
312 ));
313 }
314
315 if let Some(prefs) = ¶ms.model_preferences {
317 validate_model_preferences(prefs)?;
318 }
319
320 Ok(())
321}
322
323pub fn validate_content(content: &Content) -> McpResult<()> {
325 match content {
326 Content::Text { text, annotations } => {
327 if text.is_empty() {
328 return Err(McpError::Validation(
329 "Text content cannot be empty".to_string(),
330 ));
331 }
332 if let Some(annotations) = annotations {
333 validate_annotations(annotations)?;
334 }
335 }
336 Content::Image {
337 data,
338 mime_type,
339 annotations,
340 } => {
341 if data.is_empty() {
342 return Err(McpError::Validation(
343 "Image data cannot be empty".to_string(),
344 ));
345 }
346 if mime_type.is_empty() {
347 return Err(McpError::Validation(
348 "Image MIME type cannot be empty".to_string(),
349 ));
350 }
351 if !mime_type.starts_with("image/") {
352 return Err(McpError::Validation(
353 "Image MIME type must start with 'image/'".to_string(),
354 ));
355 }
356 if let Some(annotations) = annotations {
357 validate_annotations(annotations)?;
358 }
359 }
360 Content::Audio {
361 data,
362 mime_type,
363 annotations,
364 } => {
365 if data.is_empty() {
366 return Err(McpError::Validation(
367 "Audio data cannot be empty".to_string(),
368 ));
369 }
370 if mime_type.is_empty() {
371 return Err(McpError::Validation(
372 "Audio MIME type cannot be empty".to_string(),
373 ));
374 }
375 if !mime_type.starts_with("audio/") {
376 return Err(McpError::Validation(
377 "Audio MIME type must start with 'audio/'".to_string(),
378 ));
379 }
380 if let Some(annotations) = annotations {
381 validate_annotations(annotations)?;
382 }
383 }
384 Content::Resource {
385 resource,
386 annotations,
387 } => {
388 if resource.uri.is_empty() {
389 return Err(McpError::Validation(
390 "Resource URI cannot be empty".to_string(),
391 ));
392 }
393 validate_uri(&resource.uri)?;
394 if let Some(annotations) = annotations {
395 validate_annotations(annotations)?;
396 }
397 }
398 }
399
400 Ok(())
401}
402
403pub fn validate_annotations(_annotations: &Annotations) -> McpResult<()> {
405 Ok(())
408}
409
410pub fn validate_tool_annotations(_annotations: &Annotations) -> McpResult<()> {
412 Ok(())
415}
416
417pub fn validate_completion_reference(reference: &CompletionReference) -> McpResult<()> {
419 match reference {
420 CompletionReference::Prompt { name } => {
421 if name.is_empty() {
422 return Err(McpError::Validation(
423 "Completion prompt name cannot be empty".to_string(),
424 ));
425 }
426 }
427 CompletionReference::Resource { uri } => {
428 if uri.is_empty() {
429 return Err(McpError::Validation(
430 "Completion resource URI cannot be empty".to_string(),
431 ));
432 }
433 validate_uri(uri)?;
434 }
435 CompletionReference::Tool { name } => {
436 if name.is_empty() {
437 return Err(McpError::Validation(
438 "Completion tool name cannot be empty".to_string(),
439 ));
440 }
441 }
442 }
443
444 Ok(())
445}
446
447pub fn validate_completion_argument(argument: &CompletionArgument) -> McpResult<()> {
449 if argument.name.is_empty() {
450 return Err(McpError::Validation(
451 "Completion argument name cannot be empty".to_string(),
452 ));
453 }
454
455 Ok(())
457}
458
459pub fn validate_complete_params(params: &CompleteParams) -> McpResult<()> {
461 validate_completion_reference(¶ms.reference)?;
462 validate_completion_argument(¶ms.argument)?;
463
464 Ok(())
465}
466
467pub fn validate_root(root: &Root) -> McpResult<()> {
469 if root.uri.is_empty() {
470 return Err(McpError::Validation("Root URI cannot be empty".to_string()));
471 }
472
473 if !root.uri.starts_with("file://") {
475 return Err(McpError::Validation(
476 "Root URI must start with 'file://'".to_string(),
477 ));
478 }
479
480 Ok(())
481}
482
483pub fn validate_model_preferences(preferences: &ModelPreferences) -> McpResult<()> {
485 if let Some(cost) = preferences.cost_priority {
486 if !(0.0..=1.0).contains(&cost) {
487 return Err(McpError::Validation(
488 "Cost priority must be between 0.0 and 1.0".to_string(),
489 ));
490 }
491 }
492
493 if let Some(speed) = preferences.speed_priority {
494 if !(0.0..=1.0).contains(&speed) {
495 return Err(McpError::Validation(
496 "Speed priority must be between 0.0 and 1.0".to_string(),
497 ));
498 }
499 }
500
501 if let Some(quality) = preferences.quality_priority {
502 if !(0.0..=1.0).contains(&quality) {
503 return Err(McpError::Validation(
504 "Quality priority must be between 0.0 and 1.0".to_string(),
505 ));
506 }
507 }
508
509 Ok(())
510}
511pub fn validate_uri(uri: &str) -> McpResult<()> {
512 if uri.is_empty() {
513 return Err(McpError::Validation("URI cannot be empty".to_string()));
514 }
515
516 if !uri.contains("://") && !uri.starts_with('/') && !uri.starts_with("file:") {
518 return Err(McpError::Validation(
519 "URI must have a scheme or be an absolute path".to_string(),
520 ));
521 }
522
523 Ok(())
524}
525
526pub fn validate_method_name(method: &str) -> McpResult<()> {
528 if method.is_empty() {
529 return Err(McpError::Validation(
530 "Method name cannot be empty".to_string(),
531 ));
532 }
533
534 match method {
536 methods::INITIALIZE
537 | methods::INITIALIZED
538 | methods::PING
539 | methods::TOOLS_LIST
540 | methods::TOOLS_CALL
541 | methods::TOOLS_LIST_CHANGED
542 | methods::RESOURCES_LIST
543 | methods::RESOURCES_TEMPLATES_LIST | methods::RESOURCES_READ
545 | methods::RESOURCES_SUBSCRIBE
546 | methods::RESOURCES_UNSUBSCRIBE
547 | methods::RESOURCES_UPDATED
548 | methods::RESOURCES_LIST_CHANGED
549 | methods::PROMPTS_LIST
550 | methods::PROMPTS_GET
551 | methods::PROMPTS_LIST_CHANGED
552 | methods::SAMPLING_CREATE_MESSAGE
553 | methods::ROOTS_LIST | methods::ROOTS_LIST_CHANGED | methods::COMPLETION_COMPLETE | methods::LOGGING_SET_LEVEL
557 | methods::LOGGING_MESSAGE
558 | methods::PROGRESS
559 | methods::CANCELLED => Ok(()), _ => {
561 if method.contains('/') || method.contains('.') {
563 Ok(())
564 } else {
565 Err(McpError::Validation(format!(
566 "Unknown or invalid method name: {}",
567 method
568 )))
569 }
570 }
571 }
572}
573
574pub fn validate_server_capabilities(_capabilities: &ServerCapabilities) -> McpResult<()> {
576 Ok(())
579}
580
581pub fn validate_client_capabilities(_capabilities: &ClientCapabilities) -> McpResult<()> {
583 Ok(())
586}
587
588pub fn validate_progress_params(params: &ProgressNotificationParams) -> McpResult<()> {
590 if !(0.0..=1.0).contains(¶ms.progress) {
591 return Err(McpError::Validation(
592 "Progress must be between 0.0 and 1.0".to_string(),
593 ));
594 }
595
596 Ok(())
597}
598
599pub fn validate_logging_message_params(params: &LoggingMessageNotificationParams) -> McpResult<()> {
601 if params.data.is_null() {
603 return Err(McpError::Validation(
604 "Log message data cannot be null".to_string(),
605 ));
606 }
607
608 Ok(())
609}
610
611pub fn validate_mcp_request(method: &str, params: Option<&Value>) -> McpResult<()> {
613 validate_method_name(method)?;
614
615 if let Some(params_value) = params {
616 match method {
617 methods::INITIALIZE => {
618 let params: InitializeParams = serde_json::from_value(params_value.clone())
619 .map_err(|e| {
620 McpError::Validation(format!("Invalid initialize params: {}", e))
621 })?;
622 validate_initialize_params(¶ms)?;
623 }
624 methods::TOOLS_CALL => {
625 let params: CallToolParams =
626 serde_json::from_value(params_value.clone()).map_err(|e| {
627 McpError::Validation(format!("Invalid call tool params: {}", e))
628 })?;
629 validate_call_tool_params(¶ms)?;
630 }
631 methods::RESOURCES_READ => {
632 let params: ReadResourceParams = serde_json::from_value(params_value.clone())
633 .map_err(|e| {
634 McpError::Validation(format!("Invalid read resource params: {}", e))
635 })?;
636 validate_read_resource_params(¶ms)?;
637 }
638 methods::PROMPTS_GET => {
639 let params: GetPromptParams = serde_json::from_value(params_value.clone())
640 .map_err(|e| {
641 McpError::Validation(format!("Invalid get prompt params: {}", e))
642 })?;
643 validate_get_prompt_params(¶ms)?;
644 }
645 methods::SAMPLING_CREATE_MESSAGE => {
646 let params: CreateMessageParams = serde_json::from_value(params_value.clone())
647 .map_err(|e| {
648 McpError::Validation(format!("Invalid create message params: {}", e))
649 })?;
650 validate_create_message_params(¶ms)?;
651 }
652 methods::COMPLETION_COMPLETE => {
653 let params: CompleteParams = serde_json::from_value(params_value.clone())
655 .map_err(|e| McpError::Validation(format!("Invalid complete params: {}", e)))?;
656 validate_complete_params(¶ms)?;
657 }
658 methods::PROGRESS => {
659 let params: ProgressNotificationParams =
660 serde_json::from_value(params_value.clone()).map_err(|e| {
661 McpError::Validation(format!("Invalid progress params: {}", e))
662 })?;
663 validate_progress_params(¶ms)?;
664 }
665 methods::LOGGING_MESSAGE => {
666 let params: LoggingMessageNotificationParams =
667 serde_json::from_value(params_value.clone()).map_err(|e| {
668 McpError::Validation(format!("Invalid logging message params: {}", e))
669 })?;
670 validate_logging_message_params(¶ms)?;
671 }
672 _ => {
673 if !params_value.is_object() && !params_value.is_null() {
675 return Err(McpError::Validation(
676 "Parameters must be a JSON object or null".to_string(),
677 ));
678 }
679 }
680 }
681 }
682
683 Ok(())
684}
685
686#[cfg(test)]
687mod tests {
688 use super::*;
689 use serde_json::json;
690
691 #[test]
692 fn test_validate_jsonrpc_request() {
693 let valid_request = JsonRpcRequest {
694 jsonrpc: "2.0".to_string(),
695 id: json!(1),
696 method: "test_method".to_string(),
697 params: None,
698 };
699 assert!(validate_jsonrpc_request(&valid_request).is_ok());
700
701 let invalid_request = JsonRpcRequest {
702 jsonrpc: "1.0".to_string(),
703 id: json!(1),
704 method: "test_method".to_string(),
705 params: None,
706 };
707 assert!(validate_jsonrpc_request(&invalid_request).is_err());
708 }
709
710 #[test]
711 fn test_validate_uri() {
712 assert!(validate_uri("https://example.com").is_ok());
713 assert!(validate_uri("file:///path/to/file").is_ok());
714 assert!(validate_uri("/absolute/path").is_ok());
715 assert!(validate_uri("").is_err());
716 assert!(validate_uri("invalid").is_err());
717 }
718
719 #[test]
720 fn test_validate_tool_info() {
721 let valid_tool = Tool {
722 name: "test_tool".to_string(),
723 description: Some("A test tool".to_string()),
724 input_schema: ToolInputSchema {
725 schema_type: "object".to_string(),
726 properties: Some(
727 json!({
728 "param": {"type": "string"}
729 })
730 .as_object()
731 .unwrap()
732 .iter()
733 .map(|(k, v)| (k.clone(), v.clone()))
734 .collect(),
735 ),
736 required: None,
737 additional_properties: std::collections::HashMap::new(),
738 },
739 annotations: None,
740 };
741 assert!(validate_tool_info(&valid_tool).is_ok());
742
743 let invalid_tool = Tool {
744 name: "".to_string(),
745 description: None,
746 input_schema: ToolInputSchema {
747 schema_type: "string".to_string(), properties: None,
749 required: None,
750 additional_properties: std::collections::HashMap::new(),
751 },
752 annotations: None,
753 };
754 assert!(validate_tool_info(&invalid_tool).is_err());
755 }
756
757 #[test]
758 fn test_validate_create_message_params() {
759 let valid_params = CreateMessageParams {
760 messages: vec![SamplingMessage::user_text("Hello")],
761 model_preferences: None,
762 system_prompt: None,
763 include_context: None,
764 max_tokens: 100,
765 temperature: None,
766 stop_sequences: None,
767 metadata: None,
768 meta: None,
769 };
770 assert!(validate_create_message_params(&valid_params).is_ok());
771
772 let invalid_params = CreateMessageParams {
773 messages: vec![],
774 model_preferences: None,
775 system_prompt: None,
776 include_context: None,
777 max_tokens: 0, temperature: None,
779 stop_sequences: None,
780 metadata: None,
781 meta: None,
782 };
783 assert!(validate_create_message_params(&invalid_params).is_err());
784 }
785
786 #[test]
787 fn test_validate_content() {
788 let valid_text = Content::text("Hello, world!");
789 assert!(validate_content(&valid_text).is_ok());
790
791 let valid_image = Content::image("base64data", "image/png");
792 assert!(validate_content(&valid_image).is_ok());
793
794 let valid_audio = Content::audio("base64data", "audio/wav");
796 assert!(validate_content(&valid_audio).is_ok());
797
798 let invalid_text = Content::Text {
799 text: "".to_string(),
800 annotations: None,
801 };
802 assert!(validate_content(&invalid_text).is_err());
803
804 let invalid_image = Content::Image {
805 data: "data".to_string(),
806 mime_type: "text/plain".to_string(), annotations: None,
808 };
809 assert!(validate_content(&invalid_image).is_err());
810
811 let invalid_audio = Content::Audio {
812 data: "data".to_string(),
813 mime_type: "image/png".to_string(), annotations: None,
815 };
816 assert!(validate_content(&invalid_audio).is_err());
817 }
818
819 #[test]
820 fn test_validate_method_name() {
821 assert!(validate_method_name(methods::INITIALIZE).is_ok());
822 assert!(validate_method_name(methods::TOOLS_LIST).is_ok());
823 assert!(validate_method_name("custom/method").is_ok());
824 assert!(validate_method_name("custom.method").is_ok());
825 assert!(validate_method_name("").is_err());
826 }
827
828 #[test]
829 fn test_validate_mcp_request() {
830 let init_params = json!({
831 "clientInfo": {
832 "name": "test-client",
833 "version": "1.0.0"
834 },
835 "capabilities": {},
836 "protocolVersion": "2025-03-26"
837 });
838
839 assert!(validate_mcp_request(methods::INITIALIZE, Some(&init_params)).is_ok());
840 assert!(validate_mcp_request(methods::PING, None).is_ok());
841 assert!(validate_mcp_request("", None).is_err());
842
843 assert!(validate_mcp_request(methods::ROOTS_LIST, None).is_ok());
845 assert!(validate_mcp_request(methods::COMPLETION_COMPLETE, None).is_ok());
846 assert!(validate_mcp_request(methods::RESOURCES_TEMPLATES_LIST, None).is_ok());
847 }
848}