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