1use std::collections::{BTreeMap, HashMap};
13use std::future::Future;
14use std::pin::Pin;
15use std::sync::{Arc, Mutex};
16use std::time::Duration;
17
18use asupersync::Cx;
19use fastmcp_core::{
20 McpCatalogKind, McpContext, McpError, McpLogLevel, McpOutcome, McpResult, NotificationSender,
21 Outcome, ProgressReporter, SessionState,
22};
23use fastmcp_protocol::common_types::ExactNonNegativeJsonNumber;
24use fastmcp_protocol::common_types::{
25 AbsoluteUri, Annotations, ContentBlock, EmbeddedResourceContents, OpenMetadata, RawIcon,
26};
27use fastmcp_protocol::{
28 AdmittedFinalFormSchema, CacheScope, CacheTtl, CompleteResult, CompletionValues, Content,
29 CoreResultDiscriminatorPolicy, DecodedResult, ExactJsonValue, FinalCallToolResult,
30 FinalCompletionParams, FinalCompletionReference, FinalCompletionValues,
31 FinalEmbeddedCreateMessageParams, FinalEmbeddedElicitationParams,
32 FinalEmbeddedFormElicitationParams, FinalEmbeddedInputRequest, FinalEmbeddedRootsListParams,
33 FinalEmbeddedUrlElicitationParams, FinalGetPromptResult, FinalProgressNotificationParams,
34 FinalPrompt, FinalPromptMessage, FinalReadResourceResult, FinalResource, FinalResourceTemplate,
35 FinalTool, Icon, InputRequiredResult, JsonRpcRequest, LegacyCompletionParams,
36 LegacyCompletionReference, LogLevel, LogMessageParams, ProgressMarker, ProgressParams, Prompt,
37 PromptMessage, Resource, ResourceContent, ResourceTemplate, ResultMeta, ResultPeerEra, Tool,
38 ToolAnnotations, decode_peer_result, encode_result, exact_json_from_serde,
39};
40
41use crate::bidirectional::MrtrCompletedInputs;
42#[cfg(feature = "proxy")]
43use crate::proxy::ProxyClient;
44#[cfg(feature = "tasks")]
45use crate::tasks::FinalTaskWorkDescriptor;
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub(crate) enum FinalResourceUriUse {
58 CatalogResource,
60 CatalogTemplate,
62 ResourceReadTarget,
64 ResourceReadContents,
66 PromptResourceLink,
68 PromptEmbeddedResource,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
79pub(crate) struct ResourceUriUsePolicy {
80 client_direct_https: bool,
81}
82
83impl ResourceUriUsePolicy {
84 #[must_use]
86 pub(crate) const fn server_mediated() -> Self {
87 Self {
88 client_direct_https: false,
89 }
90 }
91
92 #[must_use]
94 pub(crate) const fn from_client_direct_https(client_direct_https: bool) -> Self {
95 Self {
96 client_direct_https,
97 }
98 }
99
100 #[must_use]
102 pub(crate) fn admits(self, uri: &AbsoluteUri, use_site: FinalResourceUriUse) -> bool {
103 if !uri.has_scheme("https") {
104 return true;
105 }
106 self.client_direct_https
107 && matches!(
108 use_site,
109 FinalResourceUriUse::CatalogResource
110 | FinalResourceUriUse::CatalogTemplate
111 | FinalResourceUriUse::PromptResourceLink
112 )
113 }
114
115 #[must_use]
119 pub(crate) fn admits_template(self, uri_template: &str) -> bool {
120 let uses_https = uri_template
121 .split_once(':')
122 .is_some_and(|(scheme, _)| scheme.eq_ignore_ascii_case("https"));
123 !uses_https || self.client_direct_https
124 }
125}
126
127pub(crate) struct FinalProgressRuntime<F>
140where
141 F: Fn(JsonRpcRequest) + Send + Sync,
142{
143 marker: ProgressMarker,
144 send_fn: F,
145 state: Mutex<FinalProgressRuntimeState>,
146}
147
148#[derive(Clone, Copy, Debug, PartialEq, Eq)]
149enum FinalProgressRuntimePhase {
150 Open,
151 Finalizing,
152 Cancelled,
153}
154
155struct FinalProgressRuntimeState {
156 phase: FinalProgressRuntimePhase,
157 last_accepted_progress: Option<ExactNonNegativeJsonNumber>,
158 pending: Option<JsonRpcRequest>,
159}
160
161impl Default for FinalProgressRuntimeState {
162 fn default() -> Self {
163 Self {
164 phase: FinalProgressRuntimePhase::Open,
165 last_accepted_progress: None,
166 pending: None,
167 }
168 }
169}
170
171impl<F> FinalProgressRuntime<F>
172where
173 F: Fn(JsonRpcRequest) + Send + Sync,
174{
175 #[must_use]
177 pub(crate) fn new(marker: ProgressMarker, send_fn: F) -> Self {
178 Self {
179 marker,
180 send_fn,
181 state: Mutex::new(FinalProgressRuntimeState::default()),
182 }
183 }
184
185 pub(crate) fn into_reporter(self: Arc<Self>) -> ProgressReporter
188 where
189 Self: 'static,
190 {
191 ProgressReporter::with_marker(
192 serde_json::to_value(&self.marker).unwrap_or(serde_json::Value::Null),
193 self,
194 )
195 }
196
197 pub(crate) fn flush_pending(&self) -> bool {
203 let mut state = self
204 .state
205 .lock()
206 .unwrap_or_else(std::sync::PoisonError::into_inner);
207 if state.phase != FinalProgressRuntimePhase::Open {
208 return false;
209 }
210 self.emit_pending_locked(&mut state)
211 }
212
213 pub(crate) fn finalize(&self) -> bool {
219 let mut state = self
220 .state
221 .lock()
222 .unwrap_or_else(std::sync::PoisonError::into_inner);
223 if state.phase != FinalProgressRuntimePhase::Open {
224 return false;
225 }
226 state.phase = FinalProgressRuntimePhase::Finalizing;
227 self.emit_pending_locked(&mut state);
228 true
229 }
230
231 pub(crate) fn cancel(&self) -> bool {
235 let mut state = self
236 .state
237 .lock()
238 .unwrap_or_else(std::sync::PoisonError::into_inner);
239 if state.phase != FinalProgressRuntimePhase::Open {
240 return false;
241 }
242 state.phase = FinalProgressRuntimePhase::Cancelled;
243 state.pending = None;
244 true
245 }
246
247 fn enqueue_exact(
248 &self,
249 progress: ExactNonNegativeJsonNumber,
250 total: Option<ExactNonNegativeJsonNumber>,
251 message: Option<&str>,
252 ) {
253 let params = FinalProgressNotificationParams {
254 progress_token: self.marker.clone(),
255 progress: progress.clone(),
256 total,
257 message: message.map(str::to_owned),
258 meta: None,
259 additional: BTreeMap::new(),
260 };
261 let Ok(serialized_params) = serde_json::to_value(params) else {
262 log::warn!(
263 target: "fastmcp_rust::handler",
264 "final progress notification rejected; reason=serialization_failure"
265 );
266 return;
267 };
268 let notification =
269 JsonRpcRequest::notification("notifications/progress", Some(serialized_params));
270
271 let mut state = self
272 .state
273 .lock()
274 .unwrap_or_else(std::sync::PoisonError::into_inner);
275 if state.phase != FinalProgressRuntimePhase::Open {
276 return;
277 }
278 if state
279 .last_accepted_progress
280 .as_ref()
281 .is_some_and(|last| progress.cmp(last).is_le())
282 {
283 log::debug!(
284 target: "fastmcp_rust::handler",
285 "final progress notification rejected; reason=non_monotonic_progress"
286 );
287 return;
288 }
289 state.last_accepted_progress = Some(progress);
290 state.pending = Some(notification);
294 }
295
296 fn emit_pending_locked(&self, state: &mut FinalProgressRuntimeState) -> bool {
297 let Some(notification) = state.pending.take() else {
298 return false;
299 };
300 if crate::catch_extension_unwind(|| (self.send_fn)(notification)).is_err() {
301 log::error!(
302 target: "fastmcp_rust::handler",
303 "progress notification callback terminated unexpectedly; detail=panic_payload_redacted"
304 );
305 }
306 true
307 }
308}
309
310impl<F> NotificationSender for FinalProgressRuntime<F>
311where
312 F: Fn(JsonRpcRequest) + Send + Sync,
313{
314 fn send_progress_exact(
315 &self,
316 progress: serde_json::Number,
317 total: Option<serde_json::Number>,
318 message: Option<&str>,
319 ) {
320 let progress = match ExactNonNegativeJsonNumber::try_from_number(progress) {
321 Ok(progress) => progress,
322 Err(_) => {
323 log::warn!(
324 target: "fastmcp_rust::handler",
325 "final progress notification rejected; reason=invalid_finite_numeric_value"
326 );
327 return;
328 }
329 };
330 let total = match total {
331 Some(total) => match ExactNonNegativeJsonNumber::try_from_number(total) {
332 Ok(total) => Some(total),
333 Err(_) => {
334 log::warn!(
335 target: "fastmcp_rust::handler",
336 "final progress notification rejected; reason=invalid_finite_numeric_value"
337 );
338 return;
339 }
340 },
341 None => None,
342 };
343 self.enqueue_exact(progress, total, message);
344 }
345
346 fn send_progress(&self, progress: f64, total: Option<f64>, message: Option<&str>) {
347 let Some(progress) = exact_finite_progress_from_f64(progress) else {
348 log::warn!(
349 target: "fastmcp_rust::handler",
350 "final progress notification rejected; reason=invalid_finite_numeric_value"
351 );
352 return;
353 };
354 let total = match total {
355 Some(total) => match exact_finite_progress_from_f64(total) {
356 Some(total) => Some(total),
357 None => {
358 log::warn!(
359 target: "fastmcp_rust::handler",
360 "final progress notification rejected; reason=invalid_finite_numeric_value"
361 );
362 return;
363 }
364 },
365 None => None,
366 };
367 self.enqueue_exact(progress, total, message);
368 }
369}
370
371impl<F> std::fmt::Debug for FinalProgressRuntime<F>
372where
373 F: Fn(JsonRpcRequest) + Send + Sync,
374{
375 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
376 f.debug_struct("FinalProgressRuntime")
377 .finish_non_exhaustive()
378 }
379}
380
381pub struct ProgressNotificationSender<F>
392where
393 F: Fn(JsonRpcRequest) + Send + Sync,
394{
395 marker: ProgressMarker,
397 final_protocol: bool,
400 send_fn: F,
402}
403
404impl<F> ProgressNotificationSender<F>
405where
406 F: Fn(JsonRpcRequest) + Send + Sync,
407{
408 pub fn new(marker: ProgressMarker, send_fn: F) -> Self {
410 Self {
411 marker,
412 final_protocol: false,
413 send_fn,
414 }
415 }
416
417 pub fn new_final(marker: ProgressMarker, send_fn: F) -> Self {
424 Self {
425 marker,
426 final_protocol: true,
427 send_fn,
428 }
429 }
430
431 pub fn into_reporter(self) -> ProgressReporter
433 where
434 Self: 'static,
435 {
436 ProgressReporter::with_marker(
437 serde_json::to_value(&self.marker).unwrap_or(serde_json::Value::Null),
438 Arc::new(self),
439 )
440 }
441
442 fn send_progress_with_serializer<E>(
443 &self,
444 progress: f64,
445 total: Option<f64>,
446 message: Option<&str>,
447 serialize: impl FnOnce(&ProgressParams) -> Result<serde_json::Value, E>,
448 ) {
449 if !progress.is_finite() || total.is_some_and(|value| !value.is_finite()) {
450 log::warn!(
451 target: "fastmcp_rust::handler",
452 "progress notification rejected; reason=non_finite_numeric_value"
453 );
454 return;
455 }
456
457 let params = match total {
458 Some(value) => ProgressParams::with_total(self.marker.clone(), progress, value),
459 None => ProgressParams::new(self.marker.clone(), progress),
460 };
461
462 let params = if let Some(value) = message {
463 params.with_message(value)
464 } else {
465 params
466 };
467
468 let Ok(serialized_params) = serialize(¶ms) else {
469 log::warn!(
470 target: "fastmcp_rust::handler",
471 "progress notification rejected; reason=serialization_failure"
472 );
473 return;
474 };
475
476 let notification =
477 JsonRpcRequest::notification("notifications/progress", Some(serialized_params));
478 if crate::catch_extension_unwind(|| (self.send_fn)(notification)).is_err() {
479 log::error!(
480 target: "fastmcp_rust::handler",
481 "progress notification callback terminated unexpectedly; detail=panic_payload_redacted"
482 );
483 }
484 }
485
486 fn send_final_progress_exact(
492 &self,
493 progress: ExactNonNegativeJsonNumber,
494 total: Option<ExactNonNegativeJsonNumber>,
495 message: Option<&str>,
496 ) {
497 if !self.final_protocol {
498 log::warn!(
499 target: "fastmcp_rust::handler",
500 "final progress notification rejected; reason=legacy_sender"
501 );
502 return;
503 }
504 let params = FinalProgressNotificationParams {
505 progress_token: self.marker.clone(),
506 progress,
507 total,
508 message: message.map(str::to_owned),
509 meta: None,
510 additional: BTreeMap::new(),
511 };
512 let Ok(serialized_params) = serde_json::to_value(params) else {
513 log::warn!(
514 target: "fastmcp_rust::handler",
515 "final progress notification rejected; reason=serialization_failure"
516 );
517 return;
518 };
519 let notification =
520 JsonRpcRequest::notification("notifications/progress", Some(serialized_params));
521 if crate::catch_extension_unwind(|| (self.send_fn)(notification)).is_err() {
522 log::error!(
523 target: "fastmcp_rust::handler",
524 "progress notification callback terminated unexpectedly; detail=panic_payload_redacted"
525 );
526 }
527 }
528}
529
530impl<F> NotificationSender for ProgressNotificationSender<F>
531where
532 F: Fn(JsonRpcRequest) + Send + Sync,
533{
534 fn send_progress_exact(
535 &self,
536 progress: serde_json::Number,
537 total: Option<serde_json::Number>,
538 message: Option<&str>,
539 ) {
540 if !self.final_protocol {
541 log::warn!(
542 target: "fastmcp_rust::handler",
543 "final progress notification rejected; reason=legacy_sender"
544 );
545 return;
546 }
547 let progress = match ExactNonNegativeJsonNumber::try_from_number(progress) {
548 Ok(progress) => progress,
549 Err(_) => {
550 log::warn!(
551 target: "fastmcp_rust::handler",
552 "final progress notification rejected; reason=invalid_finite_numeric_value"
553 );
554 return;
555 }
556 };
557 let total = match total {
558 Some(total) => match ExactNonNegativeJsonNumber::try_from_number(total) {
559 Ok(total) => Some(total),
560 Err(_) => {
561 log::warn!(
562 target: "fastmcp_rust::handler",
563 "final progress notification rejected; reason=invalid_finite_numeric_value"
564 );
565 return;
566 }
567 },
568 None => None,
569 };
570 self.send_final_progress_exact(progress, total, message);
571 }
572
573 fn send_progress(&self, progress: f64, total: Option<f64>, message: Option<&str>) {
574 if self.final_protocol {
575 let Some(progress) = exact_finite_progress_from_f64(progress) else {
576 log::warn!(
577 target: "fastmcp_rust::handler",
578 "final progress notification rejected; reason=invalid_finite_numeric_value"
579 );
580 return;
581 };
582 let total = match total {
583 Some(total) => match exact_finite_progress_from_f64(total) {
584 Some(total) => Some(total),
585 None => {
586 log::warn!(
587 target: "fastmcp_rust::handler",
588 "final progress notification rejected; reason=invalid_finite_numeric_value"
589 );
590 return;
591 }
592 },
593 None => None,
594 };
595 self.send_final_progress_exact(progress, total, message);
596 return;
597 }
598 self.send_progress_with_serializer(progress, total, message, |params| {
599 serde_json::to_value(params)
600 });
601 }
602}
603
604fn exact_finite_progress_from_f64(value: f64) -> Option<ExactNonNegativeJsonNumber> {
605 value
606 .is_finite()
607 .then(|| ExactNonNegativeJsonNumber::parse(&value.to_string()).ok())
608 .flatten()
609}
610
611impl<F> std::fmt::Debug for ProgressNotificationSender<F>
612where
613 F: Fn(JsonRpcRequest) + Send + Sync,
614{
615 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
616 f.debug_struct("ProgressNotificationSender")
617 .finish_non_exhaustive()
618 }
619}
620
621pub(crate) struct LogNotificationSender<F> {
623 send_fn: F,
624}
625
626impl<F> LogNotificationSender<F>
627where
628 F: Fn(JsonRpcRequest) + Send + Sync,
629{
630 pub(crate) fn new(send_fn: F) -> Self {
631 Self { send_fn }
632 }
633}
634
635impl<F> NotificationSender for LogNotificationSender<F>
636where
637 F: Fn(JsonRpcRequest) + Send + Sync,
638{
639 fn send_progress(&self, _progress: f64, _total: Option<f64>, _message: Option<&str>) {}
640
641 fn send_log(&self, level: McpLogLevel, logger: Option<&str>, data: serde_json::Value) {
642 let params = LogMessageParams {
643 level: protocol_log_level(level),
644 logger: logger.map(str::to_owned),
645 data,
646 };
647 let Ok(payload) = serde_json::to_value(params) else {
648 return;
649 };
650 let notification = JsonRpcRequest::notification("notifications/message", Some(payload));
651 if crate::catch_extension_unwind(|| (self.send_fn)(notification)).is_err() {
652 log::error!(
653 target: "fastmcp_rust::handler",
654 "log notification callback terminated unexpectedly; detail=panic_payload_redacted"
655 );
656 }
657 }
658
659 fn send_catalog_changed(&self, kind: McpCatalogKind) {
660 let method = match kind {
661 McpCatalogKind::Tools => "notifications/tools/list_changed",
662 McpCatalogKind::Resources => "notifications/resources/list_changed",
663 McpCatalogKind::Prompts => "notifications/prompts/list_changed",
664 };
665 let notification = JsonRpcRequest::notification(method, Some(serde_json::json!({})));
666 if crate::catch_extension_unwind(|| (self.send_fn)(notification)).is_err() {
667 log::error!(
668 target: "fastmcp_rust::handler",
669 "catalog change notification callback terminated unexpectedly; detail=panic_payload_redacted"
670 );
671 }
672 }
673
674 fn send_resource_updated(&self, uri: &str) {
675 let params = fastmcp_protocol::ResourceUpdatedNotificationParams {
676 uri: uri.to_owned(),
677 };
678 let Ok(payload) = serde_json::to_value(params) else {
679 return;
680 };
681 let notification =
682 JsonRpcRequest::notification("notifications/resources/updated", Some(payload));
683 if crate::catch_extension_unwind(|| (self.send_fn)(notification)).is_err() {
684 log::error!(
685 target: "fastmcp_rust::handler",
686 "resource update notification callback terminated unexpectedly; detail=panic_payload_redacted"
687 );
688 }
689 }
690}
691
692pub(crate) fn mcp_log_level(level: LogLevel) -> McpLogLevel {
693 match level {
694 LogLevel::Debug => McpLogLevel::Debug,
695 LogLevel::Info => McpLogLevel::Info,
696 LogLevel::Notice => McpLogLevel::Notice,
697 LogLevel::Warning => McpLogLevel::Warning,
698 LogLevel::Error => McpLogLevel::Error,
699 LogLevel::Critical => McpLogLevel::Critical,
700 LogLevel::Alert => McpLogLevel::Alert,
701 LogLevel::Emergency => McpLogLevel::Emergency,
702 }
703}
704
705fn protocol_log_level(level: McpLogLevel) -> LogLevel {
706 match level {
707 McpLogLevel::Debug => LogLevel::Debug,
708 McpLogLevel::Info => LogLevel::Info,
709 McpLogLevel::Notice => LogLevel::Notice,
710 McpLogLevel::Warning => LogLevel::Warning,
711 McpLogLevel::Error => LogLevel::Error,
712 McpLogLevel::Critical => LogLevel::Critical,
713 McpLogLevel::Alert => LogLevel::Alert,
714 McpLogLevel::Emergency => LogLevel::Emergency,
715 }
716}
717
718#[derive(Clone, Default)]
720pub struct BidirectionalSenders {
721 pub sampling: Option<Arc<dyn fastmcp_core::SamplingSender>>,
723 pub elicitation: Option<Arc<dyn fastmcp_core::ElicitationSender>>,
725 pub roots: Option<Arc<dyn fastmcp_core::RootsProvider>>,
727}
728
729impl BidirectionalSenders {
730 #[must_use]
732 pub fn new() -> Self {
733 Self::default()
734 }
735
736 #[must_use]
738 pub fn with_sampling(mut self, sender: Arc<dyn fastmcp_core::SamplingSender>) -> Self {
739 self.sampling = Some(sender);
740 self
741 }
742
743 #[must_use]
745 pub fn with_elicitation(mut self, sender: Arc<dyn fastmcp_core::ElicitationSender>) -> Self {
746 self.elicitation = Some(sender);
747 self
748 }
749
750 #[must_use]
752 pub fn with_roots(mut self, provider: Arc<dyn fastmcp_core::RootsProvider>) -> Self {
753 self.roots = Some(provider);
754 self
755 }
756}
757
758impl std::fmt::Debug for BidirectionalSenders {
759 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
760 f.debug_struct("BidirectionalSenders")
761 .field("sampling", &self.sampling.is_some())
762 .field("elicitation", &self.elicitation.is_some())
763 .field("roots", &self.roots.is_some())
764 .finish()
765 }
766}
767
768pub fn create_context_with_progress<F>(
770 cx: asupersync::Cx,
771 request_id: u64,
772 progress_marker: Option<ProgressMarker>,
773 state: Option<SessionState>,
774 send_fn: F,
775) -> McpContext
776where
777 F: Fn(JsonRpcRequest) + Send + Sync + 'static,
778{
779 create_context_with_progress_and_senders(cx, request_id, progress_marker, state, send_fn, None)
780}
781
782pub fn create_context_with_progress_and_senders<F>(
784 cx: asupersync::Cx,
785 request_id: u64,
786 progress_marker: Option<ProgressMarker>,
787 state: Option<SessionState>,
788 send_fn: F,
789 senders: Option<&BidirectionalSenders>,
790) -> McpContext
791where
792 F: Fn(JsonRpcRequest) + Send + Sync + 'static,
793{
794 let mut ctx = match (progress_marker, state) {
795 (Some(marker), Some(state)) => {
796 let sender = ProgressNotificationSender::new(marker, send_fn);
797 McpContext::with_state_and_progress(cx, request_id, state, sender.into_reporter())
798 }
799 (Some(marker), None) => {
800 let sender = ProgressNotificationSender::new(marker, send_fn);
801 McpContext::with_progress(cx, request_id, sender.into_reporter())
802 }
803 (None, Some(state)) => McpContext::with_state(cx, request_id, state),
804 (None, None) => McpContext::new(cx, request_id),
805 };
806
807 if let Some(senders) = senders {
809 if let Some(ref sampling) = senders.sampling {
810 ctx = ctx.with_sampling(sampling.clone());
811 }
812 if let Some(ref elicitation) = senders.elicitation {
813 ctx = ctx.with_elicitation(elicitation.clone());
814 }
815 if let Some(ref roots) = senders.roots {
816 ctx = ctx.with_roots_provider(roots.clone());
817 }
818 }
819
820 ctx
821}
822
823pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
825
826#[derive(Debug, Clone)]
834pub enum FinalMethodOutcome<T> {
835 Complete(CompleteResult<T>),
837 InputRequired(InputRequiredResult),
839}
840
841impl<T> From<CompleteResult<T>> for FinalMethodOutcome<T> {
842 fn from(result: CompleteResult<T>) -> Self {
843 Self::Complete(result)
844 }
845}
846
847#[derive(Debug, Clone, Copy, PartialEq, Eq)]
853pub enum FinalResourceReadCacheHintProvenance {
854 RouterPolicy,
856 Explicit,
858}
859
860pub enum FinalToolOutcome {
867 Complete(CompleteResult<FinalCallToolResult>),
869 InputRequired(InputRequiredResult),
874 #[cfg(feature = "tasks")]
876 CreateTask {
877 work_descriptor: FinalTaskWorkDescriptor,
883 status_message: Option<String>,
885 },
886}
887
888impl From<CompleteResult<FinalCallToolResult>> for FinalToolOutcome {
889 fn from(result: CompleteResult<FinalCallToolResult>) -> Self {
890 Self::Complete(result)
891 }
892}
893
894impl From<InputRequiredResult> for FinalToolOutcome {
895 fn from(result: InputRequiredResult) -> Self {
896 Self::InputRequired(result)
897 }
898}
899
900#[derive(Debug, Clone)]
909pub struct FinalElicitation {
910 input_key: String,
911 parameters: FinalEmbeddedElicitationParams,
912}
913
914impl FinalElicitation {
915 fn new(
916 context: &McpContext,
917 input_key: impl Into<String>,
918 parameters: FinalEmbeddedElicitationParams,
919 ) -> McpResult<Self> {
920 let supported = match ¶meters {
921 FinalEmbeddedElicitationParams::Form(_) => context.client_supports_elicitation_form(),
922 FinalEmbeddedElicitationParams::Url(_) => context.client_supports_elicitation_url(),
923 };
924 if !supported {
925 return Err(McpError::invalid_request(
926 "Final elicitation mode is not advertised by the client",
927 ));
928 }
929 let input_key = input_key.into();
930 if input_key.is_empty() {
931 return Err(McpError::invalid_params(
932 "Final elicitation input key must not be empty",
933 ));
934 }
935 Ok(Self {
936 input_key,
937 parameters,
938 })
939 }
940
941 #[must_use]
943 pub fn input_key(&self) -> &str {
944 &self.input_key
945 }
946
947 pub fn into_input_required(self) -> McpResult<InputRequiredResult> {
951 let descriptor = FinalEmbeddedInputRequest::Elicitation(self.parameters);
952 let wire = serde_json::json!({self.input_key: descriptor});
953 let ExactJsonValue::Object(input_requests) = exact_json_from_serde(&wire)
954 .map_err(|error| McpError::invalid_params(error.to_string()))?
955 else {
956 return Err(McpError::internal_error(
957 "Final elicitation input requests must encode as an object",
958 ));
959 };
960 InputRequiredResult::new(Some(input_requests), None, ResultMeta::empty())
961 .map_err(|error| McpError::invalid_params(error.to_string()))
962 }
963}
964
965pub trait FinalElicitationContextExt {
973 fn final_elicitation_form(
975 &self,
976 input_key: impl Into<String>,
977 message: impl Into<String>,
978 requested_schema: serde_json::Value,
979 ) -> McpResult<FinalElicitation>;
980
981 fn final_elicitation_url(
983 &self,
984 input_key: impl Into<String>,
985 message: impl Into<String>,
986 url: impl Into<String>,
987 ) -> McpResult<FinalElicitation>;
988}
989
990impl FinalElicitationContextExt for McpContext {
991 fn final_elicitation_form(
992 &self,
993 input_key: impl Into<String>,
994 message: impl Into<String>,
995 requested_schema: serde_json::Value,
996 ) -> McpResult<FinalElicitation> {
997 let requested_schema = AdmittedFinalFormSchema::admit(requested_schema)
998 .map_err(|error| McpError::invalid_params(error.to_string()))?;
999 FinalElicitation::new(
1000 self,
1001 input_key,
1002 FinalEmbeddedElicitationParams::Form(FinalEmbeddedFormElicitationParams {
1003 mode: fastmcp_protocol::ElicitMode::Form,
1004 message: message.into(),
1005 requested_schema,
1006 }),
1007 )
1008 }
1009
1010 fn final_elicitation_url(
1011 &self,
1012 input_key: impl Into<String>,
1013 message: impl Into<String>,
1014 url: impl Into<String>,
1015 ) -> McpResult<FinalElicitation> {
1016 let url =
1017 AbsoluteUri::parse(url).map_err(|error| McpError::invalid_params(error.to_string()))?;
1018 FinalElicitation::new(
1019 self,
1020 input_key,
1021 FinalEmbeddedElicitationParams::Url(FinalEmbeddedUrlElicitationParams {
1022 mode: fastmcp_protocol::ElicitMode::Url,
1023 message: message.into(),
1024 url,
1025 }),
1026 )
1027 }
1028}
1029
1030#[derive(Debug, Clone)]
1036pub struct FinalSampling {
1037 input_key: String,
1038 parameters: FinalEmbeddedCreateMessageParams,
1039}
1040
1041impl FinalSampling {
1042 fn new(
1043 context: &McpContext,
1044 input_key: impl Into<String>,
1045 parameters: FinalEmbeddedCreateMessageParams,
1046 ) -> McpResult<Self> {
1047 if !context.client_supports_sampling() {
1048 return Err(McpError::invalid_request(
1049 "Final sampling is not advertised by the client",
1050 ));
1051 }
1052 let input_key = input_key.into();
1053 if input_key.is_empty() {
1054 return Err(McpError::invalid_params(
1055 "Final sampling input key must not be empty",
1056 ));
1057 }
1058 Ok(Self {
1059 input_key,
1060 parameters,
1061 })
1062 }
1063
1064 #[must_use]
1066 pub fn input_key(&self) -> &str {
1067 &self.input_key
1068 }
1069
1070 pub fn into_input_required(self) -> McpResult<InputRequiredResult> {
1072 let descriptor = FinalEmbeddedInputRequest::Sampling(self.parameters);
1073 let wire = serde_json::json!({self.input_key: descriptor});
1074 let ExactJsonValue::Object(input_requests) = exact_json_from_serde(&wire)
1075 .map_err(|error| McpError::invalid_params(error.to_string()))?
1076 else {
1077 return Err(McpError::internal_error(
1078 "Final sampling input requests must encode as an object",
1079 ));
1080 };
1081 InputRequiredResult::new(Some(input_requests), None, ResultMeta::empty())
1082 .map_err(|error| McpError::invalid_params(error.to_string()))
1083 }
1084}
1085
1086pub trait FinalSamplingContextExt {
1092 fn final_sampling(
1094 &self,
1095 input_key: impl Into<String>,
1096 parameters: FinalEmbeddedCreateMessageParams,
1097 ) -> McpResult<FinalSampling>;
1098}
1099
1100impl FinalSamplingContextExt for McpContext {
1101 fn final_sampling(
1102 &self,
1103 input_key: impl Into<String>,
1104 parameters: FinalEmbeddedCreateMessageParams,
1105 ) -> McpResult<FinalSampling> {
1106 FinalSampling::new(self, input_key, parameters)
1107 }
1108}
1109
1110#[derive(Debug, Clone)]
1116pub struct FinalRoots {
1117 input_key: String,
1118 parameters: FinalEmbeddedRootsListParams,
1119}
1120
1121impl FinalRoots {
1122 fn new(
1123 context: &McpContext,
1124 input_key: impl Into<String>,
1125 parameters: FinalEmbeddedRootsListParams,
1126 ) -> McpResult<Self> {
1127 if !context.client_supports_roots() {
1128 return Err(McpError::invalid_request(
1129 "Final roots listing is not advertised by the client",
1130 ));
1131 }
1132 let input_key = input_key.into();
1133 if input_key.is_empty() {
1134 return Err(McpError::invalid_params(
1135 "Final roots input key must not be empty",
1136 ));
1137 }
1138 Ok(Self {
1139 input_key,
1140 parameters,
1141 })
1142 }
1143
1144 #[must_use]
1146 pub fn input_key(&self) -> &str {
1147 &self.input_key
1148 }
1149
1150 pub fn into_input_required(self) -> McpResult<InputRequiredResult> {
1152 let descriptor = FinalEmbeddedInputRequest::Roots(self.parameters);
1153 let wire = serde_json::json!({self.input_key: descriptor});
1154 let ExactJsonValue::Object(input_requests) = exact_json_from_serde(&wire)
1155 .map_err(|error| McpError::invalid_params(error.to_string()))?
1156 else {
1157 return Err(McpError::internal_error(
1158 "Final roots input requests must encode as an object",
1159 ));
1160 };
1161 InputRequiredResult::new(Some(input_requests), None, ResultMeta::empty())
1162 .map_err(|error| McpError::invalid_params(error.to_string()))
1163 }
1164}
1165
1166pub trait FinalRootsContextExt {
1172 fn final_roots(
1174 &self,
1175 input_key: impl Into<String>,
1176 parameters: FinalEmbeddedRootsListParams,
1177 ) -> McpResult<FinalRoots>;
1178}
1179
1180impl FinalRootsContextExt for McpContext {
1181 fn final_roots(
1182 &self,
1183 input_key: impl Into<String>,
1184 parameters: FinalEmbeddedRootsListParams,
1185 ) -> McpResult<FinalRoots> {
1186 FinalRoots::new(self, input_key, parameters)
1187 }
1188}
1189
1190#[cfg(feature = "tasks")]
1191const UNDECLARED_FINAL_TASK_OUTCOME_ERROR: &str =
1192 "tool returned CreateTask without declaring final Tasks capability";
1193
1194#[cfg(feature = "tasks")]
1195fn admit_declared_final_tool_outcome(
1196 declares_final_tasks: bool,
1197 outcome: FinalToolOutcome,
1198) -> McpResult<FinalToolOutcome> {
1199 if matches!(&outcome, FinalToolOutcome::CreateTask { .. }) && !declares_final_tasks {
1200 return Err(McpError::invalid_request(
1201 UNDECLARED_FINAL_TASK_OUTCOME_ERROR,
1202 ));
1203 }
1204
1205 Ok(outcome)
1206}
1207
1208pub type UriParams = HashMap<String, String>;
1210
1211pub(crate) fn encode_final_complete_result<T: serde::Serialize>(
1220 payload: T,
1221) -> McpResult<serde_json::Value> {
1222 let serde_json::Value::Object(mut members) =
1223 serde_json::to_value(payload).map_err(McpError::from)?
1224 else {
1225 return Err(McpError::internal_error(
1226 "modern complete result payload must serialize as an object",
1227 ));
1228 };
1229
1230 if members.contains_key("resultType") {
1231 return Err(McpError::internal_error(
1232 "modern complete result payload must not select a result type",
1233 ));
1234 }
1235 members.insert(
1236 "resultType".to_string(),
1237 serde_json::Value::String("complete".to_string()),
1238 );
1239
1240 let encoded = serde_json::to_string(&members).map_err(McpError::from)?;
1241 let (decoded, diagnostic) = decode_peer_result(
1242 &encoded,
1243 ResultPeerEra::Modern,
1244 &CoreResultDiscriminatorPolicy,
1245 )
1246 .map_err(|_| McpError::internal_error("modern complete result violates the final contract"))?;
1247
1248 if diagnostic.is_some() || !matches!(decoded, DecodedResult::Complete(_)) {
1249 return Err(McpError::internal_error(
1250 "modern complete result violates the final contract",
1251 ));
1252 }
1253
1254 serde_json::from_str(&encode_result(&decoded)).map_err(McpError::from)
1255}
1256
1257pub(crate) fn empty_final_result_meta() -> McpResult<ResultMeta> {
1263 let (decoded, diagnostic) = decode_peer_result(
1264 r#"{"resultType":"complete"}"#,
1265 ResultPeerEra::Modern,
1266 &CoreResultDiscriminatorPolicy,
1267 )
1268 .map_err(|_| McpError::internal_error("empty final result metadata is invalid"))?;
1269 if diagnostic.is_some() {
1270 return Err(McpError::internal_error(
1271 "empty final result metadata must select the complete discriminator",
1272 ));
1273 }
1274 let DecodedResult::Complete(empty_result) = decoded else {
1275 return Err(McpError::internal_error(
1276 "empty final result metadata must select the complete result",
1277 ));
1278 };
1279 Ok(empty_result.meta)
1280}
1281
1282pub fn promote_legacy_tool_content(
1294 content: Vec<Content>,
1295) -> McpResult<CompleteResult<FinalCallToolResult>> {
1296 let content = content
1297 .into_iter()
1298 .map(|content| match content {
1299 Content::Text { text } => Ok(ContentBlock::Text {
1300 text,
1301 annotations: None,
1302 meta: None,
1303 additional: BTreeMap::new(),
1304 }),
1305 Content::Image { data, mime_type } => Ok(ContentBlock::Image {
1306 data,
1307 mime_type,
1308 annotations: None,
1309 meta: None,
1310 additional: BTreeMap::new(),
1311 }),
1312 Content::Audio { data, mime_type } => Ok(ContentBlock::Audio {
1313 data,
1314 mime_type,
1315 annotations: None,
1316 meta: None,
1317 additional: BTreeMap::new(),
1318 }),
1319 Content::Resource { resource } => {
1320 let uri = AbsoluteUri::parse(resource.uri).map_err(|error| {
1321 McpError::internal_error(format!(
1322 "legacy tool resource cannot be promoted to the final handler result: {error}",
1323 ))
1324 })?;
1325 let embedded = match (resource.text, resource.blob) {
1326 (Some(text), None) => EmbeddedResourceContents::Text {
1327 uri,
1328 text,
1329 mime_type: resource.mime_type,
1330 meta: None,
1331 additional: BTreeMap::new(),
1332 },
1333 (None, Some(blob)) => EmbeddedResourceContents::Blob {
1334 uri,
1335 blob,
1336 mime_type: resource.mime_type,
1337 meta: None,
1338 additional: BTreeMap::new(),
1339 },
1340 _ => {
1341 return Err(McpError::internal_error(
1342 "legacy tool resource cannot be promoted without exactly one text or blob payload",
1343 ));
1344 }
1345 };
1346 Ok(ContentBlock::Resource {
1347 resource: embedded,
1348 annotations: None,
1349 meta: None,
1350 additional: BTreeMap::new(),
1351 })
1352 }
1353 })
1354 .collect::<McpResult<Vec<_>>>()?;
1355
1356 Ok(CompleteResult::new(
1357 FinalCallToolResult {
1358 content,
1359 is_error: false,
1360 structured_content: None,
1361 },
1362 empty_final_result_meta()?,
1363 ))
1364}
1365
1366pub(crate) const DEFAULT_FINAL_RESOURCE_TTL_MS: u64 = 60 * 60 * 1_000;
1367
1368pub fn promote_legacy_resource_contents(
1379 contents: Vec<ResourceContent>,
1380) -> McpResult<CompleteResult<FinalReadResourceResult>> {
1381 let contents = contents
1382 .into_iter()
1383 .map(|resource| {
1384 let promoted = promote_legacy_tool_content(vec![Content::Resource { resource }])?;
1385 let Some(ContentBlock::Resource { resource, .. }) =
1386 promoted.payload.content.into_iter().next()
1387 else {
1388 return Err(McpError::internal_error(
1389 "legacy resource content did not promote to a final embedded resource",
1390 ));
1391 };
1392 Ok(resource)
1393 })
1394 .collect::<McpResult<Vec<_>>>()?;
1395
1396 Ok(CompleteResult::new(
1397 FinalReadResourceResult {
1398 contents,
1399 ttl_ms: CacheTtl::milliseconds(DEFAULT_FINAL_RESOURCE_TTL_MS),
1400 cache_scope: CacheScope::Private,
1401 },
1402 empty_final_result_meta()?,
1403 ))
1404}
1405
1406pub fn promote_legacy_prompt_messages(
1416 messages: Vec<PromptMessage>,
1417) -> McpResult<CompleteResult<FinalGetPromptResult>> {
1418 let messages = messages
1419 .into_iter()
1420 .map(|PromptMessage { role, content }| {
1421 let promoted = promote_legacy_tool_content(vec![content])?;
1422 let Some(content) = promoted.payload.content.into_iter().next() else {
1423 return Err(McpError::internal_error(
1424 "legacy prompt content did not promote to a final content block",
1425 ));
1426 };
1427 Ok(FinalPromptMessage { role, content })
1428 })
1429 .collect::<McpResult<Vec<_>>>()?;
1430
1431 Ok(CompleteResult::new(
1432 FinalGetPromptResult {
1433 description: None,
1434 messages,
1435 },
1436 empty_final_result_meta()?,
1437 ))
1438}
1439
1440#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1442pub enum ToolErrorKind {
1443 InputValidation,
1445 Handler,
1447}
1448
1449#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1456pub enum FinalToolSchemaAuthority {
1457 Local,
1459 Upstream,
1461}
1462
1463#[derive(Debug)]
1469pub struct UpstreamFinalToolSchemaRegistration {
1470 _proxy_registration: (),
1471}
1472
1473impl UpstreamFinalToolSchemaRegistration {
1474 pub(crate) const fn exact_proxy() -> Self {
1475 Self {
1476 _proxy_registration: (),
1477 }
1478 }
1479}
1480
1481pub trait ToolHandler: Send + Sync {
1499 fn definition(&self) -> Tool;
1501
1502 fn icon(&self) -> Option<&Icon> {
1507 None
1508 }
1509
1510 fn version(&self) -> Option<&str> {
1515 None
1516 }
1517
1518 fn tags(&self) -> &[String] {
1523 &[]
1524 }
1525
1526 fn annotations(&self) -> Option<&ToolAnnotations> {
1532 None
1533 }
1534
1535 fn output_schema(&self) -> Option<serde_json::Value> {
1541 None
1542 }
1543
1544 fn final_title(&self) -> Option<&str> {
1550 None
1551 }
1552
1553 fn final_icons(&self) -> Option<&[RawIcon]> {
1558 None
1559 }
1560
1561 fn final_metadata(&self) -> Option<&OpenMetadata> {
1563 None
1564 }
1565
1566 fn final_definition(&self) -> Option<FinalTool> {
1575 None
1576 }
1577
1578 fn final_tool_schema_authority(&self) -> FinalToolSchemaAuthority {
1585 FinalToolSchemaAuthority::Local
1586 }
1587
1588 fn upstream_final_tool_schema_registration(
1594 &self,
1595 ) -> Option<UpstreamFinalToolSchemaRegistration> {
1596 None
1597 }
1598
1599 fn final_tool_error_structured_content(
1608 &self,
1609 _kind: ToolErrorKind,
1610 ) -> Option<serde_json::Value> {
1611 None
1612 }
1613
1614 fn timeout(&self) -> Option<Duration> {
1628 None
1629 }
1630
1631 fn call(&self, ctx: &McpContext, arguments: serde_json::Value) -> McpResult<Vec<Content>>;
1637
1638 fn call_async<'a>(
1649 &'a self,
1650 ctx: &'a McpContext,
1651 arguments: serde_json::Value,
1652 ) -> BoxFuture<'a, McpOutcome<Vec<Content>>> {
1653 Box::pin(async move {
1654 match self.call(ctx, arguments) {
1655 Ok(v) => Outcome::Ok(v),
1656 Err(e) => Outcome::Err(e),
1657 }
1658 })
1659 }
1660
1661 fn call_final(
1669 &self,
1670 ctx: &McpContext,
1671 arguments: serde_json::Value,
1672 ) -> McpResult<CompleteResult<FinalCallToolResult>> {
1673 promote_legacy_tool_content(self.call(ctx, arguments)?)
1674 }
1675
1676 fn call_final_async<'a>(
1680 &'a self,
1681 ctx: &'a McpContext,
1682 arguments: serde_json::Value,
1683 ) -> BoxFuture<'a, McpOutcome<CompleteResult<FinalCallToolResult>>> {
1684 Box::pin(async move {
1685 match self.call_final(ctx, arguments) {
1686 Ok(value) => Outcome::Ok(value),
1687 Err(error) => Outcome::Err(error),
1688 }
1689 })
1690 }
1691
1692 fn call_async_in_request<'a>(
1700 &'a self,
1701 ctx: &'a McpContext,
1702 _request_cx: &'a Cx,
1703 arguments: serde_json::Value,
1704 ) -> BoxFuture<'a, McpOutcome<Vec<Content>>> {
1705 self.call_async(ctx, arguments)
1706 }
1707
1708 fn call_final_async_in_request<'a>(
1713 &'a self,
1714 ctx: &'a McpContext,
1715 _request_cx: &'a Cx,
1716 arguments: serde_json::Value,
1717 ) -> BoxFuture<'a, McpOutcome<CompleteResult<FinalCallToolResult>>> {
1718 self.call_final_async(ctx, arguments)
1719 }
1720
1721 fn declares_final_tasks(&self) -> bool {
1729 false
1730 }
1731
1732 fn declares_final_mrtr(&self) -> bool {
1740 false
1741 }
1742
1743 fn call_final_outcome(
1749 &self,
1750 ctx: &McpContext,
1751 arguments: serde_json::Value,
1752 ) -> McpResult<FinalToolOutcome> {
1753 self.call_final(ctx, arguments)
1754 .map(FinalToolOutcome::Complete)
1755 }
1756
1757 fn call_final_outcome_async<'a>(
1759 &'a self,
1760 ctx: &'a McpContext,
1761 arguments: serde_json::Value,
1762 ) -> BoxFuture<'a, McpOutcome<FinalToolOutcome>> {
1763 Box::pin(async move {
1764 match self.call_final_outcome(ctx, arguments) {
1765 Ok(value) => {
1766 #[cfg(feature = "tasks")]
1767 {
1768 match admit_declared_final_tool_outcome(self.declares_final_tasks(), value)
1769 {
1770 Ok(value) => Outcome::Ok(value),
1771 Err(error) => Outcome::Err(error),
1772 }
1773 }
1774 #[cfg(not(feature = "tasks"))]
1775 {
1776 Outcome::Ok(value)
1777 }
1778 }
1779 Err(error) => Outcome::Err(error),
1780 }
1781 })
1782 }
1783
1784 fn call_final_outcome_async_in_request<'a>(
1786 &'a self,
1787 ctx: &'a McpContext,
1788 _request_cx: &'a Cx,
1789 arguments: serde_json::Value,
1790 ) -> BoxFuture<'a, McpOutcome<FinalToolOutcome>> {
1791 Box::pin(async move {
1792 match self.call_final_outcome_async(ctx, arguments).await {
1793 Outcome::Ok(value) => {
1794 #[cfg(feature = "tasks")]
1795 {
1796 match admit_declared_final_tool_outcome(self.declares_final_tasks(), value)
1797 {
1798 Ok(value) => Outcome::Ok(value),
1799 Err(error) => Outcome::Err(error),
1800 }
1801 }
1802 #[cfg(not(feature = "tasks"))]
1803 {
1804 Outcome::Ok(value)
1805 }
1806 }
1807 Outcome::Err(error) => Outcome::Err(error),
1808 Outcome::Cancelled(cancelled) => Outcome::Cancelled(cancelled),
1809 Outcome::Panicked(panic) => Outcome::Panicked(panic),
1810 }
1811 })
1812 }
1813
1814 fn call_final_outcome_async_resuming_in_request<'a>(
1824 &'a self,
1825 ctx: &'a McpContext,
1826 request_cx: &'a Cx,
1827 arguments: serde_json::Value,
1828 _resume_inputs: Option<&'a MrtrCompletedInputs>,
1829 ) -> BoxFuture<'a, McpOutcome<FinalToolOutcome>> {
1830 self.call_final_outcome_async_in_request(ctx, request_cx, arguments)
1831 }
1832}
1833
1834pub trait ResourceHandler: Send + Sync {
1849 fn definition(&self) -> Resource;
1851
1852 fn final_client_direct_https(&self) -> bool {
1856 false
1857 }
1858
1859 fn declares_final_mrtr(&self) -> bool {
1865 false
1866 }
1867
1868 fn template(&self) -> Option<ResourceTemplate> {
1870 None
1871 }
1872
1873 fn on_subscribe(&self, _ctx: &McpContext, _uri: &str) -> McpResult<()> {
1878 Ok(())
1879 }
1880
1881 fn on_unsubscribe(&self, _ctx: &McpContext, _uri: &str) -> McpResult<()> {
1883 Ok(())
1884 }
1885
1886 fn final_definition(&self) -> Option<FinalResource> {
1890 None
1891 }
1892
1893 fn final_template_definition(&self) -> Option<FinalResourceTemplate> {
1897 None
1898 }
1899
1900 fn final_title(&self) -> Option<&str> {
1902 None
1903 }
1904
1905 fn final_icons(&self) -> Option<&[RawIcon]> {
1907 None
1908 }
1909
1910 fn final_annotations(&self) -> Option<&Annotations> {
1912 None
1913 }
1914
1915 fn final_metadata(&self) -> Option<&OpenMetadata> {
1917 None
1918 }
1919
1920 fn final_template_title(&self) -> Option<&str> {
1924 None
1925 }
1926
1927 fn final_template_icons(&self) -> Option<&[RawIcon]> {
1931 None
1932 }
1933
1934 fn final_template_annotations(&self) -> Option<&Annotations> {
1938 None
1939 }
1940
1941 fn final_template_metadata(&self) -> Option<&OpenMetadata> {
1945 None
1946 }
1947
1948 fn icon(&self) -> Option<&Icon> {
1953 None
1954 }
1955
1956 fn version(&self) -> Option<&str> {
1961 None
1962 }
1963
1964 fn tags(&self) -> &[String] {
1969 &[]
1970 }
1971
1972 fn timeout(&self) -> Option<Duration> {
1980 None
1981 }
1982
1983 fn read(&self, ctx: &McpContext) -> McpResult<Vec<ResourceContent>>;
1989
1990 fn read_with_uri(
1994 &self,
1995 ctx: &McpContext,
1996 _uri: &str,
1997 _params: &UriParams,
1998 ) -> McpResult<Vec<ResourceContent>> {
1999 self.read(ctx)
2000 }
2001
2002 fn read_async_with_uri<'a>(
2006 &'a self,
2007 ctx: &'a McpContext,
2008 uri: &'a str,
2009 params: &'a UriParams,
2010 ) -> BoxFuture<'a, McpOutcome<Vec<ResourceContent>>> {
2011 Box::pin(async move {
2012 if params.is_empty() {
2013 self.read_async(ctx).await
2014 } else {
2015 match self.read_with_uri(ctx, uri, params) {
2016 Ok(v) => Outcome::Ok(v),
2017 Err(e) => Outcome::Err(e),
2018 }
2019 }
2020 })
2021 }
2022
2023 fn read_async<'a>(
2032 &'a self,
2033 ctx: &'a McpContext,
2034 ) -> BoxFuture<'a, McpOutcome<Vec<ResourceContent>>> {
2035 Box::pin(async move {
2036 match self.read(ctx) {
2037 Ok(v) => Outcome::Ok(v),
2038 Err(e) => Outcome::Err(e),
2039 }
2040 })
2041 }
2042
2043 fn read_final(&self, ctx: &McpContext) -> McpResult<CompleteResult<FinalReadResourceResult>> {
2050 promote_legacy_resource_contents(self.read(ctx)?)
2051 }
2052
2053 fn final_resource_read_cache_hint_provenance(&self) -> FinalResourceReadCacheHintProvenance {
2060 FinalResourceReadCacheHintProvenance::RouterPolicy
2061 }
2062
2063 fn read_final_with_uri(
2065 &self,
2066 ctx: &McpContext,
2067 uri: &str,
2068 params: &UriParams,
2069 ) -> McpResult<CompleteResult<FinalReadResourceResult>> {
2070 promote_legacy_resource_contents(self.read_with_uri(ctx, uri, params)?)
2071 }
2072
2073 fn read_final_outcome(
2080 &self,
2081 ctx: &McpContext,
2082 ) -> McpResult<FinalMethodOutcome<FinalReadResourceResult>> {
2083 self.read_final(ctx).map(FinalMethodOutcome::Complete)
2084 }
2085
2086 fn read_final_outcome_with_uri(
2089 &self,
2090 ctx: &McpContext,
2091 uri: &str,
2092 params: &UriParams,
2093 ) -> McpResult<FinalMethodOutcome<FinalReadResourceResult>> {
2094 if params.is_empty() {
2095 self.read_final_outcome(ctx)
2096 } else {
2097 self.read_final_with_uri(ctx, uri, params)
2098 .map(FinalMethodOutcome::Complete)
2099 }
2100 }
2101
2102 fn read_final_async<'a>(
2104 &'a self,
2105 ctx: &'a McpContext,
2106 ) -> BoxFuture<'a, McpOutcome<CompleteResult<FinalReadResourceResult>>> {
2107 Box::pin(async move {
2108 match self.read_final(ctx) {
2109 Ok(value) => Outcome::Ok(value),
2110 Err(error) => Outcome::Err(error),
2111 }
2112 })
2113 }
2114
2115 fn read_final_async_with_uri<'a>(
2117 &'a self,
2118 ctx: &'a McpContext,
2119 uri: &'a str,
2120 params: &'a UriParams,
2121 ) -> BoxFuture<'a, McpOutcome<CompleteResult<FinalReadResourceResult>>> {
2122 Box::pin(async move {
2123 if params.is_empty() {
2124 self.read_final_async(ctx).await
2125 } else {
2126 match self.read_final_with_uri(ctx, uri, params) {
2127 Ok(value) => Outcome::Ok(value),
2128 Err(error) => Outcome::Err(error),
2129 }
2130 }
2131 })
2132 }
2133
2134 fn read_final_outcome_async<'a>(
2137 &'a self,
2138 ctx: &'a McpContext,
2139 ) -> BoxFuture<'a, McpOutcome<FinalMethodOutcome<FinalReadResourceResult>>> {
2140 Box::pin(async move {
2141 match self.read_final_outcome(ctx) {
2142 Ok(value) => Outcome::Ok(value),
2143 Err(error) => Outcome::Err(error),
2144 }
2145 })
2146 }
2147
2148 fn read_final_outcome_async_with_uri<'a>(
2151 &'a self,
2152 ctx: &'a McpContext,
2153 uri: &'a str,
2154 params: &'a UriParams,
2155 ) -> BoxFuture<'a, McpOutcome<FinalMethodOutcome<FinalReadResourceResult>>> {
2156 Box::pin(async move {
2157 if params.is_empty() {
2158 self.read_final_outcome_async(ctx).await
2159 } else {
2160 match self.read_final_outcome_with_uri(ctx, uri, params) {
2161 Ok(value) => Outcome::Ok(value),
2162 Err(error) => Outcome::Err(error),
2163 }
2164 }
2165 })
2166 }
2167
2168 fn read_async_with_uri_in_request<'a>(
2175 &'a self,
2176 ctx: &'a McpContext,
2177 _request_cx: &'a Cx,
2178 uri: &'a str,
2179 params: &'a UriParams,
2180 ) -> BoxFuture<'a, McpOutcome<Vec<ResourceContent>>> {
2181 self.read_async_with_uri(ctx, uri, params)
2182 }
2183
2184 fn read_final_async_with_uri_in_request<'a>(
2186 &'a self,
2187 ctx: &'a McpContext,
2188 _request_cx: &'a Cx,
2189 uri: &'a str,
2190 params: &'a UriParams,
2191 ) -> BoxFuture<'a, McpOutcome<CompleteResult<FinalReadResourceResult>>> {
2192 self.read_final_async_with_uri(ctx, uri, params)
2193 }
2194
2195 fn read_final_outcome_async_with_uri_in_request<'a>(
2198 &'a self,
2199 ctx: &'a McpContext,
2200 _request_cx: &'a Cx,
2201 uri: &'a str,
2202 params: &'a UriParams,
2203 ) -> BoxFuture<'a, McpOutcome<FinalMethodOutcome<FinalReadResourceResult>>> {
2204 self.read_final_outcome_async_with_uri(ctx, uri, params)
2205 }
2206
2207 fn read_final_outcome_async_with_uri_resuming_in_request<'a>(
2212 &'a self,
2213 ctx: &'a McpContext,
2214 request_cx: &'a Cx,
2215 uri: &'a str,
2216 params: &'a UriParams,
2217 _resume_inputs: Option<&'a MrtrCompletedInputs>,
2218 ) -> BoxFuture<'a, McpOutcome<FinalMethodOutcome<FinalReadResourceResult>>> {
2219 self.read_final_outcome_async_with_uri_in_request(ctx, request_cx, uri, params)
2220 }
2221}
2222
2223pub trait PromptHandler: Send + Sync {
2237 fn definition(&self) -> Prompt;
2239
2240 fn final_client_direct_https(&self) -> bool {
2244 false
2245 }
2246
2247 fn declares_final_mrtr(&self) -> bool {
2253 false
2254 }
2255
2256 fn final_definition(&self) -> Option<FinalPrompt> {
2260 None
2261 }
2262
2263 fn final_title(&self) -> Option<&str> {
2265 None
2266 }
2267
2268 fn final_icons(&self) -> Option<&[RawIcon]> {
2270 None
2271 }
2272
2273 fn final_metadata(&self) -> Option<&OpenMetadata> {
2275 None
2276 }
2277
2278 fn icon(&self) -> Option<&Icon> {
2283 None
2284 }
2285
2286 fn version(&self) -> Option<&str> {
2291 None
2292 }
2293
2294 fn tags(&self) -> &[String] {
2299 &[]
2300 }
2301
2302 fn timeout(&self) -> Option<Duration> {
2310 None
2311 }
2312
2313 fn get(
2319 &self,
2320 ctx: &McpContext,
2321 arguments: std::collections::HashMap<String, String>,
2322 ) -> McpResult<Vec<PromptMessage>>;
2323
2324 fn get_async<'a>(
2333 &'a self,
2334 ctx: &'a McpContext,
2335 arguments: std::collections::HashMap<String, String>,
2336 ) -> BoxFuture<'a, McpOutcome<Vec<PromptMessage>>> {
2337 Box::pin(async move {
2338 match self.get(ctx, arguments) {
2339 Ok(v) => Outcome::Ok(v),
2340 Err(e) => Outcome::Err(e),
2341 }
2342 })
2343 }
2344
2345 fn get_final(
2351 &self,
2352 ctx: &McpContext,
2353 arguments: std::collections::HashMap<String, String>,
2354 ) -> McpResult<CompleteResult<FinalGetPromptResult>> {
2355 promote_legacy_prompt_messages(self.get(ctx, arguments)?)
2356 }
2357
2358 fn get_final_outcome(
2365 &self,
2366 ctx: &McpContext,
2367 arguments: std::collections::HashMap<String, String>,
2368 ) -> McpResult<FinalMethodOutcome<FinalGetPromptResult>> {
2369 self.get_final(ctx, arguments)
2370 .map(FinalMethodOutcome::Complete)
2371 }
2372
2373 fn get_final_async<'a>(
2375 &'a self,
2376 ctx: &'a McpContext,
2377 arguments: std::collections::HashMap<String, String>,
2378 ) -> BoxFuture<'a, McpOutcome<CompleteResult<FinalGetPromptResult>>> {
2379 Box::pin(async move {
2380 match self.get_final(ctx, arguments) {
2381 Ok(value) => Outcome::Ok(value),
2382 Err(error) => Outcome::Err(error),
2383 }
2384 })
2385 }
2386
2387 fn get_final_outcome_async<'a>(
2390 &'a self,
2391 ctx: &'a McpContext,
2392 arguments: std::collections::HashMap<String, String>,
2393 ) -> BoxFuture<'a, McpOutcome<FinalMethodOutcome<FinalGetPromptResult>>> {
2394 Box::pin(async move {
2395 match self.get_final_outcome(ctx, arguments) {
2396 Ok(value) => Outcome::Ok(value),
2397 Err(error) => Outcome::Err(error),
2398 }
2399 })
2400 }
2401
2402 fn get_async_in_request<'a>(
2408 &'a self,
2409 ctx: &'a McpContext,
2410 _request_cx: &'a Cx,
2411 arguments: std::collections::HashMap<String, String>,
2412 ) -> BoxFuture<'a, McpOutcome<Vec<PromptMessage>>> {
2413 self.get_async(ctx, arguments)
2414 }
2415
2416 fn get_final_async_in_request<'a>(
2418 &'a self,
2419 ctx: &'a McpContext,
2420 _request_cx: &'a Cx,
2421 arguments: std::collections::HashMap<String, String>,
2422 ) -> BoxFuture<'a, McpOutcome<CompleteResult<FinalGetPromptResult>>> {
2423 self.get_final_async(ctx, arguments)
2424 }
2425
2426 fn get_final_outcome_async_in_request<'a>(
2429 &'a self,
2430 ctx: &'a McpContext,
2431 _request_cx: &'a Cx,
2432 arguments: std::collections::HashMap<String, String>,
2433 ) -> BoxFuture<'a, McpOutcome<FinalMethodOutcome<FinalGetPromptResult>>> {
2434 self.get_final_outcome_async(ctx, arguments)
2435 }
2436
2437 fn get_final_outcome_async_resuming_in_request<'a>(
2442 &'a self,
2443 ctx: &'a McpContext,
2444 request_cx: &'a Cx,
2445 arguments: std::collections::HashMap<String, String>,
2446 _resume_inputs: Option<&'a MrtrCompletedInputs>,
2447 ) -> BoxFuture<'a, McpOutcome<FinalMethodOutcome<FinalGetPromptResult>>> {
2448 self.get_final_outcome_async_in_request(ctx, request_cx, arguments)
2449 }
2450}
2451
2452pub trait CompletionHandler: Send + Sync {
2460 fn timeout(&self) -> Option<Duration> {
2465 None
2466 }
2467
2468 fn complete_legacy(
2470 &self,
2471 ctx: &McpContext,
2472 params: LegacyCompletionParams,
2473 ) -> McpResult<CompletionValues>;
2474
2475 fn complete_final(
2477 &self,
2478 ctx: &McpContext,
2479 params: FinalCompletionParams,
2480 ) -> McpResult<FinalCompletionValues>;
2481
2482 fn complete_legacy_async<'a>(
2486 &'a self,
2487 ctx: &'a McpContext,
2488 params: LegacyCompletionParams,
2489 ) -> BoxFuture<'a, McpOutcome<CompletionValues>> {
2490 Box::pin(async move {
2491 match self.complete_legacy(ctx, params) {
2492 Ok(values) => Outcome::Ok(values),
2493 Err(error) => Outcome::Err(error),
2494 }
2495 })
2496 }
2497
2498 fn complete_legacy_async_in_request<'a>(
2503 &'a self,
2504 ctx: &'a McpContext,
2505 _request_cx: &'a Cx,
2506 params: LegacyCompletionParams,
2507 ) -> BoxFuture<'a, McpOutcome<CompletionValues>> {
2508 self.complete_legacy_async(ctx, params)
2509 }
2510
2511 fn complete_final_async<'a>(
2515 &'a self,
2516 ctx: &'a McpContext,
2517 params: FinalCompletionParams,
2518 ) -> BoxFuture<'a, McpOutcome<FinalCompletionValues>> {
2519 Box::pin(async move {
2520 match self.complete_final(ctx, params) {
2521 Ok(values) => Outcome::Ok(values),
2522 Err(error) => Outcome::Err(error),
2523 }
2524 })
2525 }
2526
2527 fn complete_final_async_in_request<'a>(
2532 &'a self,
2533 ctx: &'a McpContext,
2534 _request_cx: &'a Cx,
2535 params: FinalCompletionParams,
2536 ) -> BoxFuture<'a, McpOutcome<FinalCompletionValues>> {
2537 self.complete_final_async(ctx, params)
2538 }
2539}
2540
2541pub type BoxedToolHandler = Box<dyn ToolHandler>;
2543
2544pub type BoxedResourceHandler = Box<dyn ResourceHandler>;
2546
2547pub type BoxedPromptHandler = Box<dyn PromptHandler>;
2549
2550pub type BoxedCompletionHandler = Box<dyn CompletionHandler>;
2552
2553#[cfg(feature = "proxy")]
2559pub(crate) struct FinalProxyResourceHandler {
2560 legacy: Resource,
2561 final_definition: FinalResource,
2562 external_uri: String,
2563 client: ProxyClient,
2564}
2565
2566#[cfg(feature = "proxy")]
2567impl FinalProxyResourceHandler {
2568 pub(crate) fn new(final_definition: FinalResource, client: ProxyClient) -> Self {
2569 let external_uri = final_definition.uri.as_str().to_owned();
2570 let legacy = Resource {
2571 uri: external_uri.clone(),
2572 name: final_definition.name.clone(),
2573 description: final_definition.description.clone(),
2574 mime_type: final_definition.mime_type.clone(),
2575 icon: None,
2576 version: None,
2577 tags: Vec::new(),
2578 };
2579 Self {
2580 legacy,
2581 final_definition,
2582 external_uri,
2583 client,
2584 }
2585 }
2586}
2587
2588#[cfg(feature = "proxy")]
2589impl ResourceHandler for FinalProxyResourceHandler {
2590 fn definition(&self) -> Resource {
2591 self.legacy.clone()
2592 }
2593
2594 fn final_definition(&self) -> Option<FinalResource> {
2595 Some(self.final_definition.clone())
2596 }
2597
2598 fn final_resource_read_cache_hint_provenance(&self) -> FinalResourceReadCacheHintProvenance {
2599 FinalResourceReadCacheHintProvenance::Explicit
2600 }
2601
2602 fn read(&self, ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
2603 self.client.read_resource(ctx, &self.external_uri)
2604 }
2605
2606 fn declares_final_mrtr(&self) -> bool {
2607 true
2608 }
2609
2610 fn read_final(&self, ctx: &McpContext) -> McpResult<CompleteResult<FinalReadResourceResult>> {
2611 self.client.read_resource_final(ctx, &self.external_uri)
2612 }
2613
2614 fn read_final_outcome(
2615 &self,
2616 ctx: &McpContext,
2617 ) -> McpResult<FinalMethodOutcome<FinalReadResourceResult>> {
2618 self.client
2619 .read_resource_final_outcome(ctx, &self.external_uri, None)
2620 }
2621
2622 fn read_final_outcome_async_with_uri_resuming_in_request<'a>(
2623 &'a self,
2624 ctx: &'a McpContext,
2625 _request_cx: &'a Cx,
2626 _uri: &'a str,
2627 _params: &'a UriParams,
2628 resume_inputs: Option<&'a MrtrCompletedInputs>,
2629 ) -> BoxFuture<'a, McpOutcome<FinalMethodOutcome<FinalReadResourceResult>>> {
2630 Box::pin(async move {
2631 match self
2632 .client
2633 .read_resource_final_outcome(ctx, &self.external_uri, resume_inputs)
2634 {
2635 Ok(result) => Outcome::Ok(result),
2636 Err(error) => Outcome::Err(error),
2637 }
2638 })
2639 }
2640}
2641
2642#[cfg(feature = "proxy")]
2644pub(crate) struct FinalProxyResourceTemplateHandler {
2645 legacy_template: ResourceTemplate,
2646 final_definition: FinalResourceTemplate,
2647 external_uri_template: String,
2648 client: ProxyClient,
2649}
2650
2651#[cfg(feature = "proxy")]
2652impl FinalProxyResourceTemplateHandler {
2653 pub(crate) fn new(final_definition: FinalResourceTemplate, client: ProxyClient) -> Self {
2654 let external_uri_template = final_definition.uri_template.clone();
2655 let legacy_template = ResourceTemplate {
2656 uri_template: external_uri_template.clone(),
2657 name: final_definition.name.clone(),
2658 description: final_definition.description.clone(),
2659 mime_type: final_definition.mime_type.clone(),
2660 icon: None,
2661 version: None,
2662 tags: Vec::new(),
2663 };
2664 Self {
2665 legacy_template,
2666 final_definition,
2667 external_uri_template,
2668 client,
2669 }
2670 }
2671}
2672
2673#[cfg(feature = "proxy")]
2674impl ResourceHandler for FinalProxyResourceTemplateHandler {
2675 fn definition(&self) -> Resource {
2676 Resource {
2677 uri: self.legacy_template.uri_template.clone(),
2678 name: self.legacy_template.name.clone(),
2679 description: self.legacy_template.description.clone(),
2680 mime_type: self.legacy_template.mime_type.clone(),
2681 icon: None,
2682 version: None,
2683 tags: Vec::new(),
2684 }
2685 }
2686
2687 fn template(&self) -> Option<ResourceTemplate> {
2688 Some(self.legacy_template.clone())
2689 }
2690
2691 fn final_template_definition(&self) -> Option<FinalResourceTemplate> {
2692 Some(self.final_definition.clone())
2693 }
2694
2695 fn final_resource_read_cache_hint_provenance(&self) -> FinalResourceReadCacheHintProvenance {
2696 FinalResourceReadCacheHintProvenance::Explicit
2697 }
2698
2699 fn read(&self, ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
2700 self.client.read_resource(ctx, &self.external_uri_template)
2701 }
2702
2703 fn read_with_uri(
2704 &self,
2705 ctx: &McpContext,
2706 uri: &str,
2707 _params: &UriParams,
2708 ) -> McpResult<Vec<ResourceContent>> {
2709 self.client.read_resource(ctx, uri)
2710 }
2711
2712 fn read_final(&self, ctx: &McpContext) -> McpResult<CompleteResult<FinalReadResourceResult>> {
2713 self.client
2714 .read_resource_final(ctx, &self.external_uri_template)
2715 }
2716
2717 fn read_final_with_uri(
2718 &self,
2719 ctx: &McpContext,
2720 uri: &str,
2721 _params: &UriParams,
2722 ) -> McpResult<CompleteResult<FinalReadResourceResult>> {
2723 self.client.read_resource_final(ctx, uri)
2724 }
2725
2726 fn declares_final_mrtr(&self) -> bool {
2727 true
2728 }
2729
2730 fn read_final_outcome(
2731 &self,
2732 ctx: &McpContext,
2733 ) -> McpResult<FinalMethodOutcome<FinalReadResourceResult>> {
2734 self.client
2735 .read_resource_final_outcome(ctx, &self.external_uri_template, None)
2736 }
2737
2738 fn read_final_outcome_with_uri(
2739 &self,
2740 ctx: &McpContext,
2741 uri: &str,
2742 _params: &UriParams,
2743 ) -> McpResult<FinalMethodOutcome<FinalReadResourceResult>> {
2744 self.client.read_resource_final_outcome(ctx, uri, None)
2745 }
2746
2747 fn read_final_outcome_async_with_uri_resuming_in_request<'a>(
2748 &'a self,
2749 ctx: &'a McpContext,
2750 _request_cx: &'a Cx,
2751 uri: &'a str,
2752 _params: &'a UriParams,
2753 resume_inputs: Option<&'a MrtrCompletedInputs>,
2754 ) -> BoxFuture<'a, McpOutcome<FinalMethodOutcome<FinalReadResourceResult>>> {
2755 Box::pin(async move {
2756 match self
2757 .client
2758 .read_resource_final_outcome(ctx, uri, resume_inputs)
2759 {
2760 Ok(result) => Outcome::Ok(result),
2761 Err(error) => Outcome::Err(error),
2762 }
2763 })
2764 }
2765}
2766
2767#[cfg(feature = "proxy")]
2769pub(crate) struct FinalProxyPromptHandler {
2770 legacy: Prompt,
2771 final_definition: FinalPrompt,
2772 external_name: String,
2773 client: ProxyClient,
2774}
2775
2776#[cfg(feature = "proxy")]
2777impl FinalProxyPromptHandler {
2778 pub(crate) fn new(final_definition: FinalPrompt, client: ProxyClient) -> Self {
2779 let external_name = final_definition.name.clone();
2780 let legacy = Prompt {
2781 name: external_name.clone(),
2782 description: final_definition.description.clone(),
2783 arguments: final_definition
2784 .arguments
2785 .as_ref()
2786 .map(|arguments| {
2787 arguments
2788 .iter()
2789 .map(|argument| fastmcp_protocol::PromptArgument {
2790 name: argument.name.clone(),
2791 description: argument.description.clone(),
2792 required: argument.required.unwrap_or(false),
2793 })
2794 .collect()
2795 })
2796 .unwrap_or_default(),
2797 icon: None,
2798 version: None,
2799 tags: Vec::new(),
2800 };
2801 Self {
2802 legacy,
2803 final_definition,
2804 external_name,
2805 client,
2806 }
2807 }
2808
2809 pub(crate) fn with_prefix(
2813 mut final_definition: FinalPrompt,
2814 prefix: &str,
2815 client: ProxyClient,
2816 ) -> Self {
2817 let external_name = final_definition.name.clone();
2818 final_definition.name = format!("{prefix}/{}", final_definition.name);
2819 let mut handler = Self::new(final_definition, client);
2820 handler.external_name = external_name;
2821 handler
2822 }
2823}
2824
2825#[cfg(feature = "proxy")]
2826impl PromptHandler for FinalProxyPromptHandler {
2827 fn definition(&self) -> Prompt {
2828 self.legacy.clone()
2829 }
2830
2831 fn final_definition(&self) -> Option<FinalPrompt> {
2832 Some(self.final_definition.clone())
2833 }
2834
2835 fn get(
2836 &self,
2837 ctx: &McpContext,
2838 arguments: HashMap<String, String>,
2839 ) -> McpResult<Vec<PromptMessage>> {
2840 self.client.get_prompt(ctx, &self.external_name, arguments)
2841 }
2842
2843 fn declares_final_mrtr(&self) -> bool {
2844 true
2845 }
2846
2847 fn get_final(
2848 &self,
2849 ctx: &McpContext,
2850 arguments: HashMap<String, String>,
2851 ) -> McpResult<CompleteResult<FinalGetPromptResult>> {
2852 self.client
2853 .get_prompt_final(ctx, &self.external_name, arguments)
2854 }
2855
2856 fn get_final_outcome(
2857 &self,
2858 ctx: &McpContext,
2859 arguments: HashMap<String, String>,
2860 ) -> McpResult<FinalMethodOutcome<FinalGetPromptResult>> {
2861 self.client
2862 .get_prompt_final_outcome(ctx, &self.external_name, arguments, None)
2863 }
2864
2865 fn get_final_outcome_async_resuming_in_request<'a>(
2866 &'a self,
2867 ctx: &'a McpContext,
2868 _request_cx: &'a Cx,
2869 arguments: HashMap<String, String>,
2870 resume_inputs: Option<&'a MrtrCompletedInputs>,
2871 ) -> BoxFuture<'a, McpOutcome<FinalMethodOutcome<FinalGetPromptResult>>> {
2872 Box::pin(async move {
2873 match self.client.get_prompt_final_outcome(
2874 ctx,
2875 &self.external_name,
2876 arguments,
2877 resume_inputs,
2878 ) {
2879 Ok(result) => Outcome::Ok(result),
2880 Err(error) => Outcome::Err(error),
2881 }
2882 })
2883 }
2884}
2885
2886pub struct MountedToolHandler {
2894 inner: BoxedToolHandler,
2895 mounted_name: String,
2896}
2897
2898impl MountedToolHandler {
2899 pub fn new(inner: BoxedToolHandler, mounted_name: String) -> Self {
2901 Self {
2902 inner,
2903 mounted_name,
2904 }
2905 }
2906}
2907
2908impl ToolHandler for MountedToolHandler {
2909 fn definition(&self) -> Tool {
2910 let mut def = self.inner.definition();
2911 def.name.clone_from(&self.mounted_name);
2912 def
2913 }
2914
2915 fn icon(&self) -> Option<&Icon> {
2916 self.inner.icon()
2917 }
2918
2919 fn version(&self) -> Option<&str> {
2920 self.inner.version()
2921 }
2922
2923 fn tags(&self) -> &[String] {
2924 self.inner.tags()
2925 }
2926
2927 fn annotations(&self) -> Option<&ToolAnnotations> {
2928 self.inner.annotations()
2929 }
2930
2931 fn output_schema(&self) -> Option<serde_json::Value> {
2932 self.inner.output_schema()
2933 }
2934
2935 fn final_title(&self) -> Option<&str> {
2936 self.inner.final_title()
2937 }
2938
2939 fn final_icons(&self) -> Option<&[RawIcon]> {
2940 self.inner.final_icons()
2941 }
2942
2943 fn final_metadata(&self) -> Option<&OpenMetadata> {
2944 self.inner.final_metadata()
2945 }
2946
2947 fn final_definition(&self) -> Option<FinalTool> {
2948 let mut definition = self.inner.final_definition()?;
2949 definition.name.clone_from(&self.mounted_name);
2950 Some(definition)
2951 }
2952
2953 fn final_tool_schema_authority(&self) -> FinalToolSchemaAuthority {
2954 self.inner.final_tool_schema_authority()
2955 }
2956
2957 fn upstream_final_tool_schema_registration(
2958 &self,
2959 ) -> Option<UpstreamFinalToolSchemaRegistration> {
2960 self.inner.upstream_final_tool_schema_registration()
2961 }
2962
2963 fn final_tool_error_structured_content(
2964 &self,
2965 kind: ToolErrorKind,
2966 ) -> Option<serde_json::Value> {
2967 self.inner.final_tool_error_structured_content(kind)
2968 }
2969
2970 fn declares_final_tasks(&self) -> bool {
2971 self.inner.declares_final_tasks()
2972 }
2973
2974 fn declares_final_mrtr(&self) -> bool {
2975 self.inner.declares_final_mrtr()
2976 }
2977
2978 fn timeout(&self) -> Option<Duration> {
2979 self.inner.timeout()
2980 }
2981
2982 fn call(&self, ctx: &McpContext, arguments: serde_json::Value) -> McpResult<Vec<Content>> {
2983 self.inner.call(ctx, arguments)
2984 }
2985
2986 fn call_async<'a>(
2987 &'a self,
2988 ctx: &'a McpContext,
2989 arguments: serde_json::Value,
2990 ) -> BoxFuture<'a, McpOutcome<Vec<Content>>> {
2991 self.inner.call_async(ctx, arguments)
2992 }
2993
2994 fn call_async_in_request<'a>(
2995 &'a self,
2996 ctx: &'a McpContext,
2997 request_cx: &'a Cx,
2998 arguments: serde_json::Value,
2999 ) -> BoxFuture<'a, McpOutcome<Vec<Content>>> {
3000 self.inner.call_async_in_request(ctx, request_cx, arguments)
3001 }
3002
3003 fn call_final(
3004 &self,
3005 ctx: &McpContext,
3006 arguments: serde_json::Value,
3007 ) -> McpResult<CompleteResult<FinalCallToolResult>> {
3008 self.inner.call_final(ctx, arguments)
3009 }
3010
3011 fn call_final_async<'a>(
3012 &'a self,
3013 ctx: &'a McpContext,
3014 arguments: serde_json::Value,
3015 ) -> BoxFuture<'a, McpOutcome<CompleteResult<FinalCallToolResult>>> {
3016 self.inner.call_final_async(ctx, arguments)
3017 }
3018
3019 fn call_final_async_in_request<'a>(
3020 &'a self,
3021 ctx: &'a McpContext,
3022 request_cx: &'a Cx,
3023 arguments: serde_json::Value,
3024 ) -> BoxFuture<'a, McpOutcome<CompleteResult<FinalCallToolResult>>> {
3025 self.inner
3026 .call_final_async_in_request(ctx, request_cx, arguments)
3027 }
3028
3029 fn call_final_outcome(
3030 &self,
3031 ctx: &McpContext,
3032 arguments: serde_json::Value,
3033 ) -> McpResult<FinalToolOutcome> {
3034 self.inner.call_final_outcome(ctx, arguments)
3035 }
3036
3037 fn call_final_outcome_async<'a>(
3038 &'a self,
3039 ctx: &'a McpContext,
3040 arguments: serde_json::Value,
3041 ) -> BoxFuture<'a, McpOutcome<FinalToolOutcome>> {
3042 self.inner.call_final_outcome_async(ctx, arguments)
3043 }
3044
3045 fn call_final_outcome_async_in_request<'a>(
3046 &'a self,
3047 ctx: &'a McpContext,
3048 request_cx: &'a Cx,
3049 arguments: serde_json::Value,
3050 ) -> BoxFuture<'a, McpOutcome<FinalToolOutcome>> {
3051 self.inner
3052 .call_final_outcome_async_in_request(ctx, request_cx, arguments)
3053 }
3054
3055 fn call_final_outcome_async_resuming_in_request<'a>(
3056 &'a self,
3057 ctx: &'a McpContext,
3058 request_cx: &'a Cx,
3059 arguments: serde_json::Value,
3060 resume_inputs: Option<&'a MrtrCompletedInputs>,
3061 ) -> BoxFuture<'a, McpOutcome<FinalToolOutcome>> {
3062 self.inner.call_final_outcome_async_resuming_in_request(
3063 ctx,
3064 request_cx,
3065 arguments,
3066 resume_inputs,
3067 )
3068 }
3069}
3070
3071pub struct MountedResourceHandler {
3075 inner: BoxedResourceHandler,
3076 source_uri: String,
3077 mounted_uri: String,
3078 mount_prefix: Option<String>,
3079 mounted_template: Option<ResourceTemplate>,
3080}
3081
3082impl MountedResourceHandler {
3083 pub fn new(inner: BoxedResourceHandler, source_uri: String, mounted_uri: String) -> Self {
3086 let mount_prefix = Self::infer_mount_prefix(&source_uri, &mounted_uri);
3087 Self {
3088 inner,
3089 source_uri,
3090 mounted_uri,
3091 mount_prefix,
3092 mounted_template: None,
3093 }
3094 }
3095
3096 pub fn with_template(
3098 inner: BoxedResourceHandler,
3099 source_uri: String,
3100 mounted_uri: String,
3101 mounted_template: ResourceTemplate,
3102 ) -> Self {
3103 let mount_prefix = Self::infer_mount_prefix(&source_uri, &mounted_uri);
3104 Self {
3105 inner,
3106 source_uri,
3107 mounted_uri,
3108 mount_prefix,
3109 mounted_template: Some(mounted_template),
3110 }
3111 }
3112
3113 fn infer_mount_prefix(source_uri: &str, mounted_uri: &str) -> Option<String> {
3114 mounted_uri
3115 .strip_suffix(source_uri)
3116 .filter(|prefix| !prefix.is_empty())
3117 .map(str::to_string)
3118 }
3119
3120 fn translate_incoming_uri(&self, uri: &str) -> McpResult<String> {
3121 if uri == self.mounted_uri {
3122 return Ok(self.source_uri.clone());
3123 }
3124 match &self.mount_prefix {
3125 Some(prefix) => uri.strip_prefix(prefix).map(str::to_string).ok_or_else(|| {
3126 McpError::invalid_params("Resource URI does not match the mounted namespace")
3127 }),
3128 None if self.mounted_uri == self.source_uri => Ok(uri.to_string()),
3129 None => Err(McpError::invalid_params(
3130 "Resource URI does not match the mounted resource",
3131 )),
3132 }
3133 }
3134
3135 fn translate_outgoing_contents(
3136 &self,
3137 mut contents: Vec<ResourceContent>,
3138 ) -> Vec<ResourceContent> {
3139 for content in &mut contents {
3140 if let Some(prefix) = &self.mount_prefix {
3141 content.uri = format!("{prefix}{}", content.uri);
3142 } else if content.uri == self.source_uri {
3143 content.uri.clone_from(&self.mounted_uri);
3144 }
3145 }
3146 contents
3147 }
3148
3149 fn translate_outgoing_final_uri(&self, uri: AbsoluteUri) -> McpResult<AbsoluteUri> {
3150 let translated = if let Some(prefix) = &self.mount_prefix {
3151 format!("{prefix}{}", uri.as_str())
3152 } else if uri.as_str() == self.source_uri.as_str() {
3153 self.mounted_uri.clone()
3154 } else {
3155 uri.as_str().to_owned()
3156 };
3157 AbsoluteUri::parse(translated).map_err(|error| {
3158 McpError::internal_error(format!(
3159 "mounted final resource URI is invalid after translation: {error}",
3160 ))
3161 })
3162 }
3163
3164 fn translate_outgoing_final_contents(
3165 &self,
3166 contents: Vec<EmbeddedResourceContents>,
3167 ) -> McpResult<Vec<EmbeddedResourceContents>> {
3168 contents
3169 .into_iter()
3170 .map(|content| match content {
3171 EmbeddedResourceContents::Text {
3172 uri,
3173 text,
3174 mime_type,
3175 meta,
3176 additional,
3177 } => Ok(EmbeddedResourceContents::Text {
3178 uri: self.translate_outgoing_final_uri(uri)?,
3179 text,
3180 mime_type,
3181 meta,
3182 additional,
3183 }),
3184 EmbeddedResourceContents::Blob {
3185 uri,
3186 blob,
3187 mime_type,
3188 meta,
3189 additional,
3190 } => Ok(EmbeddedResourceContents::Blob {
3191 uri: self.translate_outgoing_final_uri(uri)?,
3192 blob,
3193 mime_type,
3194 meta,
3195 additional,
3196 }),
3197 })
3198 .collect()
3199 }
3200
3201 fn translate_outgoing_final_result(
3202 &self,
3203 mut result: CompleteResult<FinalReadResourceResult>,
3204 ) -> McpResult<CompleteResult<FinalReadResourceResult>> {
3205 result.payload.contents =
3206 self.translate_outgoing_final_contents(result.payload.contents)?;
3207 Ok(result)
3208 }
3209
3210 fn translate_outgoing_final_method_outcome(
3211 &self,
3212 outcome: FinalMethodOutcome<FinalReadResourceResult>,
3213 ) -> McpResult<FinalMethodOutcome<FinalReadResourceResult>> {
3214 match outcome {
3215 FinalMethodOutcome::Complete(result) => self
3216 .translate_outgoing_final_result(result)
3217 .map(FinalMethodOutcome::Complete),
3218 FinalMethodOutcome::InputRequired(result) => {
3219 Ok(FinalMethodOutcome::InputRequired(result))
3220 }
3221 }
3222 }
3223
3224 fn translate_outgoing_final_outcome(
3225 &self,
3226 outcome: McpOutcome<CompleteResult<FinalReadResourceResult>>,
3227 ) -> McpOutcome<CompleteResult<FinalReadResourceResult>> {
3228 match outcome {
3229 Outcome::Ok(result) => match self.translate_outgoing_final_result(result) {
3230 Ok(result) => Outcome::Ok(result),
3231 Err(error) => Outcome::Err(error),
3232 },
3233 Outcome::Err(error) => Outcome::Err(error),
3234 Outcome::Cancelled(reason) => Outcome::Cancelled(reason),
3235 Outcome::Panicked(payload) => Outcome::Panicked(payload),
3236 }
3237 }
3238
3239 fn translate_outgoing_final_method_outcome_async(
3240 &self,
3241 outcome: McpOutcome<FinalMethodOutcome<FinalReadResourceResult>>,
3242 ) -> McpOutcome<FinalMethodOutcome<FinalReadResourceResult>> {
3243 match outcome {
3244 Outcome::Ok(result) => match self.translate_outgoing_final_method_outcome(result) {
3245 Ok(result) => Outcome::Ok(result),
3246 Err(error) => Outcome::Err(error),
3247 },
3248 Outcome::Err(error) => Outcome::Err(error),
3249 Outcome::Cancelled(reason) => Outcome::Cancelled(reason),
3250 Outcome::Panicked(payload) => Outcome::Panicked(payload),
3251 }
3252 }
3253}
3254
3255impl ResourceHandler for MountedResourceHandler {
3256 fn definition(&self) -> Resource {
3257 let mut def = self.inner.definition();
3258 def.uri.clone_from(&self.mounted_uri);
3259 def
3260 }
3261
3262 fn final_client_direct_https(&self) -> bool {
3263 self.inner.final_client_direct_https()
3264 }
3265
3266 fn declares_final_mrtr(&self) -> bool {
3267 self.inner.declares_final_mrtr()
3268 }
3269
3270 fn template(&self) -> Option<ResourceTemplate> {
3271 self.mounted_template.clone()
3272 }
3273
3274 fn final_title(&self) -> Option<&str> {
3275 self.inner.final_title()
3276 }
3277
3278 fn final_icons(&self) -> Option<&[RawIcon]> {
3279 self.inner.final_icons()
3280 }
3281
3282 fn final_annotations(&self) -> Option<&Annotations> {
3283 self.inner.final_annotations()
3284 }
3285
3286 fn final_metadata(&self) -> Option<&OpenMetadata> {
3287 self.inner.final_metadata()
3288 }
3289
3290 fn final_template_title(&self) -> Option<&str> {
3291 self.inner.final_template_title()
3292 }
3293
3294 fn final_template_icons(&self) -> Option<&[RawIcon]> {
3295 self.inner.final_template_icons()
3296 }
3297
3298 fn final_template_annotations(&self) -> Option<&Annotations> {
3299 self.inner.final_template_annotations()
3300 }
3301
3302 fn final_template_metadata(&self) -> Option<&OpenMetadata> {
3303 self.inner.final_template_metadata()
3304 }
3305
3306 fn icon(&self) -> Option<&Icon> {
3307 self.inner.icon()
3308 }
3309
3310 fn version(&self) -> Option<&str> {
3311 self.inner.version()
3312 }
3313
3314 fn tags(&self) -> &[String] {
3315 self.inner.tags()
3316 }
3317
3318 fn timeout(&self) -> Option<Duration> {
3319 self.inner.timeout()
3320 }
3321
3322 fn on_subscribe(&self, ctx: &McpContext, uri: &str) -> McpResult<()> {
3323 let source_uri = self.translate_incoming_uri(uri)?;
3324 self.inner.on_subscribe(ctx, &source_uri)
3325 }
3326
3327 fn on_unsubscribe(&self, ctx: &McpContext, uri: &str) -> McpResult<()> {
3328 let source_uri = self.translate_incoming_uri(uri)?;
3329 self.inner.on_unsubscribe(ctx, &source_uri)
3330 }
3331
3332 fn final_resource_read_cache_hint_provenance(&self) -> FinalResourceReadCacheHintProvenance {
3333 self.inner.final_resource_read_cache_hint_provenance()
3334 }
3335
3336 fn read(&self, ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
3337 self.inner
3338 .read(ctx)
3339 .map(|contents| self.translate_outgoing_contents(contents))
3340 }
3341
3342 fn read_with_uri(
3343 &self,
3344 ctx: &McpContext,
3345 uri: &str,
3346 params: &UriParams,
3347 ) -> McpResult<Vec<ResourceContent>> {
3348 let source_uri = self.translate_incoming_uri(uri)?;
3349 self.inner
3350 .read_with_uri(ctx, &source_uri, params)
3351 .map(|contents| self.translate_outgoing_contents(contents))
3352 }
3353
3354 fn read_async_with_uri<'a>(
3355 &'a self,
3356 ctx: &'a McpContext,
3357 uri: &'a str,
3358 params: &'a UriParams,
3359 ) -> BoxFuture<'a, McpOutcome<Vec<ResourceContent>>> {
3360 Box::pin(async move {
3361 let source_uri = match self.translate_incoming_uri(uri) {
3362 Ok(source_uri) => source_uri,
3363 Err(error) => return Outcome::Err(error),
3364 };
3365 self.inner
3366 .read_async_with_uri(ctx, &source_uri, params)
3367 .await
3368 .map(|contents| self.translate_outgoing_contents(contents))
3369 })
3370 }
3371
3372 fn read_async<'a>(
3373 &'a self,
3374 ctx: &'a McpContext,
3375 ) -> BoxFuture<'a, McpOutcome<Vec<ResourceContent>>> {
3376 Box::pin(async move {
3377 self.inner
3378 .read_async(ctx)
3379 .await
3380 .map(|contents| self.translate_outgoing_contents(contents))
3381 })
3382 }
3383
3384 fn read_final(&self, ctx: &McpContext) -> McpResult<CompleteResult<FinalReadResourceResult>> {
3385 self.inner
3386 .read_final(ctx)
3387 .and_then(|result| self.translate_outgoing_final_result(result))
3388 }
3389
3390 fn read_final_with_uri(
3391 &self,
3392 ctx: &McpContext,
3393 uri: &str,
3394 params: &UriParams,
3395 ) -> McpResult<CompleteResult<FinalReadResourceResult>> {
3396 let source_uri = self.translate_incoming_uri(uri)?;
3397 self.inner
3398 .read_final_with_uri(ctx, &source_uri, params)
3399 .and_then(|result| self.translate_outgoing_final_result(result))
3400 }
3401
3402 fn read_final_outcome(
3403 &self,
3404 ctx: &McpContext,
3405 ) -> McpResult<FinalMethodOutcome<FinalReadResourceResult>> {
3406 self.inner
3407 .read_final_outcome(ctx)
3408 .and_then(|result| self.translate_outgoing_final_method_outcome(result))
3409 }
3410
3411 fn read_final_outcome_with_uri(
3412 &self,
3413 ctx: &McpContext,
3414 uri: &str,
3415 params: &UriParams,
3416 ) -> McpResult<FinalMethodOutcome<FinalReadResourceResult>> {
3417 let source_uri = self.translate_incoming_uri(uri)?;
3418 self.inner
3419 .read_final_outcome_with_uri(ctx, &source_uri, params)
3420 .and_then(|result| self.translate_outgoing_final_method_outcome(result))
3421 }
3422
3423 fn read_final_async<'a>(
3424 &'a self,
3425 ctx: &'a McpContext,
3426 ) -> BoxFuture<'a, McpOutcome<CompleteResult<FinalReadResourceResult>>> {
3427 Box::pin(async move {
3428 self.translate_outgoing_final_outcome(self.inner.read_final_async(ctx).await)
3429 })
3430 }
3431
3432 fn read_final_async_with_uri<'a>(
3433 &'a self,
3434 ctx: &'a McpContext,
3435 uri: &'a str,
3436 params: &'a UriParams,
3437 ) -> BoxFuture<'a, McpOutcome<CompleteResult<FinalReadResourceResult>>> {
3438 Box::pin(async move {
3439 let source_uri = match self.translate_incoming_uri(uri) {
3440 Ok(source_uri) => source_uri,
3441 Err(error) => return Outcome::Err(error),
3442 };
3443 self.translate_outgoing_final_outcome(
3444 self.inner
3445 .read_final_async_with_uri(ctx, &source_uri, params)
3446 .await,
3447 )
3448 })
3449 }
3450
3451 fn read_final_outcome_async<'a>(
3452 &'a self,
3453 ctx: &'a McpContext,
3454 ) -> BoxFuture<'a, McpOutcome<FinalMethodOutcome<FinalReadResourceResult>>> {
3455 Box::pin(async move {
3456 self.translate_outgoing_final_method_outcome_async(
3457 self.inner.read_final_outcome_async(ctx).await,
3458 )
3459 })
3460 }
3461
3462 fn read_final_outcome_async_with_uri<'a>(
3463 &'a self,
3464 ctx: &'a McpContext,
3465 uri: &'a str,
3466 params: &'a UriParams,
3467 ) -> BoxFuture<'a, McpOutcome<FinalMethodOutcome<FinalReadResourceResult>>> {
3468 Box::pin(async move {
3469 let source_uri = match self.translate_incoming_uri(uri) {
3470 Ok(source_uri) => source_uri,
3471 Err(error) => return Outcome::Err(error),
3472 };
3473 self.translate_outgoing_final_method_outcome_async(
3474 self.inner
3475 .read_final_outcome_async_with_uri(ctx, &source_uri, params)
3476 .await,
3477 )
3478 })
3479 }
3480
3481 fn read_async_with_uri_in_request<'a>(
3482 &'a self,
3483 ctx: &'a McpContext,
3484 request_cx: &'a Cx,
3485 uri: &'a str,
3486 params: &'a UriParams,
3487 ) -> BoxFuture<'a, McpOutcome<Vec<ResourceContent>>> {
3488 Box::pin(async move {
3489 let source_uri = match self.translate_incoming_uri(uri) {
3490 Ok(source_uri) => source_uri,
3491 Err(error) => return Outcome::Err(error),
3492 };
3493 self.inner
3494 .read_async_with_uri_in_request(ctx, request_cx, &source_uri, params)
3495 .await
3496 .map(|contents| self.translate_outgoing_contents(contents))
3497 })
3498 }
3499
3500 fn read_final_async_with_uri_in_request<'a>(
3501 &'a self,
3502 ctx: &'a McpContext,
3503 request_cx: &'a Cx,
3504 uri: &'a str,
3505 params: &'a UriParams,
3506 ) -> BoxFuture<'a, McpOutcome<CompleteResult<FinalReadResourceResult>>> {
3507 Box::pin(async move {
3508 let source_uri = match self.translate_incoming_uri(uri) {
3509 Ok(source_uri) => source_uri,
3510 Err(error) => return Outcome::Err(error),
3511 };
3512 self.translate_outgoing_final_outcome(
3513 self.inner
3514 .read_final_async_with_uri_in_request(ctx, request_cx, &source_uri, params)
3515 .await,
3516 )
3517 })
3518 }
3519
3520 fn read_final_outcome_async_with_uri_in_request<'a>(
3521 &'a self,
3522 ctx: &'a McpContext,
3523 request_cx: &'a Cx,
3524 uri: &'a str,
3525 params: &'a UriParams,
3526 ) -> BoxFuture<'a, McpOutcome<FinalMethodOutcome<FinalReadResourceResult>>> {
3527 Box::pin(async move {
3528 let source_uri = match self.translate_incoming_uri(uri) {
3529 Ok(source_uri) => source_uri,
3530 Err(error) => return Outcome::Err(error),
3531 };
3532 self.translate_outgoing_final_method_outcome_async(
3533 self.inner
3534 .read_final_outcome_async_with_uri_in_request(
3535 ctx,
3536 request_cx,
3537 &source_uri,
3538 params,
3539 )
3540 .await,
3541 )
3542 })
3543 }
3544
3545 fn read_final_outcome_async_with_uri_resuming_in_request<'a>(
3546 &'a self,
3547 ctx: &'a McpContext,
3548 request_cx: &'a Cx,
3549 uri: &'a str,
3550 params: &'a UriParams,
3551 resume_inputs: Option<&'a MrtrCompletedInputs>,
3552 ) -> BoxFuture<'a, McpOutcome<FinalMethodOutcome<FinalReadResourceResult>>> {
3553 Box::pin(async move {
3554 let source_uri = match self.translate_incoming_uri(uri) {
3555 Ok(source_uri) => source_uri,
3556 Err(error) => return Outcome::Err(error),
3557 };
3558 self.translate_outgoing_final_method_outcome_async(
3559 self.inner
3560 .read_final_outcome_async_with_uri_resuming_in_request(
3561 ctx,
3562 request_cx,
3563 &source_uri,
3564 params,
3565 resume_inputs,
3566 )
3567 .await,
3568 )
3569 })
3570 }
3571}
3572
3573pub struct MountedPromptHandler {
3577 inner: BoxedPromptHandler,
3578 mounted_name: String,
3579}
3580
3581impl MountedPromptHandler {
3582 pub fn new(inner: BoxedPromptHandler, mounted_name: String) -> Self {
3584 Self {
3585 inner,
3586 mounted_name,
3587 }
3588 }
3589}
3590
3591impl PromptHandler for MountedPromptHandler {
3592 fn definition(&self) -> Prompt {
3593 let mut def = self.inner.definition();
3594 def.name.clone_from(&self.mounted_name);
3595 def
3596 }
3597
3598 fn final_client_direct_https(&self) -> bool {
3599 self.inner.final_client_direct_https()
3600 }
3601
3602 fn declares_final_mrtr(&self) -> bool {
3603 self.inner.declares_final_mrtr()
3604 }
3605
3606 fn final_title(&self) -> Option<&str> {
3607 self.inner.final_title()
3608 }
3609
3610 fn final_icons(&self) -> Option<&[RawIcon]> {
3611 self.inner.final_icons()
3612 }
3613
3614 fn final_metadata(&self) -> Option<&OpenMetadata> {
3615 self.inner.final_metadata()
3616 }
3617
3618 fn icon(&self) -> Option<&Icon> {
3619 self.inner.icon()
3620 }
3621
3622 fn version(&self) -> Option<&str> {
3623 self.inner.version()
3624 }
3625
3626 fn tags(&self) -> &[String] {
3627 self.inner.tags()
3628 }
3629
3630 fn timeout(&self) -> Option<Duration> {
3631 self.inner.timeout()
3632 }
3633
3634 fn get(
3635 &self,
3636 ctx: &McpContext,
3637 arguments: std::collections::HashMap<String, String>,
3638 ) -> McpResult<Vec<PromptMessage>> {
3639 self.inner.get(ctx, arguments)
3640 }
3641
3642 fn get_async<'a>(
3643 &'a self,
3644 ctx: &'a McpContext,
3645 arguments: std::collections::HashMap<String, String>,
3646 ) -> BoxFuture<'a, McpOutcome<Vec<PromptMessage>>> {
3647 self.inner.get_async(ctx, arguments)
3648 }
3649
3650 fn get_final(
3651 &self,
3652 ctx: &McpContext,
3653 arguments: std::collections::HashMap<String, String>,
3654 ) -> McpResult<CompleteResult<FinalGetPromptResult>> {
3655 self.inner.get_final(ctx, arguments)
3656 }
3657
3658 fn get_final_outcome(
3659 &self,
3660 ctx: &McpContext,
3661 arguments: std::collections::HashMap<String, String>,
3662 ) -> McpResult<FinalMethodOutcome<FinalGetPromptResult>> {
3663 self.inner.get_final_outcome(ctx, arguments)
3664 }
3665
3666 fn get_final_async<'a>(
3667 &'a self,
3668 ctx: &'a McpContext,
3669 arguments: std::collections::HashMap<String, String>,
3670 ) -> BoxFuture<'a, McpOutcome<CompleteResult<FinalGetPromptResult>>> {
3671 self.inner.get_final_async(ctx, arguments)
3672 }
3673
3674 fn get_final_outcome_async<'a>(
3675 &'a self,
3676 ctx: &'a McpContext,
3677 arguments: std::collections::HashMap<String, String>,
3678 ) -> BoxFuture<'a, McpOutcome<FinalMethodOutcome<FinalGetPromptResult>>> {
3679 self.inner.get_final_outcome_async(ctx, arguments)
3680 }
3681
3682 fn get_async_in_request<'a>(
3683 &'a self,
3684 ctx: &'a McpContext,
3685 request_cx: &'a Cx,
3686 arguments: std::collections::HashMap<String, String>,
3687 ) -> BoxFuture<'a, McpOutcome<Vec<PromptMessage>>> {
3688 self.inner.get_async_in_request(ctx, request_cx, arguments)
3689 }
3690
3691 fn get_final_async_in_request<'a>(
3692 &'a self,
3693 ctx: &'a McpContext,
3694 request_cx: &'a Cx,
3695 arguments: std::collections::HashMap<String, String>,
3696 ) -> BoxFuture<'a, McpOutcome<CompleteResult<FinalGetPromptResult>>> {
3697 self.inner
3698 .get_final_async_in_request(ctx, request_cx, arguments)
3699 }
3700
3701 fn get_final_outcome_async_in_request<'a>(
3702 &'a self,
3703 ctx: &'a McpContext,
3704 request_cx: &'a Cx,
3705 arguments: std::collections::HashMap<String, String>,
3706 ) -> BoxFuture<'a, McpOutcome<FinalMethodOutcome<FinalGetPromptResult>>> {
3707 self.inner
3708 .get_final_outcome_async_in_request(ctx, request_cx, arguments)
3709 }
3710
3711 fn get_final_outcome_async_resuming_in_request<'a>(
3712 &'a self,
3713 ctx: &'a McpContext,
3714 request_cx: &'a Cx,
3715 arguments: std::collections::HashMap<String, String>,
3716 resume_inputs: Option<&'a MrtrCompletedInputs>,
3717 ) -> BoxFuture<'a, McpOutcome<FinalMethodOutcome<FinalGetPromptResult>>> {
3718 self.inner.get_final_outcome_async_resuming_in_request(
3719 ctx,
3720 request_cx,
3721 arguments,
3722 resume_inputs,
3723 )
3724 }
3725}
3726
3727pub(crate) struct MountedCompletionHandler {
3732 inner: BoxedCompletionHandler,
3733 prompt_names: HashMap<String, String>,
3734 resource_templates: HashMap<String, String>,
3735}
3736
3737impl MountedCompletionHandler {
3738 pub(crate) fn for_prompt(
3739 inner: BoxedCompletionHandler,
3740 mounted_name: String,
3741 original_name: String,
3742 ) -> Self {
3743 let mut prompt_names = HashMap::new();
3744 prompt_names.insert(mounted_name, original_name);
3745 Self {
3746 inner,
3747 prompt_names,
3748 resource_templates: HashMap::new(),
3749 }
3750 }
3751
3752 pub(crate) fn for_resource_template(
3753 inner: BoxedCompletionHandler,
3754 mounted_uri: String,
3755 original_uri: String,
3756 ) -> Self {
3757 let mut resource_templates = HashMap::new();
3758 resource_templates.insert(mounted_uri, original_uri);
3759 Self {
3760 inner,
3761 prompt_names: HashMap::new(),
3762 resource_templates,
3763 }
3764 }
3765
3766 fn rewrite_legacy(&self, mut params: LegacyCompletionParams) -> LegacyCompletionParams {
3767 match &mut params.reference {
3768 LegacyCompletionReference::Prompt { name } => {
3769 if let Some(original) = self.prompt_names.get(name) {
3770 name.clone_from(original);
3771 }
3772 }
3773 LegacyCompletionReference::Resource { uri } => {
3774 if let Some(original) = self.resource_templates.get(uri) {
3775 uri.clone_from(original);
3776 }
3777 }
3778 }
3779 params
3780 }
3781
3782 fn rewrite_final(&self, mut params: FinalCompletionParams) -> FinalCompletionParams {
3783 match &mut params.reference {
3784 FinalCompletionReference::Prompt { name }
3785 | FinalCompletionReference::PromptWithTitle { name, .. } => {
3786 if let Some(original) = self.prompt_names.get(name) {
3787 name.clone_from(original);
3788 }
3789 }
3790 FinalCompletionReference::Resource { uri } => {
3791 if let Some(original) = self.resource_templates.get(uri) {
3792 uri.clone_from(original);
3793 }
3794 }
3795 }
3796 params
3797 }
3798}
3799
3800impl CompletionHandler for MountedCompletionHandler {
3801 fn timeout(&self) -> Option<Duration> {
3802 self.inner.timeout()
3803 }
3804
3805 fn complete_legacy(
3806 &self,
3807 ctx: &McpContext,
3808 params: LegacyCompletionParams,
3809 ) -> McpResult<CompletionValues> {
3810 self.inner.complete_legacy(ctx, self.rewrite_legacy(params))
3811 }
3812
3813 fn complete_final(
3814 &self,
3815 ctx: &McpContext,
3816 params: FinalCompletionParams,
3817 ) -> McpResult<FinalCompletionValues> {
3818 self.inner.complete_final(ctx, self.rewrite_final(params))
3819 }
3820
3821 fn complete_legacy_async_in_request<'a>(
3822 &'a self,
3823 ctx: &'a McpContext,
3824 request_cx: &'a Cx,
3825 params: LegacyCompletionParams,
3826 ) -> BoxFuture<'a, McpOutcome<CompletionValues>> {
3827 self.inner
3828 .complete_legacy_async_in_request(ctx, request_cx, self.rewrite_legacy(params))
3829 }
3830
3831 fn complete_final_async_in_request<'a>(
3832 &'a self,
3833 ctx: &'a McpContext,
3834 request_cx: &'a Cx,
3835 params: FinalCompletionParams,
3836 ) -> BoxFuture<'a, McpOutcome<FinalCompletionValues>> {
3837 self.inner
3838 .complete_final_async_in_request(ctx, request_cx, self.rewrite_final(params))
3839 }
3840}
3841
3842pub(crate) struct PrefixedFallbackCompletionHandler {
3848 inner: BoxedCompletionHandler,
3849 prompt_prefix: Option<String>,
3850 template_prefix: Option<String>,
3851}
3852
3853impl PrefixedFallbackCompletionHandler {
3854 pub(crate) fn new(
3855 inner: BoxedCompletionHandler,
3856 prompt_prefix: Option<&str>,
3857 template_prefix: Option<&str>,
3858 ) -> Self {
3859 Self {
3860 inner,
3861 prompt_prefix: nonempty_mount_prefix(prompt_prefix),
3862 template_prefix: nonempty_mount_prefix(template_prefix),
3863 }
3864 }
3865
3866 fn rewrite_legacy(&self, mut params: LegacyCompletionParams) -> LegacyCompletionParams {
3867 match &mut params.reference {
3868 LegacyCompletionReference::Prompt { name } => {
3869 if let Some(original) = strip_mount_prefix(name, self.prompt_prefix.as_deref()) {
3870 *name = original;
3871 }
3872 }
3873 LegacyCompletionReference::Resource { uri } => {
3874 if let Some(original) = strip_mount_prefix(uri, self.template_prefix.as_deref()) {
3875 *uri = original;
3876 }
3877 }
3878 }
3879 params
3880 }
3881
3882 fn rewrite_final(&self, mut params: FinalCompletionParams) -> FinalCompletionParams {
3883 match &mut params.reference {
3884 FinalCompletionReference::Prompt { name }
3885 | FinalCompletionReference::PromptWithTitle { name, .. } => {
3886 if let Some(original) = strip_mount_prefix(name, self.prompt_prefix.as_deref()) {
3887 *name = original;
3888 }
3889 }
3890 FinalCompletionReference::Resource { uri } => {
3891 if let Some(original) = strip_mount_prefix(uri, self.template_prefix.as_deref()) {
3892 *uri = original;
3893 }
3894 }
3895 }
3896 params
3897 }
3898}
3899
3900fn nonempty_mount_prefix(prefix: Option<&str>) -> Option<String> {
3901 prefix
3902 .filter(|prefix| !prefix.is_empty())
3903 .map(str::to_owned)
3904}
3905
3906fn strip_mount_prefix(value: &str, prefix: Option<&str>) -> Option<String> {
3907 let prefix = prefix?;
3908 value
3909 .strip_prefix(prefix)
3910 .and_then(|rest| rest.strip_prefix('/'))
3911 .map(str::to_owned)
3912}
3913
3914impl CompletionHandler for PrefixedFallbackCompletionHandler {
3915 fn timeout(&self) -> Option<Duration> {
3916 self.inner.timeout()
3917 }
3918
3919 fn complete_legacy(
3920 &self,
3921 ctx: &McpContext,
3922 params: LegacyCompletionParams,
3923 ) -> McpResult<CompletionValues> {
3924 self.inner.complete_legacy(ctx, self.rewrite_legacy(params))
3925 }
3926
3927 fn complete_final(
3928 &self,
3929 ctx: &McpContext,
3930 params: FinalCompletionParams,
3931 ) -> McpResult<FinalCompletionValues> {
3932 self.inner.complete_final(ctx, self.rewrite_final(params))
3933 }
3934
3935 fn complete_legacy_async_in_request<'a>(
3936 &'a self,
3937 ctx: &'a McpContext,
3938 request_cx: &'a Cx,
3939 params: LegacyCompletionParams,
3940 ) -> BoxFuture<'a, McpOutcome<CompletionValues>> {
3941 self.inner
3942 .complete_legacy_async_in_request(ctx, request_cx, self.rewrite_legacy(params))
3943 }
3944
3945 fn complete_final_async_in_request<'a>(
3946 &'a self,
3947 ctx: &'a McpContext,
3948 request_cx: &'a Cx,
3949 params: FinalCompletionParams,
3950 ) -> BoxFuture<'a, McpOutcome<FinalCompletionValues>> {
3951 self.inner
3952 .complete_final_async_in_request(ctx, request_cx, self.rewrite_final(params))
3953 }
3954}
3955
3956#[cfg(test)]
3957mod tests {
3958 use super::*;
3959 use asupersync::Cx;
3960 use std::sync::{
3961 Mutex, OnceLock,
3962 atomic::{AtomicUsize, Ordering},
3963 };
3964
3965 fn input_required_result(request_state: &str) -> InputRequiredResult {
3966 let input = format!(
3967 r#"{{"resultType":"input_required","inputRequests":{{"confirmation":{{"type":"boolean"}}}},"requestState":"{request_state}"}}"#
3968 );
3969 let (decoded, diagnostic) = decode_peer_result(
3970 &input,
3971 ResultPeerEra::Modern,
3972 &CoreResultDiscriminatorPolicy,
3973 )
3974 .expect("final input-required result is admitted");
3975 assert_eq!(diagnostic, None);
3976 let DecodedResult::InputRequired(result) = decoded else {
3977 panic!("input-required discriminator selects its final result branch");
3978 };
3979 result
3980 }
3981
3982 fn encode_input_required(result: &InputRequiredResult) -> String {
3983 encode_result(&DecodedResult::InputRequired(result.clone()))
3984 }
3985
3986 #[test]
3987 fn handler_final_complete_contract_positive() {
3988 let payload = serde_json::json!({
3989 "content": [{"type": "text", "text": "shipped handler result"}],
3990 "isError": false,
3991 });
3992
3993 let encoded = encode_final_complete_result(payload.clone())
3994 .expect("a complete handler payload is admitted by the final result contract");
3995
3996 assert_eq!(
3997 encoded.get("resultType"),
3998 Some(&serde_json::json!("complete"))
3999 );
4000 assert_eq!(encoded.get("content"), payload.get("content"));
4001 assert_eq!(encoded.get("isError"), payload.get("isError"));
4002 }
4003
4004 #[test]
4005 fn handler_final_complete_contract_planted_negative() {
4006 let baseline = serde_json::json!({
4007 "content": [{"type": "text", "text": "shipped handler result"}],
4008 "isError": false,
4009 });
4010 let mut planted = baseline.clone();
4011 planted
4012 .as_object_mut()
4013 .expect("complete payload is an object")
4014 .insert(
4015 "resultType".to_string(),
4016 serde_json::json!("input_required"),
4017 );
4018
4019 assert_eq!(
4020 baseline.get("content"),
4021 planted.get("content"),
4022 "the discriminator is the sole planted dimension"
4023 );
4024 assert_eq!(baseline.get("isError"), planted.get("isError"));
4025 let planted_before = planted.clone();
4026
4027 encode_final_complete_result(baseline)
4028 .expect("the baseline must remain a valid complete payload");
4029 let error = encode_final_complete_result(planted.clone())
4030 .expect_err("a handler cannot preselect a different final result discriminator");
4031
4032 assert_eq!(error.code, fastmcp_core::McpErrorCode::InternalError);
4033 assert_eq!(
4034 planted, planted_before,
4035 "the rejected payload remains unchanged for callers that retry or log it"
4036 );
4037 }
4038
4039 fn custom_icon() -> &'static Icon {
4040 static ICON: OnceLock<Icon> = OnceLock::new();
4041 ICON.get_or_init(|| Icon::new("https://example.test/component.svg"))
4042 }
4043
4044 #[test]
4047 fn progress_sender_sends_notification_without_total() {
4048 let sent = Arc::new(Mutex::new(Vec::new()));
4049 let sent_clone = Arc::clone(&sent);
4050 let sender = ProgressNotificationSender::new(ProgressMarker::from("tok-1"), move |req| {
4051 sent_clone.lock().unwrap().push(req);
4052 });
4053
4054 sender.send_progress(0.5, None, None);
4055
4056 let messages = sent.lock().unwrap();
4057 assert_eq!(messages.len(), 1);
4058 assert_eq!(messages[0].method, "notifications/progress");
4059 let params = messages[0].params.as_ref().unwrap();
4060 assert_eq!(params["progress"], 0.5);
4061 assert!(params.get("total").is_none() || params["total"].is_null());
4062 }
4063
4064 #[test]
4065 fn progress_sender_sends_notification_with_total() {
4066 let sent = Arc::new(Mutex::new(Vec::new()));
4067 let sent_clone = Arc::clone(&sent);
4068 let sender = ProgressNotificationSender::new(ProgressMarker::from("tok-2"), move |req| {
4069 sent_clone.lock().unwrap().push(req);
4070 });
4071
4072 sender.send_progress(3.0, Some(10.0), None);
4073
4074 let messages = sent.lock().unwrap();
4075 let params = messages[0].params.as_ref().unwrap();
4076 assert_eq!(params["progress"], 3.0);
4077 assert_eq!(params["total"], 10.0);
4078 }
4079
4080 #[test]
4081 fn public_final_context_progress_preserves_beyond_f64_number_lexemes() {
4082 let sent = Arc::new(Mutex::new(Vec::new()));
4083 let sent_clone = Arc::clone(&sent);
4084 let reporter = ProgressNotificationSender::new_final(
4085 ProgressMarker::from("final-progress"),
4086 move |request| {
4087 sent_clone
4088 .lock()
4089 .expect("notification collection is not poisoned")
4090 .push(request);
4091 },
4092 )
4093 .into_reporter();
4094 let context = McpContext::with_progress(Cx::for_testing(), 2715, reporter);
4095 let progress: serde_json::Number =
4096 serde_json::from_str("1e400").expect("arbitrary-precision progress parses");
4097 let total: serde_json::Number =
4098 serde_json::from_str("1e400").expect("arbitrary-precision total parses");
4099 context.report_progress_exact(progress, Some(total), Some("retained"));
4100
4101 let notifications = sent
4102 .lock()
4103 .expect("notification collection is not poisoned");
4104 assert_eq!(notifications.len(), 1);
4105 let wire = serde_json::to_string(
4106 notifications[0]
4107 .params
4108 .as_ref()
4109 .expect("notification has parameters"),
4110 )
4111 .expect("final progress parameters serialize");
4112 assert!(wire.contains("\"progressToken\":\"final-progress\""));
4113 assert!(wire.contains("\"progress\":1e+400"));
4118 assert!(wire.contains("\"total\":1e+400"));
4119 }
4120
4121 #[test]
4122 fn public_legacy_context_exact_progress_emits_nothing() {
4123 let sent = Arc::new(Mutex::new(Vec::new()));
4124 let sent_clone = Arc::clone(&sent);
4125 let reporter = ProgressNotificationSender::new(
4126 ProgressMarker::from("ordinary-final-progress"),
4127 move |request| {
4128 sent_clone
4129 .lock()
4130 .expect("notification collection is not poisoned")
4131 .push(request);
4132 },
4133 )
4134 .into_reporter();
4135 let context = McpContext::with_progress(Cx::for_testing(), 2766, reporter);
4136 context.report_progress_exact(
4137 serde_json::from_str("1e400").expect("arbitrary-precision progress parses"),
4138 Some(serde_json::from_str("1e400").expect("arbitrary-precision total parses")),
4139 Some("complete"),
4140 );
4141 assert!(
4142 sent.lock()
4143 .expect("notification collection is not poisoned")
4144 .is_empty(),
4145 "the otherwise identical exact progress must not cross a legacy sender"
4146 );
4147 }
4148
4149 #[test]
4150 fn public_final_context_exact_progress_admits_signed_and_greater_than_total_values() {
4151 let sent = Arc::new(Mutex::new(Vec::new()));
4152 let sent_clone = Arc::clone(&sent);
4153 let reporter = ProgressNotificationSender::new_final(
4154 ProgressMarker::from("unconstrained-final-progress"),
4155 move |request| {
4156 sent_clone
4157 .lock()
4158 .expect("notification collection is not poisoned")
4159 .push(request);
4160 },
4161 )
4162 .into_reporter();
4163 let context = McpContext::with_progress(Cx::for_testing(), 2804, reporter);
4164
4165 context.report_progress_exact(
4166 serde_json::from_str("1e400").expect("unchanged progress parses"),
4167 Some(serde_json::from_str("1e399").expect("one-variable smaller total parses")),
4168 Some("complete"),
4169 );
4170 context.report_progress_exact(
4171 serde_json::from_str("-1").expect("negative progress parses"),
4172 Some(serde_json::from_str("-2").expect("negative total parses")),
4173 Some("rollback"),
4174 );
4175
4176 let notifications = sent
4177 .lock()
4178 .expect("notification collection is not poisoned");
4179 assert_eq!(notifications.len(), 2);
4180 let first = serde_json::to_string(
4181 notifications[0]
4182 .params
4183 .as_ref()
4184 .expect("first notification has parameters"),
4185 )
4186 .expect("first final progress parameters serialize");
4187 let second = serde_json::to_string(
4188 notifications[1]
4189 .params
4190 .as_ref()
4191 .expect("second notification has parameters"),
4192 )
4193 .expect("second final progress parameters serialize");
4194 assert!(first.contains("\"progress\":1e+400"));
4195 assert!(first.contains("\"total\":1e+399"));
4196 assert!(second.contains("\"progress\":-1"));
4197 assert!(second.contains("\"total\":-2"));
4198 }
4199
4200 fn final_progress_number(source: &str) -> serde_json::Number {
4201 serde_json::from_str(source).expect("finite JSON number parses")
4202 }
4203
4204 #[test]
4205 fn final_progress_runtime_accepts_negative_and_greater_than_total_values() {
4206 let sent = Arc::new(Mutex::new(Vec::new()));
4207 let sent_clone = Arc::clone(&sent);
4208 let runtime = Arc::new(FinalProgressRuntime::new(
4209 ProgressMarker::from("runtime-progress"),
4210 move |request| {
4211 sent_clone
4212 .lock()
4213 .expect("notification collection is not poisoned")
4214 .push(request);
4215 },
4216 ));
4217
4218 runtime.send_progress_exact(
4219 final_progress_number("-2"),
4220 Some(final_progress_number("-3")),
4221 Some("negative"),
4222 );
4223 assert!(runtime.flush_pending());
4224 runtime.send_progress_exact(
4225 final_progress_number("12000"),
4226 Some(final_progress_number("11999")),
4227 Some("beyond total"),
4228 );
4229 assert!(runtime.finalize());
4230
4231 let sent = sent
4232 .lock()
4233 .expect("notification collection is not poisoned");
4234 assert_eq!(sent.len(), 2);
4235 assert_eq!(sent[0].params.as_ref().unwrap()["progress"], -2);
4236 assert_eq!(sent[0].params.as_ref().unwrap()["total"], -3);
4237 assert_eq!(sent[1].params.as_ref().unwrap()["progress"], 12_000);
4238 assert_eq!(sent[1].params.as_ref().unwrap()["total"], 11_999);
4239 }
4240
4241 #[test]
4242 fn final_progress_runtime_rejects_regression_without_replacing_pending_value() {
4243 let sent = Arc::new(Mutex::new(Vec::new()));
4244 let sent_clone = Arc::clone(&sent);
4245 let runtime =
4246 FinalProgressRuntime::new(ProgressMarker::from("runtime-regression"), move |request| {
4247 sent_clone
4248 .lock()
4249 .expect("notification collection is not poisoned")
4250 .push(request);
4251 });
4252
4253 runtime.send_progress_exact(
4254 final_progress_number("12"),
4255 Some(final_progress_number("11")),
4256 Some("accepted"),
4257 );
4258 runtime.send_progress_exact(
4261 final_progress_number("11"),
4262 Some(final_progress_number("10")),
4263 Some("regression"),
4264 );
4265 assert!(runtime.finalize());
4266
4267 let sent = sent
4268 .lock()
4269 .expect("notification collection is not poisoned");
4270 assert_eq!(sent.len(), 1);
4271 assert_eq!(sent[0].params.as_ref().unwrap()["progress"], 12);
4272 assert_eq!(sent[0].params.as_ref().unwrap()["total"], 11);
4273 assert_eq!(
4274 sent[0].params.as_ref().unwrap()["message"],
4275 "accepted",
4276 "the rejected frame leaves the pending observable unchanged"
4277 );
4278 }
4279
4280 #[test]
4281 fn final_progress_runtime_coalesces_increasing_updates_to_the_latest_value() {
4282 let sent = Arc::new(Mutex::new(Vec::new()));
4283 let sent_clone = Arc::clone(&sent);
4284 let runtime =
4285 FinalProgressRuntime::new(ProgressMarker::from("runtime-coalesce"), move |request| {
4286 sent_clone
4287 .lock()
4288 .expect("notification collection is not poisoned")
4289 .push(request);
4290 });
4291
4292 for progress in [1, 2, 3] {
4293 runtime.send_progress_exact(
4294 final_progress_number(&progress.to_string()),
4295 None,
4296 Some("coalesced"),
4297 );
4298 }
4299 assert!(runtime.flush_pending());
4300 assert!(!runtime.flush_pending());
4301
4302 let sent = sent
4303 .lock()
4304 .expect("notification collection is not poisoned");
4305 assert_eq!(sent.len(), 1);
4306 assert_eq!(sent[0].params.as_ref().unwrap()["progress"], 3);
4307 }
4308
4309 #[test]
4310 fn final_progress_runtime_cancellation_discards_pending_while_finalization_flushes_it() {
4311 let finalized = Arc::new(Mutex::new(Vec::new()));
4312 let finalized_clone = Arc::clone(&finalized);
4313 let finalizing_runtime =
4314 FinalProgressRuntime::new(ProgressMarker::from("runtime-finalize"), move |request| {
4315 finalized_clone
4316 .lock()
4317 .expect("notification collection is not poisoned")
4318 .push(request);
4319 });
4320 finalizing_runtime.send_progress_exact(final_progress_number("1"), None, None);
4321 finalizing_runtime.send_progress_exact(final_progress_number("2"), None, None);
4322 assert!(finalizing_runtime.finalize());
4323 assert!(!finalizing_runtime.cancel());
4324 assert_eq!(
4325 finalized
4326 .lock()
4327 .expect("notification collection is not poisoned")
4328 .as_slice()[0]
4329 .params
4330 .as_ref()
4331 .unwrap()["progress"],
4332 2
4333 );
4334
4335 let cancelled = Arc::new(Mutex::new(Vec::new()));
4336 let cancelled_clone = Arc::clone(&cancelled);
4337 let cancelled_runtime =
4338 FinalProgressRuntime::new(ProgressMarker::from("runtime-cancel"), move |request| {
4339 cancelled_clone
4340 .lock()
4341 .expect("notification collection is not poisoned")
4342 .push(request);
4343 });
4344 cancelled_runtime.send_progress_exact(final_progress_number("1"), None, None);
4345 cancelled_runtime.send_progress_exact(final_progress_number("2"), None, None);
4346 assert!(cancelled_runtime.cancel());
4347 assert!(!cancelled_runtime.finalize());
4348 assert!(!cancelled_runtime.flush_pending());
4349 assert!(
4350 cancelled
4351 .lock()
4352 .expect("notification collection is not poisoned")
4353 .is_empty(),
4354 "cancellation differs only in winning the terminal race"
4355 );
4356 }
4357
4358 #[test]
4359 fn progress_sender_sends_notification_with_message() {
4360 let sent = Arc::new(Mutex::new(Vec::new()));
4361 let sent_clone = Arc::clone(&sent);
4362 let sender = ProgressNotificationSender::new(ProgressMarker::from("tok-3"), move |req| {
4363 sent_clone.lock().unwrap().push(req);
4364 });
4365
4366 sender.send_progress(1.0, Some(5.0), Some("loading"));
4367
4368 let messages = sent.lock().unwrap();
4369 let params = messages[0].params.as_ref().unwrap();
4370 assert_eq!(params["message"], "loading");
4371 }
4372
4373 #[test]
4374 fn progress_sender_rejects_non_finite_progress_and_total() {
4375 let sent = Arc::new(Mutex::new(Vec::new()));
4376 let sent_clone = Arc::clone(&sent);
4377 let sender =
4378 ProgressNotificationSender::new(ProgressMarker::from("finite-check"), move |request| {
4379 sent_clone.lock().unwrap().push(request);
4380 });
4381
4382 for (progress, total) in [
4383 (f64::NAN, None),
4384 (f64::INFINITY, None),
4385 (f64::NEG_INFINITY, None),
4386 (1.0, Some(f64::NAN)),
4387 (1.0, Some(f64::INFINITY)),
4388 (1.0, Some(f64::NEG_INFINITY)),
4389 ] {
4390 sender.send_progress(progress, total, Some("must not be sent"));
4391 }
4392
4393 assert!(sent.lock().unwrap().is_empty());
4394 }
4395
4396 #[test]
4397 fn progress_sender_rejects_serialization_failure() {
4398 let sent = Arc::new(Mutex::new(Vec::new()));
4399 let sent_clone = Arc::clone(&sent);
4400 let sender = ProgressNotificationSender::new(
4401 ProgressMarker::from("serialization-check"),
4402 move |request| {
4403 sent_clone.lock().unwrap().push(request);
4404 },
4405 );
4406
4407 sender.send_progress_with_serializer(1.0, Some(2.0), None, |_| {
4408 Result::<serde_json::Value, ()>::Err(())
4409 });
4410
4411 assert!(sent.lock().unwrap().is_empty());
4412 }
4413
4414 #[test]
4415 fn progress_sender_contains_callback_panic() {
4416 let attempts = Arc::new(std::sync::atomic::AtomicUsize::new(0));
4417 let callback_attempts = Arc::clone(&attempts);
4418 let sender =
4419 ProgressNotificationSender::new(ProgressMarker::from("panic-check"), move |_request| {
4420 callback_attempts.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
4421 panic!("progress callback panic payload");
4422 });
4423
4424 sender.send_progress(1.0, Some(2.0), None);
4425
4426 assert_eq!(attempts.load(std::sync::atomic::Ordering::Relaxed), 1);
4427 }
4428
4429 #[test]
4430 fn progress_sender_debug_redacts_marker() {
4431 let canary = "progress-marker-debug-canary";
4432 let sender = ProgressNotificationSender::new(ProgressMarker::from(canary), |_| {});
4433 let debug = format!("{:?}", sender);
4434
4435 assert!(debug.contains("ProgressNotificationSender"));
4436 assert!(!debug.contains(canary));
4437 assert!(!debug.contains("marker"));
4438 }
4439
4440 #[test]
4441 fn progress_sender_into_reporter() {
4442 let sender = ProgressNotificationSender::new(ProgressMarker::from("tok-rpt"), |_| {});
4443 let _reporter = sender.into_reporter();
4444 }
4445
4446 #[test]
4447 fn final_progress_runtime_into_reporter_retains_the_request_marker() {
4448 let runtime = Arc::new(FinalProgressRuntime::new(
4449 ProgressMarker::from("final-runtime-marker"),
4450 |_| {},
4451 ));
4452 let reporter = runtime.into_reporter();
4453 assert_eq!(
4454 reporter.marker(),
4455 Some(&serde_json::json!("final-runtime-marker")),
4456 "as_proxy correlates inbound progressToken from the request-owned final reporter"
4457 );
4458 }
4459
4460 #[test]
4463 fn bidirectional_senders_default_is_empty() {
4464 let senders = BidirectionalSenders::new();
4465 assert!(senders.sampling.is_none());
4466 assert!(senders.elicitation.is_none());
4467 assert!(senders.roots.is_none());
4468 }
4469
4470 #[test]
4471 fn bidirectional_senders_debug_shows_presence() {
4472 let senders = BidirectionalSenders::new();
4473 let debug = format!("{:?}", senders);
4474 assert!(debug.contains("sampling: false"));
4475 assert!(debug.contains("elicitation: false"));
4476 assert!(debug.contains("roots: false"));
4477 }
4478
4479 #[test]
4482 fn create_context_no_progress_no_state() {
4483 let cx = Cx::for_testing();
4484 let ctx = create_context_with_progress(cx, 42, None, None, |_| {});
4485 assert_eq!(ctx.request_id(), 42);
4486 }
4487
4488 #[test]
4489 fn create_context_with_progress_marker() {
4490 let cx = Cx::for_testing();
4491 let marker = ProgressMarker::from("ctx-pm");
4492 let ctx = create_context_with_progress(cx, 7, Some(marker), None, |_| {});
4493 assert_eq!(ctx.request_id(), 7);
4494 }
4495
4496 #[test]
4497 fn create_context_with_state_only() {
4498 let cx = Cx::for_testing();
4499 let state = SessionState::new();
4500 state.set("k", &"v");
4501 let ctx = create_context_with_progress(cx, 10, None, Some(state), |_| {});
4502 let val: Option<String> = ctx.get_state("k");
4503 assert_eq!(val.as_deref(), Some("v"));
4504 }
4505
4506 #[test]
4507 fn create_context_with_progress_and_state() {
4508 let cx = Cx::for_testing();
4509 let marker = ProgressMarker::from("both");
4510 let state = SessionState::new();
4511 let ctx = create_context_with_progress(cx, 99, Some(marker), Some(state), |_| {});
4512 assert_eq!(ctx.request_id(), 99);
4513 }
4514
4515 struct StubTool;
4518
4519 impl ToolHandler for StubTool {
4520 fn definition(&self) -> Tool {
4521 Tool {
4522 name: "stub".to_string(),
4523 description: Some("a stub tool".to_string()),
4524 input_schema: serde_json::json!({"type": "object"}),
4525 output_schema: None,
4526 icon: None,
4527 version: None,
4528 tags: vec![],
4529 annotations: None,
4530 }
4531 }
4532
4533 fn call(&self, _ctx: &McpContext, args: serde_json::Value) -> McpResult<Vec<Content>> {
4534 Ok(vec![Content::text(format!("echo: {args}"))])
4535 }
4536 }
4537
4538 #[test]
4539 fn tool_handler_defaults_return_none() {
4540 let tool = StubTool;
4541 assert!(tool.icon().is_none());
4542 assert!(tool.version().is_none());
4543 assert!(tool.tags().is_empty());
4544 assert!(tool.annotations().is_none());
4545 assert!(tool.output_schema().is_none());
4546 assert_eq!(
4547 tool.final_tool_schema_authority(),
4548 FinalToolSchemaAuthority::Local
4549 );
4550 assert!(tool.timeout().is_none());
4551 }
4552
4553 #[test]
4554 fn tool_handler_call_sync() {
4555 let tool = StubTool;
4556 let cx = Cx::for_testing();
4557 let ctx = McpContext::new(cx, 1);
4558 let result = tool.call(&ctx, serde_json::json!({"x": 1})).unwrap();
4559 assert_eq!(result.len(), 1);
4560 }
4561
4562 #[test]
4563 fn tool_handler_final_surface_promotes_legacy_content_without_changing_legacy_call() {
4564 let tool = StubTool;
4565 let cx = Cx::for_testing();
4566 let ctx = McpContext::new(cx, 1);
4567 let legacy = tool
4568 .call(&ctx, serde_json::json!({"x": 1}))
4569 .expect("legacy handler result");
4570 let final_result = tool
4571 .call_final(&ctx, serde_json::json!({"x": 1}))
4572 .expect("legacy handler promotes into the final result algebra");
4573
4574 assert!(matches!(legacy.as_slice(), [Content::Text { .. }]));
4575 assert!(matches!(
4576 final_result.payload.content.as_slice(),
4577 [ContentBlock::Text { .. }]
4578 ));
4579 assert!(final_result.meta.server_info.is_none());
4580 }
4581
4582 #[test]
4583 fn tool_handler_default_final_outcome_is_complete_and_preserves_legacy_adapter() {
4584 let tool = StubTool;
4585 let cx = Cx::for_testing();
4586 let ctx = McpContext::new(cx, 1);
4587
4588 let final_outcome = tool
4589 .call_final_outcome(&ctx, serde_json::json!({"x": 1}))
4590 .expect("default final outcome promotes the legacy result");
4591 let FinalToolOutcome::Complete(final_result) = final_outcome else {
4592 panic!("a default tool handler must select the final complete branch");
4593 };
4594 assert!(matches!(
4595 final_result.payload.content.as_slice(),
4596 [ContentBlock::Text { text, .. }] if text == "echo: {\"x\":1}"
4597 ));
4598
4599 let legacy = tool
4600 .call(&ctx, serde_json::json!({"x": 1}))
4601 .expect("legacy adapter remains callable after the final outcome");
4602 assert!(matches!(
4603 legacy.as_slice(),
4604 [Content::Text { text }] if text == "echo: {\"x\":1}"
4605 ));
4606 }
4607
4608 #[cfg(feature = "tasks")]
4609 #[test]
4610 fn tool_handler_task_creation_outcome_preserves_legacy_adapter() {
4611 struct TaskCreatingTool {
4612 legacy_calls: AtomicUsize,
4613 }
4614
4615 impl ToolHandler for TaskCreatingTool {
4616 fn definition(&self) -> Tool {
4617 StubTool.definition()
4618 }
4619
4620 fn declares_final_tasks(&self) -> bool {
4621 true
4622 }
4623
4624 fn call(
4625 &self,
4626 _ctx: &McpContext,
4627 _arguments: serde_json::Value,
4628 ) -> McpResult<Vec<Content>> {
4629 self.legacy_calls.fetch_add(1, Ordering::Relaxed);
4630 Ok(vec![Content::text("exact legacy completion")])
4631 }
4632
4633 fn call_final_outcome(
4634 &self,
4635 _ctx: &McpContext,
4636 _arguments: serde_json::Value,
4637 ) -> McpResult<FinalToolOutcome> {
4638 Ok(FinalToolOutcome::CreateTask {
4639 work_descriptor: FinalTaskWorkDescriptor::new(serde_json::json!({
4640 "operation": "durable-tool-work",
4641 }))?,
4642 status_message: Some("awaiting durable work".to_owned()),
4643 })
4644 }
4645 }
4646
4647 let tool = TaskCreatingTool {
4648 legacy_calls: AtomicUsize::new(0),
4649 };
4650 let cx = Cx::for_testing();
4651 let ctx = McpContext::new(cx, 1);
4652 let request_cx = Cx::for_testing();
4653
4654 let Outcome::Ok(final_outcome) = fastmcp_core::block_on(
4655 tool.call_final_outcome_async_in_request(&ctx, &request_cx, serde_json::json!({})),
4656 ) else {
4657 panic!("declared task-capable handler selects a router-owned task creation");
4658 };
4659 let FinalToolOutcome::CreateTask {
4660 work_descriptor,
4661 status_message: Some(status_message),
4662 } = final_outcome
4663 else {
4664 panic!("task-capable handler must retain non-null initial work and its status");
4665 };
4666 assert_eq!(
4667 work_descriptor.as_value(),
4668 &serde_json::json!({"operation": "durable-tool-work"})
4669 );
4670 assert_eq!(status_message, "awaiting durable work");
4671 assert_eq!(tool.legacy_calls.load(Ordering::Relaxed), 0);
4672
4673 let legacy = tool
4674 .call(&ctx, serde_json::json!({}))
4675 .expect("legacy adapter remains exact for a task-capable handler");
4676 assert!(
4677 matches!(legacy.as_slice(), [Content::Text { text }] if text == "exact legacy completion")
4678 );
4679 assert_eq!(tool.legacy_calls.load(Ordering::Relaxed), 1);
4680 }
4681
4682 #[cfg(feature = "tasks")]
4683 #[test]
4684 fn tool_handler_undeclared_task_creation_fails_closed_and_preserves_legacy_adapter() {
4685 struct UndeclaredTaskCreatingTool {
4686 legacy_calls: AtomicUsize,
4687 }
4688
4689 impl ToolHandler for UndeclaredTaskCreatingTool {
4690 fn definition(&self) -> Tool {
4691 StubTool.definition()
4692 }
4693
4694 fn call(
4695 &self,
4696 _ctx: &McpContext,
4697 _arguments: serde_json::Value,
4698 ) -> McpResult<Vec<Content>> {
4699 self.legacy_calls.fetch_add(1, Ordering::Relaxed);
4700 Ok(vec![Content::text("exact legacy completion")])
4701 }
4702
4703 fn call_final_outcome(
4704 &self,
4705 _ctx: &McpContext,
4706 _arguments: serde_json::Value,
4707 ) -> McpResult<FinalToolOutcome> {
4708 Ok(FinalToolOutcome::CreateTask {
4709 work_descriptor: FinalTaskWorkDescriptor::new(serde_json::json!({
4710 "operation": "must-not-run-without-declaration",
4711 }))?,
4712 status_message: None,
4713 })
4714 }
4715 }
4716
4717 let tool = UndeclaredTaskCreatingTool {
4718 legacy_calls: AtomicUsize::new(0),
4719 };
4720 assert!(
4721 !tool.declares_final_tasks(),
4722 "the declaration is opt-in and defaults to false"
4723 );
4724 let cx = Cx::for_testing();
4725 let ctx = McpContext::new(cx, 1);
4726 let request_cx = Cx::for_testing();
4727
4728 let outcome = fastmcp_core::block_on(tool.call_final_outcome_async_in_request(
4729 &ctx,
4730 &request_cx,
4731 serde_json::json!({}),
4732 ));
4733 let Outcome::Err(error) = outcome else {
4734 panic!("an undeclared task outcome must fail closed");
4735 };
4736 assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidRequest);
4737 assert_eq!(error.message, UNDECLARED_FINAL_TASK_OUTCOME_ERROR);
4738
4739 let legacy = tool
4740 .call(&ctx, serde_json::json!({}))
4741 .expect("legacy adapter remains exact when final task outcome is rejected");
4742 assert!(
4743 matches!(legacy.as_slice(), [Content::Text { text }] if text == "exact legacy completion")
4744 );
4745 assert_eq!(tool.legacy_calls.load(Ordering::Relaxed), 1);
4746 }
4747
4748 #[cfg(feature = "tasks")]
4749 #[test]
4750 fn task_creation_work_descriptor_rejects_null_before_an_outcome_can_be_constructed() {
4751 let error = FinalTaskWorkDescriptor::new(serde_json::Value::Null)
4752 .expect_err("a task-capable handler must not request inert null work");
4753
4754 assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidParams);
4755 assert_eq!(
4756 error.message,
4757 "Final task work descriptor must identify an application operation"
4758 );
4759 }
4760
4761 #[test]
4762 fn declared_task_capable_handler_preserves_input_required_algebra() {
4763 struct InputRequiredTool {
4764 result: InputRequiredResult,
4765 legacy_calls: AtomicUsize,
4766 }
4767
4768 impl ToolHandler for InputRequiredTool {
4769 fn definition(&self) -> Tool {
4770 StubTool.definition()
4771 }
4772
4773 fn declares_final_tasks(&self) -> bool {
4774 true
4775 }
4776
4777 fn call(
4778 &self,
4779 _ctx: &McpContext,
4780 _arguments: serde_json::Value,
4781 ) -> McpResult<Vec<Content>> {
4782 self.legacy_calls.fetch_add(1, Ordering::Relaxed);
4783 Ok(vec![Content::text("legacy projection")])
4784 }
4785
4786 fn call_final_outcome(
4787 &self,
4788 _ctx: &McpContext,
4789 _arguments: serde_json::Value,
4790 ) -> McpResult<FinalToolOutcome> {
4791 Ok(FinalToolOutcome::InputRequired(self.result.clone()))
4792 }
4793 }
4794
4795 let tool = InputRequiredTool {
4796 result: input_required_result("retry-tool-7"),
4797 legacy_calls: AtomicUsize::new(0),
4798 };
4799 let expected = encode_input_required(&tool.result);
4800 let cx = Cx::for_testing();
4801 let ctx = McpContext::new(cx, 1);
4802
4803 let outcome = tool
4804 .call_final_outcome(&ctx, serde_json::json!({}))
4805 .expect("task-capable final handler may select input_required");
4806
4807 let FinalToolOutcome::InputRequired(result) = outcome else {
4808 panic!("final handler must preserve the input-required result branch");
4809 };
4810 assert_eq!(encode_input_required(&result), expected);
4811 assert_eq!(tool.legacy_calls.load(Ordering::Relaxed), 0);
4812 }
4813
4814 #[test]
4815 fn tool_handler_legacy_projection_leaves_unprojectable_input_state_unchanged() {
4816 struct InputRequiredTool {
4817 result: InputRequiredResult,
4818 legacy_calls: AtomicUsize,
4819 }
4820
4821 impl ToolHandler for InputRequiredTool {
4822 fn definition(&self) -> Tool {
4823 StubTool.definition()
4824 }
4825
4826 fn call(
4827 &self,
4828 _ctx: &McpContext,
4829 _arguments: serde_json::Value,
4830 ) -> McpResult<Vec<Content>> {
4831 self.legacy_calls.fetch_add(1, Ordering::Relaxed);
4832 Ok(vec![Content::text("legacy projection")])
4833 }
4834
4835 fn call_final_outcome(
4836 &self,
4837 _ctx: &McpContext,
4838 _arguments: serde_json::Value,
4839 ) -> McpResult<FinalToolOutcome> {
4840 Ok(FinalToolOutcome::InputRequired(self.result.clone()))
4841 }
4842 }
4843
4844 let tool = InputRequiredTool {
4845 result: input_required_result("retry-tool-7"),
4846 legacy_calls: AtomicUsize::new(0),
4847 };
4848 let original = encode_input_required(&tool.result);
4849 let cx = Cx::for_testing();
4850 let ctx = McpContext::new(cx, 1);
4851
4852 let legacy = tool
4853 .call(&ctx, serde_json::json!({}))
4854 .expect("legacy handler result remains exact");
4855
4856 assert!(
4857 matches!(legacy.as_slice(), [Content::Text { text }] if text == "legacy projection")
4858 );
4859 assert_eq!(tool.legacy_calls.load(Ordering::Relaxed), 1);
4860 assert_eq!(
4861 encode_input_required(&tool.result),
4862 original,
4863 "legacy projection must not coerce or mutate final-only requestState"
4864 );
4865 }
4866
4867 #[test]
4868 fn tool_handler_final_catalog_accessors_preserve_final_only_fields() {
4869 struct FinalCatalogTool {
4870 metadata: OpenMetadata,
4871 icons: Vec<RawIcon>,
4872 }
4873
4874 impl ToolHandler for FinalCatalogTool {
4875 fn definition(&self) -> Tool {
4876 (StubTool).definition()
4877 }
4878
4879 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
4880 Ok(vec![Content::text("final catalog")])
4881 }
4882
4883 fn final_title(&self) -> Option<&str> {
4884 Some("Final Title")
4885 }
4886
4887 fn final_icons(&self) -> Option<&[RawIcon]> {
4888 Some(&self.icons)
4889 }
4890
4891 fn final_metadata(&self) -> Option<&OpenMetadata> {
4892 Some(&self.metadata)
4893 }
4894 }
4895
4896 let metadata = OpenMetadata::try_from_entries([(
4897 "com.example/catalog".to_owned(),
4898 serde_json::json!({"preserve": true}),
4899 )])
4900 .expect("final metadata");
4901 let icons = vec![RawIcon::try_new("https://example.test/icon.png").expect("final icon")];
4902 let tool = FinalCatalogTool { metadata, icons };
4903
4904 assert_eq!(tool.final_title(), Some("Final Title"));
4905 assert_eq!(tool.final_icons().map(<[RawIcon]>::len), Some(1));
4906 assert_eq!(
4907 tool.final_metadata()
4908 .and_then(|metadata| metadata.get("com.example/catalog")),
4909 Some(&serde_json::json!({"preserve": true}))
4910 );
4911 assert!(tool.output_schema().is_none());
4912 }
4913
4914 #[test]
4915 fn tool_handler_call_sync_error() {
4916 struct FailTool;
4917 impl ToolHandler for FailTool {
4918 fn definition(&self) -> Tool {
4919 Tool {
4920 name: "fail".to_string(),
4921 description: None,
4922 input_schema: serde_json::json!({"type": "object"}),
4923 output_schema: None,
4924 icon: None,
4925 version: None,
4926 tags: vec![],
4927 annotations: None,
4928 }
4929 }
4930 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
4931 Err(McpError::internal_error("boom"))
4932 }
4933 }
4934
4935 let tool = FailTool;
4936 let cx = Cx::for_testing();
4937 let ctx = McpContext::new(cx, 1);
4938 let err = tool.call(&ctx, serde_json::json!({})).unwrap_err();
4939 assert!(err.message.contains("boom"));
4940 }
4941
4942 struct StubResource;
4945
4946 impl ResourceHandler for StubResource {
4947 fn definition(&self) -> Resource {
4948 Resource {
4949 uri: "file:///stub".to_string(),
4950 name: "stub".to_string(),
4951 description: None,
4952 mime_type: Some("text/plain".to_string()),
4953 icon: None,
4954 version: None,
4955 tags: vec![],
4956 }
4957 }
4958
4959 fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
4960 Ok(vec![ResourceContent {
4961 uri: "file:///stub".to_string(),
4962 mime_type: Some("text/plain".to_string()),
4963 text: Some("hello".to_string()),
4964 blob: None,
4965 }])
4966 }
4967 }
4968
4969 #[test]
4970 fn resource_handler_defaults_return_none() {
4971 let res = StubResource;
4972 assert!(res.template().is_none());
4973 assert!(res.icon().is_none());
4974 assert!(res.version().is_none());
4975 assert!(res.tags().is_empty());
4976 assert!(res.timeout().is_none());
4977 }
4978
4979 #[test]
4980 fn resource_handler_read_with_uri_delegates_to_read() {
4981 let res = StubResource;
4982 let cx = Cx::for_testing();
4983 let ctx = McpContext::new(cx, 1);
4984 let params = UriParams::new();
4985 let result = res.read_with_uri(&ctx, "file:///stub", ¶ms).unwrap();
4986 assert_eq!(result.len(), 1);
4987 }
4988
4989 #[test]
4990 fn resource_handler_final_surface_promotes_legacy_content_without_changing_legacy_read() {
4991 let resource = StubResource;
4992 let cx = Cx::for_testing();
4993 let ctx = McpContext::new(cx, 1);
4994 let legacy = resource.read(&ctx).expect("legacy handler result");
4995 let final_result = resource
4996 .read_final(&ctx)
4997 .expect("legacy resource promotes into the final result algebra");
4998
4999 assert_eq!(legacy[0].uri, "file:///stub");
5000 assert!(matches!(
5001 final_result.payload.contents.as_slice(),
5002 [EmbeddedResourceContents::Text { uri, text, .. }]
5003 if uri.as_str() == "file:///stub" && text == "hello"
5004 ));
5005 assert_eq!(
5006 final_result.payload.ttl_ms.as_str(),
5007 DEFAULT_FINAL_RESOURCE_TTL_MS.to_string()
5008 );
5009 assert_eq!(final_result.payload.cache_scope, CacheScope::Private);
5010 }
5011
5012 #[test]
5013 fn final_resource_default_preserves_uri_without_template_params() {
5014 struct UriAwareResource;
5015
5016 impl ResourceHandler for UriAwareResource {
5017 fn definition(&self) -> Resource {
5018 StubResource.definition()
5019 }
5020
5021 fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
5022 Err(McpError::internal_error(
5023 "final URI dispatch must not fall back to read",
5024 ))
5025 }
5026
5027 fn read_with_uri(
5028 &self,
5029 _ctx: &McpContext,
5030 uri: &str,
5031 _params: &UriParams,
5032 ) -> McpResult<Vec<ResourceContent>> {
5033 Ok(vec![ResourceContent {
5034 uri: uri.to_owned(),
5035 mime_type: Some("text/plain".to_owned()),
5036 text: Some("matched URI".to_owned()),
5037 blob: None,
5038 }])
5039 }
5040 }
5041
5042 let resource = UriAwareResource;
5043 let cx = Cx::for_testing();
5044 let ctx = McpContext::new(cx, 1);
5045 let result = resource
5046 .read_final_with_uri(&ctx, "file:///requested", &UriParams::new())
5047 .expect("final resource dispatch preserves its URI without template parameters");
5048
5049 assert!(matches!(
5050 result.payload.contents.as_slice(),
5051 [EmbeddedResourceContents::Text { uri, text, .. }]
5052 if uri.as_str() == "file:///requested" && text == "matched URI"
5053 ));
5054 }
5055
5056 struct StubPrompt;
5059
5060 impl PromptHandler for StubPrompt {
5061 fn definition(&self) -> Prompt {
5062 Prompt {
5063 name: "stub".to_string(),
5064 description: Some("a stub prompt".to_string()),
5065 arguments: vec![],
5066 icon: None,
5067 version: None,
5068 tags: vec![],
5069 }
5070 }
5071
5072 fn get(
5073 &self,
5074 _ctx: &McpContext,
5075 _arguments: HashMap<String, String>,
5076 ) -> McpResult<Vec<PromptMessage>> {
5077 Ok(vec![])
5078 }
5079 }
5080
5081 #[test]
5082 fn prompt_handler_defaults_return_none() {
5083 let prompt = StubPrompt;
5084 assert!(prompt.icon().is_none());
5085 assert!(prompt.version().is_none());
5086 assert!(prompt.tags().is_empty());
5087 assert!(prompt.timeout().is_none());
5088 }
5089
5090 #[test]
5091 fn prompt_handler_final_surface_promotes_legacy_messages_without_changing_legacy_get() {
5092 let prompt = StubPrompt;
5093 let cx = Cx::for_testing();
5094 let ctx = McpContext::new(cx, 1);
5095 let legacy = prompt
5096 .get(&ctx, HashMap::new())
5097 .expect("legacy handler result");
5098 let final_result = prompt
5099 .get_final(&ctx, HashMap::new())
5100 .expect("legacy prompt promotes into the final result algebra");
5101
5102 assert!(legacy.is_empty());
5103 assert!(final_result.payload.messages.is_empty());
5104 assert!(final_result.payload.description.is_none());
5105 assert!(final_result.meta.server_info.is_none());
5106 }
5107
5108 #[test]
5111 fn mounted_tool_handler_overrides_name() {
5112 let inner = Box::new(StubTool) as BoxedToolHandler;
5113 let mounted = MountedToolHandler::new(inner, "prefix_stub".to_string());
5114 let def = mounted.definition();
5115 assert_eq!(def.name, "prefix_stub");
5116 assert_eq!(def.description.as_deref(), Some("a stub tool"));
5117 }
5118
5119 #[test]
5120 fn mounted_tool_handler_delegates_defaults() {
5121 let inner = Box::new(StubTool) as BoxedToolHandler;
5122 let mounted = MountedToolHandler::new(inner, "m_stub".to_string());
5123 assert!(mounted.tags().is_empty());
5124 assert!(mounted.annotations().is_none());
5125 assert!(mounted.output_schema().is_none());
5126 assert_eq!(
5127 mounted.final_tool_schema_authority(),
5128 FinalToolSchemaAuthority::Local
5129 );
5130 assert!(mounted.timeout().is_none());
5131 }
5132
5133 #[test]
5134 fn mounted_tool_handler_preserves_upstream_schema_authority() {
5135 struct UpstreamSchemaTool;
5136
5137 impl ToolHandler for UpstreamSchemaTool {
5138 fn definition(&self) -> Tool {
5139 Tool {
5140 name: "upstream-schema".to_string(),
5141 description: None,
5142 input_schema: serde_json::json!({"type": "object"}),
5143 output_schema: Some(serde_json::json!({"type": "string"})),
5144 icon: None,
5145 version: None,
5146 tags: Vec::new(),
5147 annotations: None,
5148 }
5149 }
5150
5151 fn final_definition(&self) -> Option<FinalTool> {
5152 Some(
5153 serde_json::from_value(serde_json::json!({
5154 "name": "upstream-schema",
5155 "inputSchema": {"type": "object"},
5156 "outputSchema": {"type": "string"},
5157 "_meta": {"com.example/proxy": {"retained": true}}
5158 }))
5159 .expect("the exact-final mounted fixture is valid"),
5160 )
5161 }
5162
5163 fn final_tool_schema_authority(&self) -> FinalToolSchemaAuthority {
5164 FinalToolSchemaAuthority::Upstream
5165 }
5166
5167 fn upstream_final_tool_schema_registration(
5168 &self,
5169 ) -> Option<UpstreamFinalToolSchemaRegistration> {
5170 Some(UpstreamFinalToolSchemaRegistration::exact_proxy())
5171 }
5172
5173 fn call(
5174 &self,
5175 _ctx: &McpContext,
5176 _arguments: serde_json::Value,
5177 ) -> McpResult<Vec<Content>> {
5178 Ok(Vec::new())
5179 }
5180 }
5181
5182 let mounted = MountedToolHandler::new(
5183 Box::new(UpstreamSchemaTool) as BoxedToolHandler,
5184 "m_upstream".to_string(),
5185 );
5186 assert_eq!(
5187 mounted.final_tool_schema_authority(),
5188 FinalToolSchemaAuthority::Upstream,
5189 "mounting must not turn an exact-final proxy into a locally validated handler"
5190 );
5191 assert!(
5192 mounted.upstream_final_tool_schema_registration().is_some(),
5193 "mounting must retain the sealed upstream-schema registration"
5194 );
5195 assert_eq!(
5196 mounted
5197 .final_definition()
5198 .expect("mounted handler retains the exact final definition")
5199 .output_schema,
5200 Some(serde_json::json!({"type": "string"}))
5201 );
5202 }
5203
5204 #[test]
5205 fn mounted_tool_handler_delegates_call() {
5206 let inner = Box::new(StubTool) as BoxedToolHandler;
5207 let mounted = MountedToolHandler::new(inner, "m_stub".to_string());
5208 let cx = Cx::for_testing();
5209 let ctx = McpContext::new(cx, 1);
5210 let result = mounted.call(&ctx, serde_json::json!({})).unwrap();
5211 assert!(!result.is_empty());
5212 }
5213
5214 #[test]
5217 fn mounted_resource_handler_overrides_uri() {
5218 let inner = Box::new(StubResource) as BoxedResourceHandler;
5219 let mounted = MountedResourceHandler::new(
5220 inner,
5221 "file:///stub".to_string(),
5222 "file:///mounted".to_string(),
5223 );
5224 let def = mounted.definition();
5225 assert_eq!(def.uri, "file:///mounted");
5226 assert_eq!(def.name, "stub");
5227 }
5228
5229 #[test]
5230 fn mounted_resource_handler_template_none_by_default() {
5231 let inner = Box::new(StubResource) as BoxedResourceHandler;
5232 let mounted =
5233 MountedResourceHandler::new(inner, "file:///stub".to_string(), "file:///m".to_string());
5234 assert!(mounted.template().is_none());
5235 }
5236
5237 #[test]
5238 fn mounted_resource_handler_with_template() {
5239 let inner = Box::new(StubResource) as BoxedResourceHandler;
5240 let tmpl = ResourceTemplate {
5241 uri_template: "file:///items/{id}".to_string(),
5242 name: "items".to_string(),
5243 description: None,
5244 mime_type: None,
5245 icon: None,
5246 version: None,
5247 tags: vec![],
5248 };
5249 let mounted = MountedResourceHandler::with_template(
5250 inner,
5251 "file:///items/{id}".to_string(),
5252 "file:///items/{id}".to_string(),
5253 tmpl,
5254 );
5255 let t = mounted.template().expect("template set");
5256 assert_eq!(t.uri_template, "file:///items/{id}");
5257 }
5258
5259 #[test]
5260 fn mounted_resource_handler_delegates_read() {
5261 let inner = Box::new(StubResource) as BoxedResourceHandler;
5262 let mounted =
5263 MountedResourceHandler::new(inner, "file:///stub".to_string(), "file:///m".to_string());
5264 let cx = Cx::for_testing();
5265 let ctx = McpContext::new(cx, 1);
5266 let result = mounted.read(&ctx).unwrap();
5267 assert_eq!(result.len(), 1);
5268 assert_eq!(result[0].uri, "file:///m");
5269 }
5270
5271 #[test]
5272 fn mounted_resource_handler_delegates_tags() {
5273 let inner = Box::new(StubResource) as BoxedResourceHandler;
5274 let mounted =
5275 MountedResourceHandler::new(inner, "file:///stub".to_string(), "file:///m".to_string());
5276 assert!(mounted.tags().is_empty());
5277 }
5278
5279 #[test]
5282 fn mounted_prompt_handler_overrides_name() {
5283 let inner = Box::new(StubPrompt) as BoxedPromptHandler;
5284 let mounted = MountedPromptHandler::new(inner, "ns_stub".to_string());
5285 let def = mounted.definition();
5286 assert_eq!(def.name, "ns_stub");
5287 assert_eq!(def.description.as_deref(), Some("a stub prompt"));
5288 }
5289
5290 #[test]
5291 fn mounted_prompt_handler_delegates_defaults() {
5292 let inner = Box::new(StubPrompt) as BoxedPromptHandler;
5293 let mounted = MountedPromptHandler::new(inner, "ns_stub".to_string());
5294 assert!(mounted.tags().is_empty());
5295 assert!(mounted.timeout().is_none());
5296 }
5297
5298 #[test]
5299 fn mounted_prompt_handler_delegates_get() {
5300 let inner = Box::new(StubPrompt) as BoxedPromptHandler;
5301 let mounted = MountedPromptHandler::new(inner, "ns_stub".to_string());
5302 let cx = Cx::for_testing();
5303 let ctx = McpContext::new(cx, 1);
5304 let result = mounted.get(&ctx, HashMap::new()).unwrap();
5305 assert!(result.is_empty());
5306 }
5307
5308 struct DummySamplingSender;
5311 impl fastmcp_core::SamplingSender for DummySamplingSender {
5312 fn create_message(
5313 &self,
5314 _request: fastmcp_core::SamplingRequest,
5315 ) -> std::pin::Pin<
5316 Box<
5317 dyn std::future::Future<Output = McpResult<fastmcp_core::SamplingResponse>>
5318 + Send
5319 + '_,
5320 >,
5321 > {
5322 Box::pin(async { Err(McpError::internal_error("stub")) })
5323 }
5324 }
5325
5326 struct DummyElicitationSender;
5327 impl fastmcp_core::ElicitationSender for DummyElicitationSender {
5328 fn elicit(
5329 &self,
5330 _request: fastmcp_core::ElicitationRequest,
5331 ) -> std::pin::Pin<
5332 Box<
5333 dyn std::future::Future<Output = McpResult<fastmcp_core::ElicitationResponse>>
5334 + Send
5335 + '_,
5336 >,
5337 > {
5338 Box::pin(async { Err(McpError::internal_error("stub")) })
5339 }
5340 }
5341
5342 struct DummyRootsProvider;
5343 impl fastmcp_core::RootsProvider for DummyRootsProvider {
5344 fn list_roots(
5345 &self,
5346 ) -> std::pin::Pin<
5347 Box<
5348 dyn std::future::Future<Output = McpResult<Vec<fastmcp_core::ClientRoot>>>
5349 + Send
5350 + '_,
5351 >,
5352 > {
5353 Box::pin(async { Ok(vec![fastmcp_core::ClientRoot::new("file:///workspace")]) })
5354 }
5355 }
5356
5357 #[test]
5358 fn bidirectional_senders_with_sampling() {
5359 let senders =
5360 BidirectionalSenders::new().with_sampling(Arc::new(DummySamplingSender) as Arc<_>);
5361 assert!(senders.sampling.is_some());
5362 assert!(senders.elicitation.is_none());
5363 }
5364
5365 #[test]
5366 fn bidirectional_senders_with_elicitation() {
5367 let senders = BidirectionalSenders::new()
5368 .with_elicitation(Arc::new(DummyElicitationSender) as Arc<_>);
5369 assert!(senders.sampling.is_none());
5370 assert!(senders.elicitation.is_some());
5371 assert!(senders.roots.is_none());
5372 }
5373
5374 #[test]
5375 fn bidirectional_senders_with_roots() {
5376 let senders =
5377 BidirectionalSenders::new().with_roots(Arc::new(DummyRootsProvider) as Arc<_>);
5378 assert!(senders.sampling.is_none());
5379 assert!(senders.elicitation.is_none());
5380 assert!(senders.roots.is_some());
5381 }
5382
5383 #[test]
5384 fn bidirectional_senders_with_both() {
5385 let senders = BidirectionalSenders::new()
5386 .with_sampling(Arc::new(DummySamplingSender) as Arc<_>)
5387 .with_elicitation(Arc::new(DummyElicitationSender) as Arc<_>);
5388 assert!(senders.sampling.is_some());
5389 assert!(senders.elicitation.is_some());
5390 }
5391
5392 #[test]
5393 fn bidirectional_senders_clone() {
5394 let senders =
5395 BidirectionalSenders::new().with_sampling(Arc::new(DummySamplingSender) as Arc<_>);
5396 let cloned = senders.clone();
5397 assert!(cloned.sampling.is_some());
5398 }
5399
5400 #[test]
5401 fn bidirectional_senders_debug_with_present() {
5402 let senders = BidirectionalSenders::new()
5403 .with_sampling(Arc::new(DummySamplingSender) as Arc<_>)
5404 .with_elicitation(Arc::new(DummyElicitationSender) as Arc<_>);
5405 let debug = format!("{:?}", senders);
5406 assert!(debug.contains("sampling: true"));
5407 assert!(debug.contains("elicitation: true"));
5408 }
5409
5410 #[test]
5413 fn create_context_with_senders_sampling() {
5414 let cx = Cx::for_testing();
5415 let senders =
5416 BidirectionalSenders::new().with_sampling(Arc::new(DummySamplingSender) as Arc<_>);
5417 let ctx =
5418 create_context_with_progress_and_senders(cx, 1, None, None, |_| {}, Some(&senders));
5419 assert_eq!(ctx.request_id(), 1);
5420 }
5421
5422 #[test]
5423 fn create_context_with_senders_elicitation() {
5424 let cx = Cx::for_testing();
5425 let senders = BidirectionalSenders::new()
5426 .with_elicitation(Arc::new(DummyElicitationSender) as Arc<_>);
5427 let ctx =
5428 create_context_with_progress_and_senders(cx, 2, None, None, |_| {}, Some(&senders));
5429 assert_eq!(ctx.request_id(), 2);
5430 }
5431
5432 #[test]
5433 fn create_context_with_senders_roots_attaches_context_authority() {
5434 let senders =
5435 BidirectionalSenders::new().with_roots(Arc::new(DummyRootsProvider) as Arc<_>);
5436 let ctx = create_context_with_progress_and_senders(
5437 Cx::for_testing(),
5438 7,
5439 None,
5440 None,
5441 |_| {},
5442 Some(&senders),
5443 );
5444
5445 assert!(ctx.can_list_roots());
5446 let roots = fastmcp_core::block_on(ctx.list_roots())
5447 .expect("attached roots provider reaches the context");
5448 assert_eq!(
5449 roots,
5450 vec![fastmcp_core::ClientRoot::new("file:///workspace")]
5451 );
5452 }
5453
5454 #[test]
5455 fn create_context_with_senders_and_progress() {
5456 let cx = Cx::for_testing();
5457 let marker = ProgressMarker::from("sp");
5458 let senders =
5459 BidirectionalSenders::new().with_sampling(Arc::new(DummySamplingSender) as Arc<_>);
5460 let ctx = create_context_with_progress_and_senders(
5461 cx,
5462 3,
5463 Some(marker),
5464 None,
5465 |_| {},
5466 Some(&senders),
5467 );
5468 assert_eq!(ctx.request_id(), 3);
5469 }
5470
5471 #[test]
5472 fn create_context_with_senders_and_state() {
5473 let cx = Cx::for_testing();
5474 let state = SessionState::new();
5475 state.set("key", &"val");
5476 let senders = BidirectionalSenders::new()
5477 .with_elicitation(Arc::new(DummyElicitationSender) as Arc<_>);
5478 let ctx = create_context_with_progress_and_senders(
5479 cx,
5480 4,
5481 None,
5482 Some(state),
5483 |_| {},
5484 Some(&senders),
5485 );
5486 let val: Option<String> = ctx.get_state("key");
5487 assert_eq!(val.as_deref(), Some("val"));
5488 }
5489
5490 #[test]
5491 fn create_context_with_all_options() {
5492 let cx = Cx::for_testing();
5493 let marker = ProgressMarker::from("all");
5494 let state = SessionState::new();
5495 let senders = BidirectionalSenders::new()
5496 .with_sampling(Arc::new(DummySamplingSender) as Arc<_>)
5497 .with_elicitation(Arc::new(DummyElicitationSender) as Arc<_>);
5498 let ctx = create_context_with_progress_and_senders(
5499 cx,
5500 5,
5501 Some(marker),
5502 Some(state),
5503 |_| {},
5504 Some(&senders),
5505 );
5506 assert_eq!(ctx.request_id(), 5);
5507 }
5508
5509 #[test]
5510 fn create_context_with_senders_none() {
5511 let cx = Cx::for_testing();
5512 let ctx = create_context_with_progress_and_senders(cx, 6, None, None, |_| {}, None);
5513 assert_eq!(ctx.request_id(), 6);
5514 assert!(!ctx.can_list_roots());
5515 }
5516
5517 struct CustomTool;
5520 impl ToolHandler for CustomTool {
5521 fn definition(&self) -> Tool {
5522 Tool {
5523 name: "custom".to_string(),
5524 description: None,
5525 input_schema: serde_json::json!({"type": "object"}),
5526 output_schema: None,
5527 icon: None,
5528 version: None,
5529 tags: vec![],
5530 annotations: None,
5531 }
5532 }
5533
5534 fn icon(&self) -> Option<&Icon> {
5535 Some(custom_icon())
5536 }
5537
5538 fn version(&self) -> Option<&str> {
5539 Some("2.0")
5540 }
5541
5542 fn timeout(&self) -> Option<Duration> {
5543 Some(Duration::from_secs(60))
5544 }
5545
5546 fn output_schema(&self) -> Option<serde_json::Value> {
5547 Some(serde_json::json!({"type": "string"}))
5548 }
5549
5550 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
5551 Ok(vec![Content::text("custom")])
5552 }
5553 }
5554
5555 #[test]
5556 fn tool_handler_custom_version() {
5557 assert_eq!(CustomTool.version(), Some("2.0"));
5558 }
5559
5560 #[test]
5561 fn tool_handler_custom_icon() {
5562 assert_eq!(
5563 CustomTool.icon().and_then(|icon| icon.src.as_deref()),
5564 Some("https://example.test/component.svg")
5565 );
5566 }
5567
5568 #[test]
5569 fn tool_handler_custom_timeout() {
5570 assert_eq!(CustomTool.timeout(), Some(Duration::from_secs(60)));
5571 }
5572
5573 #[test]
5574 fn tool_handler_custom_output_schema() {
5575 let schema = CustomTool.output_schema().unwrap();
5576 assert_eq!(schema["type"], "string");
5577 }
5578
5579 struct CustomResource;
5582 impl ResourceHandler for CustomResource {
5583 fn definition(&self) -> Resource {
5584 Resource {
5585 uri: "file:///custom".to_string(),
5586 name: "custom".to_string(),
5587 description: None,
5588 mime_type: None,
5589 icon: None,
5590 version: None,
5591 tags: vec![],
5592 }
5593 }
5594
5595 fn version(&self) -> Option<&str> {
5596 Some("1.5")
5597 }
5598
5599 fn icon(&self) -> Option<&Icon> {
5600 Some(custom_icon())
5601 }
5602
5603 fn timeout(&self) -> Option<Duration> {
5604 Some(Duration::from_secs(30))
5605 }
5606
5607 fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
5608 Ok(vec![ResourceContent {
5609 uri: "file:///custom".to_string(),
5610 mime_type: None,
5611 text: Some("data".to_string()),
5612 blob: None,
5613 }])
5614 }
5615
5616 fn read_with_uri(
5617 &self,
5618 _ctx: &McpContext,
5619 uri: &str,
5620 params: &UriParams,
5621 ) -> McpResult<Vec<ResourceContent>> {
5622 let id = params.get("id").cloned().unwrap_or_default();
5623 Ok(vec![ResourceContent {
5624 uri: uri.to_string(),
5625 mime_type: None,
5626 text: Some(format!("item:{id}")),
5627 blob: None,
5628 }])
5629 }
5630 }
5631
5632 #[test]
5633 fn resource_handler_custom_version() {
5634 assert_eq!(CustomResource.version(), Some("1.5"));
5635 }
5636
5637 #[test]
5638 fn resource_handler_custom_icon() {
5639 assert_eq!(
5640 CustomResource.icon().and_then(|icon| icon.src.as_deref()),
5641 Some("https://example.test/component.svg")
5642 );
5643 }
5644
5645 #[test]
5646 fn resource_handler_custom_timeout() {
5647 assert_eq!(CustomResource.timeout(), Some(Duration::from_secs(30)));
5648 }
5649
5650 #[test]
5651 fn resource_handler_read_with_uri_custom() {
5652 let cx = Cx::for_testing();
5653 let ctx = McpContext::new(cx, 1);
5654 let mut params = UriParams::new();
5655 params.insert("id".to_string(), "42".to_string());
5656 let result = CustomResource
5657 .read_with_uri(&ctx, "file:///items/42", ¶ms)
5658 .unwrap();
5659 assert_eq!(result[0].text.as_deref(), Some("item:42"));
5660 }
5661
5662 struct CustomPrompt;
5665 impl PromptHandler for CustomPrompt {
5666 fn definition(&self) -> Prompt {
5667 Prompt {
5668 name: "custom".to_string(),
5669 description: None,
5670 arguments: vec![],
5671 icon: None,
5672 version: None,
5673 tags: vec![],
5674 }
5675 }
5676
5677 fn version(&self) -> Option<&str> {
5678 Some("3.0")
5679 }
5680
5681 fn icon(&self) -> Option<&Icon> {
5682 Some(custom_icon())
5683 }
5684
5685 fn timeout(&self) -> Option<Duration> {
5686 Some(Duration::from_secs(10))
5687 }
5688
5689 fn get(
5690 &self,
5691 _ctx: &McpContext,
5692 _args: HashMap<String, String>,
5693 ) -> McpResult<Vec<PromptMessage>> {
5694 Ok(vec![])
5695 }
5696 }
5697
5698 #[test]
5699 fn prompt_handler_custom_version() {
5700 assert_eq!(CustomPrompt.version(), Some("3.0"));
5701 }
5702
5703 #[test]
5704 fn prompt_handler_custom_icon() {
5705 assert_eq!(
5706 CustomPrompt.icon().and_then(|icon| icon.src.as_deref()),
5707 Some("https://example.test/component.svg")
5708 );
5709 }
5710
5711 #[test]
5712 fn prompt_handler_custom_timeout() {
5713 assert_eq!(CustomPrompt.timeout(), Some(Duration::from_secs(10)));
5714 }
5715
5716 #[test]
5719 fn mounted_tool_handler_delegates_icon_and_version() {
5720 let inner = Box::new(CustomTool) as BoxedToolHandler;
5721 let mounted = MountedToolHandler::new(inner, "m_custom".to_string());
5722 assert_eq!(mounted.version(), Some("2.0"));
5723 assert_eq!(
5724 mounted.icon().and_then(|icon| icon.src.as_deref()),
5725 Some("https://example.test/component.svg")
5726 );
5727 }
5728
5729 #[test]
5730 fn mounted_tool_handler_delegates_timeout() {
5731 let inner = Box::new(CustomTool) as BoxedToolHandler;
5732 let mounted = MountedToolHandler::new(inner, "m_custom".to_string());
5733 assert_eq!(mounted.timeout(), Some(Duration::from_secs(60)));
5734 }
5735
5736 #[test]
5737 fn mounted_tool_handler_delegates_output_schema() {
5738 let inner = Box::new(CustomTool) as BoxedToolHandler;
5739 let mounted = MountedToolHandler::new(inner, "m_custom".to_string());
5740 let schema = mounted.output_schema().unwrap();
5741 assert_eq!(schema["type"], "string");
5742 }
5743
5744 #[test]
5747 fn mounted_resource_handler_delegates_icon_and_version() {
5748 let inner = Box::new(CustomResource) as BoxedResourceHandler;
5749 let mounted = MountedResourceHandler::new(
5750 inner,
5751 "file:///custom".to_string(),
5752 "ns/file:///custom".to_string(),
5753 );
5754 assert_eq!(mounted.version(), Some("1.5"));
5755 assert_eq!(
5756 mounted.icon().and_then(|icon| icon.src.as_deref()),
5757 Some("https://example.test/component.svg")
5758 );
5759 }
5760
5761 #[test]
5762 fn mounted_resource_handler_delegates_read_with_uri() {
5763 let inner = Box::new(CustomResource) as BoxedResourceHandler;
5764 let mounted = MountedResourceHandler::new(
5765 inner,
5766 "file:///custom".to_string(),
5767 "ns/file:///custom".to_string(),
5768 );
5769 let cx = Cx::for_testing();
5770 let ctx = McpContext::new(cx, 1);
5771 let mut params = UriParams::new();
5772 params.insert("id".to_string(), "99".to_string());
5773 let result = mounted
5774 .read_with_uri(&ctx, "ns/file:///items/99", ¶ms)
5775 .unwrap();
5776 assert_eq!(result[0].text.as_deref(), Some("item:99"));
5777 assert_eq!(result[0].uri, "ns/file:///items/99");
5778 assert!(
5779 mounted
5780 .read_with_uri(&ctx, "other/file:///items/99", ¶ms)
5781 .is_err()
5782 );
5783 }
5784
5785 #[test]
5786 fn mounted_resource_handler_delegates_timeout() {
5787 let inner = Box::new(CustomResource) as BoxedResourceHandler;
5788 let mounted = MountedResourceHandler::new(
5789 inner,
5790 "file:///custom".to_string(),
5791 "file:///m".to_string(),
5792 );
5793 assert_eq!(mounted.timeout(), Some(Duration::from_secs(30)));
5794 }
5795
5796 struct SubscribeProbe {
5797 subscribed: std::sync::Arc<Mutex<Vec<String>>>,
5798 unsubscribed: std::sync::Arc<Mutex<Vec<String>>>,
5799 }
5800
5801 impl SubscribeProbe {
5802 fn new() -> Self {
5803 Self {
5804 subscribed: std::sync::Arc::new(Mutex::new(Vec::new())),
5805 unsubscribed: std::sync::Arc::new(Mutex::new(Vec::new())),
5806 }
5807 }
5808 }
5809
5810 impl ResourceHandler for SubscribeProbe {
5811 fn definition(&self) -> Resource {
5812 Resource {
5813 uri: "file:///orig".to_string(),
5814 name: "probe".to_string(),
5815 description: None,
5816 mime_type: None,
5817 icon: None,
5818 version: None,
5819 tags: vec![],
5820 }
5821 }
5822
5823 fn on_subscribe(&self, _ctx: &McpContext, uri: &str) -> McpResult<()> {
5824 self.subscribed
5825 .lock()
5826 .expect("subscribe probe is not poisoned")
5827 .push(uri.to_owned());
5828 Ok(())
5829 }
5830
5831 fn on_unsubscribe(&self, _ctx: &McpContext, uri: &str) -> McpResult<()> {
5832 self.unsubscribed
5833 .lock()
5834 .expect("unsubscribe probe is not poisoned")
5835 .push(uri.to_owned());
5836 Ok(())
5837 }
5838
5839 fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
5840 Ok(Vec::new())
5841 }
5842 }
5843
5844 #[test]
5845 fn mounted_resource_handler_rewrites_subscribe_uri_to_source() {
5846 let inner = SubscribeProbe::new();
5847 let subscribed = std::sync::Arc::clone(&inner.subscribed);
5848 let unsubscribed = std::sync::Arc::clone(&inner.unsubscribed);
5849 let cx = Cx::for_testing();
5850 let ctx = McpContext::new(cx, 1);
5851 let mounted = MountedResourceHandler::new(
5852 Box::new(inner),
5853 "file:///orig".to_string(),
5854 "ns/file:///orig".to_string(),
5855 );
5856 mounted
5857 .on_subscribe(&ctx, "ns/file:///orig")
5858 .expect("mounted subscribe must reach the inner handler");
5859 mounted
5860 .on_unsubscribe(&ctx, "ns/file:///orig")
5861 .expect("mounted unsubscribe must reach the inner handler");
5862 assert_eq!(
5863 subscribed
5864 .lock()
5865 .expect("subscribe probe is not poisoned")
5866 .as_slice(),
5867 ["file:///orig"]
5868 );
5869 assert_eq!(
5870 unsubscribed
5871 .lock()
5872 .expect("unsubscribe probe is not poisoned")
5873 .as_slice(),
5874 ["file:///orig"]
5875 );
5876 }
5877
5878 #[test]
5879 fn mounted_resource_handler_subscribe_rejects_foreign_namespace() {
5880 let inner = SubscribeProbe::new();
5881 let cx = Cx::for_testing();
5882 let ctx = McpContext::new(cx, 1);
5883 let mounted = MountedResourceHandler::new(
5884 Box::new(inner),
5885 "file:///orig".to_string(),
5886 "ns/file:///orig".to_string(),
5887 );
5888 let error = mounted
5889 .on_subscribe(&ctx, "other/file:///orig")
5890 .expect_err("a foreign mount prefix must not bind the inner resource");
5891 assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidParams);
5892 }
5893
5894 #[test]
5897 fn mounted_prompt_handler_delegates_icon_and_version() {
5898 let inner = Box::new(CustomPrompt) as BoxedPromptHandler;
5899 let mounted = MountedPromptHandler::new(inner, "ns_custom".to_string());
5900 assert_eq!(mounted.version(), Some("3.0"));
5901 assert_eq!(
5902 mounted.icon().and_then(|icon| icon.src.as_deref()),
5903 Some("https://example.test/component.svg")
5904 );
5905 }
5906
5907 #[test]
5908 fn mounted_prompt_handler_delegates_timeout() {
5909 let inner = Box::new(CustomPrompt) as BoxedPromptHandler;
5910 let mounted = MountedPromptHandler::new(inner, "ns_custom".to_string());
5911 assert_eq!(mounted.timeout(), Some(Duration::from_secs(10)));
5912 }
5913
5914 #[test]
5915 fn mounted_prompt_handler_delegates_get_with_args() {
5916 let inner = Box::new(StubPrompt) as BoxedPromptHandler;
5917 let mounted = MountedPromptHandler::new(inner, "ns".to_string());
5918 let cx = Cx::for_testing();
5919 let ctx = McpContext::new(cx, 1);
5920 let mut args = HashMap::new();
5921 args.insert("key".to_string(), "value".to_string());
5922 let result = mounted.get(&ctx, args).unwrap();
5923 assert!(result.is_empty());
5924 }
5925
5926 #[test]
5929 fn progress_sender_multiple_notifications() {
5930 let sent = Arc::new(Mutex::new(Vec::new()));
5931 let sent_clone = Arc::clone(&sent);
5932 let sender = ProgressNotificationSender::new(ProgressMarker::from("multi"), move |req| {
5933 sent_clone.lock().unwrap().push(req);
5934 });
5935
5936 sender.send_progress(0.0, Some(100.0), Some("starting"));
5937 sender.send_progress(50.0, Some(100.0), None);
5938 sender.send_progress(100.0, Some(100.0), Some("done"));
5939
5940 let messages = sent.lock().unwrap();
5941 assert_eq!(messages.len(), 3);
5942 }
5943
5944 struct TaggedTool;
5947 impl ToolHandler for TaggedTool {
5948 fn definition(&self) -> Tool {
5949 Tool {
5950 name: "tagged".to_string(),
5951 description: None,
5952 input_schema: serde_json::json!({"type": "object"}),
5953 output_schema: None,
5954 icon: None,
5955 version: None,
5956 tags: vec!["db".to_string(), "read".to_string()],
5957 annotations: Some(ToolAnnotations {
5958 destructive: Some(false),
5959 idempotent: Some(true),
5960 read_only: Some(true),
5961 open_world_hint: None,
5962 }),
5963 }
5964 }
5965 fn tags(&self) -> &[String] {
5966 &[]
5968 }
5969 fn annotations(&self) -> Option<&ToolAnnotations> {
5970 None
5971 }
5972 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
5973 Ok(vec![Content::text("tagged")])
5974 }
5975 }
5976
5977 #[test]
5978 fn tool_definition_includes_tags_and_annotations() {
5979 let def = TaggedTool.definition();
5980 assert_eq!(def.tags, vec!["db".to_string(), "read".to_string()]);
5981 let ann = def.annotations.unwrap();
5982 assert_eq!(ann.destructive, Some(false));
5983 assert_eq!(ann.idempotent, Some(true));
5984 assert_eq!(ann.read_only, Some(true));
5985 }
5986
5987 #[test]
5990 fn tool_call_async_delegates_to_sync() {
5991 let tool = StubTool;
5992 let cx = Cx::for_testing();
5993 let ctx = McpContext::new(cx, 1);
5994 let outcome = fastmcp_core::block_on(tool.call_async(&ctx, serde_json::json!({"x": 1})));
5995 match outcome {
5996 Outcome::Ok(content) => assert!(!content.is_empty()),
5997 other => panic!("expected Ok, got {:?}", other),
5998 }
5999 }
6000
6001 #[test]
6002 fn resource_read_async_delegates_to_sync() {
6003 let res = StubResource;
6004 let cx = Cx::for_testing();
6005 let ctx = McpContext::new(cx, 1);
6006 let outcome = fastmcp_core::block_on(res.read_async(&ctx));
6007 match outcome {
6008 Outcome::Ok(content) => {
6009 assert_eq!(content.len(), 1);
6010 assert_eq!(content[0].text.as_deref(), Some("hello"));
6011 }
6012 other => panic!("expected Ok, got {:?}", other),
6013 }
6014 }
6015
6016 #[test]
6017 fn resource_read_async_with_uri_empty_params_uses_read_async() {
6018 let res = StubResource;
6019 let cx = Cx::for_testing();
6020 let ctx = McpContext::new(cx, 1);
6021 let params = UriParams::new(); let outcome =
6023 fastmcp_core::block_on(res.read_async_with_uri(&ctx, "file:///stub", ¶ms));
6024 match outcome {
6025 Outcome::Ok(content) => assert_eq!(content[0].text.as_deref(), Some("hello")),
6026 other => panic!("expected Ok, got {:?}", other),
6027 }
6028 }
6029
6030 #[test]
6031 fn resource_read_async_with_uri_nonempty_params_uses_read_with_uri() {
6032 let res = CustomResource;
6033 let cx = Cx::for_testing();
6034 let ctx = McpContext::new(cx, 1);
6035 let mut params = UriParams::new();
6036 params.insert("id".to_string(), "7".to_string());
6037 let outcome =
6038 fastmcp_core::block_on(res.read_async_with_uri(&ctx, "file:///items/7", ¶ms));
6039 match outcome {
6040 Outcome::Ok(content) => assert_eq!(content[0].text.as_deref(), Some("item:7")),
6041 other => panic!("expected Ok, got {:?}", other),
6042 }
6043 }
6044
6045 #[test]
6046 fn prompt_get_async_delegates_to_sync() {
6047 let prompt = StubPrompt;
6048 let cx = Cx::for_testing();
6049 let ctx = McpContext::new(cx, 1);
6050 let outcome = fastmcp_core::block_on(prompt.get_async(&ctx, HashMap::new()));
6051 match outcome {
6052 Outcome::Ok(messages) => assert!(messages.is_empty()),
6053 other => panic!("expected Ok, got {:?}", other),
6054 }
6055 }
6056
6057 #[test]
6060 fn tool_call_async_propagates_error() {
6061 struct ErrTool;
6062 impl ToolHandler for ErrTool {
6063 fn definition(&self) -> Tool {
6064 Tool {
6065 name: "err".to_string(),
6066 description: None,
6067 input_schema: serde_json::json!({"type": "object"}),
6068 output_schema: None,
6069 icon: None,
6070 version: None,
6071 tags: vec![],
6072 annotations: None,
6073 }
6074 }
6075 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
6076 Err(McpError::internal_error("async-err"))
6077 }
6078 }
6079 let cx = Cx::for_testing();
6080 let ctx = McpContext::new(cx, 1);
6081 let outcome = fastmcp_core::block_on(ErrTool.call_async(&ctx, serde_json::json!({})));
6082 match outcome {
6083 Outcome::Err(e) => assert!(e.message.contains("async-err")),
6084 other => panic!("expected Err, got {:?}", other),
6085 }
6086 }
6087
6088 #[test]
6091 fn mounted_tool_handler_delegates_call_async() {
6092 let inner = Box::new(StubTool) as BoxedToolHandler;
6093 let mounted = MountedToolHandler::new(inner, "m_stub".to_string());
6094 let cx = Cx::for_testing();
6095 let ctx = McpContext::new(cx, 1);
6096 let outcome = fastmcp_core::block_on(mounted.call_async(&ctx, serde_json::json!({})));
6097 match outcome {
6098 Outcome::Ok(content) => assert!(!content.is_empty()),
6099 other => panic!("expected Ok, got {:?}", other),
6100 }
6101 }
6102
6103 #[test]
6106 fn mounted_resource_handler_delegates_read_async() {
6107 let inner = Box::new(StubResource) as BoxedResourceHandler;
6108 let mounted =
6109 MountedResourceHandler::new(inner, "file:///stub".to_string(), "file:///m".to_string());
6110 let cx = Cx::for_testing();
6111 let ctx = McpContext::new(cx, 1);
6112 let outcome = fastmcp_core::block_on(mounted.read_async(&ctx));
6113 match outcome {
6114 Outcome::Ok(content) => {
6115 assert_eq!(content.len(), 1);
6116 assert_eq!(content[0].uri, "file:///m");
6117 }
6118 other => panic!("expected Ok, got {:?}", other),
6119 }
6120 }
6121
6122 #[test]
6123 fn mounted_resource_handler_delegates_read_async_with_uri() {
6124 let inner = Box::new(CustomResource) as BoxedResourceHandler;
6125 let mounted = MountedResourceHandler::new(
6126 inner,
6127 "file:///custom".to_string(),
6128 "ns/file:///custom".to_string(),
6129 );
6130 let cx = Cx::for_testing();
6131 let ctx = McpContext::new(cx, 1);
6132 let mut params = UriParams::new();
6133 params.insert("id".to_string(), "5".to_string());
6134 let outcome = fastmcp_core::block_on(mounted.read_async_with_uri(
6135 &ctx,
6136 "ns/file:///items/5",
6137 ¶ms,
6138 ));
6139 match outcome {
6140 Outcome::Ok(content) => {
6141 assert_eq!(content[0].text.as_deref(), Some("item:5"));
6142 assert_eq!(content[0].uri, "ns/file:///items/5");
6143 }
6144 other => panic!("expected Ok, got {:?}", other),
6145 }
6146 }
6147
6148 #[test]
6149 fn mounted_resource_template_translates_true_async_request_and_result_uri() {
6150 struct AsyncTemplateResource;
6151
6152 impl ResourceHandler for AsyncTemplateResource {
6153 fn definition(&self) -> Resource {
6154 Resource {
6155 uri: "db://placeholder".to_string(),
6156 name: "async-template".to_string(),
6157 description: None,
6158 mime_type: None,
6159 icon: None,
6160 version: None,
6161 tags: vec![],
6162 }
6163 }
6164
6165 fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
6166 panic!("the true-async override must be used")
6167 }
6168
6169 fn read_async_with_uri<'a>(
6170 &'a self,
6171 _ctx: &'a McpContext,
6172 uri: &'a str,
6173 params: &'a UriParams,
6174 ) -> BoxFuture<'a, McpOutcome<Vec<ResourceContent>>> {
6175 Box::pin(async move {
6176 Outcome::Ok(vec![ResourceContent {
6177 uri: uri.to_string(),
6178 mime_type: None,
6179 text: params.get("table").cloned(),
6180 blob: None,
6181 }])
6182 })
6183 }
6184 }
6185
6186 let mounted = MountedResourceHandler::with_template(
6187 Box::new(AsyncTemplateResource),
6188 "db://{table}".to_string(),
6189 "ns/db://{table}".to_string(),
6190 ResourceTemplate {
6191 uri_template: "ns/db://{table}".to_string(),
6192 name: "async-template".to_string(),
6193 description: None,
6194 mime_type: None,
6195 icon: None,
6196 version: None,
6197 tags: vec![],
6198 },
6199 );
6200 let cx = Cx::for_testing();
6201 let ctx = McpContext::new(cx, 1);
6202 let params = UriParams::from([("table".to_string(), "users".to_string())]);
6203
6204 let outcome =
6205 fastmcp_core::block_on(mounted.read_async_with_uri(&ctx, "ns/db://users", ¶ms));
6206 match outcome {
6207 Outcome::Ok(contents) => {
6208 assert_eq!(contents[0].uri, "ns/db://users");
6209 assert_eq!(contents[0].text.as_deref(), Some("users"));
6210 }
6211 other => panic!("expected Ok, got {other:?}"),
6212 }
6213 }
6214
6215 #[test]
6218 fn mounted_prompt_handler_delegates_get_async() {
6219 let inner = Box::new(StubPrompt) as BoxedPromptHandler;
6220 let mounted = MountedPromptHandler::new(inner, "ns".to_string());
6221 let cx = Cx::for_testing();
6222 let ctx = McpContext::new(cx, 1);
6223 let outcome = fastmcp_core::block_on(mounted.get_async(&ctx, HashMap::new()));
6224 match outcome {
6225 Outcome::Ok(messages) => assert!(messages.is_empty()),
6226 other => panic!("expected Ok, got {:?}", other),
6227 }
6228 }
6229
6230 #[test]
6233 fn progress_sender_with_message_but_no_total() {
6234 let sent = Arc::new(Mutex::new(Vec::new()));
6235 let sent_clone = Arc::clone(&sent);
6236 let sender = ProgressNotificationSender::new(ProgressMarker::from("tok-msg"), move |req| {
6237 sent_clone.lock().unwrap().push(req);
6238 });
6239
6240 sender.send_progress(2.0, None, Some("processing"));
6241
6242 let messages = sent.lock().unwrap();
6243 let params = messages[0].params.as_ref().unwrap();
6244 assert_eq!(params["progress"], 2.0);
6245 assert_eq!(params["message"], "processing");
6246 assert!(params.get("total").is_none() || params["total"].is_null());
6247 }
6248
6249 #[test]
6250 fn progress_notification_includes_progress_token() {
6251 let sent = Arc::new(Mutex::new(Vec::new()));
6252 let sent_clone = Arc::clone(&sent);
6253 let sender =
6254 ProgressNotificationSender::new(ProgressMarker::from("my-token"), move |req| {
6255 sent_clone.lock().unwrap().push(req);
6256 });
6257
6258 sender.send_progress(1.0, None, None);
6259
6260 let messages = sent.lock().unwrap();
6261 let params = messages[0].params.as_ref().unwrap();
6262 assert_eq!(params["progressToken"], "my-token");
6263 }
6264
6265 #[test]
6266 fn resource_read_async_propagates_error() {
6267 struct ErrResource;
6268 impl ResourceHandler for ErrResource {
6269 fn definition(&self) -> Resource {
6270 Resource {
6271 uri: "file:///err".to_string(),
6272 name: "err".to_string(),
6273 description: None,
6274 mime_type: None,
6275 icon: None,
6276 version: None,
6277 tags: vec![],
6278 }
6279 }
6280 fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
6281 Err(McpError::internal_error("read-fail"))
6282 }
6283 }
6284
6285 let cx = Cx::for_testing();
6286 let ctx = McpContext::new(cx, 1);
6287 let outcome = fastmcp_core::block_on(ErrResource.read_async(&ctx));
6288 match outcome {
6289 Outcome::Err(e) => assert!(e.message.contains("read-fail")),
6290 other => panic!("expected Err, got {:?}", other),
6291 }
6292 }
6293
6294 #[test]
6295 fn prompt_get_async_propagates_error() {
6296 struct ErrPrompt;
6297 impl PromptHandler for ErrPrompt {
6298 fn definition(&self) -> Prompt {
6299 Prompt {
6300 name: "err".to_string(),
6301 description: None,
6302 arguments: vec![],
6303 icon: None,
6304 version: None,
6305 tags: vec![],
6306 }
6307 }
6308 fn get(
6309 &self,
6310 _ctx: &McpContext,
6311 _args: HashMap<String, String>,
6312 ) -> McpResult<Vec<PromptMessage>> {
6313 Err(McpError::internal_error("get-fail"))
6314 }
6315 }
6316
6317 let cx = Cx::for_testing();
6318 let ctx = McpContext::new(cx, 1);
6319 let outcome = fastmcp_core::block_on(ErrPrompt.get_async(&ctx, HashMap::new()));
6320 match outcome {
6321 Outcome::Err(e) => assert!(e.message.contains("get-fail")),
6322 other => panic!("expected Err, got {:?}", other),
6323 }
6324 }
6325
6326 #[test]
6327 fn resource_read_async_with_uri_nonempty_params_propagates_error() {
6328 struct ErrWithUri;
6329 impl ResourceHandler for ErrWithUri {
6330 fn definition(&self) -> Resource {
6331 Resource {
6332 uri: "file:///err".to_string(),
6333 name: "err".to_string(),
6334 description: None,
6335 mime_type: None,
6336 icon: None,
6337 version: None,
6338 tags: vec![],
6339 }
6340 }
6341 fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
6342 Ok(vec![])
6343 }
6344 fn read_with_uri(
6345 &self,
6346 _ctx: &McpContext,
6347 _uri: &str,
6348 _params: &UriParams,
6349 ) -> McpResult<Vec<ResourceContent>> {
6350 Err(McpError::internal_error("uri-fail"))
6351 }
6352 }
6353
6354 let cx = Cx::for_testing();
6355 let ctx = McpContext::new(cx, 1);
6356 let mut params = UriParams::new();
6357 params.insert("id".to_string(), "1".to_string());
6358 let outcome =
6359 fastmcp_core::block_on(ErrWithUri.read_async_with_uri(&ctx, "file:///err", ¶ms));
6360 match outcome {
6361 Outcome::Err(e) => assert!(e.message.contains("uri-fail")),
6362 other => panic!("expected Err, got {:?}", other),
6363 }
6364 }
6365
6366 #[test]
6367 fn mounted_tool_definition_preserves_inner_fields() {
6368 let inner = Box::new(StubTool) as BoxedToolHandler;
6369 let mounted = MountedToolHandler::new(inner, "renamed".to_string());
6370 let def = mounted.definition();
6371 assert_eq!(def.name, "renamed");
6372 assert_eq!(def.description.as_deref(), Some("a stub tool"));
6373 assert_eq!(def.input_schema, serde_json::json!({"type": "object"}));
6374 }
6375
6376 fn final_sampling_parameters_for_test() -> FinalEmbeddedCreateMessageParams {
6377 serde_json::from_value(serde_json::json!({
6378 "messages": [{
6379 "role": "assistant",
6380 "content": {
6381 "type": "tool_use",
6382 "id": "weather-1",
6383 "name": "weather",
6384 "input": {"city": "Boston"},
6385 },
6386 }],
6387 "maxTokens": 16,
6388 "tools": [{"name": "weather", "inputSchema": {"type": "object"}}],
6389 "toolChoice": {"mode": "required"},
6390 }))
6391 .expect("final sampling fixture must admit")
6392 }
6393
6394 #[test]
6395 fn final_sampling_context_preserves_tool_choice_and_tool_use() {
6396 let context = McpContext::new(Cx::for_testing(), 71)
6397 .with_client_capabilities(fastmcp_core::ClientCapabilityInfo::new().with_sampling());
6398 let input_required = context
6399 .final_sampling("sample", final_sampling_parameters_for_test())
6400 .expect("advertised sampling builds final MRTR input")
6401 .into_input_required()
6402 .expect("final sampling input serializes");
6403 let wire: serde_json::Value = serde_json::from_str(&encode_result(
6404 &DecodedResult::InputRequired(input_required),
6405 ))
6406 .expect("input-required result encodes through its exact wire path");
6407 assert_eq!(
6408 wire["inputRequests"]["sample"]["params"]["toolChoice"],
6409 serde_json::json!({"mode": "required"})
6410 );
6411 assert_eq!(
6412 wire["inputRequests"]["sample"]["params"]["messages"][0]["content"]["type"],
6413 "tool_use"
6414 );
6415 }
6416
6417 #[test]
6418 fn final_sampling_context_rejects_only_removed_sampling_capability() {
6419 let parameters = final_sampling_parameters_for_test();
6420 let admitted = McpContext::new(Cx::for_testing(), 72)
6421 .with_client_capabilities(fastmcp_core::ClientCapabilityInfo::new().with_sampling());
6422 assert!(
6423 admitted
6424 .final_sampling("sample", parameters.clone())
6425 .is_ok()
6426 );
6427
6428 let rejected = McpContext::new(Cx::for_testing(), 72);
6429 let error = rejected
6430 .final_sampling("sample", parameters)
6431 .expect_err("removing only sampling capability rejects final sampling");
6432 assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidRequest);
6433 }
6434
6435 #[test]
6436 fn final_roots_context_emits_roots_list_method() {
6437 let context = McpContext::new(Cx::for_testing(), 73)
6438 .with_client_capabilities(fastmcp_core::ClientCapabilityInfo::new().with_roots(false));
6439 let input_required = context
6440 .final_roots("roots", FinalEmbeddedRootsListParams::default())
6441 .expect("advertised roots builds final MRTR input")
6442 .into_input_required()
6443 .expect("final roots input serializes");
6444 let wire: serde_json::Value = serde_json::from_str(&encode_result(
6445 &DecodedResult::InputRequired(input_required),
6446 ))
6447 .expect("input-required result encodes through its exact wire path");
6448 assert_eq!(
6449 wire["inputRequests"]["roots"]["method"],
6450 serde_json::json!("roots/list")
6451 );
6452 assert!(
6453 wire["inputRequests"]["roots"].get("params").is_none(),
6454 "empty roots params must omit the params member"
6455 );
6456 }
6457
6458 #[test]
6459 fn final_roots_context_rejects_only_removed_roots_capability() {
6460 let admitted = McpContext::new(Cx::for_testing(), 74)
6461 .with_client_capabilities(fastmcp_core::ClientCapabilityInfo::new().with_roots(false));
6462 assert!(
6463 admitted
6464 .final_roots("roots", FinalEmbeddedRootsListParams::default())
6465 .is_ok()
6466 );
6467
6468 let rejected = McpContext::new(Cx::for_testing(), 74);
6469 let error = rejected
6470 .final_roots("roots", FinalEmbeddedRootsListParams::default())
6471 .expect_err("removing only roots capability rejects final roots");
6472 assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidRequest);
6473 }
6474
6475 #[test]
6476 fn final_roots_context_rejects_empty_input_key() {
6477 let context = McpContext::new(Cx::for_testing(), 75)
6478 .with_client_capabilities(fastmcp_core::ClientCapabilityInfo::new().with_roots(false));
6479 let error = context
6480 .final_roots("", FinalEmbeddedRootsListParams::default())
6481 .expect_err("empty roots input key is invalid");
6482 assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidParams);
6483 }
6484}