mcp_protocol_sdk/protocol/
validation.rs

1//! MCP protocol validation utilities (2025-03-26)
2//!
3//! This module provides validation functions for MCP protocol messages and types,
4//! ensuring that requests and responses conform to the 2025-03-26 protocol specification,
5//! including support for audio content, annotations, and enhanced capabilities.
6
7use crate::core::error::{McpError, McpResult};
8use crate::protocol::{messages::*, methods, types::*};
9use serde_json::Value;
10
11/// Validates that a JSON-RPC message conforms to the specification
12pub 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    // Check required jsonrpc field
18    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    // Check that it has either 'method' (request/notification) or 'result'/'error' (response)
28    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        // Request or notification
35        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        // Requests must have an id, notifications must not
42        // We allow both for flexibility in parsing
43    } else if has_result || has_error {
44        // Response
45        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
65/// Validates a JSON-RPC request
66pub 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    // Method names starting with "rpc." are reserved for JSON-RPC internal methods
78    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
87/// Validates a JSON-RPC response
88pub 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    // JsonRpcResponse only has result field, not error
94    // Error responses use JsonRpcError type instead
95    Ok(())
96}
97
98/// Validates a JSON-RPC notification
99pub 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
113/// Validates initialization parameters
114pub 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
136/// Validates tool information (2025-03-26 with annotations)
137pub 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    // Validate that input_schema is a valid JSON Schema object
145    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    // Validate annotations if present
152    if let Some(annotations) = &tool.annotations {
153        validate_tool_annotations(annotations)?;
154    }
155
156    Ok(())
157}
158
159/// Validates tool call parameters
160pub 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
170/// Validates resource information (2025-03-26 with annotations)
171pub 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    // Basic URI validation - check if it looks like a valid URI
185    validate_uri(&resource.uri)?;
186
187    // Validate annotations if present
188    if let Some(annotations) = &resource.annotations {
189        validate_annotations(annotations)?;
190    }
191
192    Ok(())
193}
194
195/// Validates resource read parameters
196pub 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(&params.uri)?;
204
205    Ok(())
206}
207
208/// Validates resource content (2025-03-26)
209pub 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
240/// Validates prompt information (2025-03-26)
241pub 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
261/// Validates prompt get parameters
262pub 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
272/// Validates prompt messages
273pub 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        // Role is an enum, so it can't be empty - validate content instead
282        validate_content(&message.content)?;
283    }
284
285    Ok(())
286}
287
288/// Validates sampling messages
289pub 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        // Role is an enum, so it can't be empty - validate content instead
298        validate_content(&message.content)?;
299    }
300
301    Ok(())
302}
303
304/// Validates create message parameters (2025-03-26)
305pub fn validate_create_message_params(params: &CreateMessageParams) -> McpResult<()> {
306    validate_sampling_messages(&params.messages)?;
307
308    // max_tokens validation
309    if params.max_tokens == 0 {
310        return Err(McpError::Validation(
311            "max_tokens must be greater than 0".to_string(),
312        ));
313    }
314
315    // Validate model preferences if present
316    if let Some(prefs) = &params.model_preferences {
317        validate_model_preferences(prefs)?;
318    }
319
320    Ok(())
321}
322
323/// Validates content (2025-03-26 with audio support)
324pub 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
403/// Validates annotations (2025-03-26 NEW)
404pub fn validate_annotations(_annotations: &Annotations) -> McpResult<()> {
405    // All annotation fields are optional and have valid enum values
406    // No additional validation needed for current fields
407    Ok(())
408}
409
410/// Validates tool annotations (2025-03-26 NEW)
411pub fn validate_tool_annotations(_annotations: &Annotations) -> McpResult<()> {
412    // All tool annotation fields are optional hints, so any values are valid
413    // Future versions might add specific validation rules
414    Ok(())
415}
416
417/// Validates completion reference (2025-03-26 NEW)
418pub 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
447/// Validates completion argument (2025-03-26 NEW)
448pub 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    // Value can be empty (partial input)
456    Ok(())
457}
458
459/// Validates complete parameters (2025-03-26 NEW)
460pub fn validate_complete_params(params: &CompleteParams) -> McpResult<()> {
461    validate_completion_reference(&params.reference)?;
462    validate_completion_argument(&params.argument)?;
463
464    Ok(())
465}
466
467/// Validates root definition (2025-03-26 NEW)
468pub 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    // Root URIs must start with file:// for now
474    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
483/// Validates model preferences (2025-03-26 enhanced)
484pub 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    // Basic check for scheme
517    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
526/// Validates method name against MCP specification (2025-03-26)
527pub 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    // Check for valid MCP method patterns (2025-03-26)
535    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  // New in 2025-03-26
544        | 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  // New in 2025-03-26
554        | methods::ROOTS_LIST_CHANGED  // New in 2025-03-26
555        | methods::COMPLETION_COMPLETE  // New in 2025-03-26
556        | methods::LOGGING_SET_LEVEL
557        | methods::LOGGING_MESSAGE
558        | methods::PROGRESS
559        | methods::CANCELLED => Ok(()),  // New in 2025-03-26
560        _ => {
561            // Allow custom methods if they follow naming conventions
562            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
574/// Validates server capabilities
575pub fn validate_server_capabilities(_capabilities: &ServerCapabilities) -> McpResult<()> {
576    // All capability structures are currently valid if they exist
577    // Future versions might add validation for specific capability values
578    Ok(())
579}
580
581/// Validates client capabilities
582pub fn validate_client_capabilities(_capabilities: &ClientCapabilities) -> McpResult<()> {
583    // All capability structures are currently valid if they exist
584    // Future versions might add validation for specific capability values
585    Ok(())
586}
587
588/// Validates progress parameters (2025-03-26 enhanced)
589pub fn validate_progress_params(params: &ProgressNotificationParams) -> McpResult<()> {
590    if !(0.0..=1.0).contains(&params.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
599/// Validates logging message parameters (2025-03-26)
600pub fn validate_logging_message_params(params: &LoggingMessageNotificationParams) -> McpResult<()> {
601    // Logger name can be empty (optional), but data cannot be null
602    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
611/// Comprehensive validation for any MCP request (2025-03-26)
612pub 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(&params)?;
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(&params)?;
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(&params)?;
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(&params)?;
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(&params)?;
651            }
652            methods::COMPLETION_COMPLETE => {
653                // New in 2025-03-26
654                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(&params)?;
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(&params)?;
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(&params)?;
671            }
672            _ => {
673                // For other methods, we just validate that params is a valid JSON object if present
674                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(), // Invalid type
748                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, // Invalid max_tokens
778            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        // Test new audio content (2025-03-26)
795        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(), // Invalid MIME type for image
807            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(), // Invalid MIME type for audio
814            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        // Test new 2025-03-26 methods
844        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}