Skip to main content

tower_mcp/
context.rs

1//! Request context for MCP handlers
2//!
3//! Provides progress reporting, cancellation support, and client request capabilities
4//! for long-running operations.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use tower_mcp::context::RequestContext;
10//!
11//! async fn long_running_tool(ctx: RequestContext, input: MyInput) -> Result<CallToolResult> {
12//!     for i in 0..100 {
13//!         // Check if cancelled
14//!         if ctx.is_cancelled() {
15//!             return Err(Error::tool("Operation cancelled"));
16//!         }
17//!
18//!         // Report progress
19//!         ctx.report_progress(i as f64, Some(100.0), Some("Processing...")).await;
20//!
21//!         do_work(i).await;
22//!     }
23//!     Ok(CallToolResult::text("Done!"))
24//! }
25//! ```
26//!
27//! # Sampling (LLM requests to client)
28//!
29//! ```rust,ignore
30//! use tower_mcp::context::RequestContext;
31//! use tower_mcp::{CreateMessageParams, SamplingMessage};
32//!
33//! async fn ai_tool(ctx: RequestContext, input: MyInput) -> Result<CallToolResult> {
34//!     // Request LLM completion from the client
35//!     let params = CreateMessageParams::new(
36//!         vec![SamplingMessage::user("Summarize this text...")],
37//!         500,
38//!     );
39//!
40//!     let result = ctx.sample(params).await?;
41//!     Ok(CallToolResult::text(format!("Summary: {:?}", result.content)))
42//! }
43//! ```
44//!
45//! # Elicitation (requesting user input)
46//!
47//! ```rust,ignore
48//! use tower_mcp::context::RequestContext;
49//! use tower_mcp::{ElicitFormParams, ElicitFormSchema, ElicitMode, ElicitAction};
50//!
51//! async fn interactive_tool(ctx: RequestContext, input: MyInput) -> Result<CallToolResult> {
52//!     // Request user input via form
53//!     let params = ElicitFormParams {
54//!         mode: Some(ElicitMode::Form),
55//!         message: "Please provide additional details".to_string(),
56//!         requested_schema: ElicitFormSchema::new()
57//!             .string_field("name", Some("Your name"), true)
58//!             .number_field("age", Some("Your age"), false),
59//!         meta: None,
60//!     };
61//!
62//!     let result = ctx.elicit_form(params).await?;
63//!     if result.action == ElicitAction::Accept {
64//!         // Use the form data
65//!         Ok(CallToolResult::text(format!("Got: {:?}", result.content)))
66//!     } else {
67//!         Ok(CallToolResult::text("User declined"))
68//!     }
69//! }
70//! ```
71//!
72//! # Stateless mode: per-request metadata (`stateless` feature)
73//!
74//! With the 2026-07-28 protocol, clients do not run an initialize handshake.
75//! Instead, every request carries the client's protocol version, identity, and
76//! capabilities in a `_meta` object. JSON-RPC transports extract these fields
77//! and stash them as a [`StatelessRequestMeta`](crate::stateless::StatelessRequestMeta)
78//! extension on the [`RequestContext`], accessible via
79//! [`ctx.per_request_meta()`](RequestContext::per_request_meta).
80//!
81//! `per_request_meta()` returns `Some` when:
82//! - The `stateless` feature is compiled in, AND
83//! - The request was dispatched by a JSON-RPC transport, AND
84//! - The request's `_meta` contained at least one recognized field.
85//!
86//! It returns `None` for 2025-11-25 session-based requests and when the request
87//! carried no modern protocol metadata.
88//!
89//! The [`StatelessRequestMeta`](crate::stateless::StatelessRequestMeta) struct
90//! provides:
91//!
92//! - `protocol_version` -- the `io.modelcontextprotocol/protocolVersion` field
93//! - `client_info` -- the `io.modelcontextprotocol/clientInfo` field (name, version)
94//! - `client_capabilities` -- the `io.modelcontextprotocol/clientCapabilities` field
95//! - `log_level` -- optional per-request log level override
96//! - `progress_token` -- optional progress token for progress notifications
97//!
98//! ```rust,ignore
99//! // Requires feature = ["stateless"]
100//! use tower_mcp::context::RequestContext;
101//!
102//! async fn my_tool(ctx: RequestContext, input: MyInput) -> Result<CallToolResult> {
103//!     if let Some(meta) = ctx.per_request_meta() {
104//!         // Available for 2026-07-28 clients on JSON-RPC transports
105//!         if let Some(ref info) = meta.client_info {
106//!             tracing::info!(client = %info.name, version = %info.version, "request from");
107//!         }
108//!         if let Some(ref version) = meta.protocol_version {
109//!             tracing::debug!(protocol_version = %version);
110//!         }
111//!     }
112//!     Ok(CallToolResult::text("ok"))
113//! }
114//! ```
115
116use std::sync::atomic::{AtomicI64, Ordering};
117use std::sync::{Arc, RwLock};
118
119use async_trait::async_trait;
120use tokio::sync::mpsc;
121
122use crate::error::{Error, Result};
123use crate::protocol::{
124    CallToolResult, CancelTaskParams, CreateMessageParams, CreateMessageResult, ElicitFormParams,
125    ElicitRequestParams, ElicitResult, ElicitUrlParams, GetTaskInfoParams, GetTaskResultParams,
126    ListTasksParams, ListTasksResult, LogLevel, LoggingMessageParams, ProgressParams,
127    ProgressToken, RequestId, TaskObject, TaskStatus,
128};
129
130/// A notification to be sent to the client
131#[derive(Debug, Clone)]
132#[non_exhaustive]
133pub enum ServerNotification {
134    /// Progress update for a request
135    Progress(ProgressParams),
136    /// Log message notification
137    LogMessage(LoggingMessageParams),
138    /// A subscribed resource has been updated
139    ResourceUpdated {
140        /// The URI of the updated resource
141        uri: String,
142    },
143    /// The list of available resources has changed
144    ResourcesListChanged,
145    /// The list of available tools has changed
146    ToolsListChanged,
147    /// The list of available prompts has changed
148    PromptsListChanged,
149    /// Task status has changed, in the legacy flat shape.
150    TaskStatusChanged(crate::protocol::TaskStatusParams),
151    /// Task status has changed, in the final SEP-2663 shape.
152    ///
153    /// Carries the complete status-discriminated task, identical to what
154    /// `tasks/get` would have returned at that moment. Delivered only on
155    /// `subscriptions/listen` streams that named this task ID.
156    FinalTaskStatusChanged(crate::tasks::TaskStatusNotificationParams),
157}
158
159/// Sender for server notifications
160pub type NotificationSender = mpsc::Sender<ServerNotification>;
161
162/// Receiver for server notifications
163pub type NotificationReceiver = mpsc::Receiver<ServerNotification>;
164
165/// Create a new notification channel
166pub fn notification_channel(buffer: usize) -> (NotificationSender, NotificationReceiver) {
167    mpsc::channel(buffer)
168}
169
170// =============================================================================
171// Client Requests (Server -> Client)
172// =============================================================================
173
174/// Trait for sending requests from server to client
175///
176/// This enables bidirectional communication where the server can request
177/// actions from the client, such as sampling (LLM requests), elicitation
178/// (user input requests), and task polling (per SEP-1686).
179#[async_trait]
180pub trait ClientRequester: Send + Sync {
181    /// Send a sampling request to the client
182    ///
183    /// Returns the LLM completion result from the client.
184    async fn sample(&self, params: CreateMessageParams) -> Result<CreateMessageResult>;
185
186    /// Send an elicitation request to the client
187    ///
188    /// This requests user input from the client. The request can be either
189    /// form-based (structured input) or URL-based (redirect to external URL).
190    ///
191    /// Returns the elicitation result with the user's action and any submitted data.
192    async fn elicit(&self, params: ElicitRequestParams) -> Result<ElicitResult>;
193
194    /// Send a generic JSON-RPC request to the client.
195    ///
196    /// Used by typed helpers ([`RequestContext::get_task_info`] etc.) to
197    /// dispatch arbitrary request methods. The default implementation returns
198    /// an error so existing custom implementations of this trait keep
199    /// compiling; they only need to override this if they want to support
200    /// methods beyond `sample` and `elicit`.
201    async fn request(
202        &self,
203        method: String,
204        params: serde_json::Value,
205    ) -> Result<serde_json::Value> {
206        let _ = (method, params);
207        Err(Error::Internal(
208            "ClientRequester does not support arbitrary requests".to_string(),
209        ))
210    }
211}
212
213/// A clonable handle to a client requester
214pub type ClientRequesterHandle = Arc<dyn ClientRequester>;
215
216/// Outgoing request to be sent to the client
217#[derive(Debug)]
218pub struct OutgoingRequest {
219    /// The JSON-RPC request ID
220    pub id: RequestId,
221    /// The method name
222    pub method: String,
223    /// The request parameters as JSON
224    pub params: serde_json::Value,
225    /// Channel to send the response back
226    pub response_tx: tokio::sync::oneshot::Sender<Result<serde_json::Value>>,
227}
228
229/// Sender for outgoing requests to the client
230pub type OutgoingRequestSender = mpsc::Sender<OutgoingRequest>;
231
232/// Receiver for outgoing requests (used by transport)
233pub type OutgoingRequestReceiver = mpsc::Receiver<OutgoingRequest>;
234
235/// Create a new outgoing request channel
236pub fn outgoing_request_channel(buffer: usize) -> (OutgoingRequestSender, OutgoingRequestReceiver) {
237    mpsc::channel(buffer)
238}
239
240/// A client requester implementation that sends requests through a channel
241#[derive(Clone)]
242pub struct ChannelClientRequester {
243    request_tx: OutgoingRequestSender,
244    next_id: Arc<AtomicI64>,
245}
246
247impl ChannelClientRequester {
248    /// Create a new channel-based client requester
249    pub fn new(request_tx: OutgoingRequestSender) -> Self {
250        Self {
251            request_tx,
252            next_id: Arc::new(AtomicI64::new(1)),
253        }
254    }
255
256    /// Create a requester that draws IDs from a transport-owned allocator.
257    ///
258    /// HTTP uses one allocator per session while giving each originating POST
259    /// its own request channel. This keeps server-to-client request IDs unique
260    /// without allowing those requests to escape onto an unrelated SSE stream.
261    #[cfg(feature = "http")]
262    pub(crate) fn with_id_allocator(
263        request_tx: OutgoingRequestSender,
264        next_id: Arc<AtomicI64>,
265    ) -> Self {
266        Self {
267            request_tx,
268            next_id,
269        }
270    }
271
272    fn next_request_id(&self) -> RequestId {
273        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
274        RequestId::Number(id)
275    }
276}
277
278impl ChannelClientRequester {
279    async fn dispatch(&self, method: &str, params: serde_json::Value) -> Result<serde_json::Value> {
280        let id = self.next_request_id();
281        let (response_tx, response_rx) = tokio::sync::oneshot::channel();
282
283        let request = OutgoingRequest {
284            id,
285            method: method.to_string(),
286            params,
287            response_tx,
288        };
289
290        self.request_tx
291            .send(request)
292            .await
293            .map_err(|_| Error::Internal("Failed to send request: channel closed".to_string()))?;
294
295        response_rx.await.map_err(|_| {
296            Error::Internal("Failed to receive response: channel closed".to_string())
297        })?
298    }
299}
300
301#[async_trait]
302impl ClientRequester for ChannelClientRequester {
303    async fn sample(&self, params: CreateMessageParams) -> Result<CreateMessageResult> {
304        let params_json = serde_json::to_value(&params)
305            .map_err(|e| Error::Internal(format!("Failed to serialize params: {}", e)))?;
306        let response = self.dispatch("sampling/createMessage", params_json).await?;
307        serde_json::from_value(response)
308            .map_err(|e| Error::Internal(format!("Failed to deserialize response: {}", e)))
309    }
310
311    async fn elicit(&self, params: ElicitRequestParams) -> Result<ElicitResult> {
312        let params_json = serde_json::to_value(&params)
313            .map_err(|e| Error::Internal(format!("Failed to serialize params: {}", e)))?;
314        let response = self.dispatch("elicitation/create", params_json).await?;
315        serde_json::from_value(response)
316            .map_err(|e| Error::Internal(format!("Failed to deserialize response: {}", e)))
317    }
318
319    async fn request(
320        &self,
321        method: String,
322        params: serde_json::Value,
323    ) -> Result<serde_json::Value> {
324        self.dispatch(&method, params).await
325    }
326}
327
328/// Context for a request, providing progress, cancellation, and client request support
329#[derive(Clone)]
330pub struct RequestContext {
331    /// The request ID
332    request_id: RequestId,
333    /// Progress token (if provided by client)
334    progress_token: Option<ProgressToken>,
335    /// Cancellation signal for this request
336    cancellation: tokio_util::sync::CancellationToken,
337    /// Channel for sending notifications
338    notification_tx: Option<NotificationSender>,
339    /// Handle for sending requests to the client (for sampling, etc.)
340    client_requester: Option<ClientRequesterHandle>,
341    /// Extensions for passing data from router/middleware to handlers
342    extensions: Arc<Extensions>,
343    /// Minimum log level set by the client (shared with router for dynamic updates)
344    min_log_level: Option<Arc<RwLock<LogLevel>>>,
345}
346
347/// Type-erased extensions map for passing data to handlers.
348///
349/// Extensions allow router-level state and middleware-injected data to flow
350/// to tool handlers via the `Extension<T>` extractor.
351#[derive(Clone, Default)]
352pub struct Extensions {
353    map: std::collections::HashMap<std::any::TypeId, Arc<dyn std::any::Any + Send + Sync>>,
354}
355
356impl Extensions {
357    /// Create an empty extensions map.
358    pub fn new() -> Self {
359        Self::default()
360    }
361
362    /// Insert a value into the extensions map.
363    ///
364    /// If a value of the same type already exists, it is replaced.
365    pub fn insert<T: Send + Sync + 'static>(&mut self, val: T) {
366        self.map.insert(std::any::TypeId::of::<T>(), Arc::new(val));
367    }
368
369    /// Get a reference to a value in the extensions map.
370    ///
371    /// Returns `None` if no value of the given type has been inserted.
372    pub fn get<T: Send + Sync + 'static>(&self) -> Option<&T> {
373        self.map
374            .get(&std::any::TypeId::of::<T>())
375            .and_then(|val| val.downcast_ref::<T>())
376    }
377
378    /// Check if the extensions map contains a value of the given type.
379    pub fn contains<T: Send + Sync + 'static>(&self) -> bool {
380        self.map.contains_key(&std::any::TypeId::of::<T>())
381    }
382
383    /// Merge another extensions map into this one.
384    ///
385    /// Values from `other` will overwrite existing values of the same type.
386    pub fn merge(&mut self, other: &Extensions) {
387        for (k, v) in &other.map {
388            self.map.insert(*k, v.clone());
389        }
390    }
391
392    /// Returns the number of entries in the extensions map.
393    pub fn len(&self) -> usize {
394        self.map.len()
395    }
396
397    /// Returns `true` if the extensions map contains no entries.
398    pub fn is_empty(&self) -> bool {
399        self.map.is_empty()
400    }
401}
402
403impl std::fmt::Debug for Extensions {
404    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405        f.debug_struct("Extensions")
406            .field("len", &self.map.len())
407            .finish()
408    }
409}
410
411impl std::fmt::Debug for RequestContext {
412    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
413        f.debug_struct("RequestContext")
414            .field("request_id", &self.request_id)
415            .field("progress_token", &self.progress_token)
416            .field("cancelled", &self.cancellation.is_cancelled())
417            .finish()
418    }
419}
420
421impl RequestContext {
422    /// Create a new request context
423    pub fn new(request_id: RequestId) -> Self {
424        Self {
425            request_id,
426            progress_token: None,
427            cancellation: tokio_util::sync::CancellationToken::new(),
428            notification_tx: None,
429            client_requester: None,
430            extensions: Arc::new(Extensions::new()),
431            min_log_level: None,
432        }
433    }
434
435    /// Set the progress token
436    pub fn with_progress_token(mut self, token: ProgressToken) -> Self {
437        self.progress_token = Some(token);
438        self
439    }
440
441    /// Set the notification sender
442    pub fn with_notification_sender(mut self, tx: NotificationSender) -> Self {
443        self.notification_tx = Some(tx);
444        self
445    }
446
447    /// Set the minimum log level for filtering outgoing log notifications
448    ///
449    /// This is shared with the router so that `logging/setLevel` updates
450    /// are immediately visible to all request contexts.
451    pub fn with_min_log_level(mut self, level: Arc<RwLock<LogLevel>>) -> Self {
452        self.min_log_level = Some(level);
453        self
454    }
455
456    /// Set the client requester for server-to-client requests
457    pub fn with_client_requester(mut self, requester: ClientRequesterHandle) -> Self {
458        self.client_requester = Some(requester);
459        self
460    }
461
462    /// Set the extensions for this request context.
463    ///
464    /// Extensions allow router-level state and middleware data to flow to handlers.
465    pub fn with_extensions(mut self, extensions: Arc<Extensions>) -> Self {
466        self.extensions = extensions;
467        self
468    }
469
470    /// Get a reference to a value from the extensions map.
471    ///
472    /// Returns `None` if no value of the given type has been inserted.
473    ///
474    /// # Example
475    ///
476    /// ```rust,ignore
477    /// #[derive(Clone)]
478    /// struct CurrentUser { id: String }
479    ///
480    /// // In a handler:
481    /// if let Some(user) = ctx.extension::<CurrentUser>() {
482    ///     println!("User: {}", user.id);
483    /// }
484    /// ```
485    pub fn extension<T: Send + Sync + 'static>(&self) -> Option<&T> {
486        self.extensions.get::<T>()
487    }
488
489    /// Protocol extensions declared by both the client and server.
490    ///
491    /// Unknown or one-sided declarations are not included. The returned view
492    /// preserves each peer's settings object for extension-specific policy.
493    pub fn negotiated_extensions(&self) -> Option<&crate::NegotiatedExtensions> {
494        self.extension()
495    }
496
497    /// Get a mutable reference to the extensions.
498    ///
499    /// This allows middleware to insert data that handlers can access via
500    /// the `Extension<T>` extractor.
501    pub fn extensions_mut(&mut self) -> &mut Extensions {
502        Arc::make_mut(&mut self.extensions)
503    }
504
505    /// Get a reference to the extensions.
506    pub fn extensions(&self) -> &Extensions {
507        &self.extensions
508    }
509
510    /// SEP-2575 per-request `_meta` (protocol version, client info, client
511    /// capabilities, log level) if the transport extracted it.
512    ///
513    /// Returns `Some` for 2026-07-28 clients on JSON-RPC transports when the
514    /// request carried a `_meta` object with recognized fields. Returns `None`
515    /// when:
516    /// - The request had no `_meta` field, or
517    /// - The transport does not use [`crate::jsonrpc::JsonRpcService`], or
518    /// - The `stateless` feature is not compiled in.
519    ///
520    /// # Example
521    ///
522    /// ```rust,ignore
523    /// async fn my_tool(ctx: RequestContext, input: MyInput) -> Result<CallToolResult> {
524    ///     if let Some(meta) = ctx.per_request_meta() {
525    ///         // protocol_version, client_info, client_capabilities are all Option<_>
526    ///         if let Some(ref version) = meta.protocol_version {
527    ///             tracing::debug!(protocol_version = %version);
528    ///         }
529    ///         if let Some(ref info) = meta.client_info {
530    ///             tracing::info!(client = %info.name, version = %info.version);
531    ///         }
532    ///     }
533    ///     Ok(CallToolResult::text("ok"))
534    /// }
535    /// ```
536    #[cfg(feature = "stateless")]
537    pub fn per_request_meta(&self) -> Option<&crate::stateless::StatelessRequestMeta> {
538        self.extension::<crate::stateless::StatelessRequestMeta>()
539    }
540
541    /// SEP-2322 continuation values supplied by the client on this attempt.
542    #[cfg(feature = "stateless")]
543    pub fn mrtr(&self) -> Option<&crate::mrtr::MrtrRequest> {
544        self.extension::<crate::mrtr::MrtrRequest>()
545    }
546
547    /// Client responses from the prior MRTR round, if any.
548    #[cfg(feature = "stateless")]
549    pub fn input_responses(&self) -> Option<&crate::protocol::InputResponses> {
550        self.mrtr()
551            .and_then(crate::mrtr::MrtrRequest::input_responses)
552    }
553
554    /// Opaque request state echoed by the client, if any.
555    #[cfg(feature = "stateless")]
556    pub fn request_state(&self) -> Option<&str> {
557        self.mrtr()
558            .and_then(crate::mrtr::MrtrRequest::request_state)
559    }
560
561    /// Router-configured request-state codec shared by this handler.
562    #[cfg(feature = "stateless")]
563    pub fn request_state_codec(&self) -> Option<&crate::mrtr::RequestStateCodec> {
564        self.extension::<crate::mrtr::RequestStateCodec>()
565    }
566
567    /// Get the request ID
568    pub fn request_id(&self) -> &RequestId {
569        &self.request_id
570    }
571
572    /// Get the progress token (if any)
573    pub fn progress_token(&self) -> Option<&ProgressToken> {
574        self.progress_token.as_ref()
575    }
576
577    /// Check if the request has been cancelled
578    pub fn is_cancelled(&self) -> bool {
579        self.cancellation.is_cancelled()
580    }
581
582    /// Mark the request as cancelled
583    pub fn cancel(&self) {
584        self.cancellation.cancel();
585    }
586
587    /// Wait until the request is cancelled.
588    ///
589    /// Completes when [`cancel`](Self::cancel) is called -- by a
590    /// `notifications/cancelled` message, or by the transport when the
591    /// client disconnects before the response is delivered (HTTP
592    /// stateless mode). Useful in `tokio::select!` to abandon work early:
593    ///
594    /// ```rust,ignore
595    /// tokio::select! {
596    ///     result = do_work() => { /* ... */ }
597    ///     _ = ctx.cancelled() => return Err(Error::tool("cancelled")),
598    /// }
599    /// ```
600    pub async fn cancelled(&self) {
601        self.cancellation.cancelled().await
602    }
603
604    /// Get a cancellation token that can be shared
605    pub fn cancellation_token(&self) -> CancellationToken {
606        CancellationToken {
607            inner: self.cancellation.clone(),
608        }
609    }
610
611    /// Replace this context's cancellation source with an existing token.
612    ///
613    /// Used by transports to link a request's lifetime to an external
614    /// signal (e.g. client disconnect on the HTTP stateless path). After
615    /// this call, `is_cancelled()`, `cancelled()`, and tokens returned by
616    /// [`cancellation_token`](Self::cancellation_token) all observe the
617    /// given token.
618    pub fn with_cancellation_token(mut self, token: CancellationToken) -> Self {
619        self.cancellation = token.inner;
620        self
621    }
622
623    /// Report progress to the client
624    ///
625    /// This is a no-op if no progress token was provided or no notification sender is configured.
626    pub async fn report_progress(&self, progress: f64, total: Option<f64>, message: Option<&str>) {
627        let Some(token) = &self.progress_token else {
628            return;
629        };
630        let Some(tx) = &self.notification_tx else {
631            return;
632        };
633
634        let params = ProgressParams {
635            progress_token: token.clone(),
636            progress,
637            total,
638            message: message.map(|s| s.to_string()),
639            meta: None,
640        };
641
642        // Best effort - don't block if channel is full
643        let _ = tx.try_send(ServerNotification::Progress(params));
644    }
645
646    /// Report progress synchronously (non-async version)
647    ///
648    /// This is a no-op if no progress token was provided or no notification sender is configured.
649    pub fn report_progress_sync(&self, progress: f64, total: Option<f64>, message: Option<&str>) {
650        let Some(token) = &self.progress_token else {
651            return;
652        };
653        let Some(tx) = &self.notification_tx else {
654            return;
655        };
656
657        let params = ProgressParams {
658            progress_token: token.clone(),
659            progress,
660            total,
661            message: message.map(|s| s.to_string()),
662            meta: None,
663        };
664
665        let _ = tx.try_send(ServerNotification::Progress(params));
666    }
667
668    /// Notify subscribed clients that the tool list changed.
669    pub fn notify_tools_list_changed(&self) -> bool {
670        self.notification_tx
671            .as_ref()
672            .is_some_and(|tx| tx.try_send(ServerNotification::ToolsListChanged).is_ok())
673    }
674
675    /// Notify subscribed clients that the prompt list changed.
676    pub fn notify_prompts_list_changed(&self) -> bool {
677        self.notification_tx
678            .as_ref()
679            .is_some_and(|tx| tx.try_send(ServerNotification::PromptsListChanged).is_ok())
680    }
681
682    /// Notify subscribed clients that the resource list changed.
683    pub fn notify_resources_list_changed(&self) -> bool {
684        self.notification_tx.as_ref().is_some_and(|tx| {
685            tx.try_send(ServerNotification::ResourcesListChanged)
686                .is_ok()
687        })
688    }
689
690    /// Notify subscribed clients that one resource changed.
691    pub fn notify_resource_updated(&self, uri: impl Into<String>) -> bool {
692        self.notification_tx.as_ref().is_some_and(|tx| {
693            tx.try_send(ServerNotification::ResourceUpdated { uri: uri.into() })
694                .is_ok()
695        })
696    }
697
698    /// Send a log message notification to the client
699    ///
700    /// This is a no-op if no notification sender is configured.
701    ///
702    /// # Example
703    ///
704    /// ```rust,ignore
705    /// use tower_mcp::protocol::{LoggingMessageParams, LogLevel};
706    ///
707    /// async fn my_tool(ctx: RequestContext) {
708    ///     ctx.send_log(
709    ///         LoggingMessageParams::new(LogLevel::Info, serde_json::json!("Processing..."))
710    ///             .with_logger("my-tool")
711    ///     );
712    /// }
713    /// ```
714    pub fn send_log(&self, params: LoggingMessageParams) {
715        let Some(tx) = &self.notification_tx else {
716            return;
717        };
718
719        // The final protocol removed logging/setLevel. Log delivery is instead
720        // authorized per request: no logLevel means no log notifications.
721        #[cfg(feature = "stateless")]
722        if let Some(meta) = self.per_request_meta()
723            && meta.protocol_version.as_deref()
724                == Some(crate::protocol::PROTOCOL_VERSION_2026_07_28)
725        {
726            let Some(request_level) = meta.log_level else {
727                return;
728            };
729            let request_level = match request_level {
730                crate::stateless::LogLevel::Debug => LogLevel::Debug,
731                crate::stateless::LogLevel::Info => LogLevel::Info,
732                crate::stateless::LogLevel::Notice => LogLevel::Notice,
733                crate::stateless::LogLevel::Warning => LogLevel::Warning,
734                crate::stateless::LogLevel::Error => LogLevel::Error,
735                crate::stateless::LogLevel::Critical => LogLevel::Critical,
736                crate::stateless::LogLevel::Alert => LogLevel::Alert,
737                crate::stateless::LogLevel::Emergency => LogLevel::Emergency,
738            };
739            if params.level > request_level {
740                return;
741            }
742            let _ = tx.try_send(ServerNotification::LogMessage(params));
743            return;
744        }
745
746        // Filter by minimum log level set via logging/setLevel
747        // LogLevel derives Ord with Emergency < Alert < ... < Debug,
748        // so a message passes if its severity is at least the minimum
749        // (i.e., its ordinal is <= the minimum level's ordinal).
750        if let Some(min_level) = &self.min_log_level
751            && let Ok(min) = min_level.read()
752            && params.level > *min
753        {
754            return;
755        }
756
757        let _ = tx.try_send(ServerNotification::LogMessage(params));
758    }
759
760    /// Check if sampling is available
761    ///
762    /// Returns true if a client requester is configured and the transport
763    /// supports bidirectional communication.
764    pub fn can_sample(&self) -> bool {
765        self.client_requester.is_some()
766    }
767
768    /// Request an LLM completion from the client
769    ///
770    /// This sends a `sampling/createMessage` request to the client and waits
771    /// for the response. The client is expected to forward this to an LLM
772    /// and return the result.
773    ///
774    /// Returns an error if sampling is not available (no client requester configured).
775    ///
776    /// # Example
777    ///
778    /// ```rust,ignore
779    /// use tower_mcp::{CreateMessageParams, SamplingMessage};
780    ///
781    /// async fn my_tool(ctx: RequestContext, input: MyInput) -> Result<CallToolResult> {
782    ///     let params = CreateMessageParams::new(
783    ///         vec![SamplingMessage::user("Summarize: ...")],
784    ///         500,
785    ///     );
786    ///
787    ///     let result = ctx.sample(params).await?;
788    ///     Ok(CallToolResult::text(format!("{:?}", result.content)))
789    /// }
790    /// ```
791    pub async fn sample(&self, params: CreateMessageParams) -> Result<CreateMessageResult> {
792        let requester = self.client_requester.as_ref().ok_or_else(|| {
793            Error::Internal("Sampling not available: no client requester configured".to_string())
794        })?;
795
796        requester.sample(params).await
797    }
798
799    /// Check if elicitation is available
800    ///
801    /// Returns true if a client requester is configured and the transport
802    /// supports bidirectional communication. Note that this only checks if
803    /// the mechanism is available, not whether the client supports elicitation.
804    pub fn can_elicit(&self) -> bool {
805        self.client_requester.is_some()
806    }
807
808    /// Request user input via a form from the client
809    ///
810    /// This sends an `elicitation/create` request to the client with a form schema.
811    /// The client renders the form to the user and returns their response.
812    ///
813    /// Returns an error if elicitation is not available (no client requester configured).
814    ///
815    /// # Example
816    ///
817    /// ```rust,ignore
818    /// use tower_mcp::{ElicitFormParams, ElicitFormSchema, ElicitMode, ElicitAction};
819    ///
820    /// async fn my_tool(ctx: RequestContext, input: MyInput) -> Result<CallToolResult> {
821    ///     let params = ElicitFormParams {
822    ///         mode: Some(ElicitMode::Form),
823    ///         message: "Please enter your details".to_string(),
824    ///         requested_schema: ElicitFormSchema::new()
825    ///             .string_field("name", Some("Your name"), true),
826    ///         meta: None,
827    ///     };
828    ///
829    ///     let result = ctx.elicit_form(params).await?;
830    ///     match result.action {
831    ///         ElicitAction::Accept => {
832    ///             // Use result.content
833    ///             Ok(CallToolResult::text("Got your input!"))
834    ///         }
835    ///         _ => Ok(CallToolResult::text("User declined"))
836    ///     }
837    /// }
838    /// ```
839    pub async fn elicit_form(&self, params: ElicitFormParams) -> Result<ElicitResult> {
840        let requester = self.client_requester.as_ref().ok_or_else(|| {
841            Error::Internal("Elicitation not available: no client requester configured".to_string())
842        })?;
843
844        requester.elicit(ElicitRequestParams::Form(params)).await
845    }
846
847    /// Request user input via URL redirect from the client
848    ///
849    /// This sends an `elicitation/create` request to the client with a URL.
850    /// The client directs the user to the URL for out-of-band input collection.
851    /// The server receives the result via a callback notification.
852    ///
853    /// Returns an error if elicitation is not available (no client requester configured).
854    ///
855    /// **Protocol note:** `ElicitUrlParams::elicitation_id` and the callback
856    /// notification it correlates with are a 2025-11-25-and-earlier pattern.
857    /// The final 2026-07-28 schema removes both in favor of MRTR (SEP-2322).
858    /// Use an MRTR-capable tool, prompt, or resource handler there; the client
859    /// learns the outcome by retrying the original request instead of receiving
860    /// a completion notification.
861    ///
862    /// # Example
863    ///
864    /// ```rust,ignore
865    /// use tower_mcp::{ElicitUrlParams, ElicitMode, ElicitAction};
866    ///
867    /// async fn my_tool(ctx: RequestContext, input: MyInput) -> Result<CallToolResult> {
868    ///     let params = ElicitUrlParams {
869    ///         mode: Some(ElicitMode::Url),
870    ///         elicitation_id: "unique-id-123".to_string(),
871    ///         message: "Please authorize via the link".to_string(),
872    ///         url: "https://example.com/auth?id=unique-id-123".to_string(),
873    ///         meta: None,
874    ///     };
875    ///
876    ///     let result = ctx.elicit_url(params).await?;
877    ///     match result.action {
878    ///         ElicitAction::Accept => Ok(CallToolResult::text("Authorization complete!")),
879    ///         _ => Ok(CallToolResult::text("Authorization cancelled"))
880    ///     }
881    /// }
882    /// ```
883    pub async fn elicit_url(&self, params: ElicitUrlParams) -> Result<ElicitResult> {
884        let requester = self.client_requester.as_ref().ok_or_else(|| {
885            Error::Internal("Elicitation not available: no client requester configured".to_string())
886        })?;
887
888        requester.elicit(ElicitRequestParams::Url(params)).await
889    }
890
891    /// Request simple confirmation from the user.
892    ///
893    /// This is a convenience method for simple yes/no confirmation dialogs.
894    /// It creates an elicitation form with a single boolean "confirm" field
895    /// and returns `true` if the user accepts, `false` otherwise.
896    ///
897    /// Returns an error if elicitation is not available (no client requester configured).
898    ///
899    /// # Example
900    ///
901    /// ```rust,ignore
902    /// use tower_mcp::{RequestContext, CallToolResult};
903    ///
904    /// async fn delete_item(ctx: RequestContext) -> Result<CallToolResult> {
905    ///     let confirmed = ctx.confirm("Are you sure you want to delete this item?").await?;
906    ///     if confirmed {
907    ///         // Perform deletion
908    ///         Ok(CallToolResult::text("Item deleted"))
909    ///     } else {
910    ///         Ok(CallToolResult::text("Deletion cancelled"))
911    ///     }
912    /// }
913    /// ```
914    pub async fn confirm(&self, message: impl Into<String>) -> Result<bool> {
915        use crate::protocol::{ElicitAction, ElicitFormParams, ElicitFormSchema, ElicitMode};
916
917        let params = ElicitFormParams {
918            mode: Some(ElicitMode::Form),
919            message: message.into(),
920            requested_schema: ElicitFormSchema::new().boolean_field_with_default(
921                "confirm",
922                Some("Confirm this action"),
923                true,
924                false,
925            ),
926            meta: None,
927        };
928
929        let result = self.elicit_form(params).await?;
930        Ok(result.action == ElicitAction::Accept)
931    }
932
933    /// List tasks tracked by the connected client (legacy SEP-1686).
934    ///
935    /// Sends a `tasks/list` request to the client and returns the result.
936    /// Pass `Some(status)` to filter to a single status, or `None` for all
937    /// tasks. Pagination is exposed via [`ListTasksResult::next_cursor`];
938    /// use [`request_raw`](Self::request_raw) for cursor-driven calls.
939    ///
940    /// Returns an error if no client requester is configured or the client
941    /// does not advertise task support.
942    #[deprecated(
943        since = "0.13.0",
944        note = "final SEP-2663 removes tasks/list; a conforming peer answers \
945                MethodNotFound (-32601). Only useful against legacy SEP-1686 \
946                clients."
947    )]
948    pub async fn list_tasks(&self, status: Option<TaskStatus>) -> Result<ListTasksResult> {
949        let params = ListTasksParams {
950            status,
951            cursor: None,
952            meta: None,
953        };
954        let value = self
955            .request_raw("tasks/list", serde_json::to_value(&params)?)
956            .await?;
957        serde_json::from_value(value)
958            .map_err(|e| Error::Internal(format!("Failed to deserialize tasks/list: {e}")))
959    }
960
961    /// Fetch metadata for a single task tracked by the client (SEP-1686).
962    ///
963    /// Sends a `tasks/get` request and returns the task object, including
964    /// the current status, timestamps, and TTL.
965    pub async fn get_task_info(&self, task_id: impl Into<String>) -> Result<TaskObject> {
966        let params = GetTaskInfoParams {
967            task_id: task_id.into(),
968            meta: None,
969        };
970        let value = self
971            .request_raw("tasks/get", serde_json::to_value(&params)?)
972            .await?;
973        serde_json::from_value(value)
974            .map_err(|e| Error::Internal(format!("Failed to deserialize tasks/get: {e}")))
975    }
976
977    /// Fetch the terminal result for a task tracked by the client (legacy
978    /// SEP-1686).
979    ///
980    /// Sends a `tasks/result` request. The client is expected to block until
981    /// the task reaches a terminal state and then return the underlying
982    /// `CallToolResult`. For long-running tasks, prefer polling with
983    /// [`get_task_info`](Self::get_task_info) and only call this once the
984    /// status is terminal.
985    #[deprecated(
986        since = "0.13.0",
987        note = "final SEP-2663 removes tasks/result (results are inlined in \
988                the tasks/get DetailedTask); a conforming peer answers \
989                MethodNotFound (-32601). Only useful against legacy SEP-1686 \
990                clients."
991    )]
992    pub async fn get_task_result(&self, task_id: impl Into<String>) -> Result<CallToolResult> {
993        let params = GetTaskResultParams {
994            task_id: task_id.into(),
995            meta: None,
996        };
997        let value = self
998            .request_raw("tasks/result", serde_json::to_value(&params)?)
999            .await?;
1000        serde_json::from_value(value)
1001            .map_err(|e| Error::Internal(format!("Failed to deserialize tasks/result: {e}")))
1002    }
1003
1004    /// Cancel a task tracked by the client.
1005    ///
1006    /// Sends a `tasks/cancel` request. Per final SEP-2663 the acknowledgment
1007    /// is an empty result and the observable task status is polled via
1008    /// [`get_task_info`](Self::get_task_info); the ack body is discarded, so
1009    /// this also tolerates legacy SEP-1686 peers that return the task object.
1010    pub async fn cancel_task(
1011        &self,
1012        task_id: impl Into<String>,
1013        reason: Option<String>,
1014    ) -> Result<()> {
1015        let params = CancelTaskParams {
1016            task_id: task_id.into(),
1017            reason,
1018            meta: None,
1019        };
1020        self.request_raw("tasks/cancel", serde_json::to_value(&params)?)
1021            .await?;
1022        Ok(())
1023    }
1024
1025    /// Send an arbitrary JSON-RPC request to the client.
1026    ///
1027    /// Escape hatch for methods not covered by the typed helpers (e.g. when
1028    /// a `tasks/list` cursor needs to be passed). Most callers should prefer
1029    /// the typed methods.
1030    pub async fn request_raw(
1031        &self,
1032        method: &str,
1033        params: serde_json::Value,
1034    ) -> Result<serde_json::Value> {
1035        let requester = self.client_requester.as_ref().ok_or_else(|| {
1036            Error::Internal(
1037                "Client request not available: no client requester configured".to_string(),
1038            )
1039        })?;
1040        requester.request(method.to_string(), params).await
1041    }
1042}
1043
1044/// A token that can be used to check for, wait on, or request cancellation
1045///
1046/// Cloned tokens share the same underlying signal: cancelling any clone
1047/// cancels them all. Backed by [`tokio_util::sync::CancellationToken`],
1048/// so cancellation can also be awaited via [`cancelled`](Self::cancelled).
1049#[derive(Clone, Debug, Default)]
1050pub struct CancellationToken {
1051    inner: tokio_util::sync::CancellationToken,
1052}
1053
1054impl CancellationToken {
1055    /// Create a new, un-cancelled token
1056    pub fn new() -> Self {
1057        Self::default()
1058    }
1059
1060    /// Check if cancellation has been requested
1061    pub fn is_cancelled(&self) -> bool {
1062        self.inner.is_cancelled()
1063    }
1064
1065    /// Request cancellation
1066    pub fn cancel(&self) {
1067        self.inner.cancel();
1068    }
1069
1070    /// Wait until cancellation is requested
1071    ///
1072    /// Completes immediately if the token is already cancelled.
1073    pub async fn cancelled(&self) {
1074        self.inner.cancelled().await
1075    }
1076}
1077
1078/// Builder for creating request contexts
1079#[derive(Default)]
1080pub struct RequestContextBuilder {
1081    request_id: Option<RequestId>,
1082    progress_token: Option<ProgressToken>,
1083    notification_tx: Option<NotificationSender>,
1084    client_requester: Option<ClientRequesterHandle>,
1085    min_log_level: Option<Arc<RwLock<LogLevel>>>,
1086}
1087
1088impl RequestContextBuilder {
1089    /// Create a new builder
1090    pub fn new() -> Self {
1091        Self::default()
1092    }
1093
1094    /// Set the request ID
1095    pub fn request_id(mut self, id: RequestId) -> Self {
1096        self.request_id = Some(id);
1097        self
1098    }
1099
1100    /// Set the progress token
1101    pub fn progress_token(mut self, token: ProgressToken) -> Self {
1102        self.progress_token = Some(token);
1103        self
1104    }
1105
1106    /// Set the notification sender
1107    pub fn notification_sender(mut self, tx: NotificationSender) -> Self {
1108        self.notification_tx = Some(tx);
1109        self
1110    }
1111
1112    /// Set the client requester for server-to-client requests
1113    pub fn client_requester(mut self, requester: ClientRequesterHandle) -> Self {
1114        self.client_requester = Some(requester);
1115        self
1116    }
1117
1118    /// Set the minimum log level for filtering
1119    pub fn min_log_level(mut self, level: Arc<RwLock<LogLevel>>) -> Self {
1120        self.min_log_level = Some(level);
1121        self
1122    }
1123
1124    /// Build the request context
1125    ///
1126    /// Panics if request_id is not set.
1127    pub fn build(self) -> RequestContext {
1128        let mut ctx = RequestContext::new(self.request_id.expect("request_id is required"));
1129        if let Some(token) = self.progress_token {
1130            ctx = ctx.with_progress_token(token);
1131        }
1132        if let Some(tx) = self.notification_tx {
1133            ctx = ctx.with_notification_sender(tx);
1134        }
1135        if let Some(requester) = self.client_requester {
1136            ctx = ctx.with_client_requester(requester);
1137        }
1138        if let Some(level) = self.min_log_level {
1139            ctx = ctx.with_min_log_level(level);
1140        }
1141        ctx
1142    }
1143}
1144
1145#[cfg(test)]
1146mod tests {
1147    use super::*;
1148
1149    #[test]
1150    fn test_cancellation() {
1151        let ctx = RequestContext::new(RequestId::Number(1));
1152        assert!(!ctx.is_cancelled());
1153
1154        let token = ctx.cancellation_token();
1155        assert!(!token.is_cancelled());
1156
1157        ctx.cancel();
1158        assert!(ctx.is_cancelled());
1159        assert!(token.is_cancelled());
1160    }
1161
1162    #[tokio::test]
1163    async fn test_progress_reporting() {
1164        let (tx, mut rx) = notification_channel(10);
1165
1166        let ctx = RequestContext::new(RequestId::Number(1))
1167            .with_progress_token(ProgressToken::Number(42))
1168            .with_notification_sender(tx);
1169
1170        ctx.report_progress(50.0, Some(100.0), Some("Halfway"))
1171            .await;
1172
1173        let notification = rx.recv().await.unwrap();
1174        match notification {
1175            ServerNotification::Progress(params) => {
1176                assert_eq!(params.progress, 50.0);
1177                assert_eq!(params.total, Some(100.0));
1178                assert_eq!(params.message.as_deref(), Some("Halfway"));
1179            }
1180            _ => panic!("Expected Progress notification"),
1181        }
1182    }
1183
1184    #[tokio::test]
1185    async fn test_progress_no_token() {
1186        let (tx, mut rx) = notification_channel(10);
1187
1188        // No progress token - should be a no-op
1189        let ctx = RequestContext::new(RequestId::Number(1)).with_notification_sender(tx);
1190
1191        ctx.report_progress(50.0, Some(100.0), None).await;
1192
1193        // Channel should be empty
1194        assert!(rx.try_recv().is_err());
1195    }
1196
1197    #[test]
1198    fn test_builder() {
1199        let (tx, _rx) = notification_channel(10);
1200
1201        let ctx = RequestContextBuilder::new()
1202            .request_id(RequestId::String("req-1".to_string()))
1203            .progress_token(ProgressToken::String("prog-1".to_string()))
1204            .notification_sender(tx)
1205            .build();
1206
1207        assert_eq!(ctx.request_id(), &RequestId::String("req-1".to_string()));
1208        assert!(ctx.progress_token().is_some());
1209    }
1210
1211    #[test]
1212    fn test_can_sample_without_requester() {
1213        let ctx = RequestContext::new(RequestId::Number(1));
1214        assert!(!ctx.can_sample());
1215    }
1216
1217    #[test]
1218    fn test_can_sample_with_requester() {
1219        let (request_tx, _rx) = outgoing_request_channel(10);
1220        let requester: ClientRequesterHandle = Arc::new(ChannelClientRequester::new(request_tx));
1221
1222        let ctx = RequestContext::new(RequestId::Number(1)).with_client_requester(requester);
1223        assert!(ctx.can_sample());
1224    }
1225
1226    #[tokio::test]
1227    async fn test_sample_without_requester_fails() {
1228        use crate::protocol::{CreateMessageParams, SamplingMessage};
1229
1230        let ctx = RequestContext::new(RequestId::Number(1));
1231        let params = CreateMessageParams::new(vec![SamplingMessage::user("test")], 100);
1232
1233        let result = ctx.sample(params).await;
1234        assert!(result.is_err());
1235        assert!(
1236            result
1237                .unwrap_err()
1238                .to_string()
1239                .contains("Sampling not available")
1240        );
1241    }
1242
1243    #[test]
1244    fn test_builder_with_client_requester() {
1245        let (request_tx, _rx) = outgoing_request_channel(10);
1246        let requester: ClientRequesterHandle = Arc::new(ChannelClientRequester::new(request_tx));
1247
1248        let ctx = RequestContextBuilder::new()
1249            .request_id(RequestId::Number(1))
1250            .client_requester(requester)
1251            .build();
1252
1253        assert!(ctx.can_sample());
1254    }
1255
1256    #[test]
1257    fn test_can_elicit_without_requester() {
1258        let ctx = RequestContext::new(RequestId::Number(1));
1259        assert!(!ctx.can_elicit());
1260    }
1261
1262    #[test]
1263    fn test_can_elicit_with_requester() {
1264        let (request_tx, _rx) = outgoing_request_channel(10);
1265        let requester: ClientRequesterHandle = Arc::new(ChannelClientRequester::new(request_tx));
1266
1267        let ctx = RequestContext::new(RequestId::Number(1)).with_client_requester(requester);
1268        assert!(ctx.can_elicit());
1269    }
1270
1271    #[tokio::test]
1272    async fn test_elicit_form_without_requester_fails() {
1273        use crate::protocol::{ElicitFormSchema, ElicitMode};
1274
1275        let ctx = RequestContext::new(RequestId::Number(1));
1276        let params = ElicitFormParams {
1277            mode: Some(ElicitMode::Form),
1278            message: "Enter details".to_string(),
1279            requested_schema: ElicitFormSchema::new().string_field("name", None, true),
1280            meta: None,
1281        };
1282
1283        let result = ctx.elicit_form(params).await;
1284        assert!(result.is_err());
1285        assert!(
1286            result
1287                .unwrap_err()
1288                .to_string()
1289                .contains("Elicitation not available")
1290        );
1291    }
1292
1293    #[tokio::test]
1294    async fn test_elicit_url_without_requester_fails() {
1295        use crate::protocol::ElicitMode;
1296
1297        let ctx = RequestContext::new(RequestId::Number(1));
1298        let params = ElicitUrlParams {
1299            mode: Some(ElicitMode::Url),
1300            elicitation_id: "test-123".to_string(),
1301            message: "Please authorize".to_string(),
1302            url: "https://example.com/auth".to_string(),
1303            meta: None,
1304        };
1305
1306        let result = ctx.elicit_url(params).await;
1307        assert!(result.is_err());
1308        assert!(
1309            result
1310                .unwrap_err()
1311                .to_string()
1312                .contains("Elicitation not available")
1313        );
1314    }
1315
1316    #[tokio::test]
1317    async fn test_confirm_without_requester_fails() {
1318        let ctx = RequestContext::new(RequestId::Number(1));
1319
1320        let result = ctx.confirm("Are you sure?").await;
1321        assert!(result.is_err());
1322        assert!(
1323            result
1324                .unwrap_err()
1325                .to_string()
1326                .contains("Elicitation not available")
1327        );
1328    }
1329
1330    #[tokio::test]
1331    async fn test_send_log_filtered_by_level() {
1332        let (tx, mut rx) = notification_channel(10);
1333        let min_level = Arc::new(RwLock::new(LogLevel::Warning));
1334
1335        let ctx = RequestContext::new(RequestId::Number(1))
1336            .with_notification_sender(tx)
1337            .with_min_log_level(min_level.clone());
1338
1339        // Error is more severe than Warning — should pass through
1340        ctx.send_log(LoggingMessageParams::new(
1341            LogLevel::Error,
1342            serde_json::Value::Null,
1343        ));
1344        let msg = rx.try_recv();
1345        assert!(msg.is_ok(), "Error should pass through Warning filter");
1346
1347        // Warning is equal to min level — should pass through
1348        ctx.send_log(LoggingMessageParams::new(
1349            LogLevel::Warning,
1350            serde_json::Value::Null,
1351        ));
1352        let msg = rx.try_recv();
1353        assert!(msg.is_ok(), "Warning should pass through Warning filter");
1354
1355        // Info is less severe than Warning — should be filtered
1356        ctx.send_log(LoggingMessageParams::new(
1357            LogLevel::Info,
1358            serde_json::Value::Null,
1359        ));
1360        let msg = rx.try_recv();
1361        assert!(msg.is_err(), "Info should be filtered by Warning filter");
1362
1363        // Debug is less severe than Warning — should be filtered
1364        ctx.send_log(LoggingMessageParams::new(
1365            LogLevel::Debug,
1366            serde_json::Value::Null,
1367        ));
1368        let msg = rx.try_recv();
1369        assert!(msg.is_err(), "Debug should be filtered by Warning filter");
1370    }
1371
1372    #[tokio::test]
1373    async fn test_send_log_level_updates_dynamically() {
1374        let (tx, mut rx) = notification_channel(10);
1375        let min_level = Arc::new(RwLock::new(LogLevel::Error));
1376
1377        let ctx = RequestContext::new(RequestId::Number(1))
1378            .with_notification_sender(tx)
1379            .with_min_log_level(min_level.clone());
1380
1381        // Info should be filtered at Error level
1382        ctx.send_log(LoggingMessageParams::new(
1383            LogLevel::Info,
1384            serde_json::Value::Null,
1385        ));
1386        assert!(
1387            rx.try_recv().is_err(),
1388            "Info should be filtered at Error level"
1389        );
1390
1391        // Dynamically update to Debug (most permissive)
1392        *min_level.write().unwrap() = LogLevel::Debug;
1393
1394        // Now Info should pass through
1395        ctx.send_log(LoggingMessageParams::new(
1396            LogLevel::Info,
1397            serde_json::Value::Null,
1398        ));
1399        assert!(
1400            rx.try_recv().is_ok(),
1401            "Info should pass through after level changed to Debug"
1402        );
1403    }
1404
1405    #[tokio::test]
1406    async fn test_send_log_no_min_level_sends_all() {
1407        let (tx, mut rx) = notification_channel(10);
1408
1409        // No min_log_level set — all messages should pass through
1410        let ctx = RequestContext::new(RequestId::Number(1)).with_notification_sender(tx);
1411
1412        ctx.send_log(LoggingMessageParams::new(
1413            LogLevel::Debug,
1414            serde_json::Value::Null,
1415        ));
1416        assert!(
1417            rx.try_recv().is_ok(),
1418            "Debug should pass when no min level is set"
1419        );
1420    }
1421
1422    #[tokio::test]
1423    #[cfg(feature = "stateless")]
1424    async fn final_request_log_level_is_required_and_filters_per_request() {
1425        let (tx, mut rx) = notification_channel(10);
1426        let mut extensions = Extensions::new();
1427        extensions.insert(crate::stateless::StatelessRequestMeta {
1428            protocol_version: Some(crate::protocol::PROTOCOL_VERSION_2026_07_28.to_string()),
1429            client_capabilities: Some(Default::default()),
1430            ..Default::default()
1431        });
1432        let ctx = RequestContext::new(RequestId::Number(1))
1433            .with_notification_sender(tx.clone())
1434            .with_extensions(Arc::new(extensions));
1435        ctx.send_log(LoggingMessageParams::new(
1436            LogLevel::Emergency,
1437            serde_json::Value::Null,
1438        ));
1439        assert!(
1440            rx.try_recv().is_err(),
1441            "final requests without logLevel must receive no logs"
1442        );
1443
1444        let mut extensions = Extensions::new();
1445        extensions.insert(crate::stateless::StatelessRequestMeta {
1446            protocol_version: Some(crate::protocol::PROTOCOL_VERSION_2026_07_28.to_string()),
1447            client_capabilities: Some(Default::default()),
1448            log_level: Some(crate::stateless::LogLevel::Warning),
1449            ..Default::default()
1450        });
1451        let ctx = RequestContext::new(RequestId::Number(2))
1452            .with_notification_sender(tx)
1453            .with_extensions(Arc::new(extensions));
1454        ctx.send_log(LoggingMessageParams::new(
1455            LogLevel::Info,
1456            serde_json::Value::Null,
1457        ));
1458        assert!(rx.try_recv().is_err(), "Info must be filtered at Warning");
1459        ctx.send_log(LoggingMessageParams::new(
1460            LogLevel::Error,
1461            serde_json::Value::Null,
1462        ));
1463        assert!(rx.try_recv().is_ok(), "Error must pass at Warning");
1464    }
1465
1466    fn make_task_object(id: &str, status: TaskStatus) -> serde_json::Value {
1467        serde_json::json!({
1468            "taskId": id,
1469            "status": status,
1470            "createdAt": "2026-04-24T00:00:00Z",
1471            "lastUpdatedAt": "2026-04-24T00:00:00Z",
1472            "ttl": null
1473        })
1474    }
1475
1476    fn spawn_mock_client(
1477        mut rx: OutgoingRequestReceiver,
1478        responder: impl Fn(&str, serde_json::Value) -> serde_json::Value + Send + 'static,
1479    ) {
1480        tokio::spawn(async move {
1481            while let Some(req) = rx.recv().await {
1482                let response = responder(&req.method, req.params);
1483                let _ = req.response_tx.send(Ok(response));
1484            }
1485        });
1486    }
1487
1488    #[tokio::test]
1489    async fn test_get_task_info_round_trips() {
1490        let (tx, rx) = outgoing_request_channel(10);
1491        spawn_mock_client(rx, |method, params| {
1492            assert_eq!(method, "tasks/get");
1493            let task_id = params["taskId"].as_str().unwrap().to_string();
1494            make_task_object(&task_id, TaskStatus::Working)
1495        });
1496        let requester: ClientRequesterHandle = Arc::new(ChannelClientRequester::new(tx));
1497        let ctx = RequestContext::new(RequestId::Number(1)).with_client_requester(requester);
1498
1499        let info = ctx.get_task_info("task-123").await.unwrap();
1500        assert_eq!(info.task_id, "task-123");
1501        assert!(matches!(info.status, TaskStatus::Working));
1502    }
1503
1504    #[tokio::test]
1505    #[allow(deprecated)] // exercises the legacy SEP-1686 helper
1506    async fn test_list_tasks_round_trips() {
1507        let (tx, rx) = outgoing_request_channel(10);
1508        spawn_mock_client(rx, |method, params| {
1509            assert_eq!(method, "tasks/list");
1510            // Status filter should be forwarded
1511            assert_eq!(params["status"], serde_json::json!("working"));
1512            serde_json::json!({
1513                "tasks": [
1514                    make_task_object("task-1", TaskStatus::Working),
1515                    make_task_object("task-2", TaskStatus::Working),
1516                ]
1517            })
1518        });
1519        let requester: ClientRequesterHandle = Arc::new(ChannelClientRequester::new(tx));
1520        let ctx = RequestContext::new(RequestId::Number(1)).with_client_requester(requester);
1521
1522        let result = ctx.list_tasks(Some(TaskStatus::Working)).await.unwrap();
1523        assert_eq!(result.tasks.len(), 2);
1524        assert_eq!(result.tasks[0].task_id, "task-1");
1525    }
1526
1527    #[tokio::test]
1528    async fn test_cancel_task_forwards_reason() {
1529        let (tx, rx) = outgoing_request_channel(10);
1530        spawn_mock_client(rx, |method, params| {
1531            assert_eq!(method, "tasks/cancel");
1532            assert_eq!(params["reason"], serde_json::json!("user requested"));
1533            // SEP-2663 (final): the cancel acknowledgment is an empty result.
1534            serde_json::json!({})
1535        });
1536        let requester: ClientRequesterHandle = Arc::new(ChannelClientRequester::new(tx));
1537        let ctx = RequestContext::new(RequestId::Number(1)).with_client_requester(requester);
1538
1539        ctx.cancel_task("task-99", Some("user requested".into()))
1540            .await
1541            .expect("empty ack should succeed");
1542    }
1543
1544    #[tokio::test]
1545    async fn test_cancel_task_tolerates_legacy_task_object_ack() {
1546        // Legacy SEP-1686 peers return the task object from tasks/cancel;
1547        // the helper discards the body either way.
1548        let (tx, rx) = outgoing_request_channel(10);
1549        spawn_mock_client(rx, |method, _params| {
1550            assert_eq!(method, "tasks/cancel");
1551            make_task_object("task-99", TaskStatus::Cancelled)
1552        });
1553        let requester: ClientRequesterHandle = Arc::new(ChannelClientRequester::new(tx));
1554        let ctx = RequestContext::new(RequestId::Number(1)).with_client_requester(requester);
1555
1556        ctx.cancel_task("task-99", None)
1557            .await
1558            .expect("legacy task-object ack should also succeed");
1559    }
1560
1561    #[tokio::test]
1562    async fn test_get_task_info_without_requester_fails() {
1563        let ctx = RequestContext::new(RequestId::Number(1));
1564        let result = ctx.get_task_info("task-1").await;
1565        assert!(result.is_err());
1566        assert!(
1567            result
1568                .unwrap_err()
1569                .to_string()
1570                .contains("Client request not available")
1571        );
1572    }
1573
1574    #[tokio::test]
1575    async fn test_default_request_impl_errors() {
1576        // A custom requester that only implements sample/elicit (not request)
1577        // should reject task helpers.
1578        struct OnlySampleAndElicit;
1579
1580        #[async_trait]
1581        impl ClientRequester for OnlySampleAndElicit {
1582            async fn sample(&self, _: CreateMessageParams) -> Result<CreateMessageResult> {
1583                unreachable!()
1584            }
1585            async fn elicit(&self, _: ElicitRequestParams) -> Result<ElicitResult> {
1586                unreachable!()
1587            }
1588        }
1589
1590        let requester: ClientRequesterHandle = Arc::new(OnlySampleAndElicit);
1591        let ctx = RequestContext::new(RequestId::Number(1)).with_client_requester(requester);
1592
1593        let err = ctx.get_task_info("x").await.unwrap_err();
1594        assert!(err.to_string().contains("does not support arbitrary"));
1595    }
1596}