mcpkit_core/debug/
recorder.rs

1//! Session recording and replay utilities.
2//!
3//! The session recorder captures entire MCP sessions for later
4//! analysis, testing, or replay.
5
6use crate::protocol::Message;
7use serde::{Deserialize, Serialize};
8use std::sync::{Arc, RwLock};
9use std::time::{Duration, Instant, SystemTime};
10
11/// A recorded MCP session.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct RecordedSession {
14    /// Session name/identifier.
15    pub name: String,
16    /// When the recording started.
17    pub started_at: SystemTime,
18    /// Total duration of the session.
19    #[serde(with = "duration_serde")]
20    pub duration: Duration,
21    /// Recorded events.
22    pub events: Vec<SessionEvent>,
23    /// Session metadata.
24    pub metadata: SessionMetadata,
25}
26
27impl RecordedSession {
28    /// Create a new empty session.
29    #[must_use]
30    pub fn new(name: impl Into<String>) -> Self {
31        Self {
32            name: name.into(),
33            started_at: SystemTime::now(),
34            duration: Duration::ZERO,
35            events: Vec::new(),
36            metadata: SessionMetadata::default(),
37        }
38    }
39
40    /// Get the number of events.
41    #[must_use]
42    pub fn len(&self) -> usize {
43        self.events.len()
44    }
45
46    /// Check if empty.
47    #[must_use]
48    pub fn is_empty(&self) -> bool {
49        self.events.is_empty()
50    }
51
52    /// Get only messages (excluding lifecycle events).
53    #[must_use]
54    pub fn messages(&self) -> Vec<&Message> {
55        self.events
56            .iter()
57            .filter_map(|e| match e {
58                SessionEvent::MessageSent { message, .. }
59                | SessionEvent::MessageReceived { message, .. } => Some(message),
60                _ => None,
61            })
62            .collect()
63    }
64
65    /// Get events within a time range.
66    #[must_use]
67    pub fn events_in_range(&self, start: Duration, end: Duration) -> Vec<&SessionEvent> {
68        self.events
69            .iter()
70            .filter(|e| {
71                let offset = e.offset();
72                offset >= start && offset <= end
73            })
74            .collect()
75    }
76
77    /// Export to JSON.
78    ///
79    /// # Errors
80    ///
81    /// Returns an error if serialization fails.
82    pub fn to_json(&self) -> Result<String, serde_json::Error> {
83        serde_json::to_string_pretty(self)
84    }
85
86    /// Import from JSON.
87    ///
88    /// # Errors
89    ///
90    /// Returns an error if deserialization fails.
91    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
92        serde_json::from_str(json)
93    }
94}
95
96/// Session metadata.
97#[derive(Debug, Clone, Default, Serialize, Deserialize)]
98pub struct SessionMetadata {
99    /// Client name.
100    pub client_name: Option<String>,
101    /// Client version.
102    pub client_version: Option<String>,
103    /// Server name.
104    pub server_name: Option<String>,
105    /// Server version.
106    pub server_version: Option<String>,
107    /// Transport type.
108    pub transport: Option<String>,
109    /// Protocol version.
110    pub protocol_version: Option<String>,
111    /// Custom tags.
112    pub tags: Vec<String>,
113}
114
115/// A session event.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117#[serde(tag = "type")]
118pub enum SessionEvent {
119    /// Session started.
120    SessionStarted {
121        /// Offset from session start.
122        #[serde(with = "duration_serde")]
123        offset: Duration,
124    },
125    /// Message sent by client.
126    MessageSent {
127        /// Offset from session start.
128        #[serde(with = "duration_serde")]
129        offset: Duration,
130        /// The message.
131        message: Message,
132    },
133    /// Message received by client.
134    MessageReceived {
135        /// Offset from session start.
136        #[serde(with = "duration_serde")]
137        offset: Duration,
138        /// The message.
139        message: Message,
140    },
141    /// Error occurred.
142    Error {
143        /// Offset from session start.
144        #[serde(with = "duration_serde")]
145        offset: Duration,
146        /// Error message.
147        error: String,
148    },
149    /// Session ended.
150    SessionEnded {
151        /// Offset from session start.
152        #[serde(with = "duration_serde")]
153        offset: Duration,
154        /// Reason for ending.
155        reason: Option<String>,
156    },
157    /// Custom event.
158    Custom {
159        /// Offset from session start.
160        #[serde(with = "duration_serde")]
161        offset: Duration,
162        /// Event name.
163        name: String,
164        /// Event data.
165        data: serde_json::Value,
166    },
167}
168
169impl SessionEvent {
170    /// Get the offset from session start.
171    #[must_use]
172    pub fn offset(&self) -> Duration {
173        match self {
174            Self::SessionStarted { offset }
175            | Self::MessageSent { offset, .. }
176            | Self::MessageReceived { offset, .. }
177            | Self::Error { offset, .. }
178            | Self::SessionEnded { offset, .. }
179            | Self::Custom { offset, .. } => *offset,
180        }
181    }
182}
183
184/// Session recorder for capturing MCP sessions.
185pub struct SessionRecorder {
186    /// Session name.
187    name: String,
188    /// When recording started.
189    started: Instant,
190    /// Recording state.
191    state: Arc<RwLock<RecorderState>>,
192}
193
194struct RecorderState {
195    /// Recorded events.
196    events: Vec<SessionEvent>,
197    /// Whether recording is active.
198    recording: bool,
199    /// Session metadata.
200    metadata: SessionMetadata,
201}
202
203impl SessionRecorder {
204    /// Create a new session recorder.
205    #[must_use]
206    pub fn new(name: impl Into<String>) -> Self {
207        Self {
208            name: name.into(),
209            started: Instant::now(),
210            state: Arc::new(RwLock::new(RecorderState {
211                events: vec![SessionEvent::SessionStarted {
212                    offset: Duration::ZERO,
213                }],
214                recording: true,
215                metadata: SessionMetadata::default(),
216            })),
217        }
218    }
219
220    /// Set session metadata.
221    pub fn set_metadata(&self, metadata: SessionMetadata) {
222        if let Ok(mut state) = self.state.write() {
223            state.metadata = metadata;
224        }
225    }
226
227    /// Check if recording is active.
228    #[must_use]
229    pub fn is_recording(&self) -> bool {
230        self.state.read().map(|s| s.recording).unwrap_or(false)
231    }
232
233    /// Stop recording.
234    pub fn stop(&self, reason: Option<String>) {
235        if let Ok(mut state) = self.state.write() {
236            if state.recording {
237                state.events.push(SessionEvent::SessionEnded {
238                    offset: self.started.elapsed(),
239                    reason,
240                });
241                state.recording = false;
242            }
243        }
244    }
245
246    /// Record a sent message.
247    pub fn record_sent(&self, message: Message) {
248        if let Ok(mut state) = self.state.write() {
249            if state.recording {
250                state.events.push(SessionEvent::MessageSent {
251                    offset: self.started.elapsed(),
252                    message,
253                });
254            }
255        }
256    }
257
258    /// Record a received message.
259    pub fn record_received(&self, message: Message) {
260        if let Ok(mut state) = self.state.write() {
261            if state.recording {
262                state.events.push(SessionEvent::MessageReceived {
263                    offset: self.started.elapsed(),
264                    message,
265                });
266            }
267        }
268    }
269
270    /// Record an error.
271    pub fn record_error(&self, error: impl Into<String>) {
272        if let Ok(mut state) = self.state.write() {
273            if state.recording {
274                state.events.push(SessionEvent::Error {
275                    offset: self.started.elapsed(),
276                    error: error.into(),
277                });
278            }
279        }
280    }
281
282    /// Record a custom event.
283    pub fn record_custom(&self, name: impl Into<String>, data: serde_json::Value) {
284        if let Ok(mut state) = self.state.write() {
285            if state.recording {
286                state.events.push(SessionEvent::Custom {
287                    offset: self.started.elapsed(),
288                    name: name.into(),
289                    data,
290                });
291            }
292        }
293    }
294
295    /// Get the number of recorded events.
296    #[must_use]
297    pub fn event_count(&self) -> usize {
298        self.state.read().map(|s| s.events.len()).unwrap_or(0)
299    }
300
301    /// Finalize and return the recorded session.
302    #[must_use]
303    pub fn finalize(self) -> RecordedSession {
304        self.stop(Some("finalized".to_string()));
305
306        let (events, metadata) = self
307            .state
308            .read()
309            .map(|s| (s.events.clone(), s.metadata.clone()))
310            .unwrap_or_default();
311
312        RecordedSession {
313            name: self.name,
314            started_at: SystemTime::now() - self.started.elapsed(),
315            duration: self.started.elapsed(),
316            events,
317            metadata,
318        }
319    }
320}
321
322impl Clone for SessionRecorder {
323    fn clone(&self) -> Self {
324        Self {
325            name: self.name.clone(),
326            started: self.started,
327            state: Arc::clone(&self.state),
328        }
329    }
330}
331
332/// Serde support for Duration.
333mod duration_serde {
334    use serde::{Deserialize, Deserializer, Serialize, Serializer};
335    use std::time::Duration;
336
337    pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
338    where
339        S: Serializer,
340    {
341        duration.as_millis().serialize(serializer)
342    }
343
344    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
345    where
346        D: Deserializer<'de>,
347    {
348        let ms = u64::deserialize(deserializer)?;
349        Ok(Duration::from_millis(ms))
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356    use crate::protocol::{Request, RequestId, Response};
357
358    #[test]
359    fn test_session_recorder() {
360        let recorder = SessionRecorder::new("test-session");
361
362        recorder.record_sent(Message::Request(Request::new("test/method", 1)));
363        recorder.record_received(Message::Response(Response::success(
364            RequestId::from(1),
365            serde_json::json!({}),
366        )));
367
368        assert_eq!(recorder.event_count(), 3); // start + 2 messages
369        assert!(recorder.is_recording());
370
371        let session = recorder.finalize();
372        assert_eq!(session.name, "test-session");
373        assert_eq!(session.events.len(), 4); // start + 2 messages + end
374    }
375
376    #[test]
377    fn test_session_serialization() {
378        let recorder = SessionRecorder::new("test");
379        recorder.record_sent(Message::Request(Request::new("ping", 1)));
380        let session = recorder.finalize();
381
382        let json = session.to_json().expect("Failed to serialize");
383        let restored = RecordedSession::from_json(&json).expect("Failed to deserialize");
384
385        assert_eq!(restored.name, "test");
386        assert_eq!(restored.events.len(), session.events.len());
387    }
388
389    #[test]
390    fn test_session_metadata() {
391        let recorder = SessionRecorder::new("test");
392
393        let metadata = SessionMetadata {
394            client_name: Some("test-client".to_string()),
395            client_version: Some("1.0.0".to_string()),
396            ..Default::default()
397        };
398
399        recorder.set_metadata(metadata);
400
401        let session = recorder.finalize();
402        assert_eq!(
403            session.metadata.client_name,
404            Some("test-client".to_string())
405        );
406    }
407}