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.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_sampling_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 sampling content (2025-06-18)
324pub fn validate_sampling_content(content: &SamplingContent) -> McpResult<()> {
325    match content {
326        SamplingContent::Text {
327            text, annotations, ..
328        } => {
329            if text.is_empty() {
330                return Err(McpError::Validation(
331                    "Text content cannot be empty".to_string(),
332                ));
333            }
334            if let Some(annotations) = annotations {
335                validate_annotations(annotations)?;
336            }
337        }
338        SamplingContent::Image {
339            data,
340            mime_type,
341            annotations,
342            ..
343        } => {
344            if data.is_empty() {
345                return Err(McpError::Validation(
346                    "Image data cannot be empty".to_string(),
347                ));
348            }
349            if !mime_type.starts_with("image/") {
350                return Err(McpError::Validation(
351                    "Image MIME type must start with 'image/'".to_string(),
352                ));
353            }
354            if let Some(annotations) = annotations {
355                validate_annotations(annotations)?;
356            }
357        }
358        SamplingContent::Audio {
359            data,
360            mime_type,
361            annotations,
362            ..
363        } => {
364            if data.is_empty() {
365                return Err(McpError::Validation(
366                    "Audio data cannot be empty".to_string(),
367                ));
368            }
369            if !mime_type.starts_with("audio/") {
370                return Err(McpError::Validation(
371                    "Audio MIME type must start with 'audio/'".to_string(),
372                ));
373            }
374            if let Some(annotations) = annotations {
375                validate_annotations(annotations)?;
376            }
377        }
378    }
379    Ok(())
380}
381
382/// Validates content (2025-06-18 with ContentBlock)
383pub fn validate_content(content: &ContentBlock) -> McpResult<()> {
384    match content {
385        ContentBlock::Text {
386            text, annotations, ..
387        } => {
388            if text.is_empty() {
389                return Err(McpError::Validation(
390                    "Text content cannot be empty".to_string(),
391                ));
392            }
393            if let Some(annotations) = annotations {
394                validate_annotations(annotations)?;
395            }
396        }
397        ContentBlock::Image {
398            data,
399            mime_type,
400            annotations,
401            ..
402        } => {
403            if data.is_empty() {
404                return Err(McpError::Validation(
405                    "Image data cannot be empty".to_string(),
406                ));
407            }
408            if mime_type.is_empty() {
409                return Err(McpError::Validation(
410                    "Image MIME type cannot be empty".to_string(),
411                ));
412            }
413            if !mime_type.starts_with("image/") {
414                return Err(McpError::Validation(
415                    "Image MIME type must start with 'image/'".to_string(),
416                ));
417            }
418            if let Some(annotations) = annotations {
419                validate_annotations(annotations)?;
420            }
421        }
422        ContentBlock::Audio {
423            data,
424            mime_type,
425            annotations,
426            ..
427        } => {
428            if data.is_empty() {
429                return Err(McpError::Validation(
430                    "Audio data cannot be empty".to_string(),
431                ));
432            }
433            if mime_type.is_empty() {
434                return Err(McpError::Validation(
435                    "Audio MIME type cannot be empty".to_string(),
436                ));
437            }
438            if !mime_type.starts_with("audio/") {
439                return Err(McpError::Validation(
440                    "Audio MIME type must start with 'audio/'".to_string(),
441                ));
442            }
443            if let Some(annotations) = annotations {
444                validate_annotations(annotations)?;
445            }
446        }
447        ContentBlock::Resource {
448            resource,
449            annotations,
450            ..
451        } => {
452            // For embedded resource, validate the ResourceContents
453            match resource {
454                ResourceContents::Text { uri, text, .. } => {
455                    if uri.is_empty() {
456                        return Err(McpError::Validation(
457                            "Resource URI cannot be empty".to_string(),
458                        ));
459                    }
460                    if text.is_empty() {
461                        return Err(McpError::Validation(
462                            "Text resource content cannot be empty".to_string(),
463                        ));
464                    }
465                    validate_uri(uri)?;
466                }
467                ResourceContents::Blob { uri, blob, .. } => {
468                    if uri.is_empty() {
469                        return Err(McpError::Validation(
470                            "Resource URI cannot be empty".to_string(),
471                        ));
472                    }
473                    if blob.is_empty() {
474                        return Err(McpError::Validation(
475                            "Blob resource content cannot be empty".to_string(),
476                        ));
477                    }
478                    validate_uri(uri)?;
479                }
480            }
481            if let Some(annotations) = annotations {
482                validate_annotations(annotations)?;
483            }
484        }
485        ContentBlock::ResourceLink {
486            uri,
487            name,
488            annotations,
489            ..
490        } => {
491            if uri.is_empty() {
492                return Err(McpError::Validation(
493                    "Resource link URI cannot be empty".to_string(),
494                ));
495            }
496            if name.is_empty() {
497                return Err(McpError::Validation(
498                    "Resource link name cannot be empty".to_string(),
499                ));
500            }
501            validate_uri(uri)?;
502            if let Some(annotations) = annotations {
503                validate_annotations(annotations)?;
504            }
505        }
506    }
507
508    Ok(())
509}
510
511/// Validates annotations (2025-06-18)
512pub fn validate_annotations(annotations: &Annotations) -> McpResult<()> {
513    // Validate priority is in valid range
514    if let Some(priority) = annotations.priority {
515        if !(0.0..=1.0).contains(&priority) {
516            return Err(McpError::Validation(
517                "Annotation priority must be between 0.0 and 1.0".to_string(),
518            ));
519        }
520    }
521
522    // Validate lastModified is a valid ISO 8601 timestamp (basic check)
523    if let Some(last_modified) = &annotations.last_modified {
524        if last_modified.is_empty() {
525            return Err(McpError::Validation(
526                "Annotation lastModified cannot be empty".to_string(),
527            ));
528        }
529        // Could add more sophisticated ISO 8601 validation here
530    }
531
532    // Audience validation - all Role enum values are valid
533    Ok(())
534}
535
536/// Validates tool annotations (2025-06-18 Updated for ToolAnnotations)
537pub fn validate_tool_annotations(
538    _annotations: &crate::protocol::types::ToolAnnotations,
539) -> McpResult<()> {
540    // All tool annotation fields are optional hints, so any values are valid
541    // Future versions might add specific validation rules
542    Ok(())
543}
544
545/// Validates completion reference (2025-03-26 NEW)
546pub fn validate_completion_reference(reference: &CompletionReference) -> McpResult<()> {
547    match reference {
548        CompletionReference::Prompt { name } => {
549            if name.is_empty() {
550                return Err(McpError::Validation(
551                    "Completion prompt name cannot be empty".to_string(),
552                ));
553            }
554        }
555        CompletionReference::Resource { uri } => {
556            if uri.is_empty() {
557                return Err(McpError::Validation(
558                    "Completion resource URI cannot be empty".to_string(),
559                ));
560            }
561            validate_uri(uri)?;
562        }
563        CompletionReference::Tool { name } => {
564            if name.is_empty() {
565                return Err(McpError::Validation(
566                    "Completion tool name cannot be empty".to_string(),
567                ));
568            }
569        }
570    }
571
572    Ok(())
573}
574
575/// Validates completion argument (2025-03-26 NEW)
576pub fn validate_completion_argument(argument: &CompletionArgument) -> McpResult<()> {
577    if argument.name.is_empty() {
578        return Err(McpError::Validation(
579            "Completion argument name cannot be empty".to_string(),
580        ));
581    }
582
583    // Value can be empty (partial input)
584    Ok(())
585}
586
587/// Validates complete parameters (2025-03-26 NEW)
588pub fn validate_complete_params(params: &CompleteParams) -> McpResult<()> {
589    validate_completion_reference(&params.reference)?;
590    validate_completion_argument(&params.argument)?;
591
592    Ok(())
593}
594
595/// Validates root definition (2025-03-26 NEW)
596pub fn validate_root(root: &Root) -> McpResult<()> {
597    if root.uri.is_empty() {
598        return Err(McpError::Validation("Root URI cannot be empty".to_string()));
599    }
600
601    // Root URIs must start with file:// for now
602    if !root.uri.starts_with("file://") {
603        return Err(McpError::Validation(
604            "Root URI must start with 'file://'".to_string(),
605        ));
606    }
607
608    Ok(())
609}
610
611/// Validates model preferences (2025-03-26 enhanced)
612pub fn validate_model_preferences(preferences: &ModelPreferences) -> McpResult<()> {
613    if let Some(cost) = preferences.cost_priority {
614        if !(0.0..=1.0).contains(&cost) {
615            return Err(McpError::Validation(
616                "Cost priority must be between 0.0 and 1.0".to_string(),
617            ));
618        }
619    }
620
621    if let Some(speed) = preferences.speed_priority {
622        if !(0.0..=1.0).contains(&speed) {
623            return Err(McpError::Validation(
624                "Speed priority must be between 0.0 and 1.0".to_string(),
625            ));
626        }
627    }
628
629    if let Some(intelligence) = preferences.intelligence_priority {
630        if !(0.0..=1.0).contains(&intelligence) {
631            return Err(McpError::Validation(
632                "Intelligence priority must be between 0.0 and 1.0".to_string(),
633            ));
634        }
635    }
636
637    Ok(())
638}
639pub fn validate_uri(uri: &str) -> McpResult<()> {
640    if uri.is_empty() {
641        return Err(McpError::Validation("URI cannot be empty".to_string()));
642    }
643
644    // Basic check for scheme
645    if !uri.contains("://") && !uri.starts_with('/') && !uri.starts_with("file:") {
646        return Err(McpError::Validation(
647            "URI must have a scheme or be an absolute path".to_string(),
648        ));
649    }
650
651    Ok(())
652}
653
654/// Validates method name against MCP specification (2025-03-26)
655pub fn validate_method_name(method: &str) -> McpResult<()> {
656    if method.is_empty() {
657        return Err(McpError::Validation(
658            "Method name cannot be empty".to_string(),
659        ));
660    }
661
662    // Check for valid MCP method patterns (2025-03-26)
663    match method {
664        methods::INITIALIZE
665        | methods::INITIALIZED
666        | methods::PING
667        | methods::TOOLS_LIST
668        | methods::TOOLS_CALL
669        | methods::TOOLS_LIST_CHANGED
670        | methods::RESOURCES_LIST
671        | methods::RESOURCES_TEMPLATES_LIST  // New in 2025-03-26
672        | methods::RESOURCES_READ
673        | methods::RESOURCES_SUBSCRIBE
674        | methods::RESOURCES_UNSUBSCRIBE
675        | methods::RESOURCES_UPDATED
676        | methods::RESOURCES_LIST_CHANGED
677        | methods::PROMPTS_LIST
678        | methods::PROMPTS_GET
679        | methods::PROMPTS_LIST_CHANGED
680        | methods::SAMPLING_CREATE_MESSAGE
681        | methods::ROOTS_LIST  // New in 2025-03-26
682        | methods::ROOTS_LIST_CHANGED  // New in 2025-03-26
683        | methods::COMPLETION_COMPLETE  // New in 2025-03-26
684        | methods::LOGGING_SET_LEVEL
685        | methods::LOGGING_MESSAGE
686        | methods::PROGRESS
687        | methods::CANCELLED => Ok(()),  // New in 2025-03-26
688        _ => {
689            // Allow custom methods if they follow naming conventions
690            if method.contains('/') || method.contains('.') {
691                Ok(())
692            } else {
693                Err(McpError::Validation(format!(
694                    "Unknown or invalid method name: {method}"
695                )))
696            }
697        }
698    }
699}
700
701/// Validates server capabilities
702pub fn validate_server_capabilities(_capabilities: &ServerCapabilities) -> McpResult<()> {
703    // All capability structures are currently valid if they exist
704    // Future versions might add validation for specific capability values
705    Ok(())
706}
707
708/// Validates client capabilities
709pub fn validate_client_capabilities(_capabilities: &ClientCapabilities) -> McpResult<()> {
710    // All capability structures are currently valid if they exist
711    // Future versions might add validation for specific capability values
712    Ok(())
713}
714
715/// Validates progress parameters (2025-03-26 enhanced)
716pub fn validate_progress_params(params: &ProgressNotificationParams) -> McpResult<()> {
717    if !(0.0..=1.0).contains(&params.progress) {
718        return Err(McpError::Validation(
719            "Progress must be between 0.0 and 1.0".to_string(),
720        ));
721    }
722
723    Ok(())
724}
725
726/// Validates logging message parameters (2025-03-26)
727pub fn validate_logging_message_params(params: &LoggingMessageNotificationParams) -> McpResult<()> {
728    // Logger name can be empty (optional), but data cannot be null
729    if params.data.is_null() {
730        return Err(McpError::Validation(
731            "Log message data cannot be null".to_string(),
732        ));
733    }
734
735    Ok(())
736}
737
738/// Comprehensive validation for any MCP request (2025-03-26)
739pub fn validate_mcp_request(method: &str, params: Option<&Value>) -> McpResult<()> {
740    validate_method_name(method)?;
741
742    if let Some(params_value) = params {
743        match method {
744            methods::INITIALIZE => {
745                let params: InitializeParams = serde_json::from_value(params_value.clone())
746                    .map_err(|e| McpError::Validation(format!("Invalid initialize params: {e}")))?;
747                validate_initialize_params(&params)?;
748            }
749            methods::TOOLS_CALL => {
750                let params: CallToolParams = serde_json::from_value(params_value.clone())
751                    .map_err(|e| McpError::Validation(format!("Invalid call tool params: {e}")))?;
752                validate_call_tool_params(&params)?;
753            }
754            methods::RESOURCES_READ => {
755                let params: ReadResourceParams = serde_json::from_value(params_value.clone())
756                    .map_err(|e| {
757                        McpError::Validation(format!("Invalid read resource params: {e}"))
758                    })?;
759                validate_read_resource_params(&params)?;
760            }
761            methods::PROMPTS_GET => {
762                let params: GetPromptParams = serde_json::from_value(params_value.clone())
763                    .map_err(|e| McpError::Validation(format!("Invalid get prompt params: {e}")))?;
764                validate_get_prompt_params(&params)?;
765            }
766            methods::SAMPLING_CREATE_MESSAGE => {
767                let params: CreateMessageParams = serde_json::from_value(params_value.clone())
768                    .map_err(|e| {
769                        McpError::Validation(format!("Invalid create message params: {e}"))
770                    })?;
771                validate_create_message_params(&params)?;
772            }
773            methods::COMPLETION_COMPLETE => {
774                // New in 2025-03-26
775                let params: CompleteParams = serde_json::from_value(params_value.clone())
776                    .map_err(|e| McpError::Validation(format!("Invalid complete params: {e}")))?;
777                validate_complete_params(&params)?;
778            }
779            methods::PROGRESS => {
780                let params: ProgressNotificationParams =
781                    serde_json::from_value(params_value.clone()).map_err(|e| {
782                        McpError::Validation(format!("Invalid progress params: {e}"))
783                    })?;
784                validate_progress_params(&params)?;
785            }
786            methods::LOGGING_MESSAGE => {
787                let params: LoggingMessageNotificationParams =
788                    serde_json::from_value(params_value.clone()).map_err(|e| {
789                        McpError::Validation(format!("Invalid logging message params: {e}"))
790                    })?;
791                validate_logging_message_params(&params)?;
792            }
793            _ => {
794                // For other methods, we just validate that params is a valid JSON object if present
795                if !params_value.is_object() && !params_value.is_null() {
796                    return Err(McpError::Validation(
797                        "Parameters must be a JSON object or null".to_string(),
798                    ));
799                }
800            }
801        }
802    }
803
804    Ok(())
805}
806
807#[cfg(test)]
808mod tests {
809    use super::*;
810    use serde_json::json;
811
812    #[test]
813    fn test_validate_jsonrpc_request() {
814        let valid_request = JsonRpcRequest {
815            jsonrpc: "2.0".to_string(),
816            id: json!(1),
817            method: "test_method".to_string(),
818            params: None,
819        };
820        assert!(validate_jsonrpc_request(&valid_request).is_ok());
821
822        let invalid_request = JsonRpcRequest {
823            jsonrpc: "1.0".to_string(),
824            id: json!(1),
825            method: "test_method".to_string(),
826            params: None,
827        };
828        assert!(validate_jsonrpc_request(&invalid_request).is_err());
829    }
830
831    #[test]
832    fn test_validate_uri() {
833        assert!(validate_uri("https://example.com").is_ok());
834        assert!(validate_uri("file:///path/to/file").is_ok());
835        assert!(validate_uri("/absolute/path").is_ok());
836        assert!(validate_uri("").is_err());
837        assert!(validate_uri("invalid").is_err());
838    }
839
840    #[test]
841    fn test_validate_tool_info() {
842        let valid_tool = Tool {
843            name: "test_tool".to_string(),
844            description: Some("A test tool".to_string()),
845            input_schema: ToolInputSchema {
846                schema_type: "object".to_string(),
847                properties: Some(
848                    json!({
849                        "param": {"type": "string"}
850                    })
851                    .as_object()
852                    .unwrap()
853                    .iter()
854                    .map(|(k, v)| (k.clone(), v.clone()))
855                    .collect(),
856                ),
857                required: None,
858                additional_properties: std::collections::HashMap::new(),
859            },
860            annotations: None,
861            title: Some("Test Tool".to_string()),
862            meta: None,
863        };
864        assert!(validate_tool_info(&valid_tool).is_ok());
865
866        let invalid_tool = Tool {
867            name: "".to_string(),
868            description: None,
869            input_schema: ToolInputSchema {
870                schema_type: "string".to_string(), // Invalid type
871                properties: None,
872                required: None,
873                additional_properties: std::collections::HashMap::new(),
874            },
875            annotations: None,
876            title: None,
877            meta: None,
878        };
879        assert!(validate_tool_info(&invalid_tool).is_err());
880    }
881
882    #[test]
883    fn test_validate_create_message_params() {
884        let valid_params = CreateMessageParams {
885            messages: vec![SamplingMessage::user_text("Hello")],
886            model_preferences: None,
887            system_prompt: None,
888            include_context: None,
889            max_tokens: 100,
890            temperature: None,
891            stop_sequences: None,
892            metadata: None,
893            meta: None,
894        };
895        assert!(validate_create_message_params(&valid_params).is_ok());
896
897        let invalid_params = CreateMessageParams {
898            messages: vec![],
899            model_preferences: None,
900            system_prompt: None,
901            include_context: None,
902            max_tokens: 0, // Invalid max_tokens
903            temperature: None,
904            stop_sequences: None,
905            metadata: None,
906            meta: None,
907        };
908        assert!(validate_create_message_params(&invalid_params).is_err());
909    }
910
911    #[test]
912    fn test_validate_content() {
913        let valid_text = Content::text("Hello, world!");
914        assert!(validate_content(&valid_text).is_ok());
915
916        let valid_image = Content::image("base64data", "image/png");
917        assert!(validate_content(&valid_image).is_ok());
918
919        // Test new audio content (2025-03-26)
920        let valid_audio = Content::audio("base64data", "audio/wav");
921        assert!(validate_content(&valid_audio).is_ok());
922
923        let invalid_text = Content::Text {
924            text: "".to_string(),
925            annotations: None,
926            meta: None,
927        };
928        assert!(validate_content(&invalid_text).is_err());
929
930        let invalid_image = Content::Image {
931            data: "data".to_string(),
932            mime_type: "text/plain".to_string(), // Invalid MIME type for image
933            annotations: None,
934            meta: None,
935        };
936        assert!(validate_content(&invalid_image).is_err());
937
938        let invalid_audio = Content::Audio {
939            data: "data".to_string(),
940            mime_type: "image/png".to_string(), // Invalid MIME type for audio
941            annotations: None,
942            meta: None,
943        };
944        assert!(validate_content(&invalid_audio).is_err());
945    }
946
947    #[test]
948    fn test_validate_method_name() {
949        assert!(validate_method_name(methods::INITIALIZE).is_ok());
950        assert!(validate_method_name(methods::TOOLS_LIST).is_ok());
951        assert!(validate_method_name("custom/method").is_ok());
952        assert!(validate_method_name("custom.method").is_ok());
953        assert!(validate_method_name("").is_err());
954    }
955
956    #[test]
957    fn test_validate_mcp_request() {
958        let init_params = json!({
959            "clientInfo": {
960                "name": "test-client",
961                "version": "1.0.0"
962            },
963            "capabilities": {},
964            "protocolVersion": "2025-03-26"
965        });
966
967        assert!(validate_mcp_request(methods::INITIALIZE, Some(&init_params)).is_ok());
968        assert!(validate_mcp_request(methods::PING, None).is_ok());
969        assert!(validate_mcp_request("", None).is_err());
970
971        // Test new 2025-03-26 methods
972        assert!(validate_mcp_request(methods::ROOTS_LIST, None).is_ok());
973        assert!(validate_mcp_request(methods::COMPLETION_COMPLETE, None).is_ok());
974        assert!(validate_mcp_request(methods::RESOURCES_TEMPLATES_LIST, None).is_ok());
975    }
976}