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, Response};
49use mcpkit_core::protocol_version::ProtocolVersion;
50use mcpkit_core::types::elicitation::{ElicitRequest, ElicitResult, UrlElicitRequest};
51use mcpkit_core::types::logging::{LoggingLevel, LoggingMessageNotificationParams};
52use mcpkit_core::types::notifications::ProgressNotificationParams;
53use mcpkit_core::types::roots::{ListRootsResult, Root};
54use mcpkit_core::types::sampling::{CreateMessageRequest, CreateMessageResult};
55use mcpkit_core::types::task::TaskId;
56use std::borrow::Cow;
57use std::future::Future;
58use std::pin::Pin;
59
60/// Trait for sending messages to the peer (client or server).
61///
62/// This trait abstracts over the transport layer, allowing the context
63/// to send notifications without knowing the underlying transport.
64pub trait Peer: Send + Sync {
65    /// Send a notification to the peer.
66    fn notify(
67        &self,
68        notification: Notification,
69    ) -> Pin<Box<dyn Future<Output = Result<(), McpError>> + Send + '_>>;
70
71    /// Send a request to the peer and await its response.
72    ///
73    /// Used for server-initiated requests such as elicitation and sampling. The
74    /// implementation assigns the request id and correlates the response.
75    ///
76    /// The default implementation returns an error: a peer that has no
77    /// persistent bidirectional connection (for example a one-shot HTTP
78    /// response) cannot make server-initiated requests.
79    fn request(
80        &self,
81        method: Cow<'static, str>,
82        params: Option<serde_json::Value>,
83    ) -> Pin<Box<dyn Future<Output = Result<Response, McpError>> + Send + '_>> {
84        let _ = (method, params);
85        Box::pin(async {
86            Err(McpError::internal(
87                "this peer does not support server-initiated requests",
88            ))
89        })
90    }
91}
92
93// The cancellation token is shared with the client-side task machinery and
94// lives in `mcpkit_core::tasks`; re-exported here for path stability.
95pub use mcpkit_core::tasks::{CancellationToken, CancelledFuture};
96
97/// Request context passed to handler methods.
98///
99/// The context uses lifetime references to avoid `'static` requirements
100/// and Arc overhead. This enables:
101/// - Single-threaded async without Arc overhead
102/// - `!Send` types in handlers (important for some runtimes)
103/// - Users who need spawning can wrap in Arc themselves
104///
105/// Per the plan: "Request context - passed by reference, NO 'static requirement"
106pub struct Context<'a> {
107    /// The request ID for this operation.
108    pub request_id: &'a RequestId,
109    /// Optional progress token for reporting progress.
110    pub progress_token: Option<&'a ProgressToken>,
111    /// Client capabilities negotiated during initialization.
112    pub client_caps: &'a ClientCapabilities,
113    /// Server capabilities advertised during initialization.
114    pub server_caps: &'a ServerCapabilities,
115    /// The negotiated protocol version.
116    ///
117    /// Use this to check version-specific feature availability via
118    /// methods like `supports_tasks()`, `supports_elicitation()`, etc.
119    pub protocol_version: ProtocolVersion,
120    /// Peer for sending notifications.
121    peer: &'a dyn Peer,
122    /// Cancellation token for this request.
123    cancel: CancellationToken,
124    /// The task this request is executing as part of, if any.
125    ///
126    /// Set by [`run_augmented_tool`](crate::router::run_augmented_tool) for a
127    /// task-augmented tool call. When present, every outbound request this
128    /// context makes carries `io.modelcontextprotocol/related-task` in its
129    /// `_meta`, which the spec requires of task-related messages.
130    related_task: Option<TaskId>,
131}
132
133/// Sentinel [`RequestId`] for notification-scoped contexts (see
134/// [`Context::for_notification`]). Notifications have no request id; this is not
135/// a real JSON-RPC id.
136static NOTIFICATION_REQUEST_ID: std::sync::LazyLock<RequestId> =
137    std::sync::LazyLock::new(|| RequestId::String("__notification__".to_string()));
138
139impl<'a> Context<'a> {
140    /// Create a new context with all required references.
141    #[must_use]
142    pub fn new(
143        request_id: &'a RequestId,
144        progress_token: Option<&'a ProgressToken>,
145        client_caps: &'a ClientCapabilities,
146        server_caps: &'a ServerCapabilities,
147        protocol_version: ProtocolVersion,
148        peer: &'a dyn Peer,
149    ) -> Self {
150        Self {
151            request_id,
152            progress_token,
153            client_caps,
154            server_caps,
155            protocol_version,
156            peer,
157            cancel: CancellationToken::new(),
158            related_task: None,
159        }
160    }
161
162    /// Create a new context with a custom cancellation token.
163    #[must_use]
164    pub fn with_cancellation(
165        request_id: &'a RequestId,
166        progress_token: Option<&'a ProgressToken>,
167        client_caps: &'a ClientCapabilities,
168        server_caps: &'a ServerCapabilities,
169        protocol_version: ProtocolVersion,
170        peer: &'a dyn Peer,
171        cancel: CancellationToken,
172    ) -> Self {
173        Self {
174            request_id,
175            progress_token,
176            client_caps,
177            server_caps,
178            protocol_version,
179            peer,
180            cancel,
181            related_task: None,
182        }
183    }
184
185    /// Create a context for handling an inbound client notification.
186    ///
187    /// Notifications carry no JSON-RPC request id, so
188    /// [`request_id`](Self::request_id) is a documented sentinel
189    /// (`__notification__`) that must **not** be treated as a real id. The
190    /// context is still outbound-capable: a hook may call
191    /// [`list_roots`](Self::list_roots) or send notifications, and those
192    /// server-to-client requests allocate their own ids via the peer.
193    #[must_use]
194    pub fn for_notification(
195        client_caps: &'a ClientCapabilities,
196        server_caps: &'a ServerCapabilities,
197        protocol_version: ProtocolVersion,
198        peer: &'a dyn Peer,
199    ) -> Self {
200        Self {
201            request_id: &NOTIFICATION_REQUEST_ID,
202            progress_token: None,
203            client_caps,
204            server_caps,
205            protocol_version,
206            peer,
207            cancel: CancellationToken::new(),
208            related_task: None,
209        }
210    }
211
212    /// Associate this context with a task.
213    ///
214    /// Every outbound request made through it then carries
215    /// `_meta["io.modelcontextprotocol/related-task"]` with `task_id`, which
216    /// the spec requires of messages related to a task:
217    ///
218    /// > All requests, notifications, and responses related to a task **MUST**
219    /// > include the `io.modelcontextprotocol/related-task` key in their
220    /// > `_meta` field […] an elicitation that a task-augmented tool call
221    /// > depends on **MUST** share the same related task ID with that tool
222    /// > call's task.
223    ///
224    /// Applied automatically by the task-augmented tool path; call this only
225    /// when driving a task yourself.
226    #[must_use]
227    pub fn with_related_task(mut self, task_id: TaskId) -> Self {
228        self.related_task = Some(task_id);
229        self
230    }
231
232    /// The task this request is part of, if any.
233    #[must_use]
234    pub const fn related_task(&self) -> Option<&TaskId> {
235        self.related_task.as_ref()
236    }
237
238    /// Stamp the related-task `_meta` onto outbound request params, if this
239    /// context belongs to a task.
240    fn tag_related_task(&self, params: serde_json::Value) -> serde_json::Value {
241        match self.related_task.as_ref() {
242            Some(id) => mcpkit_core::tasks::inject_related_task(params, id),
243            None => params,
244        }
245    }
246
247    /// Check if the request has been cancelled.
248    #[must_use]
249    pub fn is_cancelled(&self) -> bool {
250        self.cancel.is_cancelled()
251    }
252
253    /// Get a future that completes when the request is cancelled.
254    pub fn cancelled(&self) -> impl Future<Output = ()> + '_ {
255        self.cancel.cancelled()
256    }
257
258    /// Get the cancellation token for this context.
259    #[must_use]
260    pub const fn cancellation_token(&self) -> &CancellationToken {
261        &self.cancel
262    }
263
264    /// Send a notification to the client.
265    ///
266    /// # Arguments
267    ///
268    /// * `method` - The notification method name
269    /// * `params` - Optional notification parameters
270    ///
271    /// # Errors
272    ///
273    /// Returns an error if the notification could not be sent.
274    pub async fn notify(
275        &self,
276        method: &str,
277        params: Option<serde_json::Value>,
278    ) -> Result<(), McpError> {
279        let notification = if let Some(p) = params {
280            Notification::with_params(method.to_string(), p)
281        } else {
282            Notification::new(method.to_string())
283        };
284        self.peer.notify(notification).await
285    }
286
287    /// Report progress for this operation.
288    ///
289    /// This sends a progress notification to the client if a progress token
290    /// was provided with the request.
291    ///
292    /// # Arguments
293    ///
294    /// * `current` - Current progress value
295    /// * `total` - Total progress value (if known)
296    /// * `message` - Optional progress message
297    ///
298    /// # Errors
299    ///
300    /// Returns an error if the notification could not be sent.
301    pub async fn progress(
302        &self,
303        current: f64,
304        total: Option<f64>,
305        message: Option<&str>,
306    ) -> Result<(), McpError> {
307        let Some(token) = self.progress_token else {
308            // No progress token, silently succeed
309            return Ok(());
310        };
311
312        let params = ProgressNotificationParams {
313            total,
314            message: message.map(String::from),
315            ..ProgressNotificationParams::new(token.clone(), current)
316        };
317
318        self.notify(
319            mcpkit_core::methods::notifications::PROGRESS,
320            Some(serde_json::to_value(params)?),
321        )
322        .await
323    }
324
325    /// Emit a `notifications/message` log to the client at `level`, optionally
326    /// tagged with a `logger` name and carrying arbitrary JSON `data`.
327    ///
328    /// # Errors
329    ///
330    /// Returns an error if the notification could not be sent.
331    pub async fn log(
332        &self,
333        level: LoggingLevel,
334        logger: Option<&str>,
335        data: serde_json::Value,
336    ) -> Result<(), McpError> {
337        let params = LoggingMessageNotificationParams {
338            logger: logger.map(String::from),
339            ..LoggingMessageNotificationParams::new(level, data)
340        };
341        self.notify(
342            mcpkit_core::methods::notifications::MESSAGE,
343            Some(serde_json::to_value(params)?),
344        )
345        .await
346    }
347
348    /// Send a request to the client and await its response.
349    ///
350    /// This is the basis for server-initiated requests (e.g. elicitation,
351    /// sampling). The peer assigns the request id and correlates the response;
352    /// the request is aborted if this context is cancelled.
353    ///
354    /// # Errors
355    ///
356    /// Returns an error if the request was cancelled, the peer does not support
357    /// requests, the request timed out, or the response carried a JSON-RPC
358    /// error.
359    pub async fn request(
360        &self,
361        method: impl Into<Cow<'static, str>>,
362        params: Option<serde_json::Value>,
363    ) -> Result<serde_json::Value, McpError> {
364        use futures::future::{Either, select};
365
366        let request = self.peer.request(method.into(), params);
367        let cancelled = self.cancel.cancelled();
368        let response = match select(request, cancelled).await {
369            Either::Left((result, _)) => result?,
370            Either::Right(((), _)) => return Err(McpError::internal("request cancelled")),
371        };
372
373        if let Some(error) = response.error {
374            return Err(McpError::internal(error.message));
375        }
376        response
377            .result
378            .ok_or_else(|| McpError::internal("response contained neither result nor error"))
379    }
380
381    /// Request structured input from the user through the client (form-mode
382    /// elicitation).
383    ///
384    /// Sends an `elicitation/create` request and awaits the user's response
385    /// (accept with content, decline, or cancel). This requires the client to
386    /// have declared the `elicitation` capability and the negotiated protocol
387    /// version to support elicitation.
388    ///
389    /// # Errors
390    ///
391    /// Returns an error if the client did not declare elicitation support, the
392    /// negotiated protocol version predates elicitation, the request was
393    /// cancelled or timed out, or the response could not be parsed.
394    pub async fn elicit(&self, request: ElicitRequest) -> Result<ElicitResult, McpError> {
395        if !self.protocol_version.supports_elicitation() {
396            return Err(McpError::internal(
397                "the negotiated protocol version does not support elicitation",
398            ));
399        }
400        if !self.client_caps.has_elicitation() {
401            return Err(McpError::internal(
402                "the client did not declare the elicitation capability",
403            ));
404        }
405
406        let params = self.tag_related_task(serde_json::to_value(&request).map_err(McpError::from)?);
407        let result = self.request("elicitation/create", Some(params)).await?;
408        serde_json::from_value(result).map_err(McpError::from)
409    }
410
411    /// Request the roots this client exposes (`roots/list`).
412    ///
413    /// Requires the client to have declared the `roots` capability.
414    ///
415    /// # Errors
416    ///
417    /// Returns an error if the client did not declare roots support, or the
418    /// request fails, times out, or the response could not be parsed.
419    pub async fn list_roots(&self) -> Result<Vec<Root>, McpError> {
420        if !self.client_caps.has_roots() {
421            return Err(McpError::internal(
422                "the client did not declare the roots capability",
423            ));
424        }
425        let result = self.request("roots/list", None).await?;
426        let result: ListRootsResult = serde_json::from_value(result).map_err(McpError::from)?;
427        Ok(result.roots)
428    }
429
430    /// Request a URL-mode elicitation: ask the client to have the user navigate
431    /// to a URL for an out-of-band interaction (e.g. authorization or payment).
432    ///
433    /// Returns the client's [`ElicitResult`] action once the user consents to
434    /// open the URL. When the out-of-band interaction later finishes, notify the
435    /// client with `ServerNotifier::elicitation_complete(elicitation_id)`.
436    ///
437    /// Gated on the client's `elicitation.url` sub-capability (which is only
438    /// declared on 2025-11-25+).
439    ///
440    /// # Security
441    ///
442    /// Per the MCP spec, the caller MUST use an unguessable `elicitation_id`
443    /// bound to a verified user identity and MUST NOT place credentials in the
444    /// URL. mcpkit provides the mechanism; associating the id with a user is the
445    /// application's responsibility (see the session-binding helpers, #86).
446    ///
447    /// # Errors
448    ///
449    /// Returns an error if the negotiated protocol version does not support
450    /// elicitation, the client did not declare URL-mode elicitation, or the
451    /// request fails.
452    pub async fn elicit_url(&self, request: UrlElicitRequest) -> Result<ElicitResult, McpError> {
453        if !self.protocol_version.supports_elicitation() {
454            return Err(McpError::internal(
455                "the negotiated protocol version does not support elicitation",
456            ));
457        }
458        if !self.client_caps.has_url_elicitation() {
459            return Err(McpError::internal(
460                "the client did not declare URL-mode elicitation support",
461            ));
462        }
463
464        let params = self.tag_related_task(serde_json::to_value(&request).map_err(McpError::from)?);
465        let result = self.request("elicitation/create", Some(params)).await?;
466        serde_json::from_value(result).map_err(McpError::from)
467    }
468
469    /// Request the client to run an LLM completion (sampling).
470    ///
471    /// Sends a `sampling/createMessage` request and awaits the generated
472    /// message. This requires the client to have declared the `sampling`
473    /// capability (sampling is available in every protocol version).
474    ///
475    /// # Errors
476    ///
477    /// Returns an error if the client did not declare sampling support, the
478    /// request was cancelled or timed out, or the response could not be parsed.
479    pub async fn create_message(
480        &self,
481        request: CreateMessageRequest,
482    ) -> Result<CreateMessageResult, McpError> {
483        if !self.client_caps.has_sampling() {
484            return Err(McpError::internal(
485                "the client did not declare the sampling capability",
486            ));
487        }
488
489        let params = self.tag_related_task(serde_json::to_value(&request).map_err(McpError::from)?);
490        let result = self.request("sampling/createMessage", Some(params)).await?;
491        serde_json::from_value(result).map_err(McpError::from)
492    }
493}
494
495impl std::fmt::Debug for Context<'_> {
496    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
497        f.debug_struct("Context")
498            .field("request_id", &self.request_id)
499            .field("progress_token", &self.progress_token)
500            .field("client_caps", &self.client_caps)
501            .field("server_caps", &self.server_caps)
502            .field("protocol_version", &self.protocol_version)
503            .field("is_cancelled", &self.is_cancelled())
504            .finish()
505    }
506}
507
508/// A no-op peer implementation for testing.
509///
510/// This peer silently accepts all notifications without sending them anywhere.
511#[derive(Debug, Clone, Copy)]
512pub struct NoOpPeer;
513
514impl Peer for NoOpPeer {
515    fn notify(
516        &self,
517        _notification: Notification,
518    ) -> Pin<Box<dyn Future<Output = Result<(), McpError>> + Send + '_>> {
519        Box::pin(async { Ok(()) })
520    }
521}
522
523/// Owned data for creating contexts.
524///
525/// This struct holds owned copies of all the data needed to create a Context.
526/// It's useful when you need to create contexts from owned data.
527pub struct ContextData {
528    /// The request ID.
529    pub request_id: RequestId,
530    /// Optional progress token.
531    pub progress_token: Option<ProgressToken>,
532    /// Client capabilities.
533    pub client_caps: ClientCapabilities,
534    /// Server capabilities.
535    pub server_caps: ServerCapabilities,
536    /// The negotiated protocol version.
537    pub protocol_version: ProtocolVersion,
538}
539
540impl ContextData {
541    /// Create a new context data struct.
542    #[must_use]
543    pub const fn new(
544        request_id: RequestId,
545        client_caps: ClientCapabilities,
546        server_caps: ServerCapabilities,
547        protocol_version: ProtocolVersion,
548    ) -> Self {
549        Self {
550            request_id,
551            progress_token: None,
552            client_caps,
553            server_caps,
554            protocol_version,
555        }
556    }
557
558    /// Set the progress token.
559    #[must_use]
560    pub fn with_progress_token(mut self, token: ProgressToken) -> Self {
561        self.progress_token = Some(token);
562        self
563    }
564
565    /// Create a context from this data with the given peer.
566    #[must_use]
567    pub fn to_context<'a>(&'a self, peer: &'a dyn Peer) -> Context<'a> {
568        Context::new(
569            &self.request_id,
570            self.progress_token.as_ref(),
571            &self.client_caps,
572            &self.server_caps,
573            self.protocol_version,
574            peer,
575        )
576    }
577}
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582
583    #[test]
584    fn test_context_creation() {
585        let request_id = RequestId::Number(1);
586        let client_caps = ClientCapabilities::default();
587        let server_caps = ServerCapabilities::default();
588        let peer = NoOpPeer;
589
590        let ctx = Context::new(
591            &request_id,
592            None,
593            &client_caps,
594            &server_caps,
595            ProtocolVersion::LATEST,
596            &peer,
597        );
598
599        assert!(!ctx.is_cancelled());
600        assert!(ctx.progress_token.is_none());
601        assert_eq!(ctx.protocol_version, ProtocolVersion::LATEST);
602    }
603
604    #[test]
605    fn test_context_with_progress_token() {
606        let request_id = RequestId::Number(1);
607        let progress_token = ProgressToken::String("token".to_string());
608        let client_caps = ClientCapabilities::default();
609        let server_caps = ServerCapabilities::default();
610        let peer = NoOpPeer;
611
612        let ctx = Context::new(
613            &request_id,
614            Some(&progress_token),
615            &client_caps,
616            &server_caps,
617            ProtocolVersion::V2025_03_26,
618            &peer,
619        );
620
621        assert!(ctx.progress_token.is_some());
622        assert_eq!(ctx.protocol_version, ProtocolVersion::V2025_03_26);
623    }
624
625    #[test]
626    fn test_context_data() {
627        let data = ContextData::new(
628            RequestId::Number(42),
629            ClientCapabilities::default(),
630            ServerCapabilities::default(),
631            ProtocolVersion::V2025_06_18,
632        )
633        .with_progress_token(ProgressToken::String("test".to_string()));
634
635        let peer = NoOpPeer;
636        let ctx = data.to_context(&peer);
637
638        assert!(ctx.progress_token.is_some());
639        assert_eq!(ctx.protocol_version, ProtocolVersion::V2025_06_18);
640        // Test feature detection via protocol version
641        assert!(ctx.protocol_version.supports_elicitation());
642        assert!(!ctx.protocol_version.supports_tasks()); // Tasks require 2025-11-25
643    }
644
645    #[tokio::test]
646    async fn list_roots_requests_and_parses_when_advertised() {
647        use mcpkit_core::protocol::Response;
648
649        struct RootsPeer;
650        impl Peer for RootsPeer {
651            fn notify(
652                &self,
653                _n: Notification,
654            ) -> Pin<Box<dyn Future<Output = Result<(), McpError>> + Send + '_>> {
655                Box::pin(async { Ok(()) })
656            }
657            fn request(
658                &self,
659                method: Cow<'static, str>,
660                _params: Option<serde_json::Value>,
661            ) -> Pin<Box<dyn Future<Output = Result<Response, McpError>> + Send + '_>> {
662                assert_eq!(method, "roots/list");
663                let result = serde_json::to_value(ListRootsResult {
664                    roots: vec![Root::new("file:///a").name("a")],
665                    meta: None,
666                })
667                .unwrap();
668                Box::pin(async move { Ok(Response::success(RequestId::Number(1), result)) })
669            }
670        }
671
672        let request_id = RequestId::Number(1);
673        let server_caps = ServerCapabilities::default();
674        let peer = RootsPeer;
675
676        // Advertised -> request sent and result parsed.
677        let client_caps = ClientCapabilities::default().with_roots();
678        let ctx = Context::new(
679            &request_id,
680            None,
681            &client_caps,
682            &server_caps,
683            ProtocolVersion::LATEST,
684            &peer,
685        );
686        let roots = ctx.list_roots().await.expect("roots listed");
687        assert_eq!(roots.len(), 1);
688        assert_eq!(roots[0].name.as_deref(), Some("a"));
689
690        // Not advertised -> error before any request.
691        let no_roots = ClientCapabilities::default();
692        let ctx = Context::new(
693            &request_id,
694            None,
695            &no_roots,
696            &server_caps,
697            ProtocolVersion::LATEST,
698            &peer,
699        );
700        assert!(ctx.list_roots().await.is_err());
701    }
702}