Skip to main content

mcpkit_core/debug/
validator.rs

1//! Protocol validation utilities.
2//!
3//! The protocol validator checks MCP message sequences for correctness,
4//! helping identify protocol violations during development.
5
6use crate::protocol::{Message, Notification, Request, RequestId, Response};
7use std::collections::{HashMap, HashSet};
8
9/// Protocol validation error.
10#[derive(Debug, Clone, thiserror::Error)]
11pub enum ValidationError {
12    /// Response without matching request.
13    #[error("orphan response: no request found for ID {id:?}")]
14    OrphanResponse {
15        /// The orphan response ID.
16        id: RequestId,
17    },
18
19    /// Duplicate request ID.
20    #[error("duplicate request ID: {id:?}")]
21    DuplicateRequestId {
22        /// The duplicate request ID.
23        id: RequestId,
24    },
25
26    /// Request without response (timed out).
27    #[error("unmatched request: {method} (ID: {id:?})")]
28    UnmatchedRequest {
29        /// The unmatched request ID.
30        id: RequestId,
31        /// The method name.
32        method: String,
33    },
34
35    /// Unknown method.
36    #[error("unknown method: {method}")]
37    UnknownMethod {
38        /// The unknown method name.
39        method: String,
40    },
41
42    /// Invalid message sequence.
43    #[error("invalid sequence: {message}")]
44    InvalidSequence {
45        /// Description of the sequence error.
46        message: String,
47    },
48
49    /// Missing required initialization.
50    #[error("missing initialization: {message}")]
51    MissingInitialization {
52        /// Description of what is missing.
53        message: String,
54    },
55}
56
57/// Result of protocol validation.
58#[derive(Debug, Clone)]
59pub struct ValidationResult {
60    /// Whether validation passed.
61    pub valid: bool,
62    /// Validation errors found.
63    pub errors: Vec<ValidationError>,
64    /// Warnings (non-fatal issues).
65    pub warnings: Vec<String>,
66    /// Summary statistics.
67    pub stats: ValidationStats,
68}
69
70impl ValidationResult {
71    /// Create a passing result.
72    #[must_use]
73    pub fn pass() -> Self {
74        Self {
75            valid: true,
76            errors: Vec::new(),
77            warnings: Vec::new(),
78            stats: ValidationStats::default(),
79        }
80    }
81
82    /// Create a failing result.
83    #[must_use]
84    pub fn fail(errors: Vec<ValidationError>) -> Self {
85        Self {
86            valid: false,
87            errors,
88            warnings: Vec::new(),
89            stats: ValidationStats::default(),
90        }
91    }
92
93    /// Add a warning.
94    pub fn add_warning(&mut self, warning: impl Into<String>) {
95        self.warnings.push(warning.into());
96    }
97}
98
99/// Validation statistics.
100#[derive(Debug, Clone, Default)]
101pub struct ValidationStats {
102    /// Total messages validated.
103    pub total_messages: usize,
104    /// Requests validated.
105    pub requests: usize,
106    /// Responses validated.
107    pub responses: usize,
108    /// Notifications validated.
109    pub notifications: usize,
110    /// Matched request-response pairs.
111    pub matched_pairs: usize,
112}
113
114/// Protocol validator for checking MCP message sequences.
115///
116/// The validator tracks the protocol state and checks for:
117/// - Request-response matching
118/// - Duplicate request IDs
119/// - Proper initialization sequence
120/// - Known methods
121#[derive(Debug)]
122pub struct ProtocolValidator {
123    /// Known request methods.
124    known_request_methods: HashSet<String>,
125    /// Known notification methods.
126    known_notification_methods: HashSet<String>,
127    /// Pending requests (waiting for response).
128    pending_requests: HashMap<RequestId, String>,
129    /// Seen request IDs (for duplicate detection).
130    seen_request_ids: HashSet<RequestId>,
131    /// Whether initialization is complete.
132    initialized: bool,
133    /// Collected errors.
134    errors: Vec<ValidationError>,
135    /// Collected warnings.
136    warnings: Vec<String>,
137    /// Stats.
138    stats: ValidationStats,
139    /// Strict mode (unknown methods are errors).
140    strict_mode: bool,
141}
142
143impl Default for ProtocolValidator {
144    fn default() -> Self {
145        Self::new()
146    }
147}
148
149impl ProtocolValidator {
150    /// Create a new validator with default MCP methods.
151    #[must_use]
152    pub fn new() -> Self {
153        let mut validator = Self {
154            known_request_methods: HashSet::new(),
155            known_notification_methods: HashSet::new(),
156            pending_requests: HashMap::new(),
157            seen_request_ids: HashSet::new(),
158            initialized: false,
159            errors: Vec::new(),
160            warnings: Vec::new(),
161            stats: ValidationStats::default(),
162            strict_mode: false,
163        };
164
165        // Add standard MCP methods
166        validator.register_request_methods(&[
167            "initialize",
168            "ping",
169            "tools/list",
170            "tools/call",
171            "resources/list",
172            "resources/read",
173            "resources/subscribe",
174            "resources/unsubscribe",
175            "prompts/list",
176            "prompts/get",
177            "logging/setLevel",
178            "completion/complete",
179            "sampling/createMessage",
180            "roots/list",
181            "elicitation/create",
182            "resources/templates/list",
183            "tasks/get",
184            "tasks/list",
185            "tasks/cancel",
186            "tasks/result",
187        ]);
188
189        validator.register_notification_methods(&[
190            "notifications/initialized",
191            "notifications/cancelled",
192            "notifications/progress",
193            "notifications/message",
194            "notifications/resources/updated",
195            "notifications/resources/list_changed",
196            "notifications/tools/list_changed",
197            "notifications/prompts/list_changed",
198            "notifications/roots/list_changed",
199            "notifications/elicitation/complete",
200            "notifications/tasks/status",
201        ]);
202
203        validator
204    }
205
206    /// Enable strict mode (unknown methods are errors).
207    #[must_use]
208    pub fn strict(mut self) -> Self {
209        self.strict_mode = true;
210        self
211    }
212
213    /// Register additional request methods.
214    pub fn register_request_methods(&mut self, methods: &[&str]) {
215        for method in methods {
216            self.known_request_methods.insert((*method).to_string());
217        }
218    }
219
220    /// Register additional notification methods.
221    pub fn register_notification_methods(&mut self, methods: &[&str]) {
222        for method in methods {
223            self.known_notification_methods
224                .insert((*method).to_string());
225        }
226    }
227
228    /// Validate a single message.
229    pub fn validate(&mut self, message: &Message) {
230        self.stats.total_messages += 1;
231
232        match message {
233            Message::Request(req) => self.validate_request(req),
234            Message::Response(res) => self.validate_response(res),
235            Message::Notification(notif) => self.validate_notification(notif),
236        }
237    }
238
239    fn validate_request(&mut self, request: &Request) {
240        self.stats.requests += 1;
241
242        // Check for duplicate ID
243        if self.seen_request_ids.contains(&request.id) {
244            self.errors.push(ValidationError::DuplicateRequestId {
245                id: request.id.clone(),
246            });
247        }
248        self.seen_request_ids.insert(request.id.clone());
249
250        // Track pending request
251        self.pending_requests
252            .insert(request.id.clone(), request.method.to_string());
253
254        // Check method
255        let method = request.method.as_ref();
256        if !self.known_request_methods.contains(method) {
257            if self.strict_mode {
258                self.errors.push(ValidationError::UnknownMethod {
259                    method: method.to_string(),
260                });
261            } else {
262                self.warnings
263                    .push(format!("Unknown request method: {method}"));
264            }
265        }
266
267        // Check initialization
268        if method == "initialize" {
269            if self.initialized {
270                self.warnings
271                    .push("Duplicate initialize request".to_string());
272            }
273        } else if !self.initialized && method != "ping" {
274            self.warnings
275                .push(format!("Request before initialization: {method}"));
276        }
277    }
278
279    fn validate_response(&mut self, response: &Response) {
280        self.stats.responses += 1;
281
282        // Check for matching request
283        if self.pending_requests.remove(&response.id).is_some() {
284            self.stats.matched_pairs += 1;
285        } else {
286            self.errors.push(ValidationError::OrphanResponse {
287                id: response.id.clone(),
288            });
289        }
290    }
291
292    fn validate_notification(&mut self, notification: &Notification) {
293        self.stats.notifications += 1;
294
295        let method = notification.method.as_ref();
296
297        // Check method
298        if !self.known_notification_methods.contains(method) {
299            if self.strict_mode {
300                self.errors.push(ValidationError::UnknownMethod {
301                    method: method.to_string(),
302                });
303            } else {
304                self.warnings
305                    .push(format!("Unknown notification method: {method}"));
306            }
307        }
308
309        // Track initialization
310        if method == "notifications/initialized" {
311            self.initialized = true;
312        }
313    }
314
315    /// Check for unmatched requests (call after all messages are validated).
316    pub fn check_unmatched_requests(&mut self) {
317        for (id, method) in self.pending_requests.drain() {
318            self.errors
319                .push(ValidationError::UnmatchedRequest { id, method });
320        }
321    }
322
323    /// Get the validation result.
324    #[must_use]
325    pub fn result(&self) -> ValidationResult {
326        ValidationResult {
327            valid: self.errors.is_empty(),
328            errors: self.errors.clone(),
329            warnings: self.warnings.clone(),
330            stats: self.stats.clone(),
331        }
332    }
333
334    /// Finalize validation and get result.
335    #[must_use]
336    pub fn finalize(mut self) -> ValidationResult {
337        self.check_unmatched_requests();
338        self.result()
339    }
340
341    /// Reset the validator state.
342    pub fn reset(&mut self) {
343        self.pending_requests.clear();
344        self.seen_request_ids.clear();
345        self.initialized = false;
346        self.errors.clear();
347        self.warnings.clear();
348        self.stats = ValidationStats::default();
349    }
350}
351
352/// Validate a sequence of messages.
353#[must_use]
354pub fn validate_message_sequence(messages: &[Message]) -> ValidationResult {
355    let mut validator = ProtocolValidator::new();
356
357    for msg in messages {
358        validator.validate(msg);
359    }
360
361    validator.finalize()
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn test_valid_sequence() {
370        let messages = vec![
371            Message::Request(Request::new("initialize", 1)),
372            Message::Response(Response::success(RequestId::from(1), serde_json::json!({}))),
373            Message::Notification(Notification::new("notifications/initialized")),
374            Message::Request(Request::new("tools/list", 2)),
375            Message::Response(Response::success(
376                RequestId::from(2),
377                serde_json::json!({ "tools": [] }),
378            )),
379        ];
380
381        let result = validate_message_sequence(&messages);
382        assert!(result.valid);
383        assert!(result.errors.is_empty());
384        assert_eq!(result.stats.matched_pairs, 2);
385    }
386
387    #[test]
388    fn test_orphan_response() {
389        let messages = vec![Message::Response(Response::success(
390            RequestId::from(999),
391            serde_json::json!({}),
392        ))];
393
394        let result = validate_message_sequence(&messages);
395        assert!(!result.valid);
396        assert!(
397            result
398                .errors
399                .iter()
400                .any(|e| matches!(e, ValidationError::OrphanResponse { .. }))
401        );
402    }
403
404    #[test]
405    fn test_duplicate_request_id() {
406        let messages = vec![
407            Message::Request(Request::new("ping", 1)),
408            Message::Request(Request::new("ping", 1)), // Duplicate!
409        ];
410
411        let result = validate_message_sequence(&messages);
412        assert!(!result.valid);
413        assert!(
414            result
415                .errors
416                .iter()
417                .any(|e| matches!(e, ValidationError::DuplicateRequestId { .. }))
418        );
419    }
420
421    #[test]
422    fn test_unmatched_request() {
423        let messages = vec![
424            Message::Request(Request::new("ping", 1)),
425            // No response!
426        ];
427
428        let result = validate_message_sequence(&messages);
429        assert!(!result.valid);
430        assert!(
431            result
432                .errors
433                .iter()
434                .any(|e| matches!(e, ValidationError::UnmatchedRequest { .. }))
435        );
436    }
437
438    #[test]
439    fn test_strict_mode_accepts_conforming_session() {
440        let mut validator = ProtocolValidator::new().strict();
441
442        for msg in [
443            Message::Request(Request::new("initialize", 1)),
444            Message::Response(Response::success(RequestId::from(1), serde_json::json!({}))),
445            Message::Notification(Notification::new("notifications/initialized")),
446            Message::Request(Request::new("tools/call", 2)),
447            Message::Response(Response::success(RequestId::from(2), serde_json::json!({}))),
448            Message::Request(Request::new("tasks/get", 3)),
449            Message::Response(Response::success(RequestId::from(3), serde_json::json!({}))),
450        ] {
451            validator.validate(&msg);
452        }
453
454        let result = validator.finalize();
455        assert!(
456            result.valid,
457            "conforming session rejected: {:?}",
458            result.errors
459        );
460        // The initialized flag must flip on the spec-named notification;
461        // otherwise every post-init request is flagged as pre-init.
462        assert!(
463            !result
464                .warnings
465                .iter()
466                .any(|w| w.contains("before initialization")),
467            "initialized flag never flipped: {:?}",
468            result.warnings
469        );
470    }
471
472    #[test]
473    fn test_strict_mode() {
474        let mut validator = ProtocolValidator::new().strict();
475
476        validator.validate(&Message::Request(Request::new("unknown/method", 1)));
477
478        let result = validator.result();
479        assert!(!result.valid);
480        assert!(
481            result
482                .errors
483                .iter()
484                .any(|e| matches!(e, ValidationError::UnknownMethod { .. }))
485        );
486    }
487}