Skip to main content

fastmcp_server/
session.rs

1//! MCP session management.
2
3use std::collections::HashSet;
4
5use fastmcp_core::SessionState;
6use fastmcp_core::logging::{debug, targets, warn};
7use fastmcp_protocol::{
8    ClientCapabilities, ClientInfo, JsonRpcRequest, LogLevel, ResourceUpdatedNotificationParams,
9    ServerCapabilities, ServerInfo,
10};
11
12use crate::NotificationSender;
13
14/// An MCP session between client and server.
15///
16/// Tracks the state of an initialized MCP connection.
17#[derive(Debug)]
18pub struct Session {
19    /// Whether the session has been initialized.
20    initialized: bool,
21    /// Client info from initialization.
22    client_info: Option<ClientInfo>,
23    /// Client capabilities from initialization.
24    client_capabilities: Option<ClientCapabilities>,
25    /// Server info.
26    server_info: ServerInfo,
27    /// Server capabilities.
28    server_capabilities: ServerCapabilities,
29    /// Negotiated protocol version.
30    protocol_version: Option<String>,
31    /// Resource subscriptions for this session.
32    resource_subscriptions: HashSet<String>,
33    /// Session-scoped log level for log notifications.
34    log_level: Option<LogLevel>,
35    /// Per-session state storage.
36    state: SessionState,
37}
38
39impl Session {
40    /// Creates a new uninitialized session.
41    #[must_use]
42    pub fn new(server_info: ServerInfo, server_capabilities: ServerCapabilities) -> Self {
43        Self {
44            initialized: false,
45            client_info: None,
46            client_capabilities: None,
47            server_info,
48            server_capabilities,
49            protocol_version: None,
50            resource_subscriptions: HashSet::new(),
51            log_level: None,
52            state: SessionState::new(),
53        }
54    }
55
56    /// Returns a reference to the session state.
57    ///
58    /// Session state persists across requests within this session and can be
59    /// used to store handler-specific data.
60    #[must_use]
61    pub fn state(&self) -> &SessionState {
62        &self.state
63    }
64
65    /// Returns whether the session has been initialized.
66    #[must_use]
67    pub fn is_initialized(&self) -> bool {
68        self.initialized
69    }
70
71    /// Initializes the session with client info.
72    pub fn initialize(
73        &mut self,
74        client_info: ClientInfo,
75        client_capabilities: ClientCapabilities,
76        protocol_version: String,
77    ) {
78        self.client_info = Some(client_info);
79        self.client_capabilities = Some(client_capabilities);
80        self.protocol_version = Some(protocol_version);
81        self.initialized = true;
82    }
83
84    /// Returns the client info if initialized.
85    #[must_use]
86    pub fn client_info(&self) -> Option<&ClientInfo> {
87        self.client_info.as_ref()
88    }
89
90    /// Returns the client capabilities if initialized.
91    #[must_use]
92    pub fn client_capabilities(&self) -> Option<&ClientCapabilities> {
93        self.client_capabilities.as_ref()
94    }
95
96    /// Returns the server info.
97    #[must_use]
98    pub fn server_info(&self) -> &ServerInfo {
99        &self.server_info
100    }
101
102    /// Returns the server capabilities.
103    #[must_use]
104    pub fn server_capabilities(&self) -> &ServerCapabilities {
105        &self.server_capabilities
106    }
107
108    /// Returns the negotiated protocol version.
109    #[must_use]
110    pub fn protocol_version(&self) -> Option<&str> {
111        self.protocol_version.as_deref()
112    }
113
114    /// Subscribes to a resource URI for this session.
115    pub fn subscribe_resource(&mut self, uri: String) {
116        self.resource_subscriptions.insert(uri);
117    }
118
119    /// Unsubscribes from a resource URI for this session.
120    pub fn unsubscribe_resource(&mut self, uri: &str) {
121        self.resource_subscriptions.remove(uri);
122    }
123
124    /// Returns true if this session is subscribed to the given resource URI.
125    #[must_use]
126    pub fn is_resource_subscribed(&self, uri: &str) -> bool {
127        self.resource_subscriptions.contains(uri)
128    }
129
130    /// Sets the session log level for log notifications.
131    pub fn set_log_level(&mut self, level: LogLevel) {
132        self.log_level = Some(level);
133    }
134
135    /// Returns the current session log level for log notifications.
136    #[must_use]
137    pub fn log_level(&self) -> Option<LogLevel> {
138        self.log_level
139    }
140
141    /// Returns whether the client supports sampling (LLM completions).
142    #[must_use]
143    pub fn supports_sampling(&self) -> bool {
144        self.client_capabilities
145            .as_ref()
146            .is_some_and(|caps| caps.sampling.is_some())
147    }
148
149    /// Returns whether the client supports elicitation (user input requests).
150    #[must_use]
151    pub fn supports_elicitation(&self) -> bool {
152        self.client_capabilities
153            .as_ref()
154            .is_some_and(|caps| caps.elicitation.is_some())
155    }
156
157    /// Returns whether the client supports roots listing.
158    #[must_use]
159    pub fn supports_roots(&self) -> bool {
160        self.client_capabilities
161            .as_ref()
162            .is_some_and(|caps| caps.roots.is_some())
163    }
164
165    /// Sends a resource updated notification if the session is subscribed.
166    ///
167    /// Returns true if a notification was sent.
168    pub fn notify_resource_updated(&self, uri: &str, sender: &NotificationSender) -> bool {
169        if !self.is_resource_subscribed(uri) {
170            return false;
171        }
172
173        let params = ResourceUpdatedNotificationParams {
174            uri: uri.to_string(),
175        };
176        let payload = match serde_json::to_value(params) {
177            Ok(value) => value,
178            Err(err) => {
179                warn!(
180                    target: targets::SESSION,
181                    "failed to serialize resource update for {}: {}",
182                    uri,
183                    err
184                );
185                return false;
186            }
187        };
188
189        debug!(
190            target: targets::SESSION,
191            "sending resource update notification for {}",
192            uri
193        );
194        sender(JsonRpcRequest::notification(
195            "notifications/resources/updated",
196            Some(payload),
197        ));
198        true
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use fastmcp_protocol::{ElicitationCapability, RootsCapability, SamplingCapability};
206    use std::sync::{Arc, Mutex};
207
208    fn make_server_info() -> ServerInfo {
209        ServerInfo {
210            name: "test".to_string(),
211            version: "1.0".to_string(),
212        }
213    }
214
215    fn make_client_info() -> ClientInfo {
216        ClientInfo {
217            name: "test-client".to_string(),
218            version: "1.0".to_string(),
219        }
220    }
221
222    fn make_session() -> Session {
223        Session::new(make_server_info(), ServerCapabilities::default())
224    }
225
226    // ── New session initial state ────────────────────────────────────
227
228    #[test]
229    fn new_session_is_not_initialized() {
230        let session = make_session();
231        assert!(!session.is_initialized());
232    }
233
234    #[test]
235    fn new_session_has_no_client_info() {
236        let session = make_session();
237        assert!(session.client_info().is_none());
238    }
239
240    #[test]
241    fn new_session_has_no_client_capabilities() {
242        let session = make_session();
243        assert!(session.client_capabilities().is_none());
244    }
245
246    #[test]
247    fn new_session_has_no_protocol_version() {
248        let session = make_session();
249        assert!(session.protocol_version().is_none());
250    }
251
252    #[test]
253    fn new_session_has_no_log_level() {
254        let session = make_session();
255        assert!(session.log_level().is_none());
256    }
257
258    #[test]
259    fn new_session_returns_server_info() {
260        let session = make_session();
261        assert_eq!(session.server_info().name, "test");
262        assert_eq!(session.server_info().version, "1.0");
263    }
264
265    #[test]
266    fn new_session_returns_server_capabilities() {
267        let caps = ServerCapabilities::default();
268        let session = Session::new(make_server_info(), caps);
269        // default caps should be accessible
270        let _ = session.server_capabilities();
271    }
272
273    // ── Initialization lifecycle ─────────────────────────────────────
274
275    #[test]
276    fn initialize_sets_initialized_flag() {
277        let mut session = make_session();
278        session.initialize(
279            make_client_info(),
280            ClientCapabilities::default(),
281            "2024-11-05".to_string(),
282        );
283        assert!(session.is_initialized());
284    }
285
286    #[test]
287    fn initialize_stores_client_info() {
288        let mut session = make_session();
289        session.initialize(
290            make_client_info(),
291            ClientCapabilities::default(),
292            "2024-11-05".to_string(),
293        );
294        let info = session.client_info().expect("client_info set");
295        assert_eq!(info.name, "test-client");
296        assert_eq!(info.version, "1.0");
297    }
298
299    #[test]
300    fn initialize_stores_client_capabilities() {
301        let mut session = make_session();
302        let caps = ClientCapabilities {
303            sampling: Some(SamplingCapability {}),
304            elicitation: None,
305            roots: None,
306        };
307        session.initialize(make_client_info(), caps, "2024-11-05".to_string());
308        let stored = session.client_capabilities().expect("caps set");
309        assert!(stored.sampling.is_some());
310    }
311
312    #[test]
313    fn initialize_stores_protocol_version() {
314        let mut session = make_session();
315        session.initialize(
316            make_client_info(),
317            ClientCapabilities::default(),
318            "2025-03-26".to_string(),
319        );
320        assert_eq!(session.protocol_version(), Some("2025-03-26"));
321    }
322
323    // ── Resource subscriptions ───────────────────────────────────────
324
325    #[test]
326    fn subscribe_and_check_resource() {
327        let mut session = make_session();
328        assert!(!session.is_resource_subscribed("file:///a.txt"));
329        session.subscribe_resource("file:///a.txt".to_string());
330        assert!(session.is_resource_subscribed("file:///a.txt"));
331    }
332
333    #[test]
334    fn unsubscribe_resource_removes_it() {
335        let mut session = make_session();
336        session.subscribe_resource("file:///a.txt".to_string());
337        session.unsubscribe_resource("file:///a.txt");
338        assert!(!session.is_resource_subscribed("file:///a.txt"));
339    }
340
341    #[test]
342    fn unsubscribe_nonexistent_resource_is_noop() {
343        let mut session = make_session();
344        // should not panic
345        session.unsubscribe_resource("file:///does-not-exist");
346        assert!(!session.is_resource_subscribed("file:///does-not-exist"));
347    }
348
349    #[test]
350    fn multiple_subscriptions_are_independent() {
351        let mut session = make_session();
352        session.subscribe_resource("a://1".to_string());
353        session.subscribe_resource("b://2".to_string());
354        assert!(session.is_resource_subscribed("a://1"));
355        assert!(session.is_resource_subscribed("b://2"));
356        session.unsubscribe_resource("a://1");
357        assert!(!session.is_resource_subscribed("a://1"));
358        assert!(session.is_resource_subscribed("b://2"));
359    }
360
361    #[test]
362    fn duplicate_subscribe_is_idempotent() {
363        let mut session = make_session();
364        session.subscribe_resource("r://x".to_string());
365        session.subscribe_resource("r://x".to_string());
366        assert!(session.is_resource_subscribed("r://x"));
367        session.unsubscribe_resource("r://x");
368        assert!(!session.is_resource_subscribed("r://x"));
369    }
370
371    // ── Log level ────────────────────────────────────────────────────
372
373    #[test]
374    fn set_log_level_and_read_back() {
375        let mut session = make_session();
376        session.set_log_level(LogLevel::Warning);
377        assert_eq!(session.log_level(), Some(LogLevel::Warning));
378    }
379
380    #[test]
381    fn set_log_level_overwrites_previous() {
382        let mut session = make_session();
383        session.set_log_level(LogLevel::Debug);
384        session.set_log_level(LogLevel::Error);
385        assert_eq!(session.log_level(), Some(LogLevel::Error));
386    }
387
388    // ── Session state ────────────────────────────────────────────────
389
390    #[test]
391    fn state_is_accessible() {
392        let session = make_session();
393        let state = session.state();
394        // fresh state should have no stored values
395        let val: Option<String> = state.get("key");
396        assert!(val.is_none());
397    }
398
399    // ── notify_resource_updated ──────────────────────────────────────
400
401    #[test]
402    fn notify_resource_updated_returns_false_when_not_subscribed() {
403        let session = make_session();
404        let sender: NotificationSender = Arc::new(|_| {});
405        assert!(!session.notify_resource_updated("file:///a.txt", &sender));
406    }
407
408    #[test]
409    fn notify_resource_updated_sends_when_subscribed() {
410        let mut session = make_session();
411        session.subscribe_resource("file:///a.txt".to_string());
412
413        let sent = Arc::new(Mutex::new(Vec::new()));
414        let sent_clone = Arc::clone(&sent);
415        let sender: NotificationSender = Arc::new(move |req| {
416            sent_clone.lock().unwrap().push(req);
417        });
418
419        let result = session.notify_resource_updated("file:///a.txt", &sender);
420        assert!(result);
421
422        let messages = sent.lock().unwrap();
423        assert_eq!(messages.len(), 1);
424        assert_eq!(messages[0].method, "notifications/resources/updated");
425    }
426
427    #[test]
428    fn notify_resource_updated_includes_uri_in_params() {
429        let mut session = make_session();
430        session.subscribe_resource("test://res".to_string());
431
432        let sent = Arc::new(Mutex::new(Vec::new()));
433        let sent_clone = Arc::clone(&sent);
434        let sender: NotificationSender = Arc::new(move |req| {
435            sent_clone.lock().unwrap().push(req);
436        });
437
438        session.notify_resource_updated("test://res", &sender);
439
440        let messages = sent.lock().unwrap();
441        let params = messages[0].params.as_ref().expect("params present");
442        let uri = params
443            .get("uri")
444            .and_then(|v| v.as_str())
445            .expect("uri field");
446        assert_eq!(uri, "test://res");
447    }
448
449    #[test]
450    fn notify_resource_updated_does_not_fire_for_other_uri() {
451        let mut session = make_session();
452        session.subscribe_resource("file:///a.txt".to_string());
453
454        let sent = Arc::new(Mutex::new(Vec::new()));
455        let sent_clone = Arc::clone(&sent);
456        let sender: NotificationSender = Arc::new(move |req| {
457            sent_clone.lock().unwrap().push(req);
458        });
459
460        let result = session.notify_resource_updated("file:///b.txt", &sender);
461        assert!(!result);
462        assert!(sent.lock().unwrap().is_empty());
463    }
464
465    // ── Debug impl ───────────────────────────────────────────────────
466
467    #[test]
468    fn session_debug_format_includes_fields() {
469        let session = make_session();
470        let debug = format!("{:?}", session);
471        assert!(debug.contains("Session"));
472        assert!(debug.contains("initialized: false"));
473    }
474
475    // ── Existing capability tests ────────────────────────────────────
476
477    #[test]
478    fn test_session_supports_sampling() {
479        let mut session = Session::new(
480            ServerInfo {
481                name: "test".to_string(),
482                version: "1.0".to_string(),
483            },
484            ServerCapabilities::default(),
485        );
486
487        // Before initialization, no capabilities
488        assert!(!session.supports_sampling());
489
490        // Initialize with sampling capability
491        session.initialize(
492            ClientInfo {
493                name: "test-client".to_string(),
494                version: "1.0".to_string(),
495            },
496            ClientCapabilities {
497                sampling: Some(SamplingCapability {}),
498                elicitation: None,
499                roots: None,
500            },
501            "2024-11-05".to_string(),
502        );
503
504        assert!(session.supports_sampling());
505        assert!(!session.supports_elicitation());
506        assert!(!session.supports_roots());
507    }
508
509    #[test]
510    fn test_session_supports_elicitation() {
511        let mut session = Session::new(
512            ServerInfo {
513                name: "test".to_string(),
514                version: "1.0".to_string(),
515            },
516            ServerCapabilities::default(),
517        );
518
519        session.initialize(
520            ClientInfo {
521                name: "test-client".to_string(),
522                version: "1.0".to_string(),
523            },
524            ClientCapabilities {
525                sampling: None,
526                elicitation: Some(ElicitationCapability::form()),
527                roots: None,
528            },
529            "2024-11-05".to_string(),
530        );
531
532        assert!(!session.supports_sampling());
533        assert!(session.supports_elicitation());
534        assert!(!session.supports_roots());
535    }
536
537    #[test]
538    fn test_session_supports_roots() {
539        let mut session = Session::new(
540            ServerInfo {
541                name: "test".to_string(),
542                version: "1.0".to_string(),
543            },
544            ServerCapabilities::default(),
545        );
546
547        session.initialize(
548            ClientInfo {
549                name: "test-client".to_string(),
550                version: "1.0".to_string(),
551            },
552            ClientCapabilities {
553                sampling: None,
554                elicitation: None,
555                roots: Some(RootsCapability { list_changed: true }),
556            },
557            "2024-11-05".to_string(),
558        );
559
560        assert!(!session.supports_sampling());
561        assert!(!session.supports_elicitation());
562        assert!(session.supports_roots());
563    }
564
565    #[test]
566    fn test_session_supports_all_capabilities() {
567        let mut session = Session::new(
568            ServerInfo {
569                name: "test".to_string(),
570                version: "1.0".to_string(),
571            },
572            ServerCapabilities::default(),
573        );
574
575        session.initialize(
576            ClientInfo {
577                name: "test-client".to_string(),
578                version: "1.0".to_string(),
579            },
580            ClientCapabilities {
581                sampling: Some(SamplingCapability {}),
582                elicitation: Some(ElicitationCapability::both()),
583                roots: Some(RootsCapability {
584                    list_changed: false,
585                }),
586            },
587            "2024-11-05".to_string(),
588        );
589
590        assert!(session.supports_sampling());
591        assert!(session.supports_elicitation());
592        assert!(session.supports_roots());
593    }
594
595    #[test]
596    fn test_session_no_capabilities() {
597        let mut session = Session::new(
598            ServerInfo {
599                name: "test".to_string(),
600                version: "1.0".to_string(),
601            },
602            ServerCapabilities::default(),
603        );
604
605        session.initialize(
606            ClientInfo {
607                name: "test-client".to_string(),
608                version: "1.0".to_string(),
609            },
610            ClientCapabilities::default(),
611            "2024-11-05".to_string(),
612        );
613
614        assert!(!session.supports_sampling());
615        assert!(!session.supports_elicitation());
616        assert!(!session.supports_roots());
617    }
618
619    // ── Re-initialization ───────────────────────────────────────────
620
621    #[test]
622    fn reinitialize_overwrites_client_info() {
623        let mut session = make_session();
624        session.initialize(
625            make_client_info(),
626            ClientCapabilities::default(),
627            "2024-11-05".to_string(),
628        );
629        session.initialize(
630            ClientInfo {
631                name: "new-client".to_string(),
632                version: "2.0".to_string(),
633            },
634            ClientCapabilities {
635                sampling: Some(SamplingCapability {}),
636                elicitation: None,
637                roots: None,
638            },
639            "2025-03-26".to_string(),
640        );
641        assert!(session.is_initialized());
642        let info = session.client_info().unwrap();
643        assert_eq!(info.name, "new-client");
644        assert_eq!(info.version, "2.0");
645        assert_eq!(session.protocol_version(), Some("2025-03-26"));
646        assert!(session.supports_sampling());
647    }
648
649    // ── State persistence through lifecycle ──────────────────────────
650
651    #[test]
652    fn state_persists_after_initialization() {
653        let mut session = make_session();
654        session.state().set("key", "before_init");
655        session.initialize(
656            make_client_info(),
657            ClientCapabilities::default(),
658            "2024-11-05".to_string(),
659        );
660        let val: Option<String> = session.state().get("key");
661        assert_eq!(val.as_deref(), Some("before_init"));
662    }
663
664    // ── Notification after unsubscribe ───────────────────────────────
665
666    #[test]
667    fn notify_resource_updated_after_unsubscribe_returns_false() {
668        let mut session = make_session();
669        session.subscribe_resource("r://x".to_string());
670
671        let sent = Arc::new(Mutex::new(Vec::new()));
672        let sent_clone = Arc::clone(&sent);
673        let sender: NotificationSender = Arc::new(move |req| {
674            sent_clone.lock().unwrap().push(req);
675        });
676
677        // First notification should fire
678        assert!(session.notify_resource_updated("r://x", &sender));
679        assert_eq!(sent.lock().unwrap().len(), 1);
680
681        // Unsubscribe and try again
682        session.unsubscribe_resource("r://x");
683        assert!(!session.notify_resource_updated("r://x", &sender));
684        // No new notifications sent
685        assert_eq!(sent.lock().unwrap().len(), 1);
686    }
687
688    // ── Subscribe → unsubscribe → re-subscribe ─────────────────────
689
690    #[test]
691    fn resubscribe_after_unsubscribe_works() {
692        let mut session = make_session();
693        session.subscribe_resource("r://x".to_string());
694        session.unsubscribe_resource("r://x");
695        assert!(!session.is_resource_subscribed("r://x"));
696        session.subscribe_resource("r://x".to_string());
697        assert!(session.is_resource_subscribed("r://x"));
698    }
699
700    // ── Debug format after initialization ───────────────────────────
701
702    #[test]
703    fn session_debug_after_init_shows_initialized_true() {
704        let mut session = make_session();
705        session.initialize(
706            make_client_info(),
707            ClientCapabilities::default(),
708            "2024-11-05".to_string(),
709        );
710        let debug = format!("{:?}", session);
711        assert!(debug.contains("initialized: true"));
712    }
713
714    // ── Non-default server capabilities ─────────────────────────────
715
716    #[test]
717    fn session_with_custom_server_capabilities() {
718        use fastmcp_protocol::{LoggingCapability, TasksCapability, ToolsCapability};
719        let caps = ServerCapabilities {
720            tools: Some(ToolsCapability { list_changed: true }),
721            logging: Some(LoggingCapability {}),
722            tasks: Some(TasksCapability {
723                list_changed: false,
724            }),
725            ..ServerCapabilities::default()
726        };
727        let session = Session::new(make_server_info(), caps);
728        assert!(session.server_capabilities().tools.is_some());
729        assert!(session.server_capabilities().logging.is_some());
730        assert!(session.server_capabilities().tasks.is_some());
731    }
732
733    // ── Log level with all variants ─────────────────────────────────
734
735    #[test]
736    fn set_log_level_all_variants() {
737        let mut session = make_session();
738        for level in [
739            LogLevel::Debug,
740            LogLevel::Info,
741            LogLevel::Warning,
742            LogLevel::Error,
743        ] {
744            session.set_log_level(level);
745            assert_eq!(session.log_level(), Some(level));
746        }
747    }
748
749    // ── Persistence across re-initialization ────────────────────────
750
751    #[test]
752    fn log_level_persists_across_reinitialization() {
753        let mut session = make_session();
754        session.set_log_level(LogLevel::Warning);
755        session.initialize(
756            make_client_info(),
757            ClientCapabilities::default(),
758            "2024-11-05".to_string(),
759        );
760        assert_eq!(session.log_level(), Some(LogLevel::Warning));
761        // Re-initialize with different client info
762        session.initialize(
763            ClientInfo {
764                name: "other".to_string(),
765                version: "2.0".to_string(),
766            },
767            ClientCapabilities::default(),
768            "2025-03-26".to_string(),
769        );
770        assert_eq!(session.log_level(), Some(LogLevel::Warning));
771    }
772
773    #[test]
774    fn resource_subscriptions_persist_across_reinitialization() {
775        let mut session = make_session();
776        session.subscribe_resource("file:///keep.txt".to_string());
777        session.initialize(
778            make_client_info(),
779            ClientCapabilities::default(),
780            "2024-11-05".to_string(),
781        );
782        assert!(session.is_resource_subscribed("file:///keep.txt"));
783    }
784
785    #[test]
786    fn state_set_after_init_persists_through_reinit() {
787        let mut session = make_session();
788        session.initialize(
789            make_client_info(),
790            ClientCapabilities::default(),
791            "2024-11-05".to_string(),
792        );
793        session.state().set("counter", 42);
794        // Re-initialize
795        session.initialize(
796            ClientInfo {
797                name: "new".to_string(),
798                version: "3.0".to_string(),
799            },
800            ClientCapabilities::default(),
801            "2025-03-26".to_string(),
802        );
803        let val: Option<i32> = session.state().get("counter");
804        assert_eq!(val, Some(42));
805    }
806
807    #[test]
808    fn notify_resource_updated_fires_independently_per_subscription() {
809        let mut session = make_session();
810        session.subscribe_resource("a://1".to_string());
811        session.subscribe_resource("b://2".to_string());
812
813        let sent = Arc::new(Mutex::new(Vec::new()));
814        let sent_clone = Arc::clone(&sent);
815        let sender: NotificationSender = Arc::new(move |req| {
816            sent_clone.lock().unwrap().push(req);
817        });
818
819        // Notify first URI
820        assert!(session.notify_resource_updated("a://1", &sender));
821        assert_eq!(sent.lock().unwrap().len(), 1);
822        let uri = sent.lock().unwrap()[0]
823            .params
824            .as_ref()
825            .unwrap()
826            .get("uri")
827            .unwrap()
828            .as_str()
829            .unwrap()
830            .to_string();
831        assert_eq!(uri, "a://1");
832
833        // Notify second URI
834        assert!(session.notify_resource_updated("b://2", &sender));
835        assert_eq!(sent.lock().unwrap().len(), 2);
836        let uri2 = sent.lock().unwrap()[1]
837            .params
838            .as_ref()
839            .unwrap()
840            .get("uri")
841            .unwrap()
842            .as_str()
843            .unwrap()
844            .to_string();
845        assert_eq!(uri2, "b://2");
846    }
847
848    #[test]
849    fn supports_elicitation_and_roots_false_before_init() {
850        let session = make_session();
851        assert!(!session.supports_elicitation());
852        assert!(!session.supports_roots());
853    }
854}