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