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