1use 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#[derive(Debug)]
18pub struct Session {
19 initialized: bool,
21 client_info: Option<ClientInfo>,
23 client_capabilities: Option<ClientCapabilities>,
25 server_info: ServerInfo,
27 server_capabilities: ServerCapabilities,
29 protocol_version: Option<String>,
31 resource_subscriptions: HashSet<String>,
33 log_level: Option<LogLevel>,
35 state: SessionState,
37}
38
39impl Session {
40 #[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 #[must_use]
61 pub fn state(&self) -> &SessionState {
62 &self.state
63 }
64
65 #[must_use]
67 pub fn is_initialized(&self) -> bool {
68 self.initialized
69 }
70
71 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 #[must_use]
86 pub fn client_info(&self) -> Option<&ClientInfo> {
87 self.client_info.as_ref()
88 }
89
90 #[must_use]
92 pub fn client_capabilities(&self) -> Option<&ClientCapabilities> {
93 self.client_capabilities.as_ref()
94 }
95
96 #[must_use]
98 pub fn server_info(&self) -> &ServerInfo {
99 &self.server_info
100 }
101
102 #[must_use]
104 pub fn server_capabilities(&self) -> &ServerCapabilities {
105 &self.server_capabilities
106 }
107
108 #[must_use]
110 pub fn protocol_version(&self) -> Option<&str> {
111 self.protocol_version.as_deref()
112 }
113
114 pub fn subscribe_resource(&mut self, uri: String) {
116 self.resource_subscriptions.insert(uri);
117 }
118
119 pub fn unsubscribe_resource(&mut self, uri: &str) {
121 self.resource_subscriptions.remove(uri);
122 }
123
124 #[must_use]
126 pub fn is_resource_subscribed(&self, uri: &str) -> bool {
127 self.resource_subscriptions.contains(uri)
128 }
129
130 pub fn set_log_level(&mut self, level: LogLevel) {
132 self.log_level = Some(level);
133 }
134
135 #[must_use]
137 pub fn log_level(&self) -> Option<LogLevel> {
138 self.log_level
139 }
140
141 #[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 #[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 #[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 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 #[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 let _ = session.server_capabilities();
271 }
272
273 #[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 #[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 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 #[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 #[test]
391 fn state_is_accessible() {
392 let session = make_session();
393 let state = session.state();
394 let val: Option<String> = state.get("key");
396 assert!(val.is_none());
397 }
398
399 #[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 #[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 #[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 assert!(!session.supports_sampling());
489
490 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 #[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 #[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 #[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 assert!(session.notify_resource_updated("r://x", &sender));
679 assert_eq!(sent.lock().unwrap().len(), 1);
680
681 session.unsubscribe_resource("r://x");
683 assert!(!session.notify_resource_updated("r://x", &sender));
684 assert_eq!(sent.lock().unwrap().len(), 1);
686 }
687
688 #[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 #[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 #[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 #[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 #[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 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 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 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 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}