Skip to main content

fastmcp_server/
handler.rs

1//! Handler traits for tools, resources, and prompts.
2//!
3//! Handlers support both synchronous and asynchronous execution patterns:
4//!
5//! - **Sync handlers**: Implement `call()`, `read()`, or `get()` directly
6//! - **Async handlers**: Override `call_async()`, `read_async()`, or `get_async()`
7//!
8//! The router always calls the async variants, which by default delegate to
9//! the sync versions. This allows gradual migration to async without breaking
10//! existing code.
11
12use 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// ============================================================================
47// Final resource URI-use admission
48// ============================================================================
49
50/// The final server emission site for one locally authored resource identity.
51///
52/// This is deliberately narrower than structural [`AbsoluteUri`] admission:
53/// a syntactically valid URI does not by itself grant authority to advertise
54/// it as a client-direct resource or embed it as server-mediated content.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub(crate) enum FinalResourceUriUse {
57    /// A resource identity in `resources/list`.
58    CatalogResource,
59    /// A resource-template identity in `resources/templates/list`.
60    CatalogTemplate,
61    /// The target of a locally handled `resources/read` request.
62    ResourceReadTarget,
63    /// One embedded resource identity in a `resources/read` complete result.
64    ResourceReadContents,
65    /// A `resource_link` authored in a final prompt result.
66    PromptResourceLink,
67    /// An embedded resource authored in a final prompt result.
68    PromptEmbeddedResource,
69}
70
71/// Policy governing locally authored final resource identities.
72///
73/// The default keeps every URI server-mediated. An application must opt in
74/// explicitly before an HTTPS identity can be advertised as a client-direct
75/// resource or linked from a prompt. HTTPS identities are never admitted for
76/// server-side `resources/read` handling or embedded resource payloads.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
78pub(crate) struct ResourceUriUsePolicy {
79    client_direct_https: bool,
80}
81
82impl ResourceUriUsePolicy {
83    /// Creates the safe default policy for server-mediated resource identities.
84    #[must_use]
85    pub(crate) const fn server_mediated() -> Self {
86        Self {
87            client_direct_https: false,
88        }
89    }
90
91    /// Builds the policy from the public handler declaration.
92    #[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    /// Returns whether one final URI is admitted at this exact local use site.
100    #[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    /// Returns whether a final resource template is admitted at its catalog
115    /// use site. Template syntax is validated separately before this policy
116    /// check; only the RFC 3986 scheme classification is relevant here.
117    #[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
126// ============================================================================
127// Progress Notification Sender
128// ============================================================================
129
130/// One request-owned final-progress staging runtime.
131///
132/// The runtime fixes its progress marker at construction, remembers the
133/// greatest admitted exact progress value, and retains at most one newer
134/// notification until a transport-owned rate tick or terminal response calls
135/// [`Self::flush_pending`] or [`Self::finalize`]. It intentionally does not
136/// elect the request's transport terminal: that authority remains with the
137/// outer server dispatch path.
138pub(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    /// Creates an open runtime for one admitted final request marker.
175    #[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    /// Creates the handler-facing reporter while retaining the runtime in the
185    /// outer request owner for later flushing or terminal finalization.
186    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    /// Emits the newest pending progress notification, if the request remains
197    /// open. A future transport rate timer owns when to invoke this primitive.
198    ///
199    /// Returns `true` only when a queued notification was committed to the
200    /// callback. Cancellation and finalization discard no additional frames.
201    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    /// Claims this runtime's terminal side of the final-progress race.
213    ///
214    /// The winner flushes its one coalesced pending update before the outer
215    /// request owner writes the JSON-RPC terminal response. Returns `false`
216    /// when cancellation had already won.
217    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    /// Cancels this progress runtime and discards its coalesced notification.
231    ///
232    /// Returns `true` only when cancellation won before finalization.
233    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        // A newer admissible value replaces, rather than grows, the one-slot
290        // queue. The latest value is therefore what rate flush/finalization
291        // observes.
292        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
380/// A notification sender that sends progress notifications via a callback.
381///
382/// This is the server-side implementation used to send notifications back
383/// to the client during handler execution. It rejects non-finite numeric
384/// fields and serialization failures, and contains callback panics so a
385/// reporting failure cannot unwind through the request handler.
386///
387/// The exact-2024 path remains immediate. Final request-owned dispatch can
388/// instead install [`FinalProgressRuntime`] and retain its explicit flush and
389/// terminal-finalization primitives in the outer transport owner.
390pub struct ProgressNotificationSender<F>
391where
392    F: Fn(JsonRpcRequest) + Send + Sync,
393{
394    /// The progress marker from the original request.
395    marker: ProgressMarker,
396    /// Whether this sender emits the exact final progress model rather than
397    /// the exact-2024 `f64` model.
398    final_protocol: bool,
399    /// Callback to send notifications.
400    send_fn: F,
401}
402
403impl<F> ProgressNotificationSender<F>
404where
405    F: Fn(JsonRpcRequest) + Send + Sync,
406{
407    /// Creates a new progress notification sender.
408    pub fn new(marker: ProgressMarker, send_fn: F) -> Self {
409        Self {
410            marker,
411            final_protocol: false,
412            send_fn,
413        }
414    }
415
416    /// Creates a progress sender for MCP 2026-07-28 handler dispatch.
417    ///
418    /// Calls made through the ordinary [`ProgressReporter`] bridge are
419    /// admitted as exact finite JSON numbers and emitted with
420    /// [`FinalProgressNotificationParams`]. The exact-2024 constructor
421    /// [`Self::new`] deliberately retains its `f64` wire model.
422    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    /// Creates a progress reporter from this sender.
431    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(&params) 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    /// Emits an exact-final progress notification after final-era admission.
486    ///
487    /// Callers use the public [`NotificationSender::send_progress_exact`]
488    /// capability; this raw-model helper is intentionally private so a legacy
489    /// sender cannot bypass its protocol-era gate.
490    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
620/// Emits `notifications/message` from handler `ctx.info()` and friends.
621pub(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/// Configuration for bidirectional senders to attach to context.
718#[derive(Clone, Default)]
719pub struct BidirectionalSenders {
720    /// Optional sampling sender for LLM completions.
721    pub sampling: Option<Arc<dyn fastmcp_core::SamplingSender>>,
722    /// Optional elicitation sender for user input requests.
723    pub elicitation: Option<Arc<dyn fastmcp_core::ElicitationSender>>,
724    /// Optional roots provider for filesystem boundaries exposed by the client.
725    pub roots: Option<Arc<dyn fastmcp_core::RootsProvider>>,
726}
727
728impl BidirectionalSenders {
729    /// Creates empty senders (no bidirectional features).
730    #[must_use]
731    pub fn new() -> Self {
732        Self::default()
733    }
734
735    /// Sets the sampling sender.
736    #[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    /// Sets the elicitation sender.
743    #[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    /// Sets the roots provider.
750    #[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
767/// Helper to create an McpContext with optional progress reporting and session state.
768pub 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
781/// Helper to create an McpContext with optional progress reporting, session state, and bidirectional senders.
782pub 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    // Attach bidirectional senders if provided
807    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
822/// A boxed future for async handler results.
823pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
824
825/// One application-authored result of a final method that may require input.
826///
827/// The `InputRequired` branch lets a handler describe its embedded input map.
828/// The router validates those descriptors, discards any handler-authored
829/// request state or open result members, and mints the only retry state it
830/// will later accept. Legacy handler defaults continue to produce only the
831/// exact legacy projection promoted as [`Self::Complete`](FinalMethodOutcome::Complete).
832#[derive(Debug, Clone)]
833pub enum FinalMethodOutcome<T> {
834    /// Complete the request with its method-specific final payload.
835    Complete(CompleteResult<T>),
836    /// Ask the final peer for additional input before retrying the request.
837    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/// Identifies who selected a complete final resource-read cache policy.
847///
848/// The legacy-to-final bridge asks the router to install its configured
849/// policy. A direct final handler or exact proxy result owns its wire cache
850/// hints, even if those values happen to equal a router default.
851#[derive(Debug, Clone, Copy, PartialEq, Eq)]
852pub enum FinalResourceReadCacheHintProvenance {
853    /// The result came from the legacy bridge and needs the router policy.
854    RouterPolicy,
855    /// The handler or upstream peer supplied explicit final cache hints.
856    Explicit,
857}
858
859/// One application-authored outcome of a final `tools/call` handler.
860///
861/// `CreateTask` is deliberately a request for the router to create durable
862/// state, not a pre-created task result. The router can therefore enforce the
863/// peer's negotiated Tasks capability before the application-owned store is
864/// mutated.
865pub enum FinalToolOutcome {
866    /// Complete this tool call synchronously through the final result algebra.
867    Complete(CompleteResult<FinalCallToolResult>),
868    /// Ask the final peer for additional input before retrying this tool call.
869    ///
870    /// The router extracts and validates its input descriptors, then mints the
871    /// retry state without coercing the result into a legacy projection.
872    InputRequired(InputRequiredResult),
873    /// Create one durable working task after negotiated capability admission.
874    #[cfg(feature = "tasks")]
875    CreateTask {
876        /// Non-null opaque application work persisted with the new task.
877        ///
878        /// [`FinalTaskWorkDescriptor::new`] is the only public constructor for
879        /// this type and rejects a null descriptor, so a task-capable handler
880        /// cannot request creation of inert work.
881        work_descriptor: FinalTaskWorkDescriptor,
882        /// Optional initial status message retained by the task state machine.
883        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/// One final-era elicitation request that a handler returns as MRTR input.
900///
901/// This is deliberately not an async request/response operation. MCP
902/// 2026-07-28 carries server-to-client input in the original method's
903/// `input_required` result, then reinvokes the handler with
904/// [`MrtrCompletedInputs`] after the client retries. A normal
905/// [`McpContext::elicit_form`] future cannot preserve its stack frame across
906/// that protocol boundary.
907#[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 &parameters {
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    /// Returns the opaque map key that correlates this elicitation on retry.
941    #[must_use]
942    pub fn input_key(&self) -> &str {
943        &self.input_key
944    }
945
946    /// Converts this request into the only handler outcome accepted by final
947    /// MRTR dispatch. The router validates the descriptor and replaces any
948    /// handler-selected retry state with a framework-minted value.
949    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
964/// Final-only elicitation construction for [`McpContext`].
965///
966/// Import this trait to use [`Self::final_elicitation_form`] or
967/// [`Self::final_elicitation_url`] inside a final handler. Return the resulting
968/// [`FinalElicitation`] through [`FinalToolOutcome::InputRequired`], then read
969/// the accepted value with [`MrtrCompletedInputs::elicitation`] on the resumed
970/// invocation.
971pub trait FinalElicitationContextExt {
972    /// Builds an embedded final form elicitation request.
973    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    /// Builds an embedded final URL elicitation request.
981    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/// One final-era sampling request that a handler returns as MRTR input.
1030///
1031/// The request retains the exact final embedded sampling schema, including
1032/// tool declarations and tool-choice controls. It is intentionally separate
1033/// from the legacy reverse-request sampling API.
1034#[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    /// Returns the opaque map key that correlates this sampling request on retry.
1064    #[must_use]
1065    pub fn input_key(&self) -> &str {
1066        &self.input_key
1067    }
1068
1069    /// Converts this sampling request into framework-admitted final MRTR input.
1070    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
1085/// Final-only sampling construction for [`McpContext`].
1086///
1087/// Import this trait to build a final embedded sampling request inside a final
1088/// handler. Return it through [`FinalToolOutcome::InputRequired`], then read
1089/// the typed final response with [`MrtrCompletedInputs::sampling`] after retry.
1090pub trait FinalSamplingContextExt {
1091    /// Builds one capability-gated final MRTR sampling descriptor.
1092    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/// One final-era roots request that a handler returns as MRTR input.
1110///
1111/// Modern server roots are not reverse JSON-RPC. The handler returns this
1112/// descriptor through [`FinalToolOutcome::InputRequired`], then reads the
1113/// accepted roots with [`MrtrCompletedInputs::roots`] after the client retries.
1114#[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    /// Returns the opaque map key that correlates this roots request on retry.
1144    #[must_use]
1145    pub fn input_key(&self) -> &str {
1146        &self.input_key
1147    }
1148
1149    /// Converts this roots request into framework-admitted final MRTR input.
1150    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
1165/// Final-only roots construction for [`McpContext`].
1166///
1167/// Import this trait to build a final embedded `roots/list` request inside a
1168/// final handler. Return it through [`FinalToolOutcome::InputRequired`], then
1169/// read the typed response with [`MrtrCompletedInputs::roots`] after retry.
1170pub trait FinalRootsContextExt {
1171    /// Builds one capability-gated final MRTR roots descriptor.
1172    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
1207/// URI template parameters extracted from a matched resource URI.
1208pub type UriParams = HashMap<String, String>;
1209
1210/// Encodes a router-produced modern success through the final result contract.
1211///
1212/// Legacy dispatch continues to serialize its method-specific result structs
1213/// directly. The stateless router, by contrast, calls this helper after a
1214/// shipped handler has completed so every successful modern response carries
1215/// the explicit `resultType: "complete"` discriminator and is admitted by the
1216/// same bounded protocol codec used for peer results. Method-specific payloads
1217/// may not pre-populate the discriminator; only this boundary selects it.
1218pub(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
1256/// Returns empty metadata for a server-authored final complete result.
1257///
1258/// Final method codecs reject a synthesized `serverInfo`. The protocol result
1259/// algebra exposes metadata through decoded complete results, so obtain the
1260/// canonical empty instance through that same bounded decoder.
1261pub(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
1281/// Promotes an exact legacy tool payload into the final complete-result algebra.
1282///
1283/// This is the compatibility direction used by legacy-only tool handlers when
1284/// a final request reaches the router. Every legacy content variant is mapped
1285/// without discarding information; malformed legacy embedded resources are
1286/// refused rather than authored as a different final resource.
1287/// Promotes legacy tool content into a complete final `tools/call` result.
1288///
1289/// Async `#[tool]` handlers that only implement [`ToolHandler::call_async`]
1290/// use this from the generated final hook so modern dispatch does not fall
1291/// through to the sync `call` rejection.
1292pub 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
1367/// Promotes legacy resource contents for a final handler default.
1368///
1369/// The trait default uses the same private one-hour cache policy as the
1370/// router's default legacy projection. Direct final handlers can override it
1371/// and author their selected cache policy without this conversion.
1372/// Promotes legacy resource contents into a complete final `resources/read` result.
1373///
1374/// Async `#[resource]` handlers that only implement
1375/// [`ResourceHandler::read_async`] use this from the generated final-outcome
1376/// hook so modern dispatch does not fall through to the sync `read` rejection.
1377pub 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
1405/// Promotes legacy prompt messages for a final handler default.
1406///
1407/// Direct final prompt handlers bypass this conversion and keep their final
1408/// common content, including open fields, intact.
1409/// Promotes legacy prompt messages into a complete final `prompts/get` result.
1410///
1411/// Async `#[prompt]` handlers that only implement [`PromptHandler::get_async`]
1412/// use this from the generated final-outcome hook so modern dispatch does not
1413/// fall through to the sync `get` rejection.
1414pub 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/// Closed framework error classes that an output-schema tool must represent.
1440#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1441pub enum ToolErrorKind {
1442    /// The structurally valid call arguments failed the registered input schema.
1443    InputValidation,
1444    /// The admitted handler returned a non-terminal tool-execution error.
1445    Handler,
1446}
1447
1448/// Legacy diagnostic label for an exact-final tool's schema ownership.
1449///
1450/// This value no longer grants admission authority. A normal handler can
1451/// report `Upstream`, so the router accepts bypasses only through the sealed
1452/// [`UpstreamFinalToolSchemaRegistration`] token issued to exact proxy
1453/// registration.
1454#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1455pub enum FinalToolSchemaAuthority {
1456    /// The local router owns final schema admission and framework error mapping.
1457    Local,
1458    /// The exact-final upstream owns final schema admission and result shaping.
1459    Upstream,
1460}
1461
1462/// Unforgeable registration proving that an exact proxy owns final schemas.
1463///
1464/// The constructor is crate-private and only proxy registration can mint this
1465/// value. Public handlers may observe the type in [`ToolHandler`], but cannot
1466/// manufacture one to bypass local schema admission.
1467#[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
1480/// Handler for a tool.
1481///
1482/// This trait is typically implemented via the `#[tool]` macro.
1483///
1484/// # Sync vs Async
1485///
1486/// By default, implement `call()` for synchronous execution. For async tools,
1487/// override `call_async()` instead. The router always calls `call_async()`,
1488/// which defaults to running `call()` in an async block.
1489///
1490/// # Return Type
1491///
1492/// Async handlers return `McpOutcome<Vec<Content>>`, a 4-valued type supporting:
1493/// - `Ok(content)` - Successful result
1494/// - `Err(McpError)` - Recoverable error
1495/// - `Cancelled` - Request was cancelled
1496/// - `Panicked` - Unrecoverable failure
1497pub trait ToolHandler: Send + Sync {
1498    /// Returns the tool definition.
1499    fn definition(&self) -> Tool;
1500
1501    /// Returns the tool's icon, if any.
1502    ///
1503    /// Default implementation returns `None`. Override to provide an icon.
1504    /// Note: Icons can also be set directly in `definition()`.
1505    fn icon(&self) -> Option<&Icon> {
1506        None
1507    }
1508
1509    /// Returns the tool's version, if any.
1510    ///
1511    /// Default implementation returns `None`. Override to provide a version.
1512    /// Note: Version can also be set directly in `definition()`.
1513    fn version(&self) -> Option<&str> {
1514        None
1515    }
1516
1517    /// Returns the tool's tags for filtering and organization.
1518    ///
1519    /// Default implementation returns an empty slice. Override to provide tags.
1520    /// Note: Tags can also be set directly in `definition()`.
1521    fn tags(&self) -> &[String] {
1522        &[]
1523    }
1524
1525    /// Returns the tool's annotations providing behavioral hints.
1526    ///
1527    /// Default implementation returns `None`. Override to provide annotations
1528    /// like `destructive`, `idempotent`, `read_only`, or `open_world_hint`.
1529    /// Note: Annotations can also be set directly in `definition()`.
1530    fn annotations(&self) -> Option<&ToolAnnotations> {
1531        None
1532    }
1533
1534    /// Returns the tool's output schema (JSON Schema).
1535    ///
1536    /// Default implementation returns `None`. Override to provide a schema
1537    /// that describes the structure of the tool's output.
1538    /// Note: Output schema can also be set directly in `definition()`.
1539    fn output_schema(&self) -> Option<serde_json::Value> {
1540        None
1541    }
1542
1543    /// Returns the final display title for this tool.
1544    ///
1545    /// This final-only field is deliberately separate from the legacy tool
1546    /// definition so modern catalog projection never synthesizes or leaks a
1547    /// legacy version/tag field.
1548    fn final_title(&self) -> Option<&str> {
1549        None
1550    }
1551
1552    /// Returns the validated final icon set for this tool.
1553    ///
1554    /// A legacy singular icon is not projected automatically because its
1555    /// optional source and scalar size hint do not form an exact final icon.
1556    fn final_icons(&self) -> Option<&[RawIcon]> {
1557        None
1558    }
1559
1560    /// Returns final open metadata for this tool's catalog entry.
1561    fn final_metadata(&self) -> Option<&OpenMetadata> {
1562        None
1563    }
1564
1565    /// Returns an exact final catalog definition when this handler owns one.
1566    ///
1567    /// Legacy-first handlers normally leave this as `None`; the router then
1568    /// freezes the ordinary [`Self::definition`] plus the individual final
1569    /// metadata hooks into a final definition. A native final handler or proxy
1570    /// should override this hook so title-bearing annotations, the complete
1571    /// icon collection, and open metadata are never projected through the
1572    /// narrower legacy [`Tool`] model.
1573    fn final_definition(&self) -> Option<FinalTool> {
1574        None
1575    }
1576
1577    /// Returns the legacy diagnostic label for this handler's exact-final schemas.
1578    ///
1579    /// Ordinary and legacy-backed handlers keep local schema admission. The
1580    /// router does not use this forgeable label for admission; exact proxy
1581    /// registration supplies a sealed token through
1582    /// [`Self::upstream_final_tool_schema_registration`] instead.
1583    fn final_tool_schema_authority(&self) -> FinalToolSchemaAuthority {
1584        FinalToolSchemaAuthority::Local
1585    }
1586
1587    /// Returns a sealed upstream-schema registration for an exact proxy.
1588    ///
1589    /// Ordinary handlers must use the default. The token has no public
1590    /// constructor, so only server-owned proxy registration can opt out of
1591    /// local input/output validation and framework-error synthesis.
1592    fn upstream_final_tool_schema_registration(
1593        &self,
1594    ) -> Option<UpstreamFinalToolSchemaRegistration> {
1595        None
1596    }
1597
1598    /// Maps a framework-authored tool error into this tool's structured output.
1599    ///
1600    /// A handler that declares `outputSchema` must return a truthful value for
1601    /// both closed error kinds. Registration invokes this hook once for each
1602    /// kind, bounds and validates the returned JSON, and stores immutable
1603    /// copies beside the admitted schemas. Returning `None`, an over-limit
1604    /// value, or a value rejected by `outputSchema` rejects registration
1605    /// before any catalog mutation.
1606    fn final_tool_error_structured_content(
1607        &self,
1608        _kind: ToolErrorKind,
1609    ) -> Option<serde_json::Value> {
1610        None
1611    }
1612
1613    /// Returns the tool's custom timeout duration.
1614    ///
1615    /// Default implementation returns `None`, meaning no additional handler
1616    /// ceiling is added. Override to specify a per-handler timeout.
1617    ///
1618    /// A non-zero value tightens the ambient/request/server budget. It never
1619    /// replaces or relaxes an earlier absolute deadline. A zero value is
1620    /// treated like `None` and therefore cannot disable an outer timeout.
1621    /// Pending async work is dropped when the legacy synchronous dispatcher's
1622    /// timer observes a comparable deadline, and a late completion is rejected.
1623    /// The dispatcher does not drive a caller's foreign virtual clock. This
1624    /// cannot preempt a blocking synchronous `call()` or guarantee child-work
1625    /// drain; handlers must still cooperate with [`McpContext::checkpoint`].
1626    fn timeout(&self) -> Option<Duration> {
1627        None
1628    }
1629
1630    /// Calls the tool synchronously with the given arguments.
1631    ///
1632    /// This is the default implementation point. Override this for simple
1633    /// synchronous tools. Returns `McpResult` which is converted to `McpOutcome`
1634    /// by the async wrapper.
1635    fn call(&self, ctx: &McpContext, arguments: serde_json::Value) -> McpResult<Vec<Content>>;
1636
1637    /// Calls the tool asynchronously with the given arguments.
1638    ///
1639    /// Override this for tools that need true async execution (e.g., I/O-bound
1640    /// operations, database queries, HTTP requests).
1641    ///
1642    /// Returns `McpOutcome` to properly represent all four states: success,
1643    /// error, cancellation, and panic.
1644    ///
1645    /// The default implementation delegates to the sync `call()` method and
1646    /// converts the `McpResult` to `McpOutcome`.
1647    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    /// Calls the tool through the final MCP 2026-07-28 result surface.
1661    ///
1662    /// Legacy-only handlers retain their exact [`Self::call`] result and are
1663    /// promoted without loss into a complete final result. Handlers that
1664    /// author final-only metadata, annotations, resource links, or a tool
1665    /// error result should override this method (or its async counterpart) so
1666    /// the router can preserve the supplied final result algebra exactly.
1667    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    /// Asynchronously calls the tool through the final result surface.
1676    ///
1677    /// The default delegates to [`Self::call_final`].
1678    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    /// Calls the tool from a request-owned structured child.
1692    ///
1693    /// Modern router dispatch supplies the child [`Cx`] that owns this handler
1694    /// invocation. Implementations that spawn or otherwise coordinate nested
1695    /// work must use this context so cancellation and completion remain within
1696    /// the request's structured lifetime. Existing handlers keep their exact
1697    /// behavior through the default delegation to [`Self::call_async`].
1698    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    /// Calls the tool's final result hook from a request-owned child.
1708    ///
1709    /// Existing final async handlers keep their behavior through the default
1710    /// delegation to [`Self::call_final_async`].
1711    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    /// Declares whether this handler can return a final Tasks `CreateTask` outcome.
1721    ///
1722    /// The router verifies this declaration only if the handler actually
1723    /// returns [`FinalToolOutcome::CreateTask`], then admits negotiated Tasks
1724    /// capability and runtime readiness before mutating task state. Handlers
1725    /// that return [`FinalToolOutcome::CreateTask`] must override this to
1726    /// return `true`.
1727    fn declares_final_tasks(&self) -> bool {
1728        false
1729    }
1730
1731    /// Declares whether this handler can use final MRTR continuations.
1732    ///
1733    /// This is an immutable handler capability, rather than an inference from
1734    /// a runtime outcome. A declared-capable handler requires a modern
1735    /// connection partition before the router invokes it, so an
1736    /// `InputRequired` outcome can never be produced without durable
1737    /// continuation ownership.
1738    fn declares_final_mrtr(&self) -> bool {
1739        false
1740    }
1741
1742    /// Calls the tool through the final complete, input-required, or task-creation surface.
1743    ///
1744    /// Existing handlers remain complete-only. A task-capable handler
1745    /// overrides this method, or its async/request-owned counterpart, and
1746    /// returns [`FinalToolOutcome::CreateTask`] without mutating task state.
1747    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    /// Asynchronously calls the final complete, input-required, or task-creation surface.
1757    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    /// Calls the disjoint final outcome from a request-owned structured child.
1784    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    /// Resumes a final tool invocation after framework-admitted MRTR input.
1814    ///
1815    /// `resume_inputs` exists only after the router consumed a
1816    /// framework-minted request state bound to the original modern operation.
1817    /// Handlers inspect its typed accessors rather than decoding client wire
1818    /// values. `#[tool]` exposes this as an
1819    /// `Option<&MrtrCompletedInputs>` user-function parameter: initial calls
1820    /// receive `None` and admitted retries receive `Some`. Existing handlers
1821    /// preserve their normal final hook by default.
1822    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
1833/// Handler for a resource.
1834///
1835/// This trait is typically implemented via the `#[resource]` macro.
1836///
1837/// # Sync vs Async
1838///
1839/// By default, implement `read()` for synchronous execution. For async resources,
1840/// override `read_async()` instead. The router uses `read_async_with_uri()` so
1841/// implementations can access matched URI parameters when needed; its default
1842/// implementation delegates to `read_async()` or `read_with_uri()`.
1843///
1844/// # Return Type
1845///
1846/// Async handlers return `McpOutcome<Vec<ResourceContent>>`, a 4-valued type.
1847pub trait ResourceHandler: Send + Sync {
1848    /// Returns the resource definition.
1849    fn definition(&self) -> Resource;
1850
1851    /// Returns whether locally authored final resource identities may use HTTPS
1852    /// at client-direct use sites. Exact MCP 2024-11-05 dispatch never consults
1853    /// this declaration.
1854    fn final_client_direct_https(&self) -> bool {
1855        false
1856    }
1857
1858    /// Declares whether this handler can use final MRTR continuations.
1859    ///
1860    /// The router consults this immutable capability before invoking a final
1861    /// resource handler on a context that has no durable modern session
1862    /// partition.
1863    fn declares_final_mrtr(&self) -> bool {
1864        false
1865    }
1866
1867    /// Returns the resource template definition, if this resource uses a URI template.
1868    fn template(&self) -> Option<ResourceTemplate> {
1869        None
1870    }
1871
1872    /// Called after a session admits `resources/subscribe` for this URI.
1873    ///
1874    /// Prefixed as_proxy handlers rewrite the inbound URI and subscribe the
1875    /// upstream so later `notify_resource_updated` is not silent.
1876    fn on_subscribe(&self, _ctx: &McpContext, _uri: &str) -> McpResult<()> {
1877        Ok(())
1878    }
1879
1880    /// Called after a session removes `resources/subscribe` for this URI.
1881    fn on_unsubscribe(&self, _ctx: &McpContext, _uri: &str) -> McpResult<()> {
1882        Ok(())
1883    }
1884
1885    /// Returns an exact final resource catalog definition, when this handler
1886    /// owns one. This bypasses lossy projection through [`Resource`], retaining
1887    /// final-only fields such as `size`, full icons, annotations, and `_meta`.
1888    fn final_definition(&self) -> Option<FinalResource> {
1889        None
1890    }
1891
1892    /// Returns an exact final resource-template catalog definition, when this
1893    /// handler owns a template. This keeps final metadata immutable at router
1894    /// registration rather than reconstructing it from the legacy template.
1895    fn final_template_definition(&self) -> Option<FinalResourceTemplate> {
1896        None
1897    }
1898
1899    /// Returns the final display title for this concrete resource.
1900    fn final_title(&self) -> Option<&str> {
1901        None
1902    }
1903
1904    /// Returns the final icon set for this concrete resource.
1905    fn final_icons(&self) -> Option<&[RawIcon]> {
1906        None
1907    }
1908
1909    /// Returns the final annotations for this concrete resource.
1910    fn final_annotations(&self) -> Option<&Annotations> {
1911        None
1912    }
1913
1914    /// Returns final open metadata for this concrete resource.
1915    fn final_metadata(&self) -> Option<&OpenMetadata> {
1916        None
1917    }
1918
1919    /// Returns the final display title for this resource template.
1920    ///
1921    /// This is used only when [`Self::template`] returns `Some`.
1922    fn final_template_title(&self) -> Option<&str> {
1923        None
1924    }
1925
1926    /// Returns the final icon set for this resource template.
1927    ///
1928    /// This is used only when [`Self::template`] returns `Some`.
1929    fn final_template_icons(&self) -> Option<&[RawIcon]> {
1930        None
1931    }
1932
1933    /// Returns the final annotations for this resource template.
1934    ///
1935    /// This is used only when [`Self::template`] returns `Some`.
1936    fn final_template_annotations(&self) -> Option<&Annotations> {
1937        None
1938    }
1939
1940    /// Returns final open metadata for this resource template.
1941    ///
1942    /// This is used only when [`Self::template`] returns `Some`.
1943    fn final_template_metadata(&self) -> Option<&OpenMetadata> {
1944        None
1945    }
1946
1947    /// Returns the resource's icon, if any.
1948    ///
1949    /// Default implementation returns `None`. Override to provide an icon.
1950    /// Note: Icons can also be set directly in `definition()`.
1951    fn icon(&self) -> Option<&Icon> {
1952        None
1953    }
1954
1955    /// Returns the resource's version, if any.
1956    ///
1957    /// Default implementation returns `None`. Override to provide a version.
1958    /// Note: Version can also be set directly in `definition()`.
1959    fn version(&self) -> Option<&str> {
1960        None
1961    }
1962
1963    /// Returns the resource's tags for filtering and organization.
1964    ///
1965    /// Default implementation returns an empty slice. Override to provide tags.
1966    /// Note: Tags can also be set directly in `definition()`.
1967    fn tags(&self) -> &[String] {
1968        &[]
1969    }
1970
1971    /// Returns the resource's custom timeout duration.
1972    ///
1973    /// Default implementation returns `None`, meaning no additional handler
1974    /// ceiling is added. A non-zero value only tightens outer budgets; zero
1975    /// cannot disable an ambient, request, or server deadline.
1976    /// A blocking synchronous `read()` cannot be preempted; if it returns
1977    /// after the deadline, its result is rejected.
1978    fn timeout(&self) -> Option<Duration> {
1979        None
1980    }
1981
1982    /// Reads the resource content synchronously.
1983    ///
1984    /// This is the default implementation point. Override this for simple
1985    /// synchronous resources. Returns `McpResult` which is converted to `McpOutcome`
1986    /// by the async wrapper.
1987    fn read(&self, ctx: &McpContext) -> McpResult<Vec<ResourceContent>>;
1988
1989    /// Reads the resource content synchronously with the matched URI and parameters.
1990    ///
1991    /// Default implementation ignores URI params and delegates to `read()`.
1992    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    /// Reads the resource content asynchronously with the matched URI and parameters.
2002    ///
2003    /// Default implementation delegates to the sync `read_with_uri()` method.
2004    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    /// Reads the resource content asynchronously.
2023    ///
2024    /// Override this for resources that need true async execution (e.g., file I/O,
2025    /// database queries, remote fetches).
2026    ///
2027    /// Returns `McpOutcome` to properly represent all four states.
2028    ///
2029    /// The default implementation delegates to the sync `read()` method.
2030    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    /// Reads the resource through the final MCP 2026-07-28 result surface.
2043    ///
2044    /// Legacy-only handlers retain their exact [`Self::read`] behavior and
2045    /// receive the standard private one-hour final cache policy. Handlers that
2046    /// author final embedded-resource metadata or a different cache policy
2047    /// should override this method (or its async counterpart).
2048    fn read_final(&self, ctx: &McpContext) -> McpResult<CompleteResult<FinalReadResourceResult>> {
2049        promote_legacy_resource_contents(self.read(ctx)?)
2050    }
2051
2052    /// Returns the provenance of complete final resource-read cache hints.
2053    ///
2054    /// The default is the legacy bridge, whose fixed wire values are only a
2055    /// temporary projection until the router applies its configured policy.
2056    /// Handlers that override [`Self::read_final`] to author a final result,
2057    /// including exact proxies, must return [`FinalResourceReadCacheHintProvenance::Explicit`].
2058    fn final_resource_read_cache_hint_provenance(&self) -> FinalResourceReadCacheHintProvenance {
2059        FinalResourceReadCacheHintProvenance::RouterPolicy
2060    }
2061
2062    /// Reads the resource through the final result surface with URI parameters.
2063    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    /// Reads the resource through the complete-or-input-required final algebra.
2073    ///
2074    /// The default preserves the exact legacy projection by promoting
2075    /// [`Self::read_final`] into the complete branch. A final-only handler may
2076    /// override this method to return `input_required` without coercing that
2077    /// state into a legacy resource result.
2078    fn read_final_outcome(
2079        &self,
2080        ctx: &McpContext,
2081    ) -> McpResult<FinalMethodOutcome<FinalReadResourceResult>> {
2082        self.read_final(ctx).map(FinalMethodOutcome::Complete)
2083    }
2084
2085    /// Reads the resource through the complete-or-input-required final algebra
2086    /// with URI parameters.
2087    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    /// Asynchronously reads the resource through the final result surface.
2102    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    /// Asynchronously reads the resource through the final result surface with URI parameters.
2115    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    /// Asynchronously reads the resource through the complete-or-input-required
2134    /// final algebra.
2135    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    /// Asynchronously reads the resource through the complete-or-input-required
2148    /// final algebra with URI parameters.
2149    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    /// Reads the resource from a request-owned structured child.
2168    ///
2169    /// Modern router dispatch supplies the child [`Cx`] that owns this read.
2170    /// Implementations with nested asynchronous work must retain this context
2171    /// rather than creating detached work. Existing handlers preserve their
2172    /// exact behavior through the default delegation.
2173    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    /// Reads the resource's final result from a request-owned structured child.
2184    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    /// Reads the resource's complete-or-input-required final outcome from a
2195    /// request-owned structured child.
2196    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    /// Resumes a final resource read after framework-admitted MRTR input.
2207    ///
2208    /// `#[resource]` maps an `Option<&MrtrCompletedInputs>` user-function
2209    /// parameter to this hook, keeping it out of URI-template parameters.
2210    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
2222/// Handler for a prompt.
2223///
2224/// This trait is typically implemented via the `#[prompt]` macro.
2225///
2226/// # Sync vs Async
2227///
2228/// By default, implement `get()` for synchronous execution. For async prompts,
2229/// override `get_async()` instead. The router always calls `get_async()`,
2230/// which defaults to running `get()` in an async block.
2231///
2232/// # Return Type
2233///
2234/// Async handlers return `McpOutcome<Vec<PromptMessage>>`, a 4-valued type.
2235pub trait PromptHandler: Send + Sync {
2236    /// Returns the prompt definition.
2237    fn definition(&self) -> Prompt;
2238
2239    /// Returns whether resource links authored by this prompt's final result
2240    /// may use client-direct HTTPS. Exact MCP 2024-11-05 dispatch never
2241    /// consults this declaration.
2242    fn final_client_direct_https(&self) -> bool {
2243        false
2244    }
2245
2246    /// Declares whether this handler can use final MRTR continuations.
2247    ///
2248    /// The router consults this immutable capability before invoking a final
2249    /// prompt handler on a context that has no durable modern session
2250    /// partition.
2251    fn declares_final_mrtr(&self) -> bool {
2252        false
2253    }
2254
2255    /// Returns an exact final prompt catalog definition, when this handler
2256    /// owns one. In particular, argument titles and absent-vs-present
2257    /// `required` values must not be projected through legacy prompt args.
2258    fn final_definition(&self) -> Option<FinalPrompt> {
2259        None
2260    }
2261
2262    /// Returns the final display title for this prompt.
2263    fn final_title(&self) -> Option<&str> {
2264        None
2265    }
2266
2267    /// Returns the final icon set for this prompt.
2268    fn final_icons(&self) -> Option<&[RawIcon]> {
2269        None
2270    }
2271
2272    /// Returns final open metadata for this prompt.
2273    fn final_metadata(&self) -> Option<&OpenMetadata> {
2274        None
2275    }
2276
2277    /// Returns the prompt's icon, if any.
2278    ///
2279    /// Default implementation returns `None`. Override to provide an icon.
2280    /// Note: Icons can also be set directly in `definition()`.
2281    fn icon(&self) -> Option<&Icon> {
2282        None
2283    }
2284
2285    /// Returns the prompt's version, if any.
2286    ///
2287    /// Default implementation returns `None`. Override to provide a version.
2288    /// Note: Version can also be set directly in `definition()`.
2289    fn version(&self) -> Option<&str> {
2290        None
2291    }
2292
2293    /// Returns the prompt's tags for filtering and organization.
2294    ///
2295    /// Default implementation returns an empty slice. Override to provide tags.
2296    /// Note: Tags can also be set directly in `definition()`.
2297    fn tags(&self) -> &[String] {
2298        &[]
2299    }
2300
2301    /// Returns the prompt's custom timeout duration.
2302    ///
2303    /// Default implementation returns `None`, meaning no additional handler
2304    /// ceiling is added. A non-zero value only tightens outer budgets; zero
2305    /// cannot disable an ambient, request, or server deadline.
2306    /// A blocking synchronous `get()` cannot be preempted; if it returns
2307    /// after the deadline, its result is rejected.
2308    fn timeout(&self) -> Option<Duration> {
2309        None
2310    }
2311
2312    /// Gets the prompt messages synchronously with the given arguments.
2313    ///
2314    /// This is the default implementation point. Override this for simple
2315    /// synchronous prompts. Returns `McpResult` which is converted to `McpOutcome`
2316    /// by the async wrapper.
2317    fn get(
2318        &self,
2319        ctx: &McpContext,
2320        arguments: std::collections::HashMap<String, String>,
2321    ) -> McpResult<Vec<PromptMessage>>;
2322
2323    /// Gets the prompt messages asynchronously with the given arguments.
2324    ///
2325    /// Override this for prompts that need true async execution (e.g., template
2326    /// fetching, dynamic content generation).
2327    ///
2328    /// Returns `McpOutcome` to properly represent all four states.
2329    ///
2330    /// The default implementation delegates to the sync `get()` method.
2331    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    /// Gets the prompt through the final MCP 2026-07-28 result surface.
2345    ///
2346    /// Legacy-only handlers retain their exact [`Self::get`] behavior. Direct
2347    /// final handlers can override this method to keep final common content
2348    /// and its open fields without a legacy projection.
2349    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    /// Gets the prompt through the complete-or-input-required final algebra.
2358    ///
2359    /// The default preserves the exact legacy projection by promoting
2360    /// [`Self::get_final`] into the complete branch. A final-only handler may
2361    /// override this method to return `input_required` without coercing that
2362    /// state into a legacy prompt result.
2363    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    /// Asynchronously gets the prompt through the final result surface.
2373    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    /// Asynchronously gets the prompt through the complete-or-input-required
2387    /// final algebra.
2388    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    /// Gets the prompt from a request-owned structured child.
2402    ///
2403    /// Modern router dispatch supplies the child [`Cx`] that owns this prompt
2404    /// evaluation. Existing handlers preserve their exact behavior through the
2405    /// default delegation to [`Self::get_async`].
2406    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    /// Gets the prompt's final result from a request-owned structured child.
2416    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    /// Gets the prompt's complete-or-input-required final outcome from a
2426    /// request-owned structured child.
2427    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    /// Resumes a final prompt invocation after framework-admitted MRTR input.
2437    ///
2438    /// `#[prompt]` maps an `Option<&MrtrCompletedInputs>` user-function
2439    /// parameter to this hook, keeping it out of prompt arguments.
2440    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
2451/// Handler for `completion/complete` in both supported protocol eras.
2452///
2453/// The two request parameter types deliberately remain distinct: the final
2454/// form carries required request metadata and optional completion context,
2455/// while the exact legacy form does not. Each callback returns its exact
2456/// era-specific completion payload; the router selects the matching result
2457/// envelope and final `resultType` contract.
2458pub trait CompletionHandler: Send + Sync {
2459    /// Returns an optional handler-specific timeout.
2460    ///
2461    /// A non-zero timeout tightens the request budget and cannot relax an
2462    /// existing deadline. Zero is treated as no handler-specific timeout.
2463    fn timeout(&self) -> Option<Duration> {
2464        None
2465    }
2466
2467    /// Completes one exact MCP 2024-11-05 request.
2468    fn complete_legacy(
2469        &self,
2470        ctx: &McpContext,
2471        params: LegacyCompletionParams,
2472    ) -> McpResult<CompletionValues>;
2473
2474    /// Completes one final MCP 2026-07-28 request.
2475    fn complete_final(
2476        &self,
2477        ctx: &McpContext,
2478        params: FinalCompletionParams,
2479    ) -> McpResult<FinalCompletionValues>;
2480
2481    /// Asynchronously completes one exact legacy request.
2482    ///
2483    /// The default delegates to [`Self::complete_legacy`].
2484    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    /// Completes one exact-legacy request from its request-owned child.
2498    ///
2499    /// Existing handlers keep their behavior through the default delegation to
2500    /// [`Self::complete_legacy_async`].
2501    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    /// Asynchronously completes one final request.
2511    ///
2512    /// The default delegates to [`Self::complete_final`].
2513    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    /// Completes one final request from its request-owned structured child.
2527    ///
2528    /// Implementations with nested asynchronous work must use `request_cx`
2529    /// so cancellation remains owned by the originating modern request.
2530    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
2540/// A boxed tool handler.
2541pub type BoxedToolHandler = Box<dyn ToolHandler>;
2542
2543/// A boxed resource handler.
2544pub type BoxedResourceHandler = Box<dyn ResourceHandler>;
2545
2546/// A boxed prompt handler.
2547pub type BoxedPromptHandler = Box<dyn PromptHandler>;
2548
2549/// A boxed completion handler.
2550pub type BoxedCompletionHandler = Box<dyn CompletionHandler>;
2551
2552/// Proxy adapter for an upstream exact-final resource catalog entry.
2553///
2554/// The legacy definition is deliberately only a dispatch fallback. The router
2555/// reads [`Self::final_definition`] during admission, so final discovery keeps
2556/// the upstream `size`, annotations, icon collection, and metadata verbatim.
2557#[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/// Proxy adapter for an upstream exact-final resource-template catalog entry.
2642#[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/// Proxy adapter for an upstream exact-final prompt catalog entry.
2767#[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    /// Exposes a final prompt below a builder namespace while retaining the
2809    /// original upstream name for forwarding. Prompt names are opaque labels,
2810    /// unlike final resource URIs, so this rewrite remains exact.
2811    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
2885// ============================================================================
2886// Mounted Handler Wrappers
2887// ============================================================================
2888
2889/// A wrapper for a tool handler that overrides its name.
2890///
2891/// Used by `mount()` to prefix tool names when mounting from another server.
2892pub struct MountedToolHandler {
2893    inner: BoxedToolHandler,
2894    mounted_name: String,
2895}
2896
2897impl MountedToolHandler {
2898    /// Creates a new mounted tool handler with the given name.
2899    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
3070/// A wrapper for a resource handler that overrides its URI.
3071///
3072/// Used by `mount()` to prefix resource URIs when mounting from another server.
3073pub 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    /// Creates a mounted resource handler from authoritative source and
3083    /// destination registry keys.
3084    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    /// Creates a new mounted resource handler with a mounted template.
3096    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
3562/// A wrapper for a prompt handler that overrides its name.
3563///
3564/// Used by `mount()` to prefix prompt names when mounting from another server.
3565pub struct MountedPromptHandler {
3566    inner: BoxedPromptHandler,
3567    mounted_name: String,
3568}
3569
3570impl MountedPromptHandler {
3571    /// Creates a new mounted prompt handler with the given name.
3572    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    // ── ProgressNotificationSender ───────────────────────────────────
3805
3806    #[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        // The pinned serde_json normalizes the exponent spelling AT PARSE
3874        // ("1e400" -> "1e+400"), before the reporter ever sees the number;
3875        // what this test proves is that the beyond-f64 VALUE survives without
3876        // an IEEE-754 conversion (which would be "inf"/an error, not 1e+400).
3877        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        // This differs only in the forbidden monotonic dimension. The total
4019        // remains smaller than progress in both frames.
4020        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    // ── BidirectionalSenders ─────────────────────────────────────────
4221
4222    #[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    // ── create_context_with_progress ─────────────────────────────────
4240
4241    #[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    // ── Minimal ToolHandler impl for testing ─────────────────────────
4276
4277    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    // ── Minimal ResourceHandler impl for testing ─────────────────────
4703
4704    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", &params).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    // ── Minimal PromptHandler impl for testing ───────────────────────
4817
4818    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    // ── MountedToolHandler ───────────────────────────────────────────
4869
4870    #[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    // ── MountedResourceHandler ───────────────────────────────────────
4975
4976    #[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    // ── MountedPromptHandler ─────────────────────────────────────────
5040
5041    #[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    // ── BidirectionalSenders builders ────────────────────────────────
5069
5070    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    // ── create_context_with_progress_and_senders ─────────────────────
5171
5172    #[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    // ── ToolHandler with overrides ───────────────────────────────────
5278
5279    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    // ── ResourceHandler with overrides ───────────────────────────────
5340
5341    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", &params)
5418            .unwrap();
5419        assert_eq!(result[0].text.as_deref(), Some("item:42"));
5420    }
5421
5422    // ── PromptHandler with overrides ─────────────────────────────────
5423
5424    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    // ── MountedToolHandler icon/version delegation ───────────────────
5477
5478    #[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    // ── MountedResourceHandler delegates ─────────────────────────────
5505
5506    #[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", &params)
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", &params)
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    // ── MountedPromptHandler delegates ───────────────────────────────
5557
5558    #[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    // ── ProgressNotificationSender multiple sends ────────────────────
5589
5590    #[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    // ── ToolHandler with custom tags and annotations ────────────────
5607
5608    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            // Return from definition for consistency
5629            &[]
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    // ── Async delegation via block_on ───────────────────────────────
5650
5651    #[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(); // empty
5684        let outcome =
5685            fastmcp_core::block_on(res.read_async_with_uri(&ctx, "file:///stub", &params));
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", &params));
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    // ── Async error delegation ──────────────────────────────────────
5720
5721    #[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    // ── MountedToolHandler async delegation ──────────────────────────
5751
5752    #[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    // ── MountedResourceHandler async delegation ─────────────────────
5766
5767    #[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            &params,
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", &params));
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    // ── MountedPromptHandler async delegation ────────────────────────
5878
5879    #[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    // ── Additional coverage ─────────────────────────────────────────
5893
5894    #[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", &params));
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}