Skip to main content

mcpkit_core/
state.rs

1//! Typestate pattern for connection lifecycle management.
2//!
3//! This module implements the typestate pattern to enforce correct
4//! connection state transitions at compile time. This prevents runtime
5//! errors from calling methods on connections in invalid states.
6//!
7//! # Connection States
8//!
9//! ```text
10//! Disconnected -> Connected -> Initializing -> Ready -> Closing -> Disconnected
11//! ```
12//!
13//! # Example
14//!
15//! ```rust
16//! use mcpkit_core::state::{Connection, Disconnected, Connected};
17//!
18//! // Connection starts in Disconnected state
19//! let conn: Connection<Disconnected> = Connection::new();
20//!
21//! // Each state has appropriate methods
22//! let id = conn.id();
23//! assert!(!id.is_empty());
24//!
25//! // Connect to transition to Connected state
26//! let connected: Connection<Connected> = conn.connect();
27//! assert!(connected.connected_at().is_some());
28//! ```
29
30use std::marker::PhantomData;
31use std::time::{Duration, Instant};
32
33use crate::capability::{
34    ClientCapabilities, ClientInfo, InitializeRequest, InitializeResult, ServerCapabilities,
35    ServerInfo,
36};
37use crate::error::McpError;
38use crate::protocol::RequestId;
39
40/// Marker type for disconnected state.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct Disconnected;
43
44/// Marker type for connected state (transport established).
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct Connected;
47
48/// Marker type for initializing state (handshake in progress).
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct Initializing;
51
52/// Marker type for ready state (fully operational).
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct Ready;
55
56/// Marker type for closing state (shutdown in progress).
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub struct Closing;
59
60/// Internal connection data shared across states.
61#[doc(hidden)]
62#[derive(Debug)]
63pub struct ConnectionInner {
64    /// Unique connection identifier.
65    pub id: String,
66    /// When the connection was established.
67    pub connected_at: Option<Instant>,
68    /// Last activity timestamp.
69    pub last_activity: Option<Instant>,
70    /// Request counter for generating IDs.
71    pub request_counter: u64,
72    /// Client info (available after initialization).
73    pub client_info: Option<ClientInfo>,
74    /// Server info (available after initialization).
75    pub server_info: Option<ServerInfo>,
76    /// Client capabilities (available after initialization).
77    pub client_capabilities: Option<ClientCapabilities>,
78    /// Server capabilities (available after initialization).
79    pub server_capabilities: Option<ServerCapabilities>,
80}
81
82impl ConnectionInner {
83    /// Create new connection inner data.
84    fn new() -> Self {
85        Self {
86            id: uuid::Uuid::new_v4().to_string(),
87            connected_at: None,
88            last_activity: None,
89            request_counter: 0,
90            client_info: None,
91            server_info: None,
92            client_capabilities: None,
93            server_capabilities: None,
94        }
95    }
96
97    /// Generate the next request ID.
98    fn next_request_id(&mut self) -> RequestId {
99        self.request_counter += 1;
100        RequestId::Number(self.request_counter)
101    }
102
103    /// Update last activity timestamp.
104    fn touch(&mut self) {
105        self.last_activity = Some(Instant::now());
106    }
107}
108
109impl Default for ConnectionInner {
110    fn default() -> Self {
111        Self::new()
112    }
113}
114
115/// A connection in a specific state.
116///
117/// The type parameter `S` represents the current state of the connection.
118/// Different methods are available depending on the state.
119#[derive(Debug)]
120pub struct Connection<S> {
121    inner: ConnectionInner,
122    _state: PhantomData<S>,
123}
124
125impl Connection<Disconnected> {
126    /// Create a new disconnected connection.
127    #[must_use]
128    pub fn new() -> Self {
129        Self {
130            inner: ConnectionInner::new(),
131            _state: PhantomData,
132        }
133    }
134
135    /// Get the connection ID.
136    #[must_use]
137    pub fn id(&self) -> &str {
138        &self.inner.id
139    }
140
141    /// Establish the connection (transition to Connected state).
142    ///
143    /// In a real implementation, this would take a transport and
144    /// establish the connection. Here we just transition the state.
145    #[must_use]
146    pub fn connect(mut self) -> Connection<Connected> {
147        self.inner.connected_at = Some(Instant::now());
148        self.inner.touch();
149        Connection {
150            inner: self.inner,
151            _state: PhantomData,
152        }
153    }
154}
155
156impl Default for Connection<Disconnected> {
157    fn default() -> Self {
158        Self::new()
159    }
160}
161
162impl Connection<Connected> {
163    /// Get the connection ID.
164    #[must_use]
165    pub fn id(&self) -> &str {
166        &self.inner.id
167    }
168
169    /// Get when the connection was established.
170    #[must_use]
171    pub const fn connected_at(&self) -> Option<Instant> {
172        self.inner.connected_at
173    }
174
175    /// Get how long the connection has been active.
176    #[must_use]
177    pub fn uptime(&self) -> Duration {
178        self.inner
179            .connected_at
180            .map(|t| t.elapsed())
181            .unwrap_or_default()
182    }
183
184    /// Begin initialization (transition to Initializing state).
185    ///
186    /// For clients: Send initialize request with client info and capabilities.
187    /// For servers: This is called when receiving an initialize request.
188    #[must_use]
189    pub fn initialize(
190        mut self,
191        client_info: ClientInfo,
192        client_capabilities: ClientCapabilities,
193    ) -> (Connection<Initializing>, InitializeRequest) {
194        self.inner.client_info = Some(client_info.clone());
195        self.inner.client_capabilities = Some(client_capabilities.clone());
196        self.inner.touch();
197
198        let request = InitializeRequest::new(client_info, client_capabilities);
199
200        (
201            Connection {
202                inner: self.inner,
203                _state: PhantomData,
204            },
205            request,
206        )
207    }
208
209    /// Disconnect (transition back to Disconnected state).
210    #[must_use]
211    pub fn disconnect(self) -> Connection<Disconnected> {
212        Connection {
213            inner: ConnectionInner::new(),
214            _state: PhantomData,
215        }
216    }
217}
218
219impl Connection<Initializing> {
220    /// Get the connection ID.
221    #[must_use]
222    pub fn id(&self) -> &str {
223        &self.inner.id
224    }
225
226    /// Get the client info.
227    #[must_use]
228    pub const fn client_info(&self) -> Option<&ClientInfo> {
229        self.inner.client_info.as_ref()
230    }
231
232    /// Get the client capabilities.
233    #[must_use]
234    pub const fn client_capabilities(&self) -> Option<&ClientCapabilities> {
235        self.inner.client_capabilities.as_ref()
236    }
237
238    /// Complete initialization (transition to Ready state).
239    ///
240    /// This is called after the initialize response is received (client)
241    /// or sent (server).
242    #[must_use]
243    pub fn complete(
244        mut self,
245        server_info: ServerInfo,
246        server_capabilities: ServerCapabilities,
247    ) -> Connection<Ready> {
248        self.inner.server_info = Some(server_info);
249        self.inner.server_capabilities = Some(server_capabilities);
250        self.inner.touch();
251
252        Connection {
253            inner: self.inner,
254            _state: PhantomData,
255        }
256    }
257
258    /// Abort initialization (transition back to Disconnected).
259    #[must_use]
260    pub fn abort(self) -> Connection<Disconnected> {
261        Connection {
262            inner: ConnectionInner::new(),
263            _state: PhantomData,
264        }
265    }
266}
267
268impl Connection<Ready> {
269    /// Get the connection ID.
270    #[must_use]
271    pub fn id(&self) -> &str {
272        &self.inner.id
273    }
274
275    /// Get when the connection was established.
276    #[must_use]
277    pub const fn connected_at(&self) -> Option<Instant> {
278        self.inner.connected_at
279    }
280
281    /// Get how long the connection has been active.
282    #[must_use]
283    pub fn uptime(&self) -> Duration {
284        self.inner
285            .connected_at
286            .map(|t| t.elapsed())
287            .unwrap_or_default()
288    }
289
290    /// Get the last activity timestamp.
291    #[must_use]
292    pub const fn last_activity(&self) -> Option<Instant> {
293        self.inner.last_activity
294    }
295
296    /// Get the client info.
297    ///
298    /// # Panics
299    ///
300    /// This should never panic if the connection was properly initialized,
301    /// as the typestate pattern ensures this is only callable in Ready state.
302    /// Use `try_client_info()` for a fallible version.
303    #[must_use]
304    pub fn client_info(&self) -> &ClientInfo {
305        self.inner
306            .client_info
307            .as_ref()
308            .expect("client_info should be set in Ready state")
309    }
310
311    /// Try to get the client info.
312    ///
313    /// Returns `None` if the client info was not set (should not happen in normal use).
314    #[must_use]
315    pub const fn try_client_info(&self) -> Option<&ClientInfo> {
316        self.inner.client_info.as_ref()
317    }
318
319    /// Get the server info.
320    ///
321    /// # Panics
322    ///
323    /// This should never panic if the connection was properly initialized,
324    /// as the typestate pattern ensures this is only callable in Ready state.
325    /// Use `try_server_info()` for a fallible version.
326    #[must_use]
327    pub fn server_info(&self) -> &ServerInfo {
328        self.inner
329            .server_info
330            .as_ref()
331            .expect("server_info should be set in Ready state")
332    }
333
334    /// Try to get the server info.
335    ///
336    /// Returns `None` if the server info was not set (should not happen in normal use).
337    #[must_use]
338    pub const fn try_server_info(&self) -> Option<&ServerInfo> {
339        self.inner.server_info.as_ref()
340    }
341
342    /// Get the client capabilities.
343    ///
344    /// # Panics
345    ///
346    /// This should never panic if the connection was properly initialized,
347    /// as the typestate pattern ensures this is only callable in Ready state.
348    /// Use `try_client_capabilities()` for a fallible version.
349    #[must_use]
350    pub fn client_capabilities(&self) -> &ClientCapabilities {
351        self.inner
352            .client_capabilities
353            .as_ref()
354            .expect("client_capabilities should be set in Ready state")
355    }
356
357    /// Try to get the client capabilities.
358    ///
359    /// Returns `None` if the client capabilities were not set (should not happen in normal use).
360    #[must_use]
361    pub const fn try_client_capabilities(&self) -> Option<&ClientCapabilities> {
362        self.inner.client_capabilities.as_ref()
363    }
364
365    /// Get the server capabilities.
366    ///
367    /// # Panics
368    ///
369    /// This should never panic if the connection was properly initialized,
370    /// as the typestate pattern ensures this is only callable in Ready state.
371    /// Use `try_server_capabilities()` for a fallible version.
372    #[must_use]
373    pub fn server_capabilities(&self) -> &ServerCapabilities {
374        self.inner
375            .server_capabilities
376            .as_ref()
377            .expect("server_capabilities should be set in Ready state")
378    }
379
380    /// Try to get the server capabilities.
381    ///
382    /// Returns `None` if the server capabilities were not set (should not happen in normal use).
383    #[must_use]
384    pub const fn try_server_capabilities(&self) -> Option<&ServerCapabilities> {
385        self.inner.server_capabilities.as_ref()
386    }
387
388    /// Generate the next request ID.
389    pub fn next_request_id(&mut self) -> RequestId {
390        self.inner.next_request_id()
391    }
392
393    /// Update the last activity timestamp.
394    pub fn touch(&mut self) {
395        self.inner.touch();
396    }
397
398    /// Check if the connection has been idle for longer than the given duration.
399    #[must_use]
400    pub fn is_idle(&self, timeout: Duration) -> bool {
401        self.inner
402            .last_activity
403            .is_some_and(|t| t.elapsed() > timeout)
404    }
405
406    /// Begin shutdown (transition to Closing state).
407    #[must_use]
408    pub fn shutdown(self) -> Connection<Closing> {
409        Connection {
410            inner: self.inner,
411            _state: PhantomData,
412        }
413    }
414}
415
416impl Connection<Closing> {
417    /// Get the connection ID.
418    #[must_use]
419    pub fn id(&self) -> &str {
420        &self.inner.id
421    }
422
423    /// Complete the shutdown (transition to Disconnected state).
424    #[must_use]
425    pub fn close(self) -> Connection<Disconnected> {
426        Connection {
427            inner: ConnectionInner::new(),
428            _state: PhantomData,
429        }
430    }
431}
432
433/// Builder for creating initialize results (used by servers).
434pub struct InitializeResultBuilder {
435    server_info: ServerInfo,
436    capabilities: ServerCapabilities,
437    instructions: Option<String>,
438}
439
440impl InitializeResultBuilder {
441    /// Create a new builder with server info.
442    #[must_use]
443    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
444        Self {
445            server_info: ServerInfo::new(name, version),
446            capabilities: ServerCapabilities::new(),
447            instructions: None,
448        }
449    }
450
451    /// Set the capabilities.
452    #[must_use]
453    pub fn capabilities(mut self, caps: ServerCapabilities) -> Self {
454        self.capabilities = caps;
455        self
456    }
457
458    /// Enable tool support.
459    #[must_use]
460    pub fn with_tools(mut self) -> Self {
461        self.capabilities = self.capabilities.with_tools();
462        self
463    }
464
465    /// Enable resource support.
466    #[must_use]
467    pub fn with_resources(mut self) -> Self {
468        self.capabilities = self.capabilities.with_resources();
469        self
470    }
471
472    /// Enable prompt support.
473    #[must_use]
474    pub fn with_prompts(mut self) -> Self {
475        self.capabilities = self.capabilities.with_prompts();
476        self
477    }
478
479    /// Enable task support.
480    #[must_use]
481    pub fn with_tasks(mut self) -> Self {
482        self.capabilities = self.capabilities.with_tasks();
483        self
484    }
485
486    /// Set instructions.
487    #[must_use]
488    pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
489        self.instructions = Some(instructions.into());
490        self
491    }
492
493    /// Build the initialize result.
494    #[must_use]
495    pub fn build(self) -> InitializeResult {
496        let mut result = InitializeResult::new(self.server_info, self.capabilities);
497        if let Some(instructions) = self.instructions {
498            result = result.instructions(instructions);
499        }
500        result
501    }
502}
503
504/// Validate that a connection can transition to the ready state.
505pub const fn validate_initialization(
506    _client_caps: &ClientCapabilities,
507    _server_caps: &ServerCapabilities,
508) -> Result<(), McpError> {
509    // For now, just return Ok. In a real implementation, you might
510    // check for required capability combinations.
511    Ok(())
512}
513
514#[cfg(test)]
515mod tests {
516    use super::*;
517
518    #[test]
519    fn test_connection_lifecycle() {
520        // Start disconnected
521        let conn: Connection<Disconnected> = Connection::new();
522        assert!(!conn.id().is_empty());
523
524        // Connect
525        let conn: Connection<Connected> = conn.connect();
526        assert!(conn.connected_at().is_some());
527
528        // Initialize
529        let client = ClientInfo::new("test", "1.0.0");
530        let caps = ClientCapabilities::new();
531        let (conn, _request): (Connection<Initializing>, _) = conn.initialize(client, caps);
532        assert!(conn.client_info().is_some());
533
534        // Complete
535        let server = ServerInfo::new("server", "1.0.0");
536        let server_caps = ServerCapabilities::new().with_tools();
537        let mut conn: Connection<Ready> = conn.complete(server, server_caps);
538        assert!(conn.server_capabilities().has_tools());
539
540        // Generate request IDs
541        let id1 = conn.next_request_id();
542        let id2 = conn.next_request_id();
543        assert_ne!(id1, id2);
544
545        // Shutdown
546        let conn: Connection<Closing> = conn.shutdown();
547        let _conn: Connection<Disconnected> = conn.close();
548    }
549
550    #[test]
551    fn test_uptime() {
552        let conn = Connection::new().connect();
553        std::thread::sleep(std::time::Duration::from_millis(10));
554        assert!(conn.uptime() >= std::time::Duration::from_millis(10));
555    }
556
557    #[test]
558    fn test_idle_detection() {
559        let client = ClientInfo::new("test", "1.0.0");
560        let server = ServerInfo::new("server", "1.0.0");
561
562        let (conn, _) = Connection::new()
563            .connect()
564            .initialize(client, ClientCapabilities::new());
565
566        let conn = conn.complete(server, ServerCapabilities::new());
567
568        // Should not be idle immediately
569        assert!(!conn.is_idle(Duration::from_secs(1)));
570    }
571
572    #[test]
573    fn test_initialize_result_builder() {
574        let result = InitializeResultBuilder::new("my-server", "1.0.0")
575            .with_tools()
576            .with_resources()
577            .instructions("Use this server to access tools and resources")
578            .build();
579
580        assert_eq!(result.server_info.name, "my-server");
581        assert!(result.capabilities.has_tools());
582        assert!(result.capabilities.has_resources());
583        assert!(result.instructions.is_some());
584    }
585
586    #[test]
587    fn test_abort_initialization() {
588        let client = ClientInfo::new("test", "1.0.0");
589        let (conn, _) = Connection::new()
590            .connect()
591            .initialize(client, ClientCapabilities::new());
592
593        // Abort should return to disconnected
594        let _conn: Connection<Disconnected> = conn.abort();
595    }
596
597    #[test]
598    fn test_disconnect_from_connected() {
599        let conn = Connection::new().connect();
600        let _conn: Connection<Disconnected> = conn.disconnect();
601    }
602
603    #[test]
604    fn test_fallible_accessors() -> Result<(), Box<dyn std::error::Error>> {
605        let client = ClientInfo::new("test-client", "1.0.0");
606        let server = ServerInfo::new("test-server", "2.0.0");
607        let client_caps = ClientCapabilities::new();
608        let server_caps = ServerCapabilities::new().with_tools();
609
610        let (conn, _) = Connection::new().connect().initialize(client, client_caps);
611
612        let conn = conn.complete(server, server_caps);
613
614        // Test fallible accessors return Some
615        assert!(conn.try_client_info().is_some());
616        assert!(conn.try_server_info().is_some());
617        assert!(conn.try_client_capabilities().is_some());
618        assert!(conn.try_server_capabilities().is_some());
619
620        // Test values are correct
621        assert_eq!(
622            conn.try_client_info().ok_or("Expected client info")?.name,
623            "test-client"
624        );
625        assert_eq!(
626            conn.try_server_info().ok_or("Expected server info")?.name,
627            "test-server"
628        );
629        assert!(
630            conn.try_server_capabilities()
631                .ok_or("Expected server capabilities")?
632                .has_tools()
633        );
634        Ok(())
635    }
636}