Skip to main content

mcpkit_server/
context.rs

1//! Request context for MCP handlers.
2//!
3//! The context provides access to the current request state and allows
4//! handlers to interact with the connection (sending notifications,
5//! progress updates, etc.).
6//!
7//! # Key Features
8//!
9//! - **Borrowing-friendly**: Uses lifetime references, NO `'static` requirement
10//! - **Progress reporting**: Send progress updates for long-running operations
11//! - **Cancellation**: Check if the request has been cancelled
12//! - **Notifications**: Send notifications back to the client via Peer trait
13//!
14//! # Example
15//!
16//! ```rust
17//! use mcpkit_server::{Context, NoOpPeer, ContextData};
18//! use mcpkit_core::capability::{ClientCapabilities, ServerCapabilities};
19//! use mcpkit_core::protocol::RequestId;
20//! use mcpkit_core::protocol_version::ProtocolVersion;
21//!
22//! // Create test context data
23//! let data = ContextData::new(
24//!     RequestId::Number(1),
25//!     ClientCapabilities::default(),
26//!     ServerCapabilities::default(),
27//!     ProtocolVersion::LATEST,
28//! );
29//! let peer = NoOpPeer;
30//!
31//! // Create a context from the data
32//! let ctx = Context::new(
33//!     &data.request_id,
34//!     data.progress_token.as_ref(),
35//!     &data.client_caps,
36//!     &data.server_caps,
37//!     data.protocol_version,
38//!     &peer,
39//! );
40//!
41//! // Check for cancellation and protocol version
42//! assert!(!ctx.is_cancelled());
43//! assert!(ctx.protocol_version.supports_tasks());
44//! ```
45
46use mcpkit_core::capability::{ClientCapabilities, ServerCapabilities};
47use mcpkit_core::error::McpError;
48use mcpkit_core::protocol::{Notification, ProgressToken, RequestId};
49use mcpkit_core::protocol_version::ProtocolVersion;
50use std::future::Future;
51use std::pin::Pin;
52use std::sync::Arc;
53use std::sync::atomic::{AtomicBool, Ordering};
54use std::task::{Context as TaskContext, Poll};
55
56use event_listener::Event;
57
58/// Trait for sending messages to the peer (client or server).
59///
60/// This trait abstracts over the transport layer, allowing the context
61/// to send notifications without knowing the underlying transport.
62pub trait Peer: Send + Sync {
63    /// Send a notification to the peer.
64    fn notify(
65        &self,
66        notification: Notification,
67    ) -> Pin<Box<dyn Future<Output = Result<(), McpError>> + Send + '_>>;
68}
69
70/// A cancellation token for tracking request cancellation.
71///
72/// Wraps an atomic flag plus an [`event_listener::Event`] so waiters can park
73/// until cancellation instead of busy-polling the flag.
74#[derive(Clone)]
75pub struct CancellationToken {
76    cancelled: Arc<AtomicBool>,
77    event: Arc<Event>,
78}
79
80impl CancellationToken {
81    /// Create a new cancellation token.
82    #[must_use]
83    pub fn new() -> Self {
84        Self {
85            cancelled: Arc::new(AtomicBool::new(false)),
86            event: Arc::new(Event::new()),
87        }
88    }
89
90    /// Check if cancellation has been requested.
91    #[must_use]
92    pub fn is_cancelled(&self) -> bool {
93        self.cancelled.load(Ordering::SeqCst)
94    }
95
96    /// Request cancellation.
97    pub fn cancel(&self) {
98        self.cancelled.store(true, Ordering::SeqCst);
99        // Wake every task currently waiting in `cancelled()`.
100        self.event.notify(usize::MAX);
101    }
102
103    /// Wait for cancellation.
104    ///
105    /// Returns a future that completes when cancellation is requested. The
106    /// future parks on an [`event_listener::Event`] and is woken by
107    /// [`cancel`](Self::cancel); it does not busy-poll.
108    #[must_use]
109    pub fn cancelled(&self) -> CancelledFuture {
110        CancelledFuture::new(self.cancelled.clone(), self.event.clone())
111    }
112}
113
114impl std::fmt::Debug for CancellationToken {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        f.debug_struct("CancellationToken")
117            .field("cancelled", &self.is_cancelled())
118            .finish()
119    }
120}
121
122impl Default for CancellationToken {
123    fn default() -> Self {
124        Self::new()
125    }
126}
127
128/// A future that completes when cancellation is requested.
129///
130/// Parks on the token's [`event_listener::Event`] until cancellation, rather
131/// than waking itself on every poll.
132pub struct CancelledFuture {
133    inner: Pin<Box<dyn Future<Output = ()> + Send>>,
134}
135
136impl CancelledFuture {
137    fn new(cancelled: Arc<AtomicBool>, event: Arc<Event>) -> Self {
138        Self {
139            inner: Box::pin(async move {
140                loop {
141                    if cancelled.load(Ordering::SeqCst) {
142                        return;
143                    }
144                    // Register a listener *before* the final flag check so a
145                    // `cancel()` that races with us cannot be missed: if it set
146                    // the flag after our first check, the re-check below catches
147                    // it; if it fires after, the listener is woken.
148                    let listener = event.listen();
149                    if cancelled.load(Ordering::SeqCst) {
150                        return;
151                    }
152                    listener.await;
153                }
154            }),
155        }
156    }
157}
158
159impl Future for CancelledFuture {
160    type Output = ();
161
162    fn poll(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
163        self.inner.as_mut().poll(cx)
164    }
165}
166
167/// Request context passed to handler methods.
168///
169/// The context uses lifetime references to avoid `'static` requirements
170/// and Arc overhead. This enables:
171/// - Single-threaded async without Arc overhead
172/// - `!Send` types in handlers (important for some runtimes)
173/// - Users who need spawning can wrap in Arc themselves
174///
175/// Per the plan: "Request context - passed by reference, NO 'static requirement"
176pub struct Context<'a> {
177    /// The request ID for this operation.
178    pub request_id: &'a RequestId,
179    /// Optional progress token for reporting progress.
180    pub progress_token: Option<&'a ProgressToken>,
181    /// Client capabilities negotiated during initialization.
182    pub client_caps: &'a ClientCapabilities,
183    /// Server capabilities advertised during initialization.
184    pub server_caps: &'a ServerCapabilities,
185    /// The negotiated protocol version.
186    ///
187    /// Use this to check version-specific feature availability via
188    /// methods like `supports_tasks()`, `supports_elicitation()`, etc.
189    pub protocol_version: ProtocolVersion,
190    /// Peer for sending notifications.
191    peer: &'a dyn Peer,
192    /// Cancellation token for this request.
193    cancel: CancellationToken,
194}
195
196impl<'a> Context<'a> {
197    /// Create a new context with all required references.
198    #[must_use]
199    pub fn new(
200        request_id: &'a RequestId,
201        progress_token: Option<&'a ProgressToken>,
202        client_caps: &'a ClientCapabilities,
203        server_caps: &'a ServerCapabilities,
204        protocol_version: ProtocolVersion,
205        peer: &'a dyn Peer,
206    ) -> Self {
207        Self {
208            request_id,
209            progress_token,
210            client_caps,
211            server_caps,
212            protocol_version,
213            peer,
214            cancel: CancellationToken::new(),
215        }
216    }
217
218    /// Create a new context with a custom cancellation token.
219    #[must_use]
220    pub fn with_cancellation(
221        request_id: &'a RequestId,
222        progress_token: Option<&'a ProgressToken>,
223        client_caps: &'a ClientCapabilities,
224        server_caps: &'a ServerCapabilities,
225        protocol_version: ProtocolVersion,
226        peer: &'a dyn Peer,
227        cancel: CancellationToken,
228    ) -> Self {
229        Self {
230            request_id,
231            progress_token,
232            client_caps,
233            server_caps,
234            protocol_version,
235            peer,
236            cancel,
237        }
238    }
239
240    /// Check if the request has been cancelled.
241    #[must_use]
242    pub fn is_cancelled(&self) -> bool {
243        self.cancel.is_cancelled()
244    }
245
246    /// Get a future that completes when the request is cancelled.
247    pub fn cancelled(&self) -> impl Future<Output = ()> + '_ {
248        self.cancel.cancelled()
249    }
250
251    /// Get the cancellation token for this context.
252    #[must_use]
253    pub const fn cancellation_token(&self) -> &CancellationToken {
254        &self.cancel
255    }
256
257    /// Send a notification to the client.
258    ///
259    /// # Arguments
260    ///
261    /// * `method` - The notification method name
262    /// * `params` - Optional notification parameters
263    ///
264    /// # Errors
265    ///
266    /// Returns an error if the notification could not be sent.
267    pub async fn notify(
268        &self,
269        method: &str,
270        params: Option<serde_json::Value>,
271    ) -> Result<(), McpError> {
272        let notification = if let Some(p) = params {
273            Notification::with_params(method.to_string(), p)
274        } else {
275            Notification::new(method.to_string())
276        };
277        self.peer.notify(notification).await
278    }
279
280    /// Report progress for this operation.
281    ///
282    /// This sends a progress notification to the client if a progress token
283    /// was provided with the request.
284    ///
285    /// # Arguments
286    ///
287    /// * `current` - Current progress value
288    /// * `total` - Total progress value (if known)
289    /// * `message` - Optional progress message
290    ///
291    /// # Errors
292    ///
293    /// Returns an error if the notification could not be sent.
294    pub async fn progress(
295        &self,
296        current: u64,
297        total: Option<u64>,
298        message: Option<&str>,
299    ) -> Result<(), McpError> {
300        let Some(token) = self.progress_token else {
301            // No progress token, silently succeed
302            return Ok(());
303        };
304
305        let params = serde_json::json!({
306            "progressToken": token,
307            "progress": current,
308            "total": total,
309            "message": message,
310        });
311
312        self.notify("notifications/progress", Some(params)).await
313    }
314}
315
316impl std::fmt::Debug for Context<'_> {
317    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318        f.debug_struct("Context")
319            .field("request_id", &self.request_id)
320            .field("progress_token", &self.progress_token)
321            .field("client_caps", &self.client_caps)
322            .field("server_caps", &self.server_caps)
323            .field("protocol_version", &self.protocol_version)
324            .field("is_cancelled", &self.is_cancelled())
325            .finish()
326    }
327}
328
329/// A no-op peer implementation for testing.
330///
331/// This peer silently accepts all notifications without sending them anywhere.
332#[derive(Debug, Clone, Copy)]
333pub struct NoOpPeer;
334
335impl Peer for NoOpPeer {
336    fn notify(
337        &self,
338        _notification: Notification,
339    ) -> Pin<Box<dyn Future<Output = Result<(), McpError>> + Send + '_>> {
340        Box::pin(async { Ok(()) })
341    }
342}
343
344/// Owned data for creating contexts.
345///
346/// This struct holds owned copies of all the data needed to create a Context.
347/// It's useful when you need to create contexts from owned data.
348pub struct ContextData {
349    /// The request ID.
350    pub request_id: RequestId,
351    /// Optional progress token.
352    pub progress_token: Option<ProgressToken>,
353    /// Client capabilities.
354    pub client_caps: ClientCapabilities,
355    /// Server capabilities.
356    pub server_caps: ServerCapabilities,
357    /// The negotiated protocol version.
358    pub protocol_version: ProtocolVersion,
359}
360
361impl ContextData {
362    /// Create a new context data struct.
363    #[must_use]
364    pub const fn new(
365        request_id: RequestId,
366        client_caps: ClientCapabilities,
367        server_caps: ServerCapabilities,
368        protocol_version: ProtocolVersion,
369    ) -> Self {
370        Self {
371            request_id,
372            progress_token: None,
373            client_caps,
374            server_caps,
375            protocol_version,
376        }
377    }
378
379    /// Set the progress token.
380    #[must_use]
381    pub fn with_progress_token(mut self, token: ProgressToken) -> Self {
382        self.progress_token = Some(token);
383        self
384    }
385
386    /// Create a context from this data with the given peer.
387    #[must_use]
388    pub fn to_context<'a>(&'a self, peer: &'a dyn Peer) -> Context<'a> {
389        Context::new(
390            &self.request_id,
391            self.progress_token.as_ref(),
392            &self.client_caps,
393            &self.server_caps,
394            self.protocol_version,
395            peer,
396        )
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403
404    #[test]
405    fn test_cancellation_token() {
406        let token = CancellationToken::new();
407        assert!(!token.is_cancelled());
408        token.cancel();
409        assert!(token.is_cancelled());
410    }
411
412    /// Regression test for #8: `cancelled()` must park on a waker instead of
413    /// busy-spinning (the old impl called `wake_by_ref()` on every poll). We
414    /// poll with a waker that counts wake-ups and assert the future does not
415    /// wake itself, then that `cancel()` wakes it and it resolves.
416    #[test]
417    fn cancelled_future_parks_and_wakes_on_cancel() {
418        use std::sync::atomic::AtomicUsize;
419        use std::task::{Wake, Waker};
420
421        struct CountingWaker(AtomicUsize);
422        impl Wake for CountingWaker {
423            fn wake(self: Arc<Self>) {
424                self.0.fetch_add(1, Ordering::SeqCst);
425            }
426            fn wake_by_ref(self: &Arc<Self>) {
427                self.0.fetch_add(1, Ordering::SeqCst);
428            }
429        }
430
431        let counter = Arc::new(CountingWaker(AtomicUsize::new(0)));
432        let waker = Waker::from(counter.clone());
433        let mut cx = TaskContext::from_waker(&waker);
434
435        let token = CancellationToken::new();
436        let mut fut = Box::pin(token.cancelled());
437
438        // First poll: not cancelled -> must be Pending and must NOT have woken
439        // itself (a busy-spin would wake immediately).
440        assert_eq!(fut.as_mut().poll(&mut cx), Poll::Pending);
441        assert_eq!(
442            counter.0.load(Ordering::SeqCst),
443            0,
444            "cancelled future must park, not busy-spin (no self-wake)"
445        );
446
447        // Cancelling wakes the registered waker and the future resolves.
448        token.cancel();
449        assert!(
450            counter.0.load(Ordering::SeqCst) >= 1,
451            "cancel() must wake the parked waiter"
452        );
453        assert_eq!(fut.as_mut().poll(&mut cx), Poll::Ready(()));
454    }
455
456    /// A token already cancelled before `cancelled()` is awaited resolves
457    /// immediately.
458    #[test]
459    fn cancelled_future_ready_when_already_cancelled() {
460        let waker = std::task::Waker::noop();
461        let mut cx = TaskContext::from_waker(waker);
462
463        let token = CancellationToken::new();
464        token.cancel();
465        let mut fut = Box::pin(token.cancelled());
466        assert_eq!(fut.as_mut().poll(&mut cx), Poll::Ready(()));
467    }
468
469    #[test]
470    fn test_context_creation() {
471        let request_id = RequestId::Number(1);
472        let client_caps = ClientCapabilities::default();
473        let server_caps = ServerCapabilities::default();
474        let peer = NoOpPeer;
475
476        let ctx = Context::new(
477            &request_id,
478            None,
479            &client_caps,
480            &server_caps,
481            ProtocolVersion::LATEST,
482            &peer,
483        );
484
485        assert!(!ctx.is_cancelled());
486        assert!(ctx.progress_token.is_none());
487        assert_eq!(ctx.protocol_version, ProtocolVersion::LATEST);
488    }
489
490    #[test]
491    fn test_context_with_progress_token() {
492        let request_id = RequestId::Number(1);
493        let progress_token = ProgressToken::String("token".to_string());
494        let client_caps = ClientCapabilities::default();
495        let server_caps = ServerCapabilities::default();
496        let peer = NoOpPeer;
497
498        let ctx = Context::new(
499            &request_id,
500            Some(&progress_token),
501            &client_caps,
502            &server_caps,
503            ProtocolVersion::V2025_03_26,
504            &peer,
505        );
506
507        assert!(ctx.progress_token.is_some());
508        assert_eq!(ctx.protocol_version, ProtocolVersion::V2025_03_26);
509    }
510
511    #[test]
512    fn test_context_data() {
513        let data = ContextData::new(
514            RequestId::Number(42),
515            ClientCapabilities::default(),
516            ServerCapabilities::default(),
517            ProtocolVersion::V2025_06_18,
518        )
519        .with_progress_token(ProgressToken::String("test".to_string()));
520
521        let peer = NoOpPeer;
522        let ctx = data.to_context(&peer);
523
524        assert!(ctx.progress_token.is_some());
525        assert_eq!(ctx.protocol_version, ProtocolVersion::V2025_06_18);
526        // Test feature detection via protocol version
527        assert!(ctx.protocol_version.supports_elicitation());
528        assert!(!ctx.protocol_version.supports_tasks()); // Tasks require 2025-11-25
529    }
530}