1use std::collections::HashMap;
13use std::future::Future;
14use std::pin::Pin;
15use std::sync::Arc;
16use std::time::Duration;
17
18use fastmcp_core::{
19 McpContext, McpOutcome, McpResult, NotificationSender, Outcome, ProgressReporter, SessionState,
20};
21use fastmcp_protocol::{
22 Content, Icon, JsonRpcRequest, ProgressMarker, ProgressParams, Prompt, PromptMessage, Resource,
23 ResourceContent, ResourceTemplate, Tool, ToolAnnotations,
24};
25
26pub struct ProgressNotificationSender<F>
35where
36 F: Fn(JsonRpcRequest) + Send + Sync,
37{
38 marker: ProgressMarker,
40 send_fn: F,
42}
43
44impl<F> ProgressNotificationSender<F>
45where
46 F: Fn(JsonRpcRequest) + Send + Sync,
47{
48 pub fn new(marker: ProgressMarker, send_fn: F) -> Self {
50 Self { marker, send_fn }
51 }
52
53 pub fn into_reporter(self) -> ProgressReporter
55 where
56 Self: 'static,
57 {
58 ProgressReporter::new(Arc::new(self))
59 }
60}
61
62impl<F> NotificationSender for ProgressNotificationSender<F>
63where
64 F: Fn(JsonRpcRequest) + Send + Sync,
65{
66 fn send_progress(&self, progress: f64, total: Option<f64>, message: Option<&str>) {
67 let params = match total {
68 Some(t) => ProgressParams::with_total(self.marker.clone(), progress, t),
69 None => ProgressParams::new(self.marker.clone(), progress),
70 };
71
72 let params = if let Some(msg) = message {
73 params.with_message(msg)
74 } else {
75 params
76 };
77
78 let notification = JsonRpcRequest::notification(
80 "notifications/progress",
81 Some(serde_json::to_value(¶ms).unwrap_or_default()),
82 );
83
84 (self.send_fn)(notification);
85 }
86}
87
88impl<F> std::fmt::Debug for ProgressNotificationSender<F>
89where
90 F: Fn(JsonRpcRequest) + Send + Sync,
91{
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 f.debug_struct("ProgressNotificationSender")
94 .field("marker", &self.marker)
95 .finish_non_exhaustive()
96 }
97}
98
99#[derive(Clone, Default)]
101pub struct BidirectionalSenders {
102 pub sampling: Option<Arc<dyn fastmcp_core::SamplingSender>>,
104 pub elicitation: Option<Arc<dyn fastmcp_core::ElicitationSender>>,
106}
107
108impl BidirectionalSenders {
109 #[must_use]
111 pub fn new() -> Self {
112 Self::default()
113 }
114
115 #[must_use]
117 pub fn with_sampling(mut self, sender: Arc<dyn fastmcp_core::SamplingSender>) -> Self {
118 self.sampling = Some(sender);
119 self
120 }
121
122 #[must_use]
124 pub fn with_elicitation(mut self, sender: Arc<dyn fastmcp_core::ElicitationSender>) -> Self {
125 self.elicitation = Some(sender);
126 self
127 }
128}
129
130impl std::fmt::Debug for BidirectionalSenders {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 f.debug_struct("BidirectionalSenders")
133 .field("sampling", &self.sampling.is_some())
134 .field("elicitation", &self.elicitation.is_some())
135 .finish()
136 }
137}
138
139pub fn create_context_with_progress<F>(
141 cx: asupersync::Cx,
142 request_id: u64,
143 progress_marker: Option<ProgressMarker>,
144 state: Option<SessionState>,
145 send_fn: F,
146) -> McpContext
147where
148 F: Fn(JsonRpcRequest) + Send + Sync + 'static,
149{
150 create_context_with_progress_and_senders(cx, request_id, progress_marker, state, send_fn, None)
151}
152
153pub fn create_context_with_progress_and_senders<F>(
155 cx: asupersync::Cx,
156 request_id: u64,
157 progress_marker: Option<ProgressMarker>,
158 state: Option<SessionState>,
159 send_fn: F,
160 senders: Option<&BidirectionalSenders>,
161) -> McpContext
162where
163 F: Fn(JsonRpcRequest) + Send + Sync + 'static,
164{
165 let mut ctx = match (progress_marker, state) {
166 (Some(marker), Some(state)) => {
167 let sender = ProgressNotificationSender::new(marker, send_fn);
168 McpContext::with_state_and_progress(cx, request_id, state, sender.into_reporter())
169 }
170 (Some(marker), None) => {
171 let sender = ProgressNotificationSender::new(marker, send_fn);
172 McpContext::with_progress(cx, request_id, sender.into_reporter())
173 }
174 (None, Some(state)) => McpContext::with_state(cx, request_id, state),
175 (None, None) => McpContext::new(cx, request_id),
176 };
177
178 if let Some(senders) = senders {
180 if let Some(ref sampling) = senders.sampling {
181 ctx = ctx.with_sampling(sampling.clone());
182 }
183 if let Some(ref elicitation) = senders.elicitation {
184 ctx = ctx.with_elicitation(elicitation.clone());
185 }
186 }
187
188 ctx
189}
190
191pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
193
194pub type UriParams = HashMap<String, String>;
196
197pub trait ToolHandler: Send + Sync {
215 fn definition(&self) -> Tool;
217
218 fn icon(&self) -> Option<&Icon> {
223 None
224 }
225
226 fn version(&self) -> Option<&str> {
231 None
232 }
233
234 fn tags(&self) -> &[String] {
239 &[]
240 }
241
242 fn annotations(&self) -> Option<&ToolAnnotations> {
248 None
249 }
250
251 fn output_schema(&self) -> Option<serde_json::Value> {
257 None
258 }
259
260 fn timeout(&self) -> Option<Duration> {
268 None
269 }
270
271 fn call(&self, ctx: &McpContext, arguments: serde_json::Value) -> McpResult<Vec<Content>>;
277
278 fn call_async<'a>(
289 &'a self,
290 ctx: &'a McpContext,
291 arguments: serde_json::Value,
292 ) -> BoxFuture<'a, McpOutcome<Vec<Content>>> {
293 Box::pin(async move {
294 match self.call(ctx, arguments) {
295 Ok(v) => Outcome::Ok(v),
296 Err(e) => Outcome::Err(e),
297 }
298 })
299 }
300}
301
302pub trait ResourceHandler: Send + Sync {
317 fn definition(&self) -> Resource;
319
320 fn template(&self) -> Option<ResourceTemplate> {
322 None
323 }
324
325 fn icon(&self) -> Option<&Icon> {
330 None
331 }
332
333 fn version(&self) -> Option<&str> {
338 None
339 }
340
341 fn tags(&self) -> &[String] {
346 &[]
347 }
348
349 fn timeout(&self) -> Option<Duration> {
354 None
355 }
356
357 fn read(&self, ctx: &McpContext) -> McpResult<Vec<ResourceContent>>;
363
364 fn read_with_uri(
368 &self,
369 ctx: &McpContext,
370 _uri: &str,
371 _params: &UriParams,
372 ) -> McpResult<Vec<ResourceContent>> {
373 self.read(ctx)
374 }
375
376 fn read_async_with_uri<'a>(
380 &'a self,
381 ctx: &'a McpContext,
382 uri: &'a str,
383 params: &'a UriParams,
384 ) -> BoxFuture<'a, McpOutcome<Vec<ResourceContent>>> {
385 Box::pin(async move {
386 if params.is_empty() {
387 self.read_async(ctx).await
388 } else {
389 match self.read_with_uri(ctx, uri, params) {
390 Ok(v) => Outcome::Ok(v),
391 Err(e) => Outcome::Err(e),
392 }
393 }
394 })
395 }
396
397 fn read_async<'a>(
406 &'a self,
407 ctx: &'a McpContext,
408 ) -> BoxFuture<'a, McpOutcome<Vec<ResourceContent>>> {
409 Box::pin(async move {
410 match self.read(ctx) {
411 Ok(v) => Outcome::Ok(v),
412 Err(e) => Outcome::Err(e),
413 }
414 })
415 }
416}
417
418pub trait PromptHandler: Send + Sync {
432 fn definition(&self) -> Prompt;
434
435 fn icon(&self) -> Option<&Icon> {
440 None
441 }
442
443 fn version(&self) -> Option<&str> {
448 None
449 }
450
451 fn tags(&self) -> &[String] {
456 &[]
457 }
458
459 fn timeout(&self) -> Option<Duration> {
464 None
465 }
466
467 fn get(
473 &self,
474 ctx: &McpContext,
475 arguments: std::collections::HashMap<String, String>,
476 ) -> McpResult<Vec<PromptMessage>>;
477
478 fn get_async<'a>(
487 &'a self,
488 ctx: &'a McpContext,
489 arguments: std::collections::HashMap<String, String>,
490 ) -> BoxFuture<'a, McpOutcome<Vec<PromptMessage>>> {
491 Box::pin(async move {
492 match self.get(ctx, arguments) {
493 Ok(v) => Outcome::Ok(v),
494 Err(e) => Outcome::Err(e),
495 }
496 })
497 }
498}
499
500pub type BoxedToolHandler = Box<dyn ToolHandler>;
502
503pub type BoxedResourceHandler = Box<dyn ResourceHandler>;
505
506pub type BoxedPromptHandler = Box<dyn PromptHandler>;
508
509pub struct MountedToolHandler {
517 inner: BoxedToolHandler,
518 mounted_name: String,
519}
520
521impl MountedToolHandler {
522 pub fn new(inner: BoxedToolHandler, mounted_name: String) -> Self {
524 Self {
525 inner,
526 mounted_name,
527 }
528 }
529}
530
531impl ToolHandler for MountedToolHandler {
532 fn definition(&self) -> Tool {
533 let mut def = self.inner.definition();
534 def.name.clone_from(&self.mounted_name);
535 def
536 }
537
538 fn tags(&self) -> &[String] {
539 self.inner.tags()
540 }
541
542 fn annotations(&self) -> Option<&ToolAnnotations> {
543 self.inner.annotations()
544 }
545
546 fn output_schema(&self) -> Option<serde_json::Value> {
547 self.inner.output_schema()
548 }
549
550 fn timeout(&self) -> Option<Duration> {
551 self.inner.timeout()
552 }
553
554 fn call(&self, ctx: &McpContext, arguments: serde_json::Value) -> McpResult<Vec<Content>> {
555 self.inner.call(ctx, arguments)
556 }
557
558 fn call_async<'a>(
559 &'a self,
560 ctx: &'a McpContext,
561 arguments: serde_json::Value,
562 ) -> BoxFuture<'a, McpOutcome<Vec<Content>>> {
563 self.inner.call_async(ctx, arguments)
564 }
565}
566
567pub struct MountedResourceHandler {
571 inner: BoxedResourceHandler,
572 mounted_uri: String,
573 mounted_template: Option<ResourceTemplate>,
574}
575
576impl MountedResourceHandler {
577 pub fn new(inner: BoxedResourceHandler, mounted_uri: String) -> Self {
579 Self {
580 inner,
581 mounted_uri,
582 mounted_template: None,
583 }
584 }
585
586 pub fn with_template(
588 inner: BoxedResourceHandler,
589 mounted_uri: String,
590 mounted_template: ResourceTemplate,
591 ) -> Self {
592 Self {
593 inner,
594 mounted_uri,
595 mounted_template: Some(mounted_template),
596 }
597 }
598}
599
600impl ResourceHandler for MountedResourceHandler {
601 fn definition(&self) -> Resource {
602 let mut def = self.inner.definition();
603 def.uri.clone_from(&self.mounted_uri);
604 def
605 }
606
607 fn template(&self) -> Option<ResourceTemplate> {
608 self.mounted_template.clone()
609 }
610
611 fn tags(&self) -> &[String] {
612 self.inner.tags()
613 }
614
615 fn timeout(&self) -> Option<Duration> {
616 self.inner.timeout()
617 }
618
619 fn read(&self, ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
620 self.inner.read(ctx)
621 }
622
623 fn read_with_uri(
624 &self,
625 ctx: &McpContext,
626 uri: &str,
627 params: &UriParams,
628 ) -> McpResult<Vec<ResourceContent>> {
629 self.inner.read_with_uri(ctx, uri, params)
630 }
631
632 fn read_async_with_uri<'a>(
633 &'a self,
634 ctx: &'a McpContext,
635 uri: &'a str,
636 params: &'a UriParams,
637 ) -> BoxFuture<'a, McpOutcome<Vec<ResourceContent>>> {
638 self.inner.read_async_with_uri(ctx, uri, params)
639 }
640
641 fn read_async<'a>(
642 &'a self,
643 ctx: &'a McpContext,
644 ) -> BoxFuture<'a, McpOutcome<Vec<ResourceContent>>> {
645 self.inner.read_async(ctx)
646 }
647}
648
649pub struct MountedPromptHandler {
653 inner: BoxedPromptHandler,
654 mounted_name: String,
655}
656
657impl MountedPromptHandler {
658 pub fn new(inner: BoxedPromptHandler, mounted_name: String) -> Self {
660 Self {
661 inner,
662 mounted_name,
663 }
664 }
665}
666
667impl PromptHandler for MountedPromptHandler {
668 fn definition(&self) -> Prompt {
669 let mut def = self.inner.definition();
670 def.name.clone_from(&self.mounted_name);
671 def
672 }
673
674 fn tags(&self) -> &[String] {
675 self.inner.tags()
676 }
677
678 fn timeout(&self) -> Option<Duration> {
679 self.inner.timeout()
680 }
681
682 fn get(
683 &self,
684 ctx: &McpContext,
685 arguments: std::collections::HashMap<String, String>,
686 ) -> McpResult<Vec<PromptMessage>> {
687 self.inner.get(ctx, arguments)
688 }
689
690 fn get_async<'a>(
691 &'a self,
692 ctx: &'a McpContext,
693 arguments: std::collections::HashMap<String, String>,
694 ) -> BoxFuture<'a, McpOutcome<Vec<PromptMessage>>> {
695 self.inner.get_async(ctx, arguments)
696 }
697}
698
699#[cfg(test)]
700mod tests {
701 use super::*;
702 use asupersync::Cx;
703 use fastmcp_core::McpError;
704 use std::sync::Mutex;
705
706 #[test]
709 fn progress_sender_sends_notification_without_total() {
710 let sent = Arc::new(Mutex::new(Vec::new()));
711 let sent_clone = Arc::clone(&sent);
712 let sender = ProgressNotificationSender::new(ProgressMarker::from("tok-1"), move |req| {
713 sent_clone.lock().unwrap().push(req);
714 });
715
716 sender.send_progress(0.5, None, None);
717
718 let messages = sent.lock().unwrap();
719 assert_eq!(messages.len(), 1);
720 assert_eq!(messages[0].method, "notifications/progress");
721 let params = messages[0].params.as_ref().unwrap();
722 assert_eq!(params["progress"], 0.5);
723 assert!(params.get("total").is_none() || params["total"].is_null());
724 }
725
726 #[test]
727 fn progress_sender_sends_notification_with_total() {
728 let sent = Arc::new(Mutex::new(Vec::new()));
729 let sent_clone = Arc::clone(&sent);
730 let sender = ProgressNotificationSender::new(ProgressMarker::from("tok-2"), move |req| {
731 sent_clone.lock().unwrap().push(req);
732 });
733
734 sender.send_progress(3.0, Some(10.0), None);
735
736 let messages = sent.lock().unwrap();
737 let params = messages[0].params.as_ref().unwrap();
738 assert_eq!(params["progress"], 3.0);
739 assert_eq!(params["total"], 10.0);
740 }
741
742 #[test]
743 fn progress_sender_sends_notification_with_message() {
744 let sent = Arc::new(Mutex::new(Vec::new()));
745 let sent_clone = Arc::clone(&sent);
746 let sender = ProgressNotificationSender::new(ProgressMarker::from("tok-3"), move |req| {
747 sent_clone.lock().unwrap().push(req);
748 });
749
750 sender.send_progress(1.0, Some(5.0), Some("loading"));
751
752 let messages = sent.lock().unwrap();
753 let params = messages[0].params.as_ref().unwrap();
754 assert_eq!(params["message"], "loading");
755 }
756
757 #[test]
758 fn progress_sender_debug_format() {
759 let sender = ProgressNotificationSender::new(ProgressMarker::from("tok-dbg"), |_| {});
760 let debug = format!("{:?}", sender);
761 assert!(debug.contains("ProgressNotificationSender"));
762 }
763
764 #[test]
765 fn progress_sender_into_reporter() {
766 let sender = ProgressNotificationSender::new(ProgressMarker::from("tok-rpt"), |_| {});
767 let _reporter = sender.into_reporter();
768 }
769
770 #[test]
773 fn bidirectional_senders_default_is_empty() {
774 let senders = BidirectionalSenders::new();
775 assert!(senders.sampling.is_none());
776 assert!(senders.elicitation.is_none());
777 }
778
779 #[test]
780 fn bidirectional_senders_debug_shows_presence() {
781 let senders = BidirectionalSenders::new();
782 let debug = format!("{:?}", senders);
783 assert!(debug.contains("sampling: false"));
784 assert!(debug.contains("elicitation: false"));
785 }
786
787 #[test]
790 fn create_context_no_progress_no_state() {
791 let cx = Cx::for_testing();
792 let ctx = create_context_with_progress(cx, 42, None, None, |_| {});
793 assert_eq!(ctx.request_id(), 42);
794 }
795
796 #[test]
797 fn create_context_with_progress_marker() {
798 let cx = Cx::for_testing();
799 let marker = ProgressMarker::from("ctx-pm");
800 let ctx = create_context_with_progress(cx, 7, Some(marker), None, |_| {});
801 assert_eq!(ctx.request_id(), 7);
802 }
803
804 #[test]
805 fn create_context_with_state_only() {
806 let cx = Cx::for_testing();
807 let state = SessionState::new();
808 state.set("k", &"v");
809 let ctx = create_context_with_progress(cx, 10, None, Some(state), |_| {});
810 let val: Option<String> = ctx.get_state("k");
811 assert_eq!(val.as_deref(), Some("v"));
812 }
813
814 #[test]
815 fn create_context_with_progress_and_state() {
816 let cx = Cx::for_testing();
817 let marker = ProgressMarker::from("both");
818 let state = SessionState::new();
819 let ctx = create_context_with_progress(cx, 99, Some(marker), Some(state), |_| {});
820 assert_eq!(ctx.request_id(), 99);
821 }
822
823 struct StubTool;
826
827 impl ToolHandler for StubTool {
828 fn definition(&self) -> Tool {
829 Tool {
830 name: "stub".to_string(),
831 description: Some("a stub tool".to_string()),
832 input_schema: serde_json::json!({"type": "object"}),
833 output_schema: None,
834 icon: None,
835 version: None,
836 tags: vec![],
837 annotations: None,
838 }
839 }
840
841 fn call(&self, _ctx: &McpContext, args: serde_json::Value) -> McpResult<Vec<Content>> {
842 Ok(vec![Content::text(format!("echo: {args}"))])
843 }
844 }
845
846 #[test]
847 fn tool_handler_defaults_return_none() {
848 let tool = StubTool;
849 assert!(tool.icon().is_none());
850 assert!(tool.version().is_none());
851 assert!(tool.tags().is_empty());
852 assert!(tool.annotations().is_none());
853 assert!(tool.output_schema().is_none());
854 assert!(tool.timeout().is_none());
855 }
856
857 #[test]
858 fn tool_handler_call_sync() {
859 let tool = StubTool;
860 let cx = Cx::for_testing();
861 let ctx = McpContext::new(cx, 1);
862 let result = tool.call(&ctx, serde_json::json!({"x": 1})).unwrap();
863 assert_eq!(result.len(), 1);
864 }
865
866 #[test]
867 fn tool_handler_call_sync_error() {
868 struct FailTool;
869 impl ToolHandler for FailTool {
870 fn definition(&self) -> Tool {
871 Tool {
872 name: "fail".to_string(),
873 description: None,
874 input_schema: serde_json::json!({"type": "object"}),
875 output_schema: None,
876 icon: None,
877 version: None,
878 tags: vec![],
879 annotations: None,
880 }
881 }
882 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
883 Err(McpError::internal_error("boom"))
884 }
885 }
886
887 let tool = FailTool;
888 let cx = Cx::for_testing();
889 let ctx = McpContext::new(cx, 1);
890 let err = tool.call(&ctx, serde_json::json!({})).unwrap_err();
891 assert!(err.message.contains("boom"));
892 }
893
894 struct StubResource;
897
898 impl ResourceHandler for StubResource {
899 fn definition(&self) -> Resource {
900 Resource {
901 uri: "file:///stub".to_string(),
902 name: "stub".to_string(),
903 description: None,
904 mime_type: Some("text/plain".to_string()),
905 icon: None,
906 version: None,
907 tags: vec![],
908 }
909 }
910
911 fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
912 Ok(vec![ResourceContent {
913 uri: "file:///stub".to_string(),
914 mime_type: Some("text/plain".to_string()),
915 text: Some("hello".to_string()),
916 blob: None,
917 }])
918 }
919 }
920
921 #[test]
922 fn resource_handler_defaults_return_none() {
923 let res = StubResource;
924 assert!(res.template().is_none());
925 assert!(res.icon().is_none());
926 assert!(res.version().is_none());
927 assert!(res.tags().is_empty());
928 assert!(res.timeout().is_none());
929 }
930
931 #[test]
932 fn resource_handler_read_with_uri_delegates_to_read() {
933 let res = StubResource;
934 let cx = Cx::for_testing();
935 let ctx = McpContext::new(cx, 1);
936 let params = UriParams::new();
937 let result = res.read_with_uri(&ctx, "file:///stub", ¶ms).unwrap();
938 assert_eq!(result.len(), 1);
939 }
940
941 struct StubPrompt;
944
945 impl PromptHandler for StubPrompt {
946 fn definition(&self) -> Prompt {
947 Prompt {
948 name: "stub".to_string(),
949 description: Some("a stub prompt".to_string()),
950 arguments: vec![],
951 icon: None,
952 version: None,
953 tags: vec![],
954 }
955 }
956
957 fn get(
958 &self,
959 _ctx: &McpContext,
960 _arguments: HashMap<String, String>,
961 ) -> McpResult<Vec<PromptMessage>> {
962 Ok(vec![])
963 }
964 }
965
966 #[test]
967 fn prompt_handler_defaults_return_none() {
968 let prompt = StubPrompt;
969 assert!(prompt.icon().is_none());
970 assert!(prompt.version().is_none());
971 assert!(prompt.tags().is_empty());
972 assert!(prompt.timeout().is_none());
973 }
974
975 #[test]
978 fn mounted_tool_handler_overrides_name() {
979 let inner = Box::new(StubTool) as BoxedToolHandler;
980 let mounted = MountedToolHandler::new(inner, "prefix_stub".to_string());
981 let def = mounted.definition();
982 assert_eq!(def.name, "prefix_stub");
983 assert_eq!(def.description.as_deref(), Some("a stub tool"));
984 }
985
986 #[test]
987 fn mounted_tool_handler_delegates_defaults() {
988 let inner = Box::new(StubTool) as BoxedToolHandler;
989 let mounted = MountedToolHandler::new(inner, "m_stub".to_string());
990 assert!(mounted.tags().is_empty());
991 assert!(mounted.annotations().is_none());
992 assert!(mounted.output_schema().is_none());
993 assert!(mounted.timeout().is_none());
994 }
995
996 #[test]
997 fn mounted_tool_handler_delegates_call() {
998 let inner = Box::new(StubTool) as BoxedToolHandler;
999 let mounted = MountedToolHandler::new(inner, "m_stub".to_string());
1000 let cx = Cx::for_testing();
1001 let ctx = McpContext::new(cx, 1);
1002 let result = mounted.call(&ctx, serde_json::json!({})).unwrap();
1003 assert!(!result.is_empty());
1004 }
1005
1006 #[test]
1009 fn mounted_resource_handler_overrides_uri() {
1010 let inner = Box::new(StubResource) as BoxedResourceHandler;
1011 let mounted = MountedResourceHandler::new(inner, "file:///mounted".to_string());
1012 let def = mounted.definition();
1013 assert_eq!(def.uri, "file:///mounted");
1014 assert_eq!(def.name, "stub");
1015 }
1016
1017 #[test]
1018 fn mounted_resource_handler_template_none_by_default() {
1019 let inner = Box::new(StubResource) as BoxedResourceHandler;
1020 let mounted = MountedResourceHandler::new(inner, "file:///m".to_string());
1021 assert!(mounted.template().is_none());
1022 }
1023
1024 #[test]
1025 fn mounted_resource_handler_with_template() {
1026 let inner = Box::new(StubResource) as BoxedResourceHandler;
1027 let tmpl = ResourceTemplate {
1028 uri_template: "file:///items/{id}".to_string(),
1029 name: "items".to_string(),
1030 description: None,
1031 mime_type: None,
1032 icon: None,
1033 version: None,
1034 tags: vec![],
1035 };
1036 let mounted =
1037 MountedResourceHandler::with_template(inner, "file:///items/{id}".to_string(), tmpl);
1038 let t = mounted.template().expect("template set");
1039 assert_eq!(t.uri_template, "file:///items/{id}");
1040 }
1041
1042 #[test]
1043 fn mounted_resource_handler_delegates_read() {
1044 let inner = Box::new(StubResource) as BoxedResourceHandler;
1045 let mounted = MountedResourceHandler::new(inner, "file:///m".to_string());
1046 let cx = Cx::for_testing();
1047 let ctx = McpContext::new(cx, 1);
1048 let result = mounted.read(&ctx).unwrap();
1049 assert_eq!(result.len(), 1);
1050 }
1051
1052 #[test]
1053 fn mounted_resource_handler_delegates_tags() {
1054 let inner = Box::new(StubResource) as BoxedResourceHandler;
1055 let mounted = MountedResourceHandler::new(inner, "file:///m".to_string());
1056 assert!(mounted.tags().is_empty());
1057 }
1058
1059 #[test]
1062 fn mounted_prompt_handler_overrides_name() {
1063 let inner = Box::new(StubPrompt) as BoxedPromptHandler;
1064 let mounted = MountedPromptHandler::new(inner, "ns_stub".to_string());
1065 let def = mounted.definition();
1066 assert_eq!(def.name, "ns_stub");
1067 assert_eq!(def.description.as_deref(), Some("a stub prompt"));
1068 }
1069
1070 #[test]
1071 fn mounted_prompt_handler_delegates_defaults() {
1072 let inner = Box::new(StubPrompt) as BoxedPromptHandler;
1073 let mounted = MountedPromptHandler::new(inner, "ns_stub".to_string());
1074 assert!(mounted.tags().is_empty());
1075 assert!(mounted.timeout().is_none());
1076 }
1077
1078 #[test]
1079 fn mounted_prompt_handler_delegates_get() {
1080 let inner = Box::new(StubPrompt) as BoxedPromptHandler;
1081 let mounted = MountedPromptHandler::new(inner, "ns_stub".to_string());
1082 let cx = Cx::for_testing();
1083 let ctx = McpContext::new(cx, 1);
1084 let result = mounted.get(&ctx, HashMap::new()).unwrap();
1085 assert!(result.is_empty());
1086 }
1087
1088 struct DummySamplingSender;
1091 impl fastmcp_core::SamplingSender for DummySamplingSender {
1092 fn create_message(
1093 &self,
1094 _request: fastmcp_core::SamplingRequest,
1095 ) -> std::pin::Pin<
1096 Box<
1097 dyn std::future::Future<Output = McpResult<fastmcp_core::SamplingResponse>>
1098 + Send
1099 + '_,
1100 >,
1101 > {
1102 Box::pin(async { Err(McpError::internal_error("stub")) })
1103 }
1104 }
1105
1106 struct DummyElicitationSender;
1107 impl fastmcp_core::ElicitationSender for DummyElicitationSender {
1108 fn elicit(
1109 &self,
1110 _request: fastmcp_core::ElicitationRequest,
1111 ) -> std::pin::Pin<
1112 Box<
1113 dyn std::future::Future<Output = McpResult<fastmcp_core::ElicitationResponse>>
1114 + Send
1115 + '_,
1116 >,
1117 > {
1118 Box::pin(async { Err(McpError::internal_error("stub")) })
1119 }
1120 }
1121
1122 #[test]
1123 fn bidirectional_senders_with_sampling() {
1124 let senders =
1125 BidirectionalSenders::new().with_sampling(Arc::new(DummySamplingSender) as Arc<_>);
1126 assert!(senders.sampling.is_some());
1127 assert!(senders.elicitation.is_none());
1128 }
1129
1130 #[test]
1131 fn bidirectional_senders_with_elicitation() {
1132 let senders = BidirectionalSenders::new()
1133 .with_elicitation(Arc::new(DummyElicitationSender) as Arc<_>);
1134 assert!(senders.sampling.is_none());
1135 assert!(senders.elicitation.is_some());
1136 }
1137
1138 #[test]
1139 fn bidirectional_senders_with_both() {
1140 let senders = BidirectionalSenders::new()
1141 .with_sampling(Arc::new(DummySamplingSender) as Arc<_>)
1142 .with_elicitation(Arc::new(DummyElicitationSender) as Arc<_>);
1143 assert!(senders.sampling.is_some());
1144 assert!(senders.elicitation.is_some());
1145 }
1146
1147 #[test]
1148 fn bidirectional_senders_clone() {
1149 let senders =
1150 BidirectionalSenders::new().with_sampling(Arc::new(DummySamplingSender) as Arc<_>);
1151 let cloned = senders.clone();
1152 assert!(cloned.sampling.is_some());
1153 }
1154
1155 #[test]
1156 fn bidirectional_senders_debug_with_present() {
1157 let senders = BidirectionalSenders::new()
1158 .with_sampling(Arc::new(DummySamplingSender) as Arc<_>)
1159 .with_elicitation(Arc::new(DummyElicitationSender) as Arc<_>);
1160 let debug = format!("{:?}", senders);
1161 assert!(debug.contains("sampling: true"));
1162 assert!(debug.contains("elicitation: true"));
1163 }
1164
1165 #[test]
1168 fn create_context_with_senders_sampling() {
1169 let cx = Cx::for_testing();
1170 let senders =
1171 BidirectionalSenders::new().with_sampling(Arc::new(DummySamplingSender) as Arc<_>);
1172 let ctx =
1173 create_context_with_progress_and_senders(cx, 1, None, None, |_| {}, Some(&senders));
1174 assert_eq!(ctx.request_id(), 1);
1175 }
1176
1177 #[test]
1178 fn create_context_with_senders_elicitation() {
1179 let cx = Cx::for_testing();
1180 let senders = BidirectionalSenders::new()
1181 .with_elicitation(Arc::new(DummyElicitationSender) as Arc<_>);
1182 let ctx =
1183 create_context_with_progress_and_senders(cx, 2, None, None, |_| {}, Some(&senders));
1184 assert_eq!(ctx.request_id(), 2);
1185 }
1186
1187 #[test]
1188 fn create_context_with_senders_and_progress() {
1189 let cx = Cx::for_testing();
1190 let marker = ProgressMarker::from("sp");
1191 let senders =
1192 BidirectionalSenders::new().with_sampling(Arc::new(DummySamplingSender) as Arc<_>);
1193 let ctx = create_context_with_progress_and_senders(
1194 cx,
1195 3,
1196 Some(marker),
1197 None,
1198 |_| {},
1199 Some(&senders),
1200 );
1201 assert_eq!(ctx.request_id(), 3);
1202 }
1203
1204 #[test]
1205 fn create_context_with_senders_and_state() {
1206 let cx = Cx::for_testing();
1207 let state = SessionState::new();
1208 state.set("key", &"val");
1209 let senders = BidirectionalSenders::new()
1210 .with_elicitation(Arc::new(DummyElicitationSender) as Arc<_>);
1211 let ctx = create_context_with_progress_and_senders(
1212 cx,
1213 4,
1214 None,
1215 Some(state),
1216 |_| {},
1217 Some(&senders),
1218 );
1219 let val: Option<String> = ctx.get_state("key");
1220 assert_eq!(val.as_deref(), Some("val"));
1221 }
1222
1223 #[test]
1224 fn create_context_with_all_options() {
1225 let cx = Cx::for_testing();
1226 let marker = ProgressMarker::from("all");
1227 let state = SessionState::new();
1228 let senders = BidirectionalSenders::new()
1229 .with_sampling(Arc::new(DummySamplingSender) as Arc<_>)
1230 .with_elicitation(Arc::new(DummyElicitationSender) as Arc<_>);
1231 let ctx = create_context_with_progress_and_senders(
1232 cx,
1233 5,
1234 Some(marker),
1235 Some(state),
1236 |_| {},
1237 Some(&senders),
1238 );
1239 assert_eq!(ctx.request_id(), 5);
1240 }
1241
1242 #[test]
1243 fn create_context_with_senders_none() {
1244 let cx = Cx::for_testing();
1245 let ctx = create_context_with_progress_and_senders(cx, 6, None, None, |_| {}, None);
1246 assert_eq!(ctx.request_id(), 6);
1247 }
1248
1249 struct CustomTool;
1252 impl ToolHandler for CustomTool {
1253 fn definition(&self) -> Tool {
1254 Tool {
1255 name: "custom".to_string(),
1256 description: None,
1257 input_schema: serde_json::json!({"type": "object"}),
1258 output_schema: None,
1259 icon: None,
1260 version: None,
1261 tags: vec![],
1262 annotations: None,
1263 }
1264 }
1265
1266 fn icon(&self) -> Option<&Icon> {
1267 None }
1269
1270 fn version(&self) -> Option<&str> {
1271 Some("2.0")
1272 }
1273
1274 fn timeout(&self) -> Option<Duration> {
1275 Some(Duration::from_secs(60))
1276 }
1277
1278 fn output_schema(&self) -> Option<serde_json::Value> {
1279 Some(serde_json::json!({"type": "string"}))
1280 }
1281
1282 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
1283 Ok(vec![Content::text("custom")])
1284 }
1285 }
1286
1287 #[test]
1288 fn tool_handler_custom_version() {
1289 assert_eq!(CustomTool.version(), Some("2.0"));
1290 }
1291
1292 #[test]
1293 fn tool_handler_custom_timeout() {
1294 assert_eq!(CustomTool.timeout(), Some(Duration::from_secs(60)));
1295 }
1296
1297 #[test]
1298 fn tool_handler_custom_output_schema() {
1299 let schema = CustomTool.output_schema().unwrap();
1300 assert_eq!(schema["type"], "string");
1301 }
1302
1303 struct CustomResource;
1306 impl ResourceHandler for CustomResource {
1307 fn definition(&self) -> Resource {
1308 Resource {
1309 uri: "file:///custom".to_string(),
1310 name: "custom".to_string(),
1311 description: None,
1312 mime_type: None,
1313 icon: None,
1314 version: None,
1315 tags: vec![],
1316 }
1317 }
1318
1319 fn version(&self) -> Option<&str> {
1320 Some("1.5")
1321 }
1322
1323 fn timeout(&self) -> Option<Duration> {
1324 Some(Duration::from_secs(30))
1325 }
1326
1327 fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
1328 Ok(vec![ResourceContent {
1329 uri: "file:///custom".to_string(),
1330 mime_type: None,
1331 text: Some("data".to_string()),
1332 blob: None,
1333 }])
1334 }
1335
1336 fn read_with_uri(
1337 &self,
1338 _ctx: &McpContext,
1339 uri: &str,
1340 params: &UriParams,
1341 ) -> McpResult<Vec<ResourceContent>> {
1342 let id = params.get("id").cloned().unwrap_or_default();
1343 Ok(vec![ResourceContent {
1344 uri: uri.to_string(),
1345 mime_type: None,
1346 text: Some(format!("item:{id}")),
1347 blob: None,
1348 }])
1349 }
1350 }
1351
1352 #[test]
1353 fn resource_handler_custom_version() {
1354 assert_eq!(CustomResource.version(), Some("1.5"));
1355 }
1356
1357 #[test]
1358 fn resource_handler_custom_timeout() {
1359 assert_eq!(CustomResource.timeout(), Some(Duration::from_secs(30)));
1360 }
1361
1362 #[test]
1363 fn resource_handler_read_with_uri_custom() {
1364 let cx = Cx::for_testing();
1365 let ctx = McpContext::new(cx, 1);
1366 let mut params = UriParams::new();
1367 params.insert("id".to_string(), "42".to_string());
1368 let result = CustomResource
1369 .read_with_uri(&ctx, "file:///items/42", ¶ms)
1370 .unwrap();
1371 assert_eq!(result[0].text.as_deref(), Some("item:42"));
1372 }
1373
1374 struct CustomPrompt;
1377 impl PromptHandler for CustomPrompt {
1378 fn definition(&self) -> Prompt {
1379 Prompt {
1380 name: "custom".to_string(),
1381 description: None,
1382 arguments: vec![],
1383 icon: None,
1384 version: None,
1385 tags: vec![],
1386 }
1387 }
1388
1389 fn version(&self) -> Option<&str> {
1390 Some("3.0")
1391 }
1392
1393 fn timeout(&self) -> Option<Duration> {
1394 Some(Duration::from_secs(10))
1395 }
1396
1397 fn get(
1398 &self,
1399 _ctx: &McpContext,
1400 _args: HashMap<String, String>,
1401 ) -> McpResult<Vec<PromptMessage>> {
1402 Ok(vec![])
1403 }
1404 }
1405
1406 #[test]
1407 fn prompt_handler_custom_version() {
1408 assert_eq!(CustomPrompt.version(), Some("3.0"));
1409 }
1410
1411 #[test]
1412 fn prompt_handler_custom_timeout() {
1413 assert_eq!(CustomPrompt.timeout(), Some(Duration::from_secs(10)));
1414 }
1415
1416 #[test]
1419 fn mounted_tool_handler_delegates_timeout() {
1420 let inner = Box::new(CustomTool) as BoxedToolHandler;
1421 let mounted = MountedToolHandler::new(inner, "m_custom".to_string());
1422 assert_eq!(mounted.timeout(), Some(Duration::from_secs(60)));
1423 }
1424
1425 #[test]
1426 fn mounted_tool_handler_delegates_output_schema() {
1427 let inner = Box::new(CustomTool) as BoxedToolHandler;
1428 let mounted = MountedToolHandler::new(inner, "m_custom".to_string());
1429 let schema = mounted.output_schema().unwrap();
1430 assert_eq!(schema["type"], "string");
1431 }
1432
1433 #[test]
1436 fn mounted_resource_handler_delegates_read_with_uri() {
1437 let inner = Box::new(CustomResource) as BoxedResourceHandler;
1438 let mounted = MountedResourceHandler::new(inner, "file:///mounted".to_string());
1439 let cx = Cx::for_testing();
1440 let ctx = McpContext::new(cx, 1);
1441 let mut params = UriParams::new();
1442 params.insert("id".to_string(), "99".to_string());
1443 let result = mounted
1444 .read_with_uri(&ctx, "file:///items/99", ¶ms)
1445 .unwrap();
1446 assert_eq!(result[0].text.as_deref(), Some("item:99"));
1447 }
1448
1449 #[test]
1450 fn mounted_resource_handler_delegates_timeout() {
1451 let inner = Box::new(CustomResource) as BoxedResourceHandler;
1452 let mounted = MountedResourceHandler::new(inner, "file:///m".to_string());
1453 assert_eq!(mounted.timeout(), Some(Duration::from_secs(30)));
1454 }
1455
1456 #[test]
1459 fn mounted_prompt_handler_delegates_timeout() {
1460 let inner = Box::new(CustomPrompt) as BoxedPromptHandler;
1461 let mounted = MountedPromptHandler::new(inner, "ns_custom".to_string());
1462 assert_eq!(mounted.timeout(), Some(Duration::from_secs(10)));
1463 }
1464
1465 #[test]
1466 fn mounted_prompt_handler_delegates_get_with_args() {
1467 let inner = Box::new(StubPrompt) as BoxedPromptHandler;
1468 let mounted = MountedPromptHandler::new(inner, "ns".to_string());
1469 let cx = Cx::for_testing();
1470 let ctx = McpContext::new(cx, 1);
1471 let mut args = HashMap::new();
1472 args.insert("key".to_string(), "value".to_string());
1473 let result = mounted.get(&ctx, args).unwrap();
1474 assert!(result.is_empty());
1475 }
1476
1477 #[test]
1480 fn progress_sender_multiple_notifications() {
1481 let sent = Arc::new(Mutex::new(Vec::new()));
1482 let sent_clone = Arc::clone(&sent);
1483 let sender = ProgressNotificationSender::new(ProgressMarker::from("multi"), move |req| {
1484 sent_clone.lock().unwrap().push(req);
1485 });
1486
1487 sender.send_progress(0.0, Some(100.0), Some("starting"));
1488 sender.send_progress(50.0, Some(100.0), None);
1489 sender.send_progress(100.0, Some(100.0), Some("done"));
1490
1491 let messages = sent.lock().unwrap();
1492 assert_eq!(messages.len(), 3);
1493 }
1494
1495 struct TaggedTool;
1498 impl ToolHandler for TaggedTool {
1499 fn definition(&self) -> Tool {
1500 Tool {
1501 name: "tagged".to_string(),
1502 description: None,
1503 input_schema: serde_json::json!({"type": "object"}),
1504 output_schema: None,
1505 icon: None,
1506 version: None,
1507 tags: vec!["db".to_string(), "read".to_string()],
1508 annotations: Some(ToolAnnotations {
1509 destructive: Some(false),
1510 idempotent: Some(true),
1511 read_only: Some(true),
1512 open_world_hint: None,
1513 }),
1514 }
1515 }
1516 fn tags(&self) -> &[String] {
1517 &[]
1519 }
1520 fn annotations(&self) -> Option<&ToolAnnotations> {
1521 None
1522 }
1523 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
1524 Ok(vec![Content::text("tagged")])
1525 }
1526 }
1527
1528 #[test]
1529 fn tool_definition_includes_tags_and_annotations() {
1530 let def = TaggedTool.definition();
1531 assert_eq!(def.tags, vec!["db".to_string(), "read".to_string()]);
1532 let ann = def.annotations.unwrap();
1533 assert_eq!(ann.destructive, Some(false));
1534 assert_eq!(ann.idempotent, Some(true));
1535 assert_eq!(ann.read_only, Some(true));
1536 }
1537
1538 #[test]
1541 fn tool_call_async_delegates_to_sync() {
1542 let tool = StubTool;
1543 let cx = Cx::for_testing();
1544 let ctx = McpContext::new(cx, 1);
1545 let outcome = fastmcp_core::block_on(tool.call_async(&ctx, serde_json::json!({"x": 1})));
1546 match outcome {
1547 Outcome::Ok(content) => assert!(!content.is_empty()),
1548 other => panic!("expected Ok, got {:?}", other),
1549 }
1550 }
1551
1552 #[test]
1553 fn resource_read_async_delegates_to_sync() {
1554 let res = StubResource;
1555 let cx = Cx::for_testing();
1556 let ctx = McpContext::new(cx, 1);
1557 let outcome = fastmcp_core::block_on(res.read_async(&ctx));
1558 match outcome {
1559 Outcome::Ok(content) => {
1560 assert_eq!(content.len(), 1);
1561 assert_eq!(content[0].text.as_deref(), Some("hello"));
1562 }
1563 other => panic!("expected Ok, got {:?}", other),
1564 }
1565 }
1566
1567 #[test]
1568 fn resource_read_async_with_uri_empty_params_uses_read_async() {
1569 let res = StubResource;
1570 let cx = Cx::for_testing();
1571 let ctx = McpContext::new(cx, 1);
1572 let params = UriParams::new(); let outcome =
1574 fastmcp_core::block_on(res.read_async_with_uri(&ctx, "file:///stub", ¶ms));
1575 match outcome {
1576 Outcome::Ok(content) => assert_eq!(content[0].text.as_deref(), Some("hello")),
1577 other => panic!("expected Ok, got {:?}", other),
1578 }
1579 }
1580
1581 #[test]
1582 fn resource_read_async_with_uri_nonempty_params_uses_read_with_uri() {
1583 let res = CustomResource;
1584 let cx = Cx::for_testing();
1585 let ctx = McpContext::new(cx, 1);
1586 let mut params = UriParams::new();
1587 params.insert("id".to_string(), "7".to_string());
1588 let outcome =
1589 fastmcp_core::block_on(res.read_async_with_uri(&ctx, "file:///items/7", ¶ms));
1590 match outcome {
1591 Outcome::Ok(content) => assert_eq!(content[0].text.as_deref(), Some("item:7")),
1592 other => panic!("expected Ok, got {:?}", other),
1593 }
1594 }
1595
1596 #[test]
1597 fn prompt_get_async_delegates_to_sync() {
1598 let prompt = StubPrompt;
1599 let cx = Cx::for_testing();
1600 let ctx = McpContext::new(cx, 1);
1601 let outcome = fastmcp_core::block_on(prompt.get_async(&ctx, HashMap::new()));
1602 match outcome {
1603 Outcome::Ok(messages) => assert!(messages.is_empty()),
1604 other => panic!("expected Ok, got {:?}", other),
1605 }
1606 }
1607
1608 #[test]
1611 fn tool_call_async_propagates_error() {
1612 struct ErrTool;
1613 impl ToolHandler for ErrTool {
1614 fn definition(&self) -> Tool {
1615 Tool {
1616 name: "err".to_string(),
1617 description: None,
1618 input_schema: serde_json::json!({"type": "object"}),
1619 output_schema: None,
1620 icon: None,
1621 version: None,
1622 tags: vec![],
1623 annotations: None,
1624 }
1625 }
1626 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
1627 Err(McpError::internal_error("async-err"))
1628 }
1629 }
1630 let cx = Cx::for_testing();
1631 let ctx = McpContext::new(cx, 1);
1632 let outcome = fastmcp_core::block_on(ErrTool.call_async(&ctx, serde_json::json!({})));
1633 match outcome {
1634 Outcome::Err(e) => assert!(e.message.contains("async-err")),
1635 other => panic!("expected Err, got {:?}", other),
1636 }
1637 }
1638
1639 #[test]
1642 fn mounted_tool_handler_delegates_call_async() {
1643 let inner = Box::new(StubTool) as BoxedToolHandler;
1644 let mounted = MountedToolHandler::new(inner, "m_stub".to_string());
1645 let cx = Cx::for_testing();
1646 let ctx = McpContext::new(cx, 1);
1647 let outcome = fastmcp_core::block_on(mounted.call_async(&ctx, serde_json::json!({})));
1648 match outcome {
1649 Outcome::Ok(content) => assert!(!content.is_empty()),
1650 other => panic!("expected Ok, got {:?}", other),
1651 }
1652 }
1653
1654 #[test]
1657 fn mounted_resource_handler_delegates_read_async() {
1658 let inner = Box::new(StubResource) as BoxedResourceHandler;
1659 let mounted = MountedResourceHandler::new(inner, "file:///m".to_string());
1660 let cx = Cx::for_testing();
1661 let ctx = McpContext::new(cx, 1);
1662 let outcome = fastmcp_core::block_on(mounted.read_async(&ctx));
1663 match outcome {
1664 Outcome::Ok(content) => assert_eq!(content.len(), 1),
1665 other => panic!("expected Ok, got {:?}", other),
1666 }
1667 }
1668
1669 #[test]
1670 fn mounted_resource_handler_delegates_read_async_with_uri() {
1671 let inner = Box::new(CustomResource) as BoxedResourceHandler;
1672 let mounted = MountedResourceHandler::new(inner, "file:///m".to_string());
1673 let cx = Cx::for_testing();
1674 let ctx = McpContext::new(cx, 1);
1675 let mut params = UriParams::new();
1676 params.insert("id".to_string(), "5".to_string());
1677 let outcome =
1678 fastmcp_core::block_on(mounted.read_async_with_uri(&ctx, "file:///items/5", ¶ms));
1679 match outcome {
1680 Outcome::Ok(content) => assert_eq!(content[0].text.as_deref(), Some("item:5")),
1681 other => panic!("expected Ok, got {:?}", other),
1682 }
1683 }
1684
1685 #[test]
1688 fn mounted_prompt_handler_delegates_get_async() {
1689 let inner = Box::new(StubPrompt) as BoxedPromptHandler;
1690 let mounted = MountedPromptHandler::new(inner, "ns".to_string());
1691 let cx = Cx::for_testing();
1692 let ctx = McpContext::new(cx, 1);
1693 let outcome = fastmcp_core::block_on(mounted.get_async(&ctx, HashMap::new()));
1694 match outcome {
1695 Outcome::Ok(messages) => assert!(messages.is_empty()),
1696 other => panic!("expected Ok, got {:?}", other),
1697 }
1698 }
1699
1700 #[test]
1703 fn progress_sender_with_message_but_no_total() {
1704 let sent = Arc::new(Mutex::new(Vec::new()));
1705 let sent_clone = Arc::clone(&sent);
1706 let sender = ProgressNotificationSender::new(ProgressMarker::from("tok-msg"), move |req| {
1707 sent_clone.lock().unwrap().push(req);
1708 });
1709
1710 sender.send_progress(2.0, None, Some("processing"));
1711
1712 let messages = sent.lock().unwrap();
1713 let params = messages[0].params.as_ref().unwrap();
1714 assert_eq!(params["progress"], 2.0);
1715 assert_eq!(params["message"], "processing");
1716 assert!(params.get("total").is_none() || params["total"].is_null());
1717 }
1718
1719 #[test]
1720 fn progress_notification_includes_progress_token() {
1721 let sent = Arc::new(Mutex::new(Vec::new()));
1722 let sent_clone = Arc::clone(&sent);
1723 let sender =
1724 ProgressNotificationSender::new(ProgressMarker::from("my-token"), move |req| {
1725 sent_clone.lock().unwrap().push(req);
1726 });
1727
1728 sender.send_progress(1.0, None, None);
1729
1730 let messages = sent.lock().unwrap();
1731 let params = messages[0].params.as_ref().unwrap();
1732 assert_eq!(params["progressToken"], "my-token");
1733 }
1734
1735 #[test]
1736 fn resource_read_async_propagates_error() {
1737 struct ErrResource;
1738 impl ResourceHandler for ErrResource {
1739 fn definition(&self) -> Resource {
1740 Resource {
1741 uri: "file:///err".to_string(),
1742 name: "err".to_string(),
1743 description: None,
1744 mime_type: None,
1745 icon: None,
1746 version: None,
1747 tags: vec![],
1748 }
1749 }
1750 fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
1751 Err(McpError::internal_error("read-fail"))
1752 }
1753 }
1754
1755 let cx = Cx::for_testing();
1756 let ctx = McpContext::new(cx, 1);
1757 let outcome = fastmcp_core::block_on(ErrResource.read_async(&ctx));
1758 match outcome {
1759 Outcome::Err(e) => assert!(e.message.contains("read-fail")),
1760 other => panic!("expected Err, got {:?}", other),
1761 }
1762 }
1763
1764 #[test]
1765 fn prompt_get_async_propagates_error() {
1766 struct ErrPrompt;
1767 impl PromptHandler for ErrPrompt {
1768 fn definition(&self) -> Prompt {
1769 Prompt {
1770 name: "err".to_string(),
1771 description: None,
1772 arguments: vec![],
1773 icon: None,
1774 version: None,
1775 tags: vec![],
1776 }
1777 }
1778 fn get(
1779 &self,
1780 _ctx: &McpContext,
1781 _args: HashMap<String, String>,
1782 ) -> McpResult<Vec<PromptMessage>> {
1783 Err(McpError::internal_error("get-fail"))
1784 }
1785 }
1786
1787 let cx = Cx::for_testing();
1788 let ctx = McpContext::new(cx, 1);
1789 let outcome = fastmcp_core::block_on(ErrPrompt.get_async(&ctx, HashMap::new()));
1790 match outcome {
1791 Outcome::Err(e) => assert!(e.message.contains("get-fail")),
1792 other => panic!("expected Err, got {:?}", other),
1793 }
1794 }
1795
1796 #[test]
1797 fn resource_read_async_with_uri_nonempty_params_propagates_error() {
1798 struct ErrWithUri;
1799 impl ResourceHandler for ErrWithUri {
1800 fn definition(&self) -> Resource {
1801 Resource {
1802 uri: "file:///err".to_string(),
1803 name: "err".to_string(),
1804 description: None,
1805 mime_type: None,
1806 icon: None,
1807 version: None,
1808 tags: vec![],
1809 }
1810 }
1811 fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
1812 Ok(vec![])
1813 }
1814 fn read_with_uri(
1815 &self,
1816 _ctx: &McpContext,
1817 _uri: &str,
1818 _params: &UriParams,
1819 ) -> McpResult<Vec<ResourceContent>> {
1820 Err(McpError::internal_error("uri-fail"))
1821 }
1822 }
1823
1824 let cx = Cx::for_testing();
1825 let ctx = McpContext::new(cx, 1);
1826 let mut params = UriParams::new();
1827 params.insert("id".to_string(), "1".to_string());
1828 let outcome =
1829 fastmcp_core::block_on(ErrWithUri.read_async_with_uri(&ctx, "file:///err", ¶ms));
1830 match outcome {
1831 Outcome::Err(e) => assert!(e.message.contains("uri-fail")),
1832 other => panic!("expected Err, got {:?}", other),
1833 }
1834 }
1835
1836 #[test]
1837 fn mounted_tool_definition_preserves_inner_fields() {
1838 let inner = Box::new(StubTool) as BoxedToolHandler;
1839 let mounted = MountedToolHandler::new(inner, "renamed".to_string());
1840 let def = mounted.definition();
1841 assert_eq!(def.name, "renamed");
1842 assert_eq!(def.description.as_deref(), Some("a stub tool"));
1843 assert_eq!(def.input_schema, serde_json::json!({"type": "object"}));
1844 }
1845}