Skip to main content

turbomcp_protocol/
validation.rs

1//! # Protocol Validation
2//!
3//! This module provides comprehensive validation for MCP protocol messages,
4//! ensuring data integrity and specification compliance.
5
6use regex::Regex;
7use serde_json::Value;
8use std::collections::{HashMap, HashSet};
9use std::sync::LazyLock;
10
11use crate::jsonrpc::{JsonRpcNotification, JsonRpcRequest, JsonRpcResponse};
12use crate::types::*;
13
14/// Cached regex for URI validation (compiled once)
15static URI_REGEX: LazyLock<Regex> =
16    LazyLock::new(|| Regex::new(r"^[a-zA-Z][a-zA-Z0-9+.-]*:").expect("Invalid URI regex pattern"));
17
18/// Cached regex for method name validation (compiled once)
19///
20/// MCP only requires a JSON-RPC method to be a string. We reject control
21/// characters and whitespace for transport safety, but otherwise allow dots,
22/// hyphens, slashes, and other extension-friendly punctuation.
23static METHOD_NAME_REGEX: LazyLock<Regex> =
24    LazyLock::new(|| Regex::new(r"^[^\s\x00-\x1F]+$").expect("Invalid method name regex pattern"));
25
26/// Protocol message validator
27#[derive(Debug, Clone)]
28pub struct ProtocolValidator {
29    /// Validation rules
30    rules: ValidationRules,
31    /// Strict validation mode
32    strict_mode: bool,
33}
34
35/// Validation rules configuration
36#[derive(Debug, Clone)]
37pub struct ValidationRules {
38    /// Maximum message size in bytes
39    pub max_message_size: usize,
40    /// Maximum batch size
41    pub max_batch_size: usize,
42    /// Maximum string length
43    pub max_string_length: usize,
44    /// Maximum array length
45    pub max_array_length: usize,
46    /// Maximum object depth
47    pub max_object_depth: usize,
48    /// Required fields per message type
49    pub required_fields: HashMap<String, HashSet<String>>,
50}
51
52impl ValidationRules {
53    /// Get the URI validation regex (cached globally)
54    #[inline]
55    pub fn uri_regex(&self) -> &Regex {
56        &URI_REGEX
57    }
58
59    /// Get the method name validation regex (cached globally)
60    #[inline]
61    pub fn method_name_regex(&self) -> &Regex {
62        &METHOD_NAME_REGEX
63    }
64}
65
66/// Validation result
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum ValidationResult {
69    /// Validation passed
70    Valid,
71    /// Validation passed with warnings
72    ValidWithWarnings(Vec<ValidationWarning>),
73    /// Validation failed
74    Invalid(Vec<ValidationError>),
75}
76
77/// Validation warning
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct ValidationWarning {
80    /// Warning code
81    pub code: String,
82    /// Warning message
83    pub message: String,
84    /// Field path (if applicable)
85    pub field_path: Option<String>,
86}
87
88/// Validation error
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct ValidationError {
91    /// Error code
92    pub code: String,
93    /// Error message
94    pub message: String,
95    /// Field path (if applicable)
96    pub field_path: Option<String>,
97}
98
99/// Validation context for tracking state during validation
100#[derive(Debug, Clone)]
101struct ValidationContext {
102    /// Current field path
103    path: Vec<String>,
104    /// Current object depth
105    depth: usize,
106    /// Accumulated warnings
107    warnings: Vec<ValidationWarning>,
108    /// Accumulated errors
109    errors: Vec<ValidationError>,
110}
111
112impl Default for ValidationRules {
113    fn default() -> Self {
114        let mut required_fields = HashMap::new();
115
116        // JSON-RPC required fields
117        required_fields.insert(
118            "request".to_string(),
119            ["jsonrpc", "method", "id"]
120                .iter()
121                .map(|s| s.to_string())
122                .collect(),
123        );
124        required_fields.insert(
125            "response".to_string(),
126            ["jsonrpc", "id"].iter().map(|s| s.to_string()).collect(),
127        );
128        required_fields.insert(
129            "notification".to_string(),
130            ["jsonrpc", "method"]
131                .iter()
132                .map(|s| s.to_string())
133                .collect(),
134        );
135
136        // MCP message required fields
137        required_fields.insert(
138            "initialize".to_string(),
139            ["protocolVersion", "capabilities", "clientInfo"]
140                .iter()
141                .map(|s| s.to_string())
142                .collect(),
143        );
144        required_fields.insert(
145            "tool".to_string(),
146            ["name", "inputSchema"]
147                .iter()
148                .map(|s| s.to_string())
149                .collect(),
150        );
151        required_fields.insert(
152            "prompt".to_string(),
153            ["name"].iter().map(|s| s.to_string()).collect(),
154        );
155        required_fields.insert(
156            "resource".to_string(),
157            ["uri", "name"].iter().map(|s| s.to_string()).collect(),
158        );
159
160        Self {
161            max_message_size: 10 * 1024 * 1024, // 10MB
162            max_batch_size: 100,
163            max_string_length: 1024 * 1024, // 1MB
164            max_array_length: 10000,
165            max_object_depth: 32,
166            required_fields,
167        }
168    }
169}
170
171impl ProtocolValidator {
172    /// Create a new validator with default rules
173    pub fn new() -> Self {
174        Self {
175            rules: ValidationRules::default(),
176            strict_mode: false,
177        }
178    }
179
180    /// Enable strict validation mode
181    pub fn with_strict_mode(mut self) -> Self {
182        self.strict_mode = true;
183        self
184    }
185
186    /// Set custom validation rules
187    pub fn with_rules(mut self, rules: ValidationRules) -> Self {
188        self.rules = rules;
189        self
190    }
191
192    /// Validate a JSON-RPC request
193    pub fn validate_request(&self, request: &JsonRpcRequest) -> ValidationResult {
194        let mut ctx = ValidationContext::new();
195
196        // Validate JSON-RPC structure (includes method name validation)
197        self.validate_jsonrpc_request(request, &mut ctx);
198
199        // Validate parameters based on method
200        if let Some(params) = &request.params {
201            self.validate_method_params(&request.method, params, &mut ctx);
202        }
203
204        ctx.into_result()
205    }
206
207    /// Validate a JSON-RPC response
208    pub fn validate_response(&self, response: &JsonRpcResponse) -> ValidationResult {
209        let mut ctx = ValidationContext::new();
210
211        // Validate JSON-RPC structure
212        self.validate_jsonrpc_response(response, &mut ctx);
213
214        // Ensure either result or error is present (but not both)
215        // Note: This validation is now enforced at the type level with JsonRpcResponsePayload enum
216        // But we still validate for completeness
217        match (response.result().is_some(), response.error().is_some()) {
218            (true, true) => {
219                ctx.add_error(
220                    "RESPONSE_BOTH_RESULT_AND_ERROR",
221                    "Response cannot have both result and error".to_string(),
222                    None,
223                );
224            }
225            (false, false) => {
226                ctx.add_error(
227                    "RESPONSE_MISSING_RESULT_OR_ERROR",
228                    "Response must have either result or error".to_string(),
229                    None,
230                );
231            }
232            _ => {} // Valid
233        }
234
235        ctx.into_result()
236    }
237
238    /// Validate a JSON-RPC notification
239    pub fn validate_notification(&self, notification: &JsonRpcNotification) -> ValidationResult {
240        let mut ctx = ValidationContext::new();
241
242        // Validate JSON-RPC structure
243        self.validate_jsonrpc_notification(notification, &mut ctx);
244
245        // Validate method name
246        self.validate_method_name(&notification.method, &mut ctx);
247
248        // Validate parameters based on method
249        if let Some(params) = &notification.params {
250            self.validate_method_params(&notification.method, params, &mut ctx);
251        }
252
253        ctx.into_result()
254    }
255
256    /// Validate MCP protocol types
257    pub fn validate_tool(&self, tool: &Tool) -> ValidationResult {
258        let mut ctx = ValidationContext::new();
259
260        // Validate tool name
261        if tool.name.is_empty() {
262            ctx.add_error(
263                "TOOL_EMPTY_NAME",
264                "Tool name cannot be empty".to_string(),
265                Some("name".to_string()),
266            );
267        }
268
269        if tool.name.len() > self.rules.max_string_length {
270            ctx.add_error(
271                "TOOL_NAME_TOO_LONG",
272                format!(
273                    "Tool name exceeds maximum length of {}",
274                    self.rules.max_string_length
275                ),
276                Some("name".to_string()),
277            );
278        }
279
280        // Validate input schema
281        self.validate_tool_input(&tool.input_schema, &mut ctx);
282
283        ctx.into_result()
284    }
285
286    /// Validate a prompt
287    pub fn validate_prompt(&self, prompt: &Prompt) -> ValidationResult {
288        let mut ctx = ValidationContext::new();
289
290        // Validate prompt name
291        if prompt.name.is_empty() {
292            ctx.add_error(
293                "PROMPT_EMPTY_NAME",
294                "Prompt name cannot be empty".to_string(),
295                Some("name".to_string()),
296            );
297        }
298
299        // Validate arguments if present
300        if let Some(arguments) = &prompt.arguments
301            && arguments.len() > self.rules.max_array_length
302        {
303            ctx.add_error(
304                "PROMPT_TOO_MANY_ARGS",
305                format!(
306                    "Prompt has too many arguments (max: {})",
307                    self.rules.max_array_length
308                ),
309                Some("arguments".to_string()),
310            );
311        }
312
313        ctx.into_result()
314    }
315
316    /// Validate a resource
317    pub fn validate_resource(&self, resource: &Resource) -> ValidationResult {
318        let mut ctx = ValidationContext::new();
319
320        // Validate URI length (defense-in-depth before regex)
321        if resource.uri.len() > self.rules.max_string_length {
322            ctx.add_error(
323                "RESOURCE_URI_TOO_LONG",
324                format!(
325                    "Resource URI exceeds maximum length of {}",
326                    self.rules.max_string_length
327                ),
328                Some("uri".to_string()),
329            );
330        }
331
332        // Validate URI format
333        if !self.rules.uri_regex().is_match(&resource.uri) {
334            ctx.add_error(
335                "RESOURCE_INVALID_URI",
336                format!("Invalid URI format: {}", resource.uri),
337                Some("uri".to_string()),
338            );
339        }
340
341        // Validate name
342        if resource.name.is_empty() {
343            ctx.add_error(
344                "RESOURCE_EMPTY_NAME",
345                "Resource name cannot be empty".to_string(),
346                Some("name".to_string()),
347            );
348        }
349
350        ctx.into_result()
351    }
352
353    /// Validate initialization request
354    pub fn validate_initialize_request(&self, request: &InitializeRequest) -> ValidationResult {
355        let mut ctx = ValidationContext::new();
356
357        // Validate protocol version
358        if !crate::SUPPORTED_VERSIONS.contains(&request.protocol_version.as_str()) {
359            ctx.add_warning(
360                "UNSUPPORTED_PROTOCOL_VERSION",
361                format!(
362                    "Protocol version {} is not officially supported",
363                    request.protocol_version
364                ),
365                Some("protocolVersion".to_string()),
366            );
367        }
368
369        // Validate client info
370        if request.client_info.name.is_empty() {
371            ctx.add_error(
372                "EMPTY_CLIENT_NAME",
373                "Client name cannot be empty".to_string(),
374                Some("clientInfo.name".to_string()),
375            );
376        }
377
378        if request.client_info.version.is_empty() {
379            ctx.add_error(
380                "EMPTY_CLIENT_VERSION",
381                "Client version cannot be empty".to_string(),
382                Some("clientInfo.version".to_string()),
383            );
384        }
385
386        ctx.into_result()
387    }
388
389    /// Validate model preferences (priority ranges must be 0.0-1.0)
390    ///
391    /// Per the current MCP schema, priority values must be in range [0.0, 1.0].
392    pub fn validate_model_preferences(
393        &self,
394        prefs: &crate::types::ModelPreferences,
395    ) -> ValidationResult {
396        let mut ctx = ValidationContext::new();
397
398        // Validate each priority field
399        let priorities = [
400            ("costPriority", prefs.cost_priority),
401            ("speedPriority", prefs.speed_priority),
402            ("intelligencePriority", prefs.intelligence_priority),
403        ];
404
405        for (name, value) in priorities {
406            if let Some(v) = value
407                && !(0.0..=1.0).contains(&v)
408            {
409                ctx.add_error(
410                    "PRIORITY_OUT_OF_RANGE",
411                    format!(
412                        "{} must be between 0.0 and 1.0 (inclusive), got {}",
413                        name, v
414                    ),
415                    Some(name.to_string()),
416                );
417            }
418        }
419
420        ctx.into_result()
421    }
422
423    /// Validate elicitation result (content required for 'accept' action)
424    ///
425    /// Per the current MCP schema, content is only present when action is `accept`.
426    pub fn validate_elicit_result(&self, result: &crate::types::ElicitResult) -> ValidationResult {
427        let mut ctx = ValidationContext::new();
428
429        use crate::types::ElicitationAction;
430
431        match result.action {
432            ElicitationAction::Accept => {
433                if result.content.is_none() {
434                    ctx.add_error(
435                        "MISSING_CONTENT_ON_ACCEPT",
436                        "ElicitResult must have content when action is 'accept'".to_string(),
437                        Some("content".to_string()),
438                    );
439                }
440            }
441            ElicitationAction::Decline | ElicitationAction::Cancel => {
442                if result.content.is_some() {
443                    ctx.add_warning(
444                        "UNEXPECTED_CONTENT",
445                        format!(
446                            "Content should not be present when action is '{:?}'",
447                            result.action
448                        ),
449                        Some("content".to_string()),
450                    );
451                }
452            }
453        }
454
455        ctx.into_result()
456    }
457
458    /// Validate elicitation schema structure
459    ///
460    /// Per the current MCP spec, schemas must be flat objects with primitive properties only.
461    pub fn validate_elicitation_schema(
462        &self,
463        schema: &crate::types::ElicitationSchema,
464    ) -> ValidationResult {
465        let mut ctx = ValidationContext::new();
466
467        // Schema type must be "object" (schema.json:585)
468        if schema.schema_type != "object" {
469            ctx.add_error(
470                "SCHEMA_NOT_OBJECT",
471                format!(
472                    "Elicitation schema type must be 'object', got '{}'",
473                    schema.schema_type
474                ),
475                Some("type".to_string()),
476            );
477        }
478
479        // Validate additionalProperties = false (flat constraint)
480        if let Some(additional) = schema.additional_properties
481            && additional
482        {
483            ctx.add_warning(
484                "ADDITIONAL_PROPERTIES_NOT_RECOMMENDED",
485                "Elicitation schemas should have additionalProperties=false for flat structure"
486                    .to_string(),
487                Some("additionalProperties".to_string()),
488            );
489        }
490
491        // Validate properties
492        for (key, prop) in &schema.properties {
493            self.validate_primitive_schema(prop, &format!("properties.{}", key), &mut ctx);
494        }
495
496        ctx.into_result()
497    }
498
499    /// Validate primitive schema definition
500    fn validate_primitive_schema(
501        &self,
502        schema: &crate::types::PrimitiveSchemaDefinition,
503        field_path: &str,
504        ctx: &mut ValidationContext,
505    ) {
506        use crate::types::PrimitiveSchemaDefinition;
507
508        match schema {
509            PrimitiveSchemaDefinition::String {
510                enum_values,
511                enum_names,
512                format,
513                ..
514            } => {
515                // Validate enum/enumNames length match (schema.json:679-708)
516                if let (Some(values), Some(names)) = (enum_values, enum_names)
517                    && values.len() != names.len()
518                {
519                    ctx.add_error(
520                        "ENUM_NAMES_LENGTH_MISMATCH",
521                        format!(
522                            "enum and enumNames arrays must have equal length: {} vs {}",
523                            values.len(),
524                            names.len()
525                        ),
526                        Some(format!("{}.enumNames", field_path)),
527                    );
528                }
529
530                // Validate format if present (schema.json:2244-2251)
531                if let Some(fmt) = format {
532                    let valid_formats = ["email", "uri", "date", "date-time"];
533                    if !valid_formats.contains(&fmt.as_str()) {
534                        ctx.add_warning(
535                            "UNKNOWN_STRING_FORMAT",
536                            format!(
537                                "Unknown format '{}', expected one of: {:?}",
538                                fmt, valid_formats
539                            ),
540                            Some(format!("{}.format", field_path)),
541                        );
542                    }
543                }
544            }
545            PrimitiveSchemaDefinition::Number { .. }
546            | PrimitiveSchemaDefinition::Integer { .. } => {
547                // Number/Integer validation could go here
548            }
549            PrimitiveSchemaDefinition::Boolean { .. } => {
550                // Boolean validation could go here
551            }
552        }
553    }
554
555    /// Validate string value against format constraints
556    ///
557    /// Validates email, uri, date, and date-time formats per the current MCP spec.
558    pub fn validate_string_format(value: &str, format: &str) -> std::result::Result<(), String> {
559        match format {
560            "email" => {
561                // RFC 5321 / RFC 5322 syntactic validation. We only catch
562                // glaringly malformed addresses here — DNS / mailbox-existence
563                // checks are out of scope for a wire validator.
564                let trimmed = value.trim();
565                if trimmed.is_empty() {
566                    return Err(format!("Invalid email format: {value}"));
567                }
568                let Some((local, domain)) = trimmed.rsplit_once('@') else {
569                    return Err(format!("Invalid email format: {value}"));
570                };
571                if local.is_empty() || domain.is_empty() {
572                    return Err(format!("Invalid email format: {value}"));
573                }
574                // Domain must have at least one dot, and no label may be
575                // empty or trailing-dot-only (rejects "a@b.", "@.", ".@.").
576                let labels: Vec<&str> = domain.split('.').collect();
577                if labels.len() < 2 || labels.iter().any(|l| l.is_empty()) {
578                    return Err(format!("Invalid email format: {value}"));
579                }
580            }
581            // URI validation per JSON Schema `format: "uri"` — must be an absolute URI
582            // with a scheme. Bare paths like "/etc/passwd" are URI-references, not URIs,
583            // and are rejected here. Use `url::Url::parse` for the actual structural check.
584            "uri" if url::Url::parse(value).is_err() => {
585                return Err(format!("Invalid URI format: {value}"));
586            }
587            "date" => {
588                // Use chrono for full month/day-range validation rather than
589                // just ASCII-digit shape checks (rejects 9999-13-99, 2025-1-7).
590                chrono::NaiveDate::parse_from_str(value, "%Y-%m-%d")
591                    .map_err(|e| format!("Date must be in ISO 8601 format (YYYY-MM-DD): {e}"))?;
592            }
593            "date-time" => {
594                // ISO 8601 datetime format: YYYY-MM-DDTHH:MM:SS[.sss][Z|±HH:MM]
595                if !value.contains('T') {
596                    return Err("DateTime must contain 'T' separator (ISO 8601 format)".to_string());
597                }
598                let parts: Vec<&str> = value.split('T').collect();
599                if parts.len() != 2 {
600                    return Err("DateTime must be in ISO 8601 format".to_string());
601                }
602                // Validate date part
603                Self::validate_string_format(parts[0], "date")?;
604                // Time part should have colons
605                if !parts[1].contains(':') {
606                    return Err("Time component must contain ':'".to_string());
607                }
608            }
609            _ => {
610                // Unknown formats don't fail validation (forward compatibility)
611            }
612        }
613        Ok(())
614    }
615
616    // Private validation methods
617
618    fn validate_jsonrpc_request(&self, request: &JsonRpcRequest, ctx: &mut ValidationContext) {
619        // Validate JSON-RPC version (implicitly "2.0" via JsonRpcVersion type)
620        // This is handled by type system during deserialization
621
622        // Validate method name - check length first, then format
623        if request.method.is_empty() {
624            ctx.add_error(
625                "EMPTY_METHOD_NAME",
626                "Method name cannot be empty".to_string(),
627                Some("method".to_string()),
628            );
629        } else if request.method.len() > self.rules.max_string_length {
630            ctx.add_error(
631                "METHOD_NAME_TOO_LONG",
632                format!(
633                    "Method name exceeds maximum length of {}",
634                    self.rules.max_string_length
635                ),
636                Some("method".to_string()),
637            );
638        } else if request.method.starts_with("rpc.") {
639            ctx.add_error(
640                "RESERVED_METHOD_NAME",
641                format!(
642                    "Method name '{}' uses reserved 'rpc.' prefix",
643                    request.method
644                ),
645                Some("method".to_string()),
646            );
647        } else if !utils::is_valid_method_name(&request.method) {
648            ctx.add_error(
649                "INVALID_METHOD_NAME",
650                format!("Invalid method name format: '{}'", request.method),
651                Some("method".to_string()),
652            );
653        }
654
655        // Request ID is always present for requests (enforced by type system)
656        // Validate ID format if needed
657        self.validate_request_id(&request.id, ctx);
658    }
659
660    fn validate_jsonrpc_response(&self, response: &JsonRpcResponse, ctx: &mut ValidationContext) {
661        // Validate JSON-RPC version (implicitly "2.0" via JsonRpcVersion type)
662        // This is handled by type system during deserialization
663
664        // Validate response has either result or error (enforced by type system)
665        // Our JsonRpcResponsePayload enum ensures mutual exclusion
666
667        // Validate response ID
668        self.validate_response_id(&response.id, ctx);
669
670        // Validate error if present
671        if let Some(error) = response.error() {
672            self.validate_jsonrpc_error(error, ctx);
673        }
674
675        // Validate result structure if present
676        if let Some(result) = response.result() {
677            self.validate_result_value(result, ctx);
678        }
679    }
680
681    fn validate_jsonrpc_notification(
682        &self,
683        notification: &JsonRpcNotification,
684        ctx: &mut ValidationContext,
685    ) {
686        // Validate JSON-RPC version (implicitly "2.0" via JsonRpcVersion type)
687        // This is handled by type system during deserialization
688
689        // Validate method name - check length first, then format
690        if notification.method.is_empty() {
691            ctx.add_error(
692                "EMPTY_METHOD_NAME",
693                "Method name cannot be empty".to_string(),
694                Some("method".to_string()),
695            );
696        } else if notification.method.len() > self.rules.max_string_length {
697            ctx.add_error(
698                "METHOD_NAME_TOO_LONG",
699                format!(
700                    "Method name exceeds maximum length of {}",
701                    self.rules.max_string_length
702                ),
703                Some("method".to_string()),
704            );
705        } else if notification.method.starts_with("rpc.") {
706            ctx.add_error(
707                "RESERVED_METHOD_NAME",
708                format!(
709                    "Method name '{}' uses reserved 'rpc.' prefix",
710                    notification.method
711                ),
712                Some("method".to_string()),
713            );
714        } else if !utils::is_valid_method_name(&notification.method) {
715            ctx.add_error(
716                "INVALID_METHOD_NAME",
717                format!("Invalid method name format: '{}'", notification.method),
718                Some("method".to_string()),
719            );
720        }
721
722        // Notifications do NOT have an ID field (enforced by type system)
723    }
724
725    fn validate_jsonrpc_error(
726        &self,
727        error: &crate::jsonrpc::JsonRpcError,
728        ctx: &mut ValidationContext,
729    ) {
730        // Error codes should be in the valid range
731        if error.code >= 0 {
732            ctx.add_warning(
733                "POSITIVE_ERROR_CODE",
734                "Error codes should be negative according to JSON-RPC spec".to_string(),
735                Some("error.code".to_string()),
736            );
737        }
738
739        if error.message.is_empty() {
740            ctx.add_error(
741                "EMPTY_ERROR_MESSAGE",
742                "Error message cannot be empty".to_string(),
743                Some("error.message".to_string()),
744            );
745        }
746    }
747
748    fn validate_method_name(&self, method: &str, ctx: &mut ValidationContext) {
749        if method.is_empty() {
750            ctx.add_error(
751                "EMPTY_METHOD_NAME",
752                "Method name cannot be empty".to_string(),
753                Some("method".to_string()),
754            );
755            return;
756        }
757
758        // JSON-RPC 2.0 reserves the `rpc.` prefix for protocol extensions and
759        // rpc-internal methods; application methods MUST NOT use it.
760        if method.starts_with("rpc.") {
761            ctx.add_error(
762                "RESERVED_METHOD_NAME",
763                format!("Method name '{method}' uses reserved 'rpc.' prefix"),
764                Some("method".to_string()),
765            );
766            return;
767        }
768
769        if !self.rules.method_name_regex().is_match(method) {
770            ctx.add_error(
771                "INVALID_METHOD_NAME",
772                format!("Invalid method name format: {method}"),
773                Some("method".to_string()),
774            );
775        }
776    }
777
778    fn validate_method_params(&self, method: &str, params: &Value, ctx: &mut ValidationContext) {
779        ctx.push_path("params".to_string());
780        self.validate_parameters(params, ctx);
781
782        // tools/list should be empty object or null.
783        if method == "tools/list"
784            && !params.is_null()
785            && !params.as_object().is_some_and(|obj| obj.is_empty())
786        {
787            ctx.add_warning(
788                "UNEXPECTED_PARAMS",
789                "tools/list should not have parameters".to_string(),
790                None,
791            );
792        }
793
794        ctx.pop_path();
795    }
796
797    fn validate_tool_input(&self, input: &ToolInputSchema, ctx: &mut ValidationContext) {
798        ctx.push_path("inputSchema".to_string());
799
800        // Validate schema type
801        if let Some(schema_type) = input.schema_type.as_ref()
802            && !schema_declares_type(schema_type, "object")
803        {
804            ctx.add_warning(
805                "NON_OBJECT_SCHEMA",
806                format!(
807                    "Tool input schema should typically be 'object', got {}",
808                    describe_schema_type(schema_type)
809                ),
810                Some("type".to_string()),
811            );
812        }
813
814        ctx.pop_path();
815    }
816
817    fn validate_value_structure(
818        &self,
819        value: &Value,
820        _expected_type: &str,
821        ctx: &mut ValidationContext,
822    ) {
823        // Prevent infinite recursion
824        if ctx.depth > self.rules.max_object_depth {
825            ctx.add_error(
826                "MAX_DEPTH_EXCEEDED",
827                format!(
828                    "Maximum object depth ({}) exceeded",
829                    self.rules.max_object_depth
830                ),
831                None,
832            );
833            return;
834        }
835
836        match value {
837            Value::Object(obj) => {
838                ctx.depth += 1;
839                for (key, val) in obj {
840                    ctx.push_path(key.clone());
841                    self.validate_value_structure(val, "unknown", ctx);
842                    ctx.pop_path();
843                }
844                ctx.depth -= 1;
845            }
846            Value::Array(arr) => {
847                if arr.len() > self.rules.max_array_length {
848                    ctx.add_error(
849                        "ARRAY_TOO_LONG",
850                        format!(
851                            "Array exceeds maximum length of {}",
852                            self.rules.max_array_length
853                        ),
854                        None,
855                    );
856                }
857
858                for (index, val) in arr.iter().enumerate() {
859                    ctx.push_path(index.to_string());
860                    self.validate_value_structure(val, "unknown", ctx);
861                    ctx.pop_path();
862                }
863            }
864            Value::String(s) if s.len() > self.rules.max_string_length => {
865                ctx.add_error(
866                    "STRING_TOO_LONG",
867                    format!(
868                        "String exceeds maximum length of {}",
869                        self.rules.max_string_length
870                    ),
871                    None,
872                );
873            }
874            _ => {} // Other types are fine
875        }
876    }
877
878    fn validate_parameters(&self, params: &Value, ctx: &mut ValidationContext) {
879        // Validate parameter structure depth and content
880        self.validate_value_structure(params, "params", ctx);
881
882        // Additional parameter-specific validation
883        match params {
884            // Validate array parameters length
885            Value::Array(arr) if arr.len() > self.rules.max_array_length => {
886                ctx.add_error(
887                    "PARAMS_ARRAY_TOO_LONG",
888                    format!(
889                        "Parameter array exceeds maximum length of {}",
890                        self.rules.max_array_length
891                    ),
892                    Some("params".to_string()),
893                );
894            }
895            _ => {
896                // Other parameter types are acceptable
897            }
898        }
899    }
900
901    fn validate_request_id(&self, _id: &crate::types::RequestId, _ctx: &mut ValidationContext) {
902        // Request ID validation
903        // ID is always present for requests (enforced by type system)
904        // Additional ID format validation could be added here if needed
905    }
906
907    fn validate_response_id(&self, id: &crate::jsonrpc::ResponseId, _ctx: &mut ValidationContext) {
908        // Validate response ID semantics
909        if id.is_null() {
910            // Null ID is only valid for parse errors
911            // This should be checked at a higher level when the error type is known
912        }
913        // Additional response ID validation could be added here
914    }
915
916    fn validate_result_value(&self, result: &Value, ctx: &mut ValidationContext) {
917        // Validate result structure depth and content
918        self.validate_value_structure(result, "result", ctx);
919
920        // Additional result validation based on method type could be added here
921        // For now, we just validate general structure
922    }
923}
924
925impl Default for ProtocolValidator {
926    fn default() -> Self {
927        Self::new()
928    }
929}
930
931fn schema_declares_type(schema_type: &Value, expected: &str) -> bool {
932    match schema_type {
933        Value::String(value) => value == expected,
934        Value::Array(values) => values.iter().any(|value| value.as_str() == Some(expected)),
935        _ => false,
936    }
937}
938
939fn describe_schema_type(schema_type: &Value) -> String {
940    match schema_type {
941        Value::String(value) => format!("'{value}'"),
942        other => other.to_string(),
943    }
944}
945
946impl ValidationContext {
947    fn new() -> Self {
948        Self {
949            path: Vec::new(),
950            depth: 0,
951            warnings: Vec::new(),
952            errors: Vec::new(),
953        }
954    }
955
956    fn push_path(&mut self, segment: String) {
957        self.path.push(segment);
958    }
959
960    fn pop_path(&mut self) {
961        self.path.pop();
962    }
963
964    fn current_path(&self) -> Option<String> {
965        if self.path.is_empty() {
966            None
967        } else {
968            Some(self.path.join("."))
969        }
970    }
971
972    fn add_error(&mut self, code: &str, message: String, field_path: Option<String>) {
973        let path = field_path.or_else(|| self.current_path());
974        self.errors.push(ValidationError {
975            code: code.to_string(),
976            message,
977            field_path: path,
978        });
979    }
980
981    fn add_warning(&mut self, code: &str, message: String, field_path: Option<String>) {
982        let path = field_path.or_else(|| self.current_path());
983        self.warnings.push(ValidationWarning {
984            code: code.to_string(),
985            message,
986            field_path: path,
987        });
988    }
989
990    fn into_result(self) -> ValidationResult {
991        if !self.errors.is_empty() {
992            ValidationResult::Invalid(self.errors)
993        } else if !self.warnings.is_empty() {
994            ValidationResult::ValidWithWarnings(self.warnings)
995        } else {
996            ValidationResult::Valid
997        }
998    }
999}
1000
1001impl ValidationResult {
1002    /// Check if validation passed (with or without warnings)
1003    pub fn is_valid(&self) -> bool {
1004        !matches!(self, ValidationResult::Invalid(_))
1005    }
1006
1007    /// Check if validation failed
1008    pub fn is_invalid(&self) -> bool {
1009        matches!(self, ValidationResult::Invalid(_))
1010    }
1011
1012    /// Check if validation has warnings
1013    pub fn has_warnings(&self) -> bool {
1014        matches!(self, ValidationResult::ValidWithWarnings(_))
1015    }
1016
1017    /// Get warnings (if any)
1018    pub fn warnings(&self) -> &[ValidationWarning] {
1019        match self {
1020            ValidationResult::ValidWithWarnings(warnings) => warnings,
1021            _ => &[],
1022        }
1023    }
1024
1025    /// Get errors (if any)
1026    pub fn errors(&self) -> &[ValidationError] {
1027        match self {
1028            ValidationResult::Invalid(errors) => errors,
1029            _ => &[],
1030        }
1031    }
1032}
1033
1034/// Utility functions for validation
1035pub mod utils {
1036    use super::*;
1037
1038    /// Create a validation error
1039    pub fn error(code: &str, message: &str) -> ValidationError {
1040        ValidationError {
1041            code: code.to_string(),
1042            message: message.to_string(),
1043            field_path: None,
1044        }
1045    }
1046
1047    /// Create a validation warning
1048    pub fn warning(code: &str, message: &str) -> ValidationWarning {
1049        ValidationWarning {
1050            code: code.to_string(),
1051            message: message.to_string(),
1052            field_path: None,
1053        }
1054    }
1055
1056    /// Check if a string is a valid URI
1057    pub fn is_valid_uri(uri: &str) -> bool {
1058        ValidationRules::default().uri_regex().is_match(uri)
1059    }
1060
1061    /// Check if a string is a valid method name
1062    pub fn is_valid_method_name(method: &str) -> bool {
1063        !method.is_empty()
1064            && !method.starts_with("rpc.")
1065            && ValidationRules::default()
1066                .method_name_regex()
1067                .is_match(method)
1068    }
1069}
1070
1071// Comprehensive tests in separate file (tokio/axum pattern)
1072// This gives us:
1073// - Better organization (tests don't clutter the implementation)
1074// - Access to private items (tests are still part of the module)
1075// - Easy to find (tests.rs is in the same directory as validation.rs)
1076#[cfg(test)]
1077mod tests;