Skip to main content

fastmcp_core/
context.rs

1//! MCP context with asupersync integration.
2//!
3//! [`McpContext`] wraps asupersync's [`Cx`] to provide request-scoped
4//! capabilities for MCP message handling (tools, resources, prompts).
5
6use std::future::Future;
7use std::pin::Pin;
8use std::sync::atomic::{AtomicU8, AtomicU32, Ordering};
9use std::sync::{Arc, Mutex};
10
11use asupersync::sync::Notify;
12use asupersync::types::{CancelReason, MAX_MASK_DEPTH};
13use asupersync::{Budget, Cx, Outcome, RegionId, TaskId, Time};
14
15#[cfg(test)]
16use asupersync::time::wall_now;
17
18use crate::{AuthContext, SessionState};
19
20const REQUEST_LEASE_UNMANAGED: u8 = 0;
21const REQUEST_LEASE_ACTIVE: u8 = 1;
22const REQUEST_LEASE_CLOSED: u8 = 2;
23const REQUEST_CANCELLATION_ACTIVE: u8 = 0;
24const REQUEST_CANCELLATION_CANCELLED: u8 = 1;
25const REQUEST_CANCELLATION_FINALIZING: u8 = 2;
26const REQUEST_AUTH_UNCOMMITTED: u8 = 0;
27const REQUEST_AUTH_ANONYMOUS: u8 = 1;
28const REQUEST_AUTH_AUTHENTICATED: u8 = 2;
29
30/// Clone-shared cooperative cancellation state for one FastMCP request.
31///
32/// This is an internal cross-crate integration type. It deliberately does not
33/// cancel the caller-owned [`Cx`], because that context may be shared by a
34/// connection loop or sibling requests. Request-owned runtime cancellation and
35/// drain still require the child-region primitive tracked by FND-04.
36#[derive(Debug, Default)]
37struct McpRequestCancellationInner {
38    state: AtomicU8,
39    notify: Notify,
40}
41
42#[derive(Clone, Debug, Default)]
43#[doc(hidden)]
44pub struct McpRequestCancellation {
45    inner: Arc<McpRequestCancellationInner>,
46}
47
48impl McpRequestCancellation {
49    /// Creates an independent FastMCP request-cancellation domain.
50    #[must_use]
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    /// Requests cooperative cancellation without mutating the ambient [`Cx`].
56    pub fn cancel(&self) -> bool {
57        let cancelled = self
58            .inner
59            .state
60            .compare_exchange(
61                REQUEST_CANCELLATION_ACTIVE,
62                REQUEST_CANCELLATION_CANCELLED,
63                Ordering::AcqRel,
64                Ordering::Acquire,
65            )
66            .is_ok();
67        if cancelled {
68            self.notify_terminal_waiters();
69        }
70        cancelled
71    }
72
73    fn notify_terminal_waiters(&self) {
74        // A user-supplied waker is allowed to panic. Terminal state is already
75        // authoritative at this point, so contain that panic after Notify has
76        // attempted to wake the registered waiter set.
77        let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
78            self.inner.notify.notify_waiters();
79        }));
80    }
81
82    /// Returns whether cooperative request cancellation has been requested.
83    #[must_use]
84    pub fn is_cancel_requested(&self) -> bool {
85        self.inner.state.load(Ordering::Acquire) == REQUEST_CANCELLATION_CANCELLED
86    }
87
88    /// Returns whether cancellation or response finalization owns the request.
89    ///
90    /// This deliberately uses one atomic snapshot. Combining separate
91    /// cancellation and finalization reads can miss an `ACTIVE -> CANCELLED`
92    /// transition that occurs between those reads.
93    #[must_use]
94    pub fn is_terminal(&self) -> bool {
95        self.inner.state.load(Ordering::Acquire) != REQUEST_CANCELLATION_ACTIVE
96    }
97
98    /// Waits until request-local cancellation wins the terminal race.
99    ///
100    /// The wait is cancel-safe and uses an armed notification followed by a
101    /// state recheck, so cancellation cannot be lost between observing the
102    /// atomic state and parking the current task.
103    pub async fn cancelled(&self) {
104        self.inner
105            .notify
106            .wait_until(|| self.is_cancel_requested())
107            .await;
108    }
109
110    /// Waits until cancellation or response finalization owns the request.
111    ///
112    /// Reverse requests use this terminal wait so retained work cannot remain
113    /// parked after the owning incoming request has begun finalization.
114    pub async fn terminated(&self) {
115        self.inner.notify.wait_until(|| self.is_terminal()).await;
116    }
117
118    /// Atomically closes the cancellation race before response finalization.
119    ///
120    /// Returns `false` only when cancellation linearized first. Once this
121    /// returns `true`, later cancellation attempts are rejected. Repeated calls
122    /// by the response path are idempotent.
123    #[must_use]
124    pub fn begin_finalization(&self) -> bool {
125        loop {
126            match self.inner.state.load(Ordering::Acquire) {
127                REQUEST_CANCELLATION_ACTIVE => {
128                    if self
129                        .inner
130                        .state
131                        .compare_exchange(
132                            REQUEST_CANCELLATION_ACTIVE,
133                            REQUEST_CANCELLATION_FINALIZING,
134                            Ordering::AcqRel,
135                            Ordering::Acquire,
136                        )
137                        .is_ok()
138                    {
139                        self.notify_terminal_waiters();
140                        return true;
141                    }
142                }
143                REQUEST_CANCELLATION_CANCELLED => return false,
144                REQUEST_CANCELLATION_FINALIZING => return true,
145                _ => return false,
146            }
147        }
148    }
149
150    /// Returns whether response finalization already owns the terminal race.
151    #[must_use]
152    pub fn is_finalizing(&self) -> bool {
153        self.inner.state.load(Ordering::Acquire) == REQUEST_CANCELLATION_FINALIZING
154    }
155}
156
157// ============================================================================
158// Notification Sender
159// ============================================================================
160
161/// Trait for sending notifications back to the client.
162///
163/// This is implemented by the server's transport layer to allow handlers
164/// to send progress updates and other notifications during execution.
165pub trait NotificationSender: Send + Sync {
166    /// Sends a progress notification to the client.
167    ///
168    /// # Arguments
169    ///
170    /// * `progress` - Current progress value
171    /// * `total` - Optional total for determinate progress
172    /// * `message` - Optional message describing current status
173    fn send_progress(&self, progress: f64, total: Option<f64>, message: Option<&str>);
174
175    /// Sends one exact-number final progress notification.
176    ///
177    /// The default deliberately does nothing. Existing and legacy senders
178    /// therefore cannot accidentally emit a final wire model merely because a
179    /// handler calls the typed progress API. Senders that explicitly support
180    /// the final protocol override this method and retain each JSON-number
181    /// lexeme through serialization.
182    fn send_progress_exact(
183        &self,
184        _progress: serde_json::Number,
185        _total: Option<serde_json::Number>,
186        _message: Option<&str>,
187    ) {
188    }
189
190    /// Sends one `notifications/message` log frame to the client.
191    ///
192    /// The default emits nothing so progress-only senders stay inert. Server
193    /// dispatch installs a sender that writes the MCP log notification after
194    /// the client has selected a minimum level with `logging/setLevel`.
195    fn send_log(&self, _level: McpLogLevel, _logger: Option<&str>, _data: serde_json::Value) {}
196
197    /// Sends one catalog `list_changed` notification after a session mutation.
198    ///
199    /// The default emits nothing so progress-only senders stay inert. Server
200    /// dispatch installs a sender that writes the matching
201    /// `notifications/{tools,resources,prompts}/list_changed` frame.
202    fn send_catalog_changed(&self, _kind: McpCatalogKind) {}
203
204    /// Sends `notifications/resources/updated` for one subscribed URI.
205    ///
206    /// The default emits nothing. Server dispatch installs a sender that
207    /// writes the resource-update frame to the current session.
208    fn send_resource_updated(&self, _uri: &str) {}
209}
210
211/// Which MCP catalog changed after a session enable/disable mutation.
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub enum McpCatalogKind {
214    Tools,
215    Resources,
216    Prompts,
217}
218
219/// Publishes catalog and resource-update events to modern `subscriptions/listen`
220/// streams. Session JSON-RPC notifications stay on [`NotificationSender`].
221pub trait CatalogChangePublisher: Send + Sync {
222    /// Returns whether at least one live listener accepted the catalog event.
223    fn publish_catalog_changed(&self, kind: McpCatalogKind) -> bool;
224    /// Returns whether at least one live listener accepted the resource update.
225    fn publish_resource_updated(&self, uri: &str) -> bool;
226}
227
228/// MCP syslog-style log severity used by handler `ctx.info()` and friends.
229///
230/// Ranking matches the protocol `LogLevel` order: debug is lowest, emergency
231/// is highest. A server must not emit a notification until the client has
232/// selected a minimum level.
233#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
234pub enum McpLogLevel {
235    Debug,
236    Info,
237    Notice,
238    Warning,
239    Error,
240    Critical,
241    Alert,
242    Emergency,
243}
244
245impl McpLogLevel {
246    /// Wire token used by `notifications/message`.
247    #[must_use]
248    pub const fn as_str(self) -> &'static str {
249        match self {
250            Self::Debug => "debug",
251            Self::Info => "info",
252            Self::Notice => "notice",
253            Self::Warning => "warning",
254            Self::Error => "error",
255            Self::Critical => "critical",
256            Self::Alert => "alert",
257            Self::Emergency => "emergency",
258        }
259    }
260
261    /// Syslog-style rank used to compare a message against the client floor.
262    #[must_use]
263    pub const fn rank(self) -> u8 {
264        match self {
265            Self::Debug => 1,
266            Self::Info => 2,
267            Self::Notice => 3,
268            Self::Warning => 4,
269            Self::Error => 5,
270            Self::Critical => 6,
271            Self::Alert => 7,
272            Self::Emergency => 8,
273        }
274    }
275}
276
277// ============================================================================
278// Roots Provider
279// ============================================================================
280
281/// A filesystem root supplied by the connected client.
282///
283/// This deliberately lives in core rather than the wire crate: a handler's
284/// authority to inspect client roots must not introduce a core-to-protocol
285/// dependency cycle.
286#[derive(Debug, Clone, PartialEq, Eq)]
287pub struct ClientRoot {
288    /// Root URI, normally a `file://` URI.
289    pub uri: String,
290    /// Optional human-readable display name.
291    pub name: Option<String>,
292}
293
294impl ClientRoot {
295    /// Creates an unnamed client root.
296    #[must_use]
297    pub fn new(uri: impl Into<String>) -> Self {
298        Self {
299            uri: uri.into(),
300            name: None,
301        }
302    }
303
304    /// Creates a named client root.
305    #[must_use]
306    pub fn with_name(uri: impl Into<String>, name: impl Into<String>) -> Self {
307        Self {
308            uri: uri.into(),
309            name: Some(name.into()),
310        }
311    }
312}
313
314/// Capability for listing filesystem roots from the connected client.
315pub trait RootsProvider: Send + Sync {
316    /// Lists the roots currently exposed by the client.
317    fn list_roots(
318        &self,
319    ) -> std::pin::Pin<
320        Box<dyn std::future::Future<Output = crate::McpResult<Vec<ClientRoot>>> + Send + '_>,
321    >;
322}
323
324// ============================================================================
325// Sampling Sender
326// ============================================================================
327
328/// Trait for sending sampling requests to the client.
329///
330/// Sampling allows the server to request LLM completions from the client.
331/// This enables agentic workflows where tools can leverage the client's
332/// LLM capabilities.
333pub trait SamplingSender: Send + Sync {
334    /// Sends a sampling/createMessage request to the client.
335    ///
336    /// # Arguments
337    ///
338    /// * `request` - The sampling request parameters
339    ///
340    /// # Returns
341    ///
342    /// The sampling response from the client, or an error if sampling failed
343    /// or the client doesn't support sampling.
344    fn create_message(
345        &self,
346        request: SamplingRequest,
347    ) -> std::pin::Pin<
348        Box<dyn std::future::Future<Output = crate::McpResult<SamplingResponse>> + Send + '_>,
349    >;
350}
351
352/// Parameters for a sampling request.
353#[derive(Debug, Clone)]
354pub struct SamplingRequest {
355    /// Conversation messages.
356    pub messages: Vec<SamplingRequestMessage>,
357    /// Maximum tokens to generate.
358    pub max_tokens: u32,
359    /// Optional system prompt.
360    pub system_prompt: Option<String>,
361    /// Sampling temperature (0.0 to 2.0).
362    pub temperature: Option<f64>,
363    /// Stop sequences to end generation.
364    pub stop_sequences: Vec<String>,
365    /// Model hints for preference.
366    pub model_hints: Vec<String>,
367}
368
369impl SamplingRequest {
370    /// Creates a new sampling request with the given messages and max tokens.
371    #[must_use]
372    pub fn new(messages: Vec<SamplingRequestMessage>, max_tokens: u32) -> Self {
373        Self {
374            messages,
375            max_tokens,
376            system_prompt: None,
377            temperature: None,
378            stop_sequences: Vec::new(),
379            model_hints: Vec::new(),
380        }
381    }
382
383    /// Creates a simple user prompt request.
384    #[must_use]
385    pub fn prompt(text: impl Into<String>, max_tokens: u32) -> Self {
386        Self::new(vec![SamplingRequestMessage::user(text)], max_tokens)
387    }
388
389    /// Sets the system prompt.
390    #[must_use]
391    pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
392        self.system_prompt = Some(prompt.into());
393        self
394    }
395
396    /// Sets the temperature.
397    #[must_use]
398    pub fn with_temperature(mut self, temp: f64) -> Self {
399        self.temperature = Some(temp);
400        self
401    }
402
403    /// Adds stop sequences.
404    #[must_use]
405    pub fn with_stop_sequences(mut self, sequences: Vec<String>) -> Self {
406        self.stop_sequences = sequences;
407        self
408    }
409
410    /// Adds model hints.
411    #[must_use]
412    pub fn with_model_hints(mut self, hints: Vec<String>) -> Self {
413        self.model_hints = hints;
414        self
415    }
416}
417
418/// A message in a sampling request.
419#[derive(Debug, Clone)]
420pub struct SamplingRequestMessage {
421    /// Message role.
422    pub role: SamplingRole,
423    /// Message text content.
424    pub text: String,
425}
426
427impl SamplingRequestMessage {
428    /// Creates a user message.
429    #[must_use]
430    pub fn user(text: impl Into<String>) -> Self {
431        Self {
432            role: SamplingRole::User,
433            text: text.into(),
434        }
435    }
436
437    /// Creates an assistant message.
438    #[must_use]
439    pub fn assistant(text: impl Into<String>) -> Self {
440        Self {
441            role: SamplingRole::Assistant,
442            text: text.into(),
443        }
444    }
445}
446
447/// Role in a sampling message.
448#[derive(Debug, Clone, Copy, PartialEq, Eq)]
449pub enum SamplingRole {
450    /// User message.
451    User,
452    /// Assistant message.
453    Assistant,
454}
455
456/// Response from a sampling request.
457#[derive(Debug, Clone)]
458pub struct SamplingResponse {
459    /// Generated text content.
460    pub text: String,
461    /// Model that was used.
462    pub model: String,
463    /// Reason generation stopped.
464    pub stop_reason: SamplingStopReason,
465}
466
467impl SamplingResponse {
468    /// Creates a new sampling response.
469    #[must_use]
470    pub fn new(text: impl Into<String>, model: impl Into<String>) -> Self {
471        Self {
472            text: text.into(),
473            model: model.into(),
474            stop_reason: SamplingStopReason::EndTurn,
475        }
476    }
477}
478
479/// Stop reason for sampling.
480#[derive(Debug, Clone, PartialEq, Eq, Default)]
481pub enum SamplingStopReason {
482    /// End of natural turn.
483    #[default]
484    EndTurn,
485    /// Hit stop sequence.
486    StopSequence,
487    /// Hit max tokens limit.
488    MaxTokens,
489    /// The peer omitted the optional wire-level stop reason.
490    Unspecified,
491    /// An open provider-defined wire-level stop reason.
492    Other(String),
493}
494
495impl SamplingStopReason {
496    /// Converts an optional wire-level stop reason without narrowing an open
497    /// provider value.
498    #[must_use]
499    pub fn from_wire_value(value: Option<String>) -> Self {
500        match value {
501            Some(value) => match value.as_str() {
502                "endTurn" => Self::EndTurn,
503                "stopSequence" => Self::StopSequence,
504                "maxTokens" => Self::MaxTokens,
505                _ => Self::Other(value),
506            },
507            None => Self::Unspecified,
508        }
509    }
510
511    /// Returns the optional wire-level value without changing an open provider
512    /// value.
513    #[must_use]
514    pub fn as_wire_value(&self) -> Option<&str> {
515        match self {
516            Self::EndTurn => Some("endTurn"),
517            Self::StopSequence => Some("stopSequence"),
518            Self::MaxTokens => Some("maxTokens"),
519            Self::Unspecified => None,
520            Self::Other(value) => Some(value),
521        }
522    }
523}
524
525/// A no-op sampling sender that always returns an error.
526///
527/// Used when the client doesn't support sampling.
528#[derive(Debug, Clone, Copy, Default)]
529pub struct NoOpSamplingSender;
530
531impl SamplingSender for NoOpSamplingSender {
532    fn create_message(
533        &self,
534        _request: SamplingRequest,
535    ) -> std::pin::Pin<
536        Box<dyn std::future::Future<Output = crate::McpResult<SamplingResponse>> + Send + '_>,
537    > {
538        Box::pin(async {
539            Err(crate::McpError::new(
540                crate::McpErrorCode::InvalidRequest,
541                "Sampling not supported: client does not have sampling capability",
542            ))
543        })
544    }
545}
546
547// ============================================================================
548// Elicitation Sender
549// ============================================================================
550
551/// Trait for sending elicitation requests to the client.
552///
553/// Elicitation allows the server to request user input from the client.
554/// This enables interactive workflows where tools can prompt users for
555/// additional information.
556pub trait ElicitationSender: Send + Sync {
557    /// Sends an elicitation/create request to the client.
558    ///
559    /// # Arguments
560    ///
561    /// * `request` - The elicitation request parameters
562    ///
563    /// # Returns
564    ///
565    /// The elicitation response from the client, or an error if elicitation
566    /// failed or the client doesn't support elicitation.
567    fn elicit(
568        &self,
569        request: ElicitationRequest,
570    ) -> std::pin::Pin<
571        Box<dyn std::future::Future<Output = crate::McpResult<ElicitationResponse>> + Send + '_>,
572    >;
573}
574
575/// Parameters for an elicitation request.
576#[derive(Debug, Clone)]
577pub struct ElicitationRequest {
578    /// Mode of elicitation (form or URL).
579    pub mode: ElicitationMode,
580    /// Message to present to the user.
581    pub message: String,
582    /// For form mode: JSON Schema for the expected response.
583    pub schema: Option<serde_json::Value>,
584    /// For URL mode: URL to navigate to.
585    pub url: Option<String>,
586    /// For URL mode: Unique elicitation ID.
587    pub elicitation_id: Option<String>,
588}
589
590impl ElicitationRequest {
591    /// Creates a form mode elicitation request.
592    #[must_use]
593    pub fn form(message: impl Into<String>, schema: serde_json::Value) -> Self {
594        Self {
595            mode: ElicitationMode::Form,
596            message: message.into(),
597            schema: Some(schema),
598            url: None,
599            elicitation_id: None,
600        }
601    }
602
603    /// Creates a URL mode elicitation request.
604    #[must_use]
605    pub fn url(
606        message: impl Into<String>,
607        url: impl Into<String>,
608        elicitation_id: impl Into<String>,
609    ) -> Self {
610        Self {
611            mode: ElicitationMode::Url,
612            message: message.into(),
613            schema: None,
614            url: Some(url.into()),
615            elicitation_id: Some(elicitation_id.into()),
616        }
617    }
618}
619
620/// Mode of elicitation.
621#[derive(Debug, Clone, Copy, PartialEq, Eq)]
622pub enum ElicitationMode {
623    /// Form mode - collect user input via in-band form.
624    Form,
625    /// URL mode - redirect user to external URL.
626    Url,
627}
628
629/// Response from an elicitation request.
630#[derive(Debug, Clone)]
631pub struct ElicitationResponse {
632    /// User's action (accept, decline, cancel).
633    pub action: ElicitationAction,
634    /// Form data (only present when action is Accept and mode is Form).
635    pub content: Option<std::collections::HashMap<String, serde_json::Value>>,
636}
637
638impl ElicitationResponse {
639    /// Creates an accepted response with form data.
640    #[must_use]
641    pub fn accept(content: std::collections::HashMap<String, serde_json::Value>) -> Self {
642        Self {
643            action: ElicitationAction::Accept,
644            content: Some(content),
645        }
646    }
647
648    /// Creates an accepted response for URL mode (no content).
649    #[must_use]
650    pub fn accept_url() -> Self {
651        Self {
652            action: ElicitationAction::Accept,
653            content: None,
654        }
655    }
656
657    /// Creates a declined response.
658    #[must_use]
659    pub fn decline() -> Self {
660        Self {
661            action: ElicitationAction::Decline,
662            content: None,
663        }
664    }
665
666    /// Creates a cancelled response.
667    #[must_use]
668    pub fn cancel() -> Self {
669        Self {
670            action: ElicitationAction::Cancel,
671            content: None,
672        }
673    }
674
675    /// Returns true if the user accepted.
676    #[must_use]
677    pub fn is_accepted(&self) -> bool {
678        matches!(self.action, ElicitationAction::Accept)
679    }
680
681    /// Returns true if the user declined.
682    #[must_use]
683    pub fn is_declined(&self) -> bool {
684        matches!(self.action, ElicitationAction::Decline)
685    }
686
687    /// Returns true if the user cancelled.
688    #[must_use]
689    pub fn is_cancelled(&self) -> bool {
690        matches!(self.action, ElicitationAction::Cancel)
691    }
692
693    /// Gets a string value from the form content.
694    #[must_use]
695    pub fn get_string(&self, key: &str) -> Option<&str> {
696        self.content.as_ref()?.get(key)?.as_str()
697    }
698
699    /// Gets a boolean value from the form content.
700    #[must_use]
701    pub fn get_bool(&self, key: &str) -> Option<bool> {
702        self.content.as_ref()?.get(key)?.as_bool()
703    }
704
705    /// Gets an integer value from the form content.
706    #[must_use]
707    pub fn get_int(&self, key: &str) -> Option<i64> {
708        self.content.as_ref()?.get(key)?.as_i64()
709    }
710}
711
712/// Action taken by the user in response to elicitation.
713#[derive(Debug, Clone, Copy, PartialEq, Eq)]
714pub enum ElicitationAction {
715    /// User accepted/submitted the form.
716    Accept,
717    /// User explicitly declined.
718    Decline,
719    /// User dismissed without choice.
720    Cancel,
721}
722
723/// A no-op elicitation sender that always returns an error.
724///
725/// Used when the client doesn't support elicitation.
726#[derive(Debug, Clone, Copy, Default)]
727pub struct NoOpElicitationSender;
728
729impl ElicitationSender for NoOpElicitationSender {
730    fn elicit(
731        &self,
732        _request: ElicitationRequest,
733    ) -> std::pin::Pin<
734        Box<dyn std::future::Future<Output = crate::McpResult<ElicitationResponse>> + Send + '_>,
735    > {
736        Box::pin(async {
737            Err(crate::McpError::new(
738                crate::McpErrorCode::InvalidRequest,
739                "Elicitation not supported: client does not have elicitation capability",
740            ))
741        })
742    }
743}
744
745// ============================================================================
746// Resource Reader (Cross-Component Access)
747// ============================================================================
748
749/// Maximum depth for nested resource reads to prevent infinite recursion.
750pub const MAX_RESOURCE_READ_DEPTH: u32 = 10;
751
752/// A single item of resource content.
753///
754/// Mirrors the protocol's ResourceContent but lives in core to avoid
755/// circular dependencies.
756#[derive(Debug, Clone)]
757pub struct ResourceContentItem {
758    /// Resource URI.
759    pub uri: String,
760    /// MIME type.
761    pub mime_type: Option<String>,
762    /// Text content (if text).
763    pub text: Option<String>,
764    /// Binary content (if blob, base64-encoded).
765    pub blob: Option<String>,
766}
767
768impl ResourceContentItem {
769    /// Creates a text resource content item.
770    #[must_use]
771    pub fn text(uri: impl Into<String>, text: impl Into<String>) -> Self {
772        Self {
773            uri: uri.into(),
774            mime_type: Some("text/plain".to_string()),
775            text: Some(text.into()),
776            blob: None,
777        }
778    }
779
780    /// Creates a JSON resource content item.
781    #[must_use]
782    pub fn json(uri: impl Into<String>, text: impl Into<String>) -> Self {
783        Self {
784            uri: uri.into(),
785            mime_type: Some("application/json".to_string()),
786            text: Some(text.into()),
787            blob: None,
788        }
789    }
790
791    /// Creates a binary resource content item.
792    #[must_use]
793    pub fn blob(
794        uri: impl Into<String>,
795        mime_type: impl Into<String>,
796        blob: impl Into<String>,
797    ) -> Self {
798        Self {
799            uri: uri.into(),
800            mime_type: Some(mime_type.into()),
801            text: None,
802            blob: Some(blob.into()),
803        }
804    }
805
806    /// Returns the text content, if present.
807    #[must_use]
808    pub fn as_text(&self) -> Option<&str> {
809        self.text.as_deref()
810    }
811
812    /// Returns the blob content, if present.
813    #[must_use]
814    pub fn as_blob(&self) -> Option<&str> {
815        self.blob.as_deref()
816    }
817
818    /// Returns true if this is a text resource.
819    #[must_use]
820    pub fn is_text(&self) -> bool {
821        self.text.is_some()
822    }
823
824    /// Returns true if this is a blob resource.
825    #[must_use]
826    pub fn is_blob(&self) -> bool {
827        self.blob.is_some()
828    }
829}
830
831/// Result of reading a resource.
832#[derive(Debug, Clone)]
833pub struct ResourceReadResult {
834    /// The content items.
835    pub contents: Vec<ResourceContentItem>,
836}
837
838impl ResourceReadResult {
839    /// Creates a new resource read result with the given contents.
840    #[must_use]
841    pub fn new(contents: Vec<ResourceContentItem>) -> Self {
842        Self { contents }
843    }
844
845    /// Creates a single-item text result.
846    #[must_use]
847    pub fn text(uri: impl Into<String>, text: impl Into<String>) -> Self {
848        Self {
849            contents: vec![ResourceContentItem::text(uri, text)],
850        }
851    }
852
853    /// Returns the first text content, if present.
854    #[must_use]
855    pub fn first_text(&self) -> Option<&str> {
856        self.contents.first().and_then(|c| c.as_text())
857    }
858
859    /// Returns the first blob content, if present.
860    #[must_use]
861    pub fn first_blob(&self) -> Option<&str> {
862        self.contents.first().and_then(|c| c.as_blob())
863    }
864}
865
866/// Trait for reading resources from within handlers.
867///
868/// This trait is implemented by the server's Router to allow tools,
869/// resources, and prompts to read other resources. It enables
870/// cross-component composition and code reuse.
871///
872/// The trait uses boxed futures to avoid complex lifetime issues
873/// with async traits.
874pub trait ResourceReader: Send + Sync {
875    /// Reads a resource by URI.
876    ///
877    /// # Arguments
878    ///
879    /// * `context` - The originating MCP request context
880    /// * `uri` - The resource URI to read
881    /// * `depth` - Current recursion depth (to prevent infinite loops)
882    ///
883    /// # Returns
884    ///
885    /// The resource contents, or an error if the resource doesn't exist
886    /// or reading fails.
887    fn read_resource<'a>(
888        &'a self,
889        context: &'a McpContext,
890        uri: &'a str,
891        depth: u32,
892    ) -> Pin<Box<dyn Future<Output = crate::McpResult<ResourceReadResult>> + Send + 'a>>;
893}
894
895// ============================================================================
896// Tool Caller (Cross-Component Access)
897// ============================================================================
898
899/// Maximum depth for nested tool calls to prevent infinite recursion.
900pub const MAX_TOOL_CALL_DEPTH: u32 = 10;
901
902/// A single item of content returned from a tool call.
903///
904/// Mirrors the protocol's Content type but lives in core to avoid
905/// circular dependencies.
906#[derive(Debug, Clone)]
907pub enum ToolContentItem {
908    /// Text content.
909    Text {
910        /// The text content.
911        text: String,
912    },
913    /// Image content (base64-encoded).
914    Image {
915        /// Base64-encoded image data.
916        data: String,
917        /// MIME type of the image.
918        mime_type: String,
919    },
920    /// Audio content (base64-encoded).
921    Audio {
922        /// Base64-encoded audio data.
923        data: String,
924        /// MIME type of the audio.
925        mime_type: String,
926    },
927    /// Embedded resource reference.
928    Resource {
929        /// Resource URI.
930        uri: String,
931        /// MIME type.
932        mime_type: Option<String>,
933        /// Text content.
934        text: Option<String>,
935        /// Binary content (base64 blob).
936        blob: Option<String>,
937    },
938}
939
940impl ToolContentItem {
941    /// Creates a text content item.
942    #[must_use]
943    pub fn text(text: impl Into<String>) -> Self {
944        Self::Text { text: text.into() }
945    }
946
947    /// Returns the text content, if this is a text item.
948    #[must_use]
949    pub fn as_text(&self) -> Option<&str> {
950        match self {
951            Self::Text { text } => Some(text),
952            _ => None,
953        }
954    }
955
956    /// Returns true if this is a text content item.
957    #[must_use]
958    pub fn is_text(&self) -> bool {
959        matches!(self, Self::Text { .. })
960    }
961}
962
963/// Result of calling a tool.
964#[derive(Debug, Clone)]
965pub struct ToolCallResult {
966    /// The content items returned by the tool.
967    pub content: Vec<ToolContentItem>,
968    /// Whether the tool returned an error.
969    pub is_error: bool,
970}
971
972impl ToolCallResult {
973    /// Creates a successful tool result with the given content.
974    #[must_use]
975    pub fn success(content: Vec<ToolContentItem>) -> Self {
976        Self {
977            content,
978            is_error: false,
979        }
980    }
981
982    /// Creates a successful tool result with a single text item.
983    #[must_use]
984    pub fn text(text: impl Into<String>) -> Self {
985        Self {
986            content: vec![ToolContentItem::text(text)],
987            is_error: false,
988        }
989    }
990
991    /// Creates an error tool result.
992    #[must_use]
993    pub fn error(message: impl Into<String>) -> Self {
994        Self {
995            content: vec![ToolContentItem::text(message)],
996            is_error: true,
997        }
998    }
999
1000    /// Returns the first text content, if present.
1001    #[must_use]
1002    pub fn first_text(&self) -> Option<&str> {
1003        self.content.first().and_then(|c| c.as_text())
1004    }
1005}
1006
1007/// Trait for calling tools from within handlers.
1008///
1009/// This trait is implemented by the server's Router to allow tools,
1010/// resources, and prompts to call other tools. It enables
1011/// cross-component composition and code reuse.
1012///
1013/// The trait uses boxed futures to avoid complex lifetime issues
1014/// with async traits.
1015pub trait ToolCaller: Send + Sync {
1016    /// Calls a tool by name with the given arguments.
1017    ///
1018    /// # Arguments
1019    ///
1020    /// * `context` - The originating MCP request context
1021    /// * `name` - The tool name to call
1022    /// * `args` - The arguments as a JSON value
1023    /// * `depth` - Current recursion depth (to prevent infinite loops)
1024    ///
1025    /// # Returns
1026    ///
1027    /// The tool result, or an error if the tool doesn't exist
1028    /// or execution fails.
1029    fn call_tool<'a>(
1030        &'a self,
1031        context: &'a McpContext,
1032        name: &'a str,
1033        args: serde_json::Value,
1034        depth: u32,
1035    ) -> Pin<Box<dyn Future<Output = crate::McpResult<ToolCallResult>> + Send + 'a>>;
1036}
1037
1038// ============================================================================
1039// Prompt Caller (Cross-Component Access)
1040// ============================================================================
1041
1042/// Maximum depth for nested prompt gets to prevent infinite recursion.
1043pub const MAX_PROMPT_GET_DEPTH: u32 = 10;
1044
1045/// Role of one prompt message returned through [`McpContext::get_prompt`].
1046#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1047pub enum PromptMessageRole {
1048    /// User-authored prompt turn.
1049    User,
1050    /// Assistant-authored prompt turn.
1051    Assistant,
1052}
1053
1054/// One prompt message returned through [`McpContext::get_prompt`].
1055#[derive(Debug, Clone, PartialEq, Eq)]
1056pub struct PromptMessageItem {
1057    /// Message role.
1058    pub role: PromptMessageRole,
1059    /// Text content when the handler authored a text block.
1060    pub text: Option<String>,
1061}
1062
1063impl PromptMessageItem {
1064    /// Creates a user text message.
1065    #[must_use]
1066    pub fn user_text(text: impl Into<String>) -> Self {
1067        Self {
1068            role: PromptMessageRole::User,
1069            text: Some(text.into()),
1070        }
1071    }
1072
1073    /// Returns the text content, if present.
1074    #[must_use]
1075    pub fn as_text(&self) -> Option<&str> {
1076        self.text.as_deref()
1077    }
1078}
1079
1080/// Result of getting a prompt from within a handler.
1081#[derive(Debug, Clone, PartialEq, Eq)]
1082pub struct PromptGetResult {
1083    /// Optional prompt description from the catalog definition.
1084    pub description: Option<String>,
1085    /// Messages returned by the prompt handler.
1086    pub messages: Vec<PromptMessageItem>,
1087}
1088
1089impl PromptGetResult {
1090    /// Creates a prompt result with the given messages.
1091    #[must_use]
1092    pub fn new(messages: Vec<PromptMessageItem>) -> Self {
1093        Self {
1094            description: None,
1095            messages,
1096        }
1097    }
1098
1099    /// Returns the first text message, if present.
1100    #[must_use]
1101    pub fn first_text(&self) -> Option<&str> {
1102        self.messages.iter().find_map(PromptMessageItem::as_text)
1103    }
1104}
1105
1106/// Trait for getting prompts from within handlers.
1107///
1108/// This trait is implemented by the server's Router to allow tools,
1109/// resources, and prompts to get other prompts. It enables
1110/// cross-component composition and code reuse.
1111pub trait PromptCaller: Send + Sync {
1112    /// Gets a prompt by name with the given arguments.
1113    fn get_prompt<'a>(
1114        &'a self,
1115        context: &'a McpContext,
1116        name: &'a str,
1117        arguments: std::collections::HashMap<String, String>,
1118        depth: u32,
1119    ) -> Pin<Box<dyn Future<Output = crate::McpResult<PromptGetResult>> + Send + 'a>>;
1120}
1121
1122// ============================================================================
1123// Capabilities Info
1124// ============================================================================
1125
1126/// Client capability information accessible from handlers.
1127///
1128/// This provides a simplified view of what capabilities the connected client
1129/// supports. Use this to adapt handler behavior based on client capabilities.
1130#[derive(Debug, Clone, Default)]
1131pub struct ClientCapabilityInfo {
1132    /// Whether the client supports sampling (LLM completions).
1133    pub sampling: bool,
1134    /// Whether the client supports elicitation (user input requests).
1135    pub elicitation: bool,
1136    /// Whether the client supports form-mode elicitation.
1137    pub elicitation_form: bool,
1138    /// Whether the client supports URL-mode elicitation.
1139    pub elicitation_url: bool,
1140    /// Whether the client supports roots listing.
1141    pub roots: bool,
1142    /// Whether the client wants list_changed notifications for roots.
1143    pub roots_list_changed: bool,
1144}
1145
1146impl ClientCapabilityInfo {
1147    /// Creates a new empty capability info (no capabilities).
1148    #[must_use]
1149    pub fn new() -> Self {
1150        Self::default()
1151    }
1152
1153    /// Creates capability info with sampling enabled.
1154    #[must_use]
1155    pub fn with_sampling(mut self) -> Self {
1156        self.sampling = true;
1157        self
1158    }
1159
1160    /// Creates capability info with elicitation enabled.
1161    #[must_use]
1162    pub fn with_elicitation(mut self, form: bool, url: bool) -> Self {
1163        self.elicitation = form || url;
1164        self.elicitation_form = form;
1165        self.elicitation_url = url;
1166        self
1167    }
1168
1169    /// Creates capability info with roots enabled.
1170    #[must_use]
1171    pub fn with_roots(mut self, list_changed: bool) -> Self {
1172        self.roots = true;
1173        self.roots_list_changed = list_changed;
1174        self
1175    }
1176}
1177
1178/// Handler-visible slice of the self-reported modern client Implementation.
1179///
1180/// This is not authority. It is the request `_meta` identity the peer
1181/// advertised. Name and version are always present when this value exists.
1182#[derive(Debug, Clone, PartialEq, Eq)]
1183pub struct ClientImplementationInfo {
1184    /// Programmatic client name.
1185    pub name: String,
1186    /// Client version.
1187    pub version: String,
1188    /// Optional display title. An empty present title remains present.
1189    pub title: Option<String>,
1190    /// Optional human-readable description.
1191    pub description: Option<String>,
1192    /// Optional website identity as advertised, not validated as authority.
1193    pub website_url: Option<String>,
1194    /// Optional icon source URIs in wire order.
1195    pub icon_sources: Vec<String>,
1196}
1197
1198impl ClientImplementationInfo {
1199    /// Constructs a handler-visible identity from required nonempty fields.
1200    #[must_use]
1201    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
1202        Self {
1203            name: name.into(),
1204            version: version.into(),
1205            title: None,
1206            description: None,
1207            website_url: None,
1208            icon_sources: Vec::new(),
1209        }
1210    }
1211
1212    /// Returns whether any Implementation extras beyond name/version are present.
1213    #[must_use]
1214    pub fn has_extras(&self) -> bool {
1215        self.title.is_some()
1216            || self.description.is_some()
1217            || self.website_url.is_some()
1218            || !self.icon_sources.is_empty()
1219    }
1220}
1221
1222/// Server capability information accessible from handlers.
1223///
1224/// This provides a simplified view of what capabilities this server advertises.
1225#[derive(Debug, Clone, Default)]
1226pub struct ServerCapabilityInfo {
1227    /// Whether the server supports tools.
1228    pub tools: bool,
1229    /// Whether the server supports resources.
1230    pub resources: bool,
1231    /// Whether resources support subscriptions.
1232    pub resources_subscribe: bool,
1233    /// Whether the server supports prompts.
1234    pub prompts: bool,
1235    /// Whether the server supports logging.
1236    pub logging: bool,
1237}
1238
1239impl ServerCapabilityInfo {
1240    /// Creates a new empty server capability info.
1241    #[must_use]
1242    pub fn new() -> Self {
1243        Self::default()
1244    }
1245
1246    /// Creates capability info with tools enabled.
1247    #[must_use]
1248    pub fn with_tools(mut self) -> Self {
1249        self.tools = true;
1250        self
1251    }
1252
1253    /// Creates capability info with resources enabled.
1254    #[must_use]
1255    pub fn with_resources(mut self, subscribe: bool) -> Self {
1256        self.resources = true;
1257        self.resources_subscribe = subscribe;
1258        self
1259    }
1260
1261    /// Creates capability info with prompts enabled.
1262    #[must_use]
1263    pub fn with_prompts(mut self) -> Self {
1264        self.prompts = true;
1265        self
1266    }
1267
1268    /// Creates capability info with logging enabled.
1269    #[must_use]
1270    pub fn with_logging(mut self) -> Self {
1271        self.logging = true;
1272        self
1273    }
1274}
1275
1276/// A no-op notification sender used when progress reporting is disabled.
1277#[derive(Debug, Clone, Copy, Default)]
1278pub struct NoOpNotificationSender;
1279
1280impl NotificationSender for NoOpNotificationSender {
1281    fn send_progress(&self, _progress: f64, _total: Option<f64>, _message: Option<&str>) {
1282        // No-op: progress reporting disabled
1283    }
1284}
1285
1286/// Progress reporter that wraps a notification sender with a progress token.
1287///
1288/// This is the concrete type stored in McpContext that handles sending
1289/// progress notifications with the correct token.
1290#[derive(Clone)]
1291pub struct ProgressReporter {
1292    sender: Arc<dyn NotificationSender>,
1293    // The request progress marker, retained opaquely as its wire JSON. Core
1294    // is the base layer and does not depend on the protocol crate's typed
1295    // `ProgressMarker`; callers convert at the boundary. The value round-trips
1296    // losslessly for proxy-relay correlation.
1297    marker: Option<serde_json::Value>,
1298}
1299
1300impl ProgressReporter {
1301    /// Creates a new progress reporter with the given sender.
1302    pub fn new(sender: Arc<dyn NotificationSender>) -> Self {
1303        Self {
1304            sender,
1305            marker: None,
1306        }
1307    }
1308
1309    /// Creates a reporter which retains the request marker it will emit.
1310    ///
1311    /// Proxy routes use this marker to correlate an upstream progress frame
1312    /// before relaying it through this request's downstream reporter.
1313    #[must_use]
1314    pub fn with_marker(marker: serde_json::Value, sender: Arc<dyn NotificationSender>) -> Self {
1315        Self {
1316            sender,
1317            marker: Some(marker),
1318        }
1319    }
1320
1321    /// Returns the request marker owned by this reporter, when it has one.
1322    #[must_use]
1323    pub fn marker(&self) -> Option<&serde_json::Value> {
1324        self.marker.as_ref()
1325    }
1326
1327    /// Reports progress to the client.
1328    ///
1329    /// # Arguments
1330    ///
1331    /// * `progress` - Current progress value (0.0 to 1.0 for fractional, or absolute)
1332    /// * `message` - Optional message describing current status
1333    pub fn report(&self, progress: f64, message: Option<&str>) {
1334        self.sender.send_progress(progress, None, message);
1335    }
1336
1337    /// Reports progress with a total for determinate progress bars.
1338    ///
1339    /// # Arguments
1340    ///
1341    /// * `progress` - Current progress value
1342    /// * `total` - Total expected value
1343    /// * `message` - Optional message describing current status
1344    pub fn report_with_total(&self, progress: f64, total: f64, message: Option<&str>) {
1345        self.sender.send_progress(progress, Some(total), message);
1346    }
1347
1348    /// Reports final progress without converting JSON-number lexemes through
1349    /// `f64`.
1350    ///
1351    /// The supplied numbers may exceed IEEE-754 range when the installed
1352    /// sender supports the final protocol. A legacy sender receives the trait
1353    /// default, which emits nothing.
1354    pub fn report_exact(
1355        &self,
1356        progress: serde_json::Number,
1357        total: Option<serde_json::Number>,
1358        message: Option<&str>,
1359    ) {
1360        self.sender.send_progress_exact(progress, total, message);
1361    }
1362}
1363
1364impl std::fmt::Debug for ProgressReporter {
1365    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1366        f.debug_struct("ProgressReporter").finish_non_exhaustive()
1367    }
1368}
1369
1370/// MCP context that wraps asupersync's capability context.
1371///
1372/// `McpContext` provides access to:
1373/// - Request-scoped identity (request ID, trace context)
1374/// - Cancellation checkpoints for cancel-safe handlers
1375/// - Cooperative budget/deadline visibility and checkpoints
1376/// - Access to the caller-supplied `Cx` for runtime primitives
1377/// - Sampling capability for LLM completions (if client supports it)
1378/// - Elicitation capability for user input requests (if client supports it)
1379/// - Cross-component resource reading (if router is attached)
1380///
1381/// Every constructor establishes a new request-accounting domain. Derive
1382/// another context for the same request by cloning the originating
1383/// `McpContext` and applying consuming builders; constructing a new context
1384/// around a cloned [`Cx`] would reset FastMCP's request-local accounting.
1385///
1386/// # Example
1387///
1388/// ```ignore
1389/// async fn my_tool(ctx: &McpContext, args: MyArgs) -> McpResult<Value> {
1390///     // Check for client disconnect
1391///     ctx.checkpoint()?;
1392///
1393///     // Do work with budget awareness
1394///     let remaining = ctx.budget();
1395///
1396///     // Request an LLM completion (if available)
1397///     let response = ctx.sample("Write a haiku about Rust", 100).await?;
1398///
1399///     // Request user input (if available)
1400///     let input = ctx.elicit_form("Enter your name", schema).await?;
1401///
1402///     // Read a resource from within a tool
1403///     let config = ctx.read_resource("config://app").await?;
1404///
1405///     // Call another tool from within a tool
1406///     let result = ctx.call_tool("other_tool", json!({"arg": "value"})).await?;
1407///
1408///     // Return result
1409///     Ok(json!({"result": response.text}))
1410/// }
1411/// ```
1412#[derive(Clone)]
1413pub struct McpContext {
1414    /// The underlying capability context.
1415    cx: Cx,
1416    /// An optional framework-owned ceiling applied to the ambient budget.
1417    ///
1418    /// This is composed with `cx.budget()` on every read so an inner operation
1419    /// can tighten, but never relax, the caller's current budget.
1420    budget_state: Arc<Mutex<FrameworkBudgetState>>,
1421    /// Mask depth for framework-owned ceilings. The underlying Cx tracks its
1422    /// own cancellation mask, but it cannot see a ceiling held only here.
1423    framework_mask_depth: Arc<AtomicU32>,
1424    /// Serializes transitions between the Cx mask and the framework mask.
1425    ///
1426    /// Checkpoint and cost-accounting operations take this lock while they
1427    /// observe both layers, so they cannot see a half-entered or half-exited
1428    /// mask when another clone enters [`McpContext::masked`].
1429    mask_transition: Arc<Mutex<()>>,
1430    /// Operation-local absolute deadline inherited by ordinary clones.
1431    ///
1432    /// Unlike the request budget ledger, this value is intentionally not
1433    /// shared when a consuming builder tightens a derived context. A nested
1434    /// handler timeout must bound that handler without permanently shortening
1435    /// its parent's remaining request lifetime.
1436    operation_deadline: Option<Time>,
1437    /// Clone-shared request capability lease.
1438    ///
1439    /// Server dispatch closes this lease when the request finishes. FastMCP
1440    /// capability calls begun after closure are rejected. This is not a drain
1441    /// barrier for an already-running call and cannot revoke direct access to
1442    /// the caller-owned [`Cx`]; request-owned runtime isolation remains a
1443    /// separate lifecycle requirement.
1444    request_lease: Arc<AtomicU8>,
1445    /// FastMCP-owned cooperative cancellation for this request domain.
1446    request_cancellation: McpRequestCancellation,
1447    /// Unique request identifier for tracing (from JSON-RPC id).
1448    request_id: u64,
1449    /// Whether this request was admitted on the MCP 2026-07-28 surface.
1450    ///
1451    /// Nested component access follows this surface so a modern parent cannot
1452    /// bypass final argument admission by calling [`Self::get_prompt`].
1453    final_request_surface: bool,
1454    /// Optional progress reporter for long-running operations.
1455    progress_reporter: Option<ProgressReporter>,
1456    /// Session state for per-session key-value storage.
1457    state: Option<SessionState>,
1458    /// Session cache partition captured when cache lookup is admitted.
1459    cache_admission_partition: Arc<Mutex<Option<([u8; 32], u64)>>>,
1460    /// Cache middleware instances that short-circuited response generation.
1461    response_cache_hits: Arc<Mutex<Vec<u64>>>,
1462    /// Request-scoped authentication context.
1463    auth: Arc<Mutex<Option<AuthContext>>>,
1464    /// Write-once authentication admission state, including committed anonymous
1465    /// requests whose handler-visible [`Self::auth`] value remains `None`.
1466    auth_state: Arc<AtomicU8>,
1467    /// Optional sampling sender for LLM completions.
1468    sampling_sender: Option<Arc<dyn SamplingSender>>,
1469    /// Optional elicitation sender for user input requests.
1470    elicitation_sender: Option<Arc<dyn ElicitationSender>>,
1471    /// Optional roots provider for filesystem boundaries exposed by the client.
1472    roots_provider: Option<Arc<dyn RootsProvider>>,
1473    /// Optional resource reader for cross-component access.
1474    resource_reader: Option<Arc<dyn ResourceReader>>,
1475    /// Current resource read depth (to prevent infinite recursion).
1476    resource_read_depth: u32,
1477    /// Optional tool caller for cross-component access.
1478    tool_caller: Option<Arc<dyn ToolCaller>>,
1479    /// Current tool call depth (to prevent infinite recursion).
1480    tool_call_depth: u32,
1481    /// Optional prompt caller for cross-component access.
1482    prompt_caller: Option<Arc<dyn PromptCaller>>,
1483    /// Current prompt get depth (to prevent infinite recursion).
1484    prompt_get_depth: u32,
1485    /// Client capability information.
1486    client_capabilities: Option<ClientCapabilityInfo>,
1487    /// Self-reported modern client Implementation identity, when advertised.
1488    client_implementation: Option<ClientImplementationInfo>,
1489    /// Server capability information.
1490    server_capabilities: Option<ServerCapabilityInfo>,
1491    /// Optional log sender for `notifications/message`.
1492    log_sender: Option<Arc<dyn NotificationSender>>,
1493    /// Minimum severity the connected client asked to receive.
1494    ///
1495    /// `None` means the client has not sent `logging/setLevel`; MCP forbids
1496    /// emitting log notifications until that floor exists.
1497    min_log_level: Option<McpLogLevel>,
1498    /// Resource URIs this session has subscribed to.
1499    resource_subscriptions: Option<Arc<std::collections::HashSet<String>>>,
1500    /// Optional publisher for modern `subscriptions/listen` catalog events.
1501    catalog_publisher: Option<Arc<dyn CatalogChangePublisher>>,
1502}
1503
1504impl std::fmt::Debug for McpContext {
1505    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1506        let budget_state = *self
1507            .budget_state
1508            .lock()
1509            .unwrap_or_else(std::sync::PoisonError::into_inner);
1510        f.debug_struct("McpContext")
1511            .field("cx", &self.cx)
1512            .field("budget_ceiling", &budget_state.ceiling)
1513            .field("ambient_poll_debits", &budget_state.ambient_poll_debits)
1514            .field("ambient_cost_debits", &budget_state.ambient_cost_debits)
1515            .field("deferred_overrun", &budget_state.deferred_overrun)
1516            .field(
1517                "framework_mask_depth",
1518                &self.framework_mask_depth.load(Ordering::Relaxed),
1519            )
1520            .field("operation_deadline", &self.operation_deadline)
1521            .field("request_lease_active", &self.request_scope_is_active())
1522            .field(
1523                "request_cancel_requested",
1524                &self.request_cancellation.is_cancel_requested(),
1525            )
1526            .field("request_id", &self.request_id)
1527            .field("final_request_surface", &self.final_request_surface)
1528            .field("progress_reporter", &self.progress_reporter)
1529            .field("state", &self.state.is_some())
1530            .field(
1531                "cache_admission_partition",
1532                &self
1533                    .cache_admission_partition
1534                    .lock()
1535                    .unwrap_or_else(std::sync::PoisonError::into_inner)
1536                    .is_some(),
1537            )
1538            .field(
1539                "response_cache_hit_count",
1540                &self
1541                    .response_cache_hits
1542                    .lock()
1543                    .unwrap_or_else(std::sync::PoisonError::into_inner)
1544                    .len(),
1545            )
1546            .field(
1547                "auth",
1548                &self
1549                    .auth
1550                    .lock()
1551                    .unwrap_or_else(std::sync::PoisonError::into_inner)
1552                    .is_some(),
1553            )
1554            .field(
1555                "auth_committed",
1556                &(self.auth_state.load(Ordering::Acquire) != REQUEST_AUTH_UNCOMMITTED),
1557            )
1558            .field("sampling_sender", &self.sampling_sender.is_some())
1559            .field("elicitation_sender", &self.elicitation_sender.is_some())
1560            .field("roots_provider", &self.roots_provider.is_some())
1561            .field("resource_reader", &self.resource_reader.is_some())
1562            .field("resource_read_depth", &self.resource_read_depth)
1563            .field("tool_caller", &self.tool_caller.is_some())
1564            .field("tool_call_depth", &self.tool_call_depth)
1565            .field("prompt_caller", &self.prompt_caller.is_some())
1566            .field("prompt_get_depth", &self.prompt_get_depth)
1567            .field("client_capabilities", &self.client_capabilities)
1568            .field("client_implementation", &self.client_implementation)
1569            .field("server_capabilities", &self.server_capabilities)
1570            .field("log_sender", &self.log_sender.is_some())
1571            .field("min_log_level", &self.min_log_level)
1572            .field(
1573                "resource_subscription_count",
1574                &self
1575                    .resource_subscriptions
1576                    .as_ref()
1577                    .map_or(0, |uris| uris.len()),
1578            )
1579            .field("catalog_publisher", &self.catalog_publisher.is_some())
1580            .finish()
1581    }
1582}
1583
1584#[derive(Clone, Copy, Debug, Default)]
1585struct FrameworkBudgetState {
1586    ceiling: Option<Budget>,
1587    /// Cumulative request-local poll units charged against the ambient Cx
1588    /// snapshot without mutating its clone-shared runtime budget.
1589    ambient_poll_debits: u32,
1590    /// Cumulative request-local cost charged against the ambient Cx snapshot.
1591    ///
1592    /// Asupersync 0.3.9 exposes the ambient budget as a read-only snapshot, so
1593    /// FastMCP cannot mutate the supplied Cx's internal quota. Keeping the
1594    /// cumulative debit here makes admission real and clone-shared without
1595    /// claiming ownership of, or cancelling, that ambient context.
1596    ambient_cost_debits: u64,
1597    /// A masked admission attempted to exceed a finite framework/ambient
1598    /// dimension. Exact depletion to zero is valid; only an actual overrun
1599    /// sets this terminal request-local condition.
1600    deferred_overrun: bool,
1601}
1602
1603impl FrameworkBudgetState {
1604    fn adjusted_ambient(self, mut ambient: Budget) -> Budget {
1605        if ambient.poll_quota != u32::MAX {
1606            ambient.poll_quota = ambient.poll_quota.saturating_sub(self.ambient_poll_debits);
1607        }
1608        if let Some(remaining) = ambient.cost_quota.as_mut() {
1609            *remaining = remaining.saturating_sub(self.ambient_cost_debits);
1610        }
1611        ambient
1612    }
1613
1614    fn effective(self, ambient: Budget) -> Budget {
1615        let ambient = self.adjusted_ambient(ambient);
1616        self.ceiling
1617            .map_or(ambient, |ceiling| ambient.meet(ceiling))
1618    }
1619}
1620
1621struct FrameworkMaskGuard<'a> {
1622    depth: &'a AtomicU32,
1623}
1624
1625/// RAII owner for a server-installed [`McpContext`] request lease.
1626///
1627/// This is an internal cross-crate integration type. Dropping it rejects new
1628/// FastMCP capability calls from every context clone in the request domain.
1629#[doc(hidden)]
1630pub struct McpContextLeaseGuard {
1631    lease: Arc<AtomicU8>,
1632}
1633
1634impl std::fmt::Debug for McpContextLeaseGuard {
1635    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1636        f.debug_struct("McpContextLeaseGuard")
1637            .field(
1638                "active",
1639                &(self.lease.load(Ordering::Acquire) == REQUEST_LEASE_ACTIVE),
1640            )
1641            .finish()
1642    }
1643}
1644
1645impl Drop for McpContextLeaseGuard {
1646    fn drop(&mut self) {
1647        self.lease.store(REQUEST_LEASE_CLOSED, Ordering::Release);
1648    }
1649}
1650
1651impl Drop for FrameworkMaskGuard<'_> {
1652    fn drop(&mut self) {
1653        self.depth.fetch_sub(1, Ordering::SeqCst);
1654    }
1655}
1656
1657impl McpContext {
1658    /// Creates a new MCP context from an asupersync Cx.
1659    ///
1660    /// This wraps the supplied context; it does not create or own a child
1661    /// region. Request-owned cancellation/drain must come from the caller's
1662    /// runtime lifecycle. This constructor establishes a new request-accounting
1663    /// domain even when `cx` is itself a clone. Same-request derivations must
1664    /// clone the resulting `McpContext` and use its consuming builders.
1665    #[must_use]
1666    pub fn new(cx: Cx, request_id: u64) -> Self {
1667        Self {
1668            cx,
1669            budget_state: Arc::new(Mutex::new(FrameworkBudgetState::default())),
1670            framework_mask_depth: Arc::new(AtomicU32::new(0)),
1671            mask_transition: Arc::new(Mutex::new(())),
1672            operation_deadline: None,
1673            request_lease: Arc::new(AtomicU8::new(REQUEST_LEASE_UNMANAGED)),
1674            request_cancellation: McpRequestCancellation::new(),
1675            request_id,
1676            final_request_surface: false,
1677            progress_reporter: None,
1678            state: None,
1679            cache_admission_partition: Arc::new(Mutex::new(None)),
1680            response_cache_hits: Arc::new(Mutex::new(Vec::new())),
1681            auth: Arc::new(Mutex::new(None)),
1682            auth_state: Arc::new(AtomicU8::new(REQUEST_AUTH_UNCOMMITTED)),
1683            sampling_sender: None,
1684            elicitation_sender: None,
1685            roots_provider: None,
1686            resource_reader: None,
1687            resource_read_depth: 0,
1688            tool_caller: None,
1689            tool_call_depth: 0,
1690            prompt_caller: None,
1691            prompt_get_depth: 0,
1692            client_capabilities: None,
1693            client_implementation: None,
1694            server_capabilities: None,
1695            log_sender: None,
1696            min_log_level: None,
1697            resource_subscriptions: None,
1698            catalog_publisher: None,
1699        }
1700    }
1701
1702    /// Creates a new MCP context with session state.
1703    ///
1704    /// Use this constructor when session state should be accessible to handlers.
1705    /// It establishes a new request-accounting domain; clone an existing
1706    /// `McpContext` when deriving another context for the same request.
1707    #[must_use]
1708    pub fn with_state(cx: Cx, request_id: u64, state: SessionState) -> Self {
1709        Self {
1710            cx,
1711            budget_state: Arc::new(Mutex::new(FrameworkBudgetState::default())),
1712            framework_mask_depth: Arc::new(AtomicU32::new(0)),
1713            mask_transition: Arc::new(Mutex::new(())),
1714            operation_deadline: None,
1715            request_lease: Arc::new(AtomicU8::new(REQUEST_LEASE_UNMANAGED)),
1716            request_cancellation: McpRequestCancellation::new(),
1717            request_id,
1718            final_request_surface: false,
1719            progress_reporter: None,
1720            state: Some(state),
1721            cache_admission_partition: Arc::new(Mutex::new(None)),
1722            response_cache_hits: Arc::new(Mutex::new(Vec::new())),
1723            auth: Arc::new(Mutex::new(None)),
1724            auth_state: Arc::new(AtomicU8::new(REQUEST_AUTH_UNCOMMITTED)),
1725            sampling_sender: None,
1726            elicitation_sender: None,
1727            roots_provider: None,
1728            resource_reader: None,
1729            resource_read_depth: 0,
1730            tool_caller: None,
1731            tool_call_depth: 0,
1732            prompt_caller: None,
1733            prompt_get_depth: 0,
1734            client_capabilities: None,
1735            client_implementation: None,
1736            server_capabilities: None,
1737            log_sender: None,
1738            min_log_level: None,
1739            resource_subscriptions: None,
1740            catalog_publisher: None,
1741        }
1742    }
1743
1744    /// Creates a new MCP context with progress reporting enabled.
1745    ///
1746    /// Use this constructor when the client has provided a progress token
1747    /// and expects progress notifications. It establishes a new
1748    /// request-accounting domain; use [`Self::with_progress_reporter`] on a
1749    /// clone when attaching progress reporting within an existing request.
1750    #[must_use]
1751    pub fn with_progress(cx: Cx, request_id: u64, reporter: ProgressReporter) -> Self {
1752        Self {
1753            cx,
1754            budget_state: Arc::new(Mutex::new(FrameworkBudgetState::default())),
1755            framework_mask_depth: Arc::new(AtomicU32::new(0)),
1756            mask_transition: Arc::new(Mutex::new(())),
1757            operation_deadline: None,
1758            request_lease: Arc::new(AtomicU8::new(REQUEST_LEASE_UNMANAGED)),
1759            request_cancellation: McpRequestCancellation::new(),
1760            request_id,
1761            final_request_surface: false,
1762            progress_reporter: Some(reporter),
1763            state: None,
1764            cache_admission_partition: Arc::new(Mutex::new(None)),
1765            response_cache_hits: Arc::new(Mutex::new(Vec::new())),
1766            auth: Arc::new(Mutex::new(None)),
1767            auth_state: Arc::new(AtomicU8::new(REQUEST_AUTH_UNCOMMITTED)),
1768            sampling_sender: None,
1769            elicitation_sender: None,
1770            roots_provider: None,
1771            resource_reader: None,
1772            resource_read_depth: 0,
1773            tool_caller: None,
1774            tool_call_depth: 0,
1775            prompt_caller: None,
1776            prompt_get_depth: 0,
1777            client_capabilities: None,
1778            client_implementation: None,
1779            server_capabilities: None,
1780            log_sender: None,
1781            min_log_level: None,
1782            resource_subscriptions: None,
1783            catalog_publisher: None,
1784        }
1785    }
1786
1787    /// Creates a new MCP context with both state and progress reporting.
1788    ///
1789    /// This constructor establishes a new request-accounting domain. Clone an
1790    /// existing `McpContext` and apply consuming builders when deriving another
1791    /// context for the same request.
1792    #[must_use]
1793    pub fn with_state_and_progress(
1794        cx: Cx,
1795        request_id: u64,
1796        state: SessionState,
1797        reporter: ProgressReporter,
1798    ) -> Self {
1799        Self {
1800            cx,
1801            budget_state: Arc::new(Mutex::new(FrameworkBudgetState::default())),
1802            framework_mask_depth: Arc::new(AtomicU32::new(0)),
1803            mask_transition: Arc::new(Mutex::new(())),
1804            operation_deadline: None,
1805            request_lease: Arc::new(AtomicU8::new(REQUEST_LEASE_UNMANAGED)),
1806            request_cancellation: McpRequestCancellation::new(),
1807            request_id,
1808            final_request_surface: false,
1809            progress_reporter: Some(reporter),
1810            state: Some(state),
1811            cache_admission_partition: Arc::new(Mutex::new(None)),
1812            response_cache_hits: Arc::new(Mutex::new(Vec::new())),
1813            auth: Arc::new(Mutex::new(None)),
1814            auth_state: Arc::new(AtomicU8::new(REQUEST_AUTH_UNCOMMITTED)),
1815            sampling_sender: None,
1816            elicitation_sender: None,
1817            roots_provider: None,
1818            resource_reader: None,
1819            resource_read_depth: 0,
1820            tool_caller: None,
1821            tool_call_depth: 0,
1822            prompt_caller: None,
1823            prompt_get_depth: 0,
1824            client_capabilities: None,
1825            client_implementation: None,
1826            server_capabilities: None,
1827            log_sender: None,
1828            min_log_level: None,
1829            resource_subscriptions: None,
1830            catalog_publisher: None,
1831        }
1832    }
1833
1834    /// Attaches a progress reporter without changing the request-accounting domain.
1835    ///
1836    /// This consuming builder preserves the shared budget, mask, authentication,
1837    /// and other request-scoped state inherited from the context being consumed.
1838    /// Use it on a clone when deriving a progress-enabled context for the same
1839    /// request.
1840    #[must_use]
1841    pub fn with_progress_reporter(mut self, reporter: ProgressReporter) -> Self {
1842        self.progress_reporter = Some(reporter);
1843        self
1844    }
1845
1846    /// Installs the sender used by [`Self::info`] and the other log helpers.
1847    #[must_use]
1848    pub fn with_log_sender(mut self, sender: Arc<dyn NotificationSender>) -> Self {
1849        self.log_sender = Some(sender);
1850        self
1851    }
1852
1853    /// Sets the client-selected minimum log level for this request.
1854    ///
1855    /// `None` keeps log notifications suppressed, matching MCP's rule that a
1856    /// server must not emit `notifications/message` until `logging/setLevel`.
1857    #[must_use]
1858    pub fn with_min_log_level(mut self, level: Option<McpLogLevel>) -> Self {
1859        self.min_log_level = level;
1860        self
1861    }
1862
1863    /// Returns the client-selected minimum log level for this request.
1864    ///
1865    /// `None` means the client has not opted into `notifications/message`.
1866    #[must_use]
1867    pub fn min_log_level(&self) -> Option<McpLogLevel> {
1868        self.min_log_level
1869    }
1870
1871    /// Records the resource URIs this session has subscribed to.
1872    #[must_use]
1873    pub fn with_resource_subscriptions(
1874        mut self,
1875        uris: impl IntoIterator<Item = impl Into<String>>,
1876    ) -> Self {
1877        self.resource_subscriptions = Some(Arc::new(uris.into_iter().map(Into::into).collect()));
1878        self
1879    }
1880
1881    /// Installs the modern `subscriptions/listen` catalog publisher.
1882    #[must_use]
1883    pub fn with_catalog_publisher(mut self, publisher: Arc<dyn CatalogChangePublisher>) -> Self {
1884        self.catalog_publisher = Some(publisher);
1885        self
1886    }
1887
1888    /// Sets the sampling sender for this context.
1889    ///
1890    /// This enables the `sample()` method to request LLM completions from
1891    /// the client.
1892    #[must_use]
1893    pub fn with_sampling(mut self, sender: Arc<dyn SamplingSender>) -> Self {
1894        self.sampling_sender = Some(sender);
1895        self
1896    }
1897
1898    /// Sets the elicitation sender for this context.
1899    ///
1900    /// This enables the `elicit()` methods to request user input from
1901    /// the client.
1902    #[must_use]
1903    pub fn with_elicitation(mut self, sender: Arc<dyn ElicitationSender>) -> Self {
1904        self.elicitation_sender = Some(sender);
1905        self
1906    }
1907
1908    /// Sets the roots provider for this context.
1909    ///
1910    /// This enables [`list_roots`](Self::list_roots) for the current request.
1911    #[must_use]
1912    pub fn with_roots_provider(mut self, provider: Arc<dyn RootsProvider>) -> Self {
1913        self.roots_provider = Some(provider);
1914        self
1915    }
1916
1917    /// Tightens the budget visible through this MCP context.
1918    ///
1919    /// The supplied ceiling is met with both the ambient [`Cx`] budget and
1920    /// any ceiling already installed on the context. Consequently,
1921    /// `Budget::INFINITE`, an absent deadline, or a later deadline cannot
1922    /// relax a tighter caller-owned limit. The ceiling and its remaining
1923    /// quotas are request-owned and shared by every clone of this context;
1924    /// tightening one clone is therefore visible to all of them.
1925    #[must_use]
1926    pub fn with_budget_ceiling(self, ceiling: Budget) -> Self {
1927        {
1928            let mut current = self
1929                .budget_state
1930                .lock()
1931                .unwrap_or_else(std::sync::PoisonError::into_inner);
1932            current.ceiling = Some(
1933                current
1934                    .ceiling
1935                    .map_or(ceiling, |budget| budget.meet(ceiling)),
1936            );
1937        }
1938        self
1939    }
1940
1941    /// Tightens only this derived operation's absolute deadline.
1942    ///
1943    /// Ordinary clones inherit the resulting deadline, but the context from
1944    /// which this consuming builder was derived is unchanged. This is the
1945    /// correct boundary for handler-local timeout metadata: nested work sees
1946    /// the tighter deadline while its parent request retains its own lifetime.
1947    /// `None` adds no deadline and can never relax an inherited one.
1948    #[must_use]
1949    pub fn with_operation_deadline(mut self, deadline: Option<Time>) -> Self {
1950        if let Some(deadline) = deadline {
1951            self.operation_deadline = Some(
1952                self.operation_deadline
1953                    .map_or(deadline, |current| current.min(deadline)),
1954            );
1955        }
1956        self
1957    }
1958
1959    /// Installs the server-created cooperative cancellation domain.
1960    ///
1961    /// This must be done before request dispatch begins. Ordinary context
1962    /// clones preserve the same domain.
1963    #[doc(hidden)]
1964    #[must_use]
1965    pub fn with_request_cancellation(mut self, cancellation: McpRequestCancellation) -> Self {
1966        // Request cancellation authority is installed exactly once, before
1967        // the server activates the request lease. A handler holding an active
1968        // clone cannot swap in a fresh token and escape peer cancellation.
1969        if self.request_lease.load(Ordering::Acquire) == REQUEST_LEASE_UNMANAGED {
1970            self.request_cancellation = cancellation;
1971        }
1972        self
1973    }
1974
1975    /// Installs a clone-shared request lease and returns the scoped context
1976    /// with its RAII owner.
1977    ///
1978    /// The server keeps the guard for exactly one dispatch. Once the guard is
1979    /// dropped, retained context clones fail liveness checks and FastMCP
1980    /// capability calls begun afterward are rejected. A context that already
1981    /// belongs to a request scope returns `None`, so an expired clone cannot
1982    /// mint fresh authority. This lease does not drain calls already in
1983    /// progress or revoke the caller-owned [`Cx`].
1984    #[doc(hidden)]
1985    #[must_use]
1986    pub fn begin_request_scope(self) -> Option<(Self, McpContextLeaseGuard)> {
1987        if self
1988            .request_lease
1989            .compare_exchange(
1990                REQUEST_LEASE_UNMANAGED,
1991                REQUEST_LEASE_ACTIVE,
1992                Ordering::AcqRel,
1993                Ordering::Acquire,
1994            )
1995            .is_err()
1996        {
1997            return None;
1998        }
1999        let guard = McpContextLeaseGuard {
2000            lease: Arc::clone(&self.request_lease),
2001        };
2002        Some((self, guard))
2003    }
2004
2005    /// Sets the resource reader for this context.
2006    ///
2007    /// This enables the `read_resource()` methods to read resources from
2008    /// within tool, resource, or prompt handlers.
2009    #[must_use]
2010    pub fn with_resource_reader(mut self, reader: Arc<dyn ResourceReader>) -> Self {
2011        self.resource_reader = Some(reader);
2012        self
2013    }
2014
2015    /// Sets the resource read depth for this context.
2016    ///
2017    /// This is used internally to track recursion depth when reading
2018    /// resources from within resource handlers.
2019    #[must_use]
2020    pub fn with_resource_read_depth(mut self, depth: u32) -> Self {
2021        self.resource_read_depth = self.resource_read_depth.max(depth);
2022        self
2023    }
2024
2025    /// Sets the tool caller for this context.
2026    ///
2027    /// This enables the `call_tool()` methods to call other tools from
2028    /// within tool, resource, or prompt handlers.
2029    #[must_use]
2030    pub fn with_tool_caller(mut self, caller: Arc<dyn ToolCaller>) -> Self {
2031        self.tool_caller = Some(caller);
2032        self
2033    }
2034
2035    /// Sets the tool call depth for this context.
2036    ///
2037    /// This is used internally to track recursion depth when calling
2038    /// tools from within tool handlers.
2039    #[must_use]
2040    pub fn with_tool_call_depth(mut self, depth: u32) -> Self {
2041        self.tool_call_depth = self.tool_call_depth.max(depth);
2042        self
2043    }
2044
2045    /// Sets the prompt caller for this context.
2046    ///
2047    /// This enables the `get_prompt()` methods to get other prompts from
2048    /// within tool, resource, or prompt handlers.
2049    #[must_use]
2050    pub fn with_prompt_caller(mut self, caller: Arc<dyn PromptCaller>) -> Self {
2051        self.prompt_caller = Some(caller);
2052        self
2053    }
2054
2055    /// Marks this context as admitted on the MCP 2026-07-28 request surface.
2056    ///
2057    /// Nested [`Self::get_prompt`] follows this flag so a modern parent keeps
2058    /// final argument admission and the final handler hook.
2059    #[must_use]
2060    pub fn with_final_request_surface(mut self, final_surface: bool) -> Self {
2061        self.final_request_surface = final_surface;
2062        self
2063    }
2064
2065    /// Returns whether this request was admitted on the MCP 2026-07-28 surface.
2066    #[must_use]
2067    pub fn is_final_request_surface(&self) -> bool {
2068        self.final_request_surface
2069    }
2070
2071    /// Sets the prompt get depth for this context.
2072    ///
2073    /// This is used internally to track recursion depth when getting
2074    /// prompts from within handlers.
2075    #[must_use]
2076    pub fn with_prompt_get_depth(mut self, depth: u32) -> Self {
2077        self.prompt_get_depth = self.prompt_get_depth.max(depth);
2078        self
2079    }
2080
2081    /// Sets the client capability information for this context.
2082    ///
2083    /// This enables handlers to check what capabilities the connected
2084    /// client supports.
2085    #[must_use]
2086    pub fn with_client_capabilities(mut self, capabilities: ClientCapabilityInfo) -> Self {
2087        self.client_capabilities = Some(capabilities);
2088        self
2089    }
2090
2091    /// Attaches the self-reported modern client Implementation identity.
2092    #[must_use]
2093    pub fn with_client_implementation(mut self, identity: ClientImplementationInfo) -> Self {
2094        self.client_implementation = Some(identity);
2095        self
2096    }
2097
2098    /// Sets the server capability information for this context.
2099    ///
2100    /// This enables handlers to check what capabilities this server
2101    /// advertises.
2102    #[must_use]
2103    pub fn with_server_capabilities(mut self, capabilities: ServerCapabilityInfo) -> Self {
2104        self.server_capabilities = Some(capabilities);
2105        self
2106    }
2107
2108    /// Returns whether progress reporting is enabled for this context.
2109    #[must_use]
2110    pub fn has_progress_reporter(&self) -> bool {
2111        self.ensure_live().is_ok() && self.progress_reporter.is_some()
2112    }
2113
2114    /// Returns the progress marker installed for this request, when available.
2115    ///
2116    /// A reporter without a marker cannot establish ownership of upstream
2117    /// progress frames and therefore must not cause proxy forwarding.
2118    #[must_use]
2119    pub fn progress_marker(&self) -> Option<&serde_json::Value> {
2120        self.ensure_live()
2121            .ok()
2122            .and_then(|()| self.progress_reporter.as_ref()?.marker())
2123    }
2124
2125    /// Reports progress on the current operation.
2126    ///
2127    /// If progress reporting is not enabled (no progress token was provided),
2128    /// this method does nothing.
2129    ///
2130    /// # Arguments
2131    ///
2132    /// * `progress` - Current progress value (0.0 to 1.0 for fractional progress)
2133    /// * `message` - Optional message describing current status
2134    ///
2135    /// # Example
2136    ///
2137    /// ```ignore
2138    /// async fn process_files(ctx: &McpContext, files: &[File]) -> McpResult<()> {
2139    ///     for (i, file) in files.iter().enumerate() {
2140    ///         ctx.report_progress(i as f64 / files.len() as f64, Some("Processing files"));
2141    ///         process_file(file).await?;
2142    ///     }
2143    ///     ctx.report_progress(1.0, Some("Complete"));
2144    ///     Ok(())
2145    /// }
2146    /// ```
2147    pub fn report_progress(&self, progress: f64, message: Option<&str>) {
2148        if self.ensure_live().is_ok()
2149            && let Some(ref reporter) = self.progress_reporter
2150        {
2151            reporter.report(progress, message);
2152        }
2153    }
2154
2155    /// Reports progress with explicit total for determinate progress bars.
2156    ///
2157    /// If progress reporting is not enabled, this method does nothing.
2158    ///
2159    /// # Arguments
2160    ///
2161    /// * `progress` - Current progress value
2162    /// * `total` - Total expected value
2163    /// * `message` - Optional message describing current status
2164    ///
2165    /// # Example
2166    ///
2167    /// ```ignore
2168    /// async fn process_items(ctx: &McpContext, items: &[Item]) -> McpResult<()> {
2169    ///     let total = items.len() as f64;
2170    ///     for (i, item) in items.iter().enumerate() {
2171    ///         ctx.report_progress_with_total(i as f64, total, Some(&format!("Item {}", i)));
2172    ///         process_item(item).await?;
2173    ///     }
2174    ///     Ok(())
2175    /// }
2176    /// ```
2177    pub fn report_progress_with_total(&self, progress: f64, total: f64, message: Option<&str>) {
2178        if self.ensure_live().is_ok()
2179            && let Some(ref reporter) = self.progress_reporter
2180        {
2181            reporter.report_with_total(progress, total, message);
2182        }
2183    }
2184
2185    /// Reports final progress while retaining the caller's exact JSON-number
2186    /// lexemes.
2187    ///
2188    /// This is a no-op unless the current request installed a final-capable
2189    /// progress reporter. The legacy `f64` progress APIs remain unchanged.
2190    pub fn report_progress_exact(
2191        &self,
2192        progress: serde_json::Number,
2193        total: Option<serde_json::Number>,
2194        message: Option<&str>,
2195    ) {
2196        if self.ensure_live().is_ok()
2197            && let Some(ref reporter) = self.progress_reporter
2198        {
2199            reporter.report_exact(progress, total, message);
2200        }
2201    }
2202
2203    /// Returns the unique request identifier.
2204    ///
2205    /// This corresponds to the JSON-RPC request ID and is useful for
2206    /// logging and tracing across the request lifecycle.
2207    #[must_use]
2208    pub fn request_id(&self) -> u64 {
2209        self.request_id
2210    }
2211
2212    /// Returns the underlying region ID from asupersync.
2213    ///
2214    /// This is the region of the caller-supplied [`Cx`]. FastMCP does not
2215    /// currently create a request-owned child region, so this identifier must
2216    /// not be interpreted as proof that spawned work is scoped to, cancelled
2217    /// with, or drained before completion of this MCP request.
2218    #[must_use]
2219    pub fn region_id(&self) -> RegionId {
2220        self.cx.region_id()
2221    }
2222
2223    /// Returns the current task ID.
2224    #[must_use]
2225    pub fn task_id(&self) -> TaskId {
2226        self.cx.task_id()
2227    }
2228
2229    fn apply_operation_deadline(&self, budget: Budget) -> Budget {
2230        self.operation_deadline.map_or(budget, |deadline| {
2231            budget.meet(Budget::new().with_deadline(deadline))
2232        })
2233    }
2234
2235    fn request_scope_is_active(&self) -> bool {
2236        self.request_lease.load(Ordering::Acquire) != REQUEST_LEASE_CLOSED
2237    }
2238
2239    /// Returns the current budget.
2240    ///
2241    /// The budget is a remaining-balance snapshot. A zero poll or cost balance
2242    /// records exact depletion; it does not retroactively fail the operation
2243    /// that consumed the final unit. Use [`ensure_live`](Self::ensure_live) for
2244    /// terminal liveness and [`checkpoint`](Self::checkpoint) or
2245    /// [`consume_cost`](Self::consume_cost) for dimension-specific admission.
2246    #[must_use]
2247    pub fn budget(&self) -> Budget {
2248        let ambient = self.cx.budget();
2249        let state = *self
2250            .budget_state
2251            .lock()
2252            .unwrap_or_else(std::sync::PoisonError::into_inner);
2253        self.apply_operation_deadline(state.effective(ambient))
2254    }
2255
2256    /// Checks if cancellation has been requested.
2257    ///
2258    /// This includes client disconnection, timeout, or explicit cancellation.
2259    /// Handlers should check this periodically and exit early if true.
2260    #[must_use]
2261    pub fn is_cancelled(&self) -> bool {
2262        self.ensure_live().is_err()
2263    }
2264
2265    /// Returns the cooperative cancellation domain owned by this request.
2266    ///
2267    /// Long-running framework integrations may await this handle instead of
2268    /// polling [`Self::is_cancelled`].  Cloning the handle never grants a way
2269    /// to replace the request's cancellation authority; it observes the same
2270    /// request-local transition installed before dispatch.
2271    #[must_use]
2272    pub fn request_cancellation(&self) -> McpRequestCancellation {
2273        self.request_cancellation.clone()
2274    }
2275
2276    /// Checks terminal request liveness without charging a poll or cost unit.
2277    ///
2278    /// A finite quota that was exactly depleted is not itself a failed
2279    /// operation. The next dimension-specific admission fails when it asks for
2280    /// unavailable work. This method rejects only explicit cancellation, an
2281    /// expired effective deadline, or a real overrun deferred by
2282    /// [`masked`](Self::masked). Explicit cancellation includes both the
2283    /// caller-owned [`Cx`] signal and FastMCP's request-local cooperative
2284    /// signal; the latter never mutates the ambient context.
2285    ///
2286    /// # Errors
2287    ///
2288    /// Returns [`CancelledError`] when a terminal liveness condition is
2289    /// observable outside a cancellation mask.
2290    pub fn ensure_live(&self) -> Result<(), CancelledError> {
2291        if !self.request_scope_is_active() {
2292            return Err(CancelledError);
2293        }
2294        let _mask_transition = self
2295            .mask_transition
2296            .lock()
2297            .unwrap_or_else(std::sync::PoisonError::into_inner);
2298        if self.framework_mask_depth.load(Ordering::SeqCst) > 0 {
2299            return Ok(());
2300        }
2301
2302        let ambient = self.cx.budget();
2303        let now = self.cx.now();
2304        let state = *self
2305            .budget_state
2306            .lock()
2307            .unwrap_or_else(std::sync::PoisonError::into_inner);
2308        let effective = self.apply_operation_deadline(state.effective(ambient));
2309        if self.request_cancellation.is_cancel_requested()
2310            || self.cx.is_cancel_requested()
2311            || effective.is_past_deadline(now)
2312            || state.deferred_overrun
2313        {
2314            return Err(CancelledError);
2315        }
2316        Ok(())
2317    }
2318
2319    /// Cooperative cancellation checkpoint.
2320    ///
2321    /// Call this at natural suspension points in your handler to allow
2322    /// graceful cancellation. Returns `Err` if cancellation is pending.
2323    ///
2324    /// # Errors
2325    ///
2326    /// Returns an error if the request has been cancelled and cancellation
2327    /// is not currently masked. Each admitted checkpoint consumes one unit
2328    /// from both a finite framework-owned poll ceiling and a finite ambient
2329    /// [`Cx`] poll snapshot. FastMCP records ambient debits in a clone-shared
2330    /// request ledger; it does not mutate or cancel the caller-owned context.
2331    ///
2332    /// This method intentionally does not call [`Cx::checkpoint`]. In the
2333    /// pinned runtime that API treats a zero cost balance as aggregate budget
2334    /// exhaustion and mutates the clone-shared cancellation state, even though
2335    /// this operation admits only the poll dimension. FastMCP observes the
2336    /// supplied context's cancellation flag and deadline without poisoning a
2337    /// caller-owned context shared by other request domains.
2338    ///
2339    /// # Example
2340    ///
2341    /// ```ignore
2342    /// async fn process_items(ctx: &McpContext, items: Vec<Item>) -> McpResult<()> {
2343    ///     for item in items {
2344    ///         ctx.checkpoint()?;  // Allow cancellation between items
2345    ///         process_item(item).await?;
2346    ///     }
2347    ///     Ok(())
2348    /// }
2349    /// ```
2350    pub fn checkpoint(&self) -> Result<(), CancelledError> {
2351        if !self.request_scope_is_active() {
2352            return Err(CancelledError);
2353        }
2354        let _mask_transition = self
2355            .mask_transition
2356            .lock()
2357            .unwrap_or_else(std::sync::PoisonError::into_inner);
2358        let masked = self.framework_mask_depth.load(Ordering::SeqCst) > 0;
2359        let ambient = self.cx.budget();
2360        let now = self.cx.now();
2361        let mut state = self
2362            .budget_state
2363            .lock()
2364            .unwrap_or_else(std::sync::PoisonError::into_inner);
2365        let adjusted_ambient = state.adjusted_ambient(ambient);
2366        let effective = self.apply_operation_deadline(
2367            state
2368                .ceiling
2369                .map_or(adjusted_ambient, |ceiling| adjusted_ambient.meet(ceiling)),
2370        );
2371        let poll_unavailable = effective.poll_quota == 0;
2372        let past_deadline = effective.is_past_deadline(now);
2373        let cancelled =
2374            self.request_cancellation.is_cancel_requested() || self.cx.is_cancel_requested();
2375        let deferred_overrun = state.deferred_overrun;
2376
2377        if !masked && (cancelled || poll_unavailable || past_deadline || deferred_overrun) {
2378            return Err(CancelledError);
2379        }
2380
2381        if poll_unavailable {
2382            debug_assert!(masked);
2383            state.deferred_overrun = true;
2384        }
2385
2386        if adjusted_ambient.poll_quota != u32::MAX {
2387            state.ambient_poll_debits = state.ambient_poll_debits.saturating_add(1);
2388        }
2389
2390        if let Some(budget) = state.ceiling.as_mut()
2391            && budget.poll_quota != u32::MAX
2392        {
2393            if budget.consume_poll().is_none() {
2394                debug_assert!(masked);
2395                state.deferred_overrun = true;
2396            }
2397        }
2398
2399        Ok(())
2400    }
2401
2402    /// Debits abstract cost units from the request budget.
2403    ///
2404    /// Cost is application-defined and is deliberately separate from poll
2405    /// accounting: [`checkpoint`](Self::checkpoint) never guesses an
2406    /// operation's cost. A successful debit is visible through
2407    /// [`budget`](Self::budget) and every clone of this context. If the
2408    /// request is explicitly cancelled or expired, or the effective ambient/
2409    /// ceiling cost budget has fewer than `cost` units remaining, this returns
2410    /// [`CancelledError`] without a partial debit. Poll accounting remains the
2411    /// responsibility of [`checkpoint`](Self::checkpoint); this method does
2412    /// not acknowledge cancellation or consume a poll checkpoint.
2413    ///
2414    /// Asupersync exposes the supplied [`Cx`] budget as a read-only snapshot.
2415    /// FastMCP therefore records cumulative, clone-shared request-local debits
2416    /// and subtracts them from the current ambient snapshot without mutating or
2417    /// cancelling the caller-owned Cx. A framework cost ceiling, when present,
2418    /// is debited independently under the same lock. A zero-unit debit succeeds
2419    /// at a zero cost quota, but still observes explicit cancellation and an
2420    /// expired deadline.
2421    ///
2422    /// Inside [`masked`](Self::masked), enforcement remains deferred just as
2423    /// it is for checkpoints. Affordable debits are still recorded, while an
2424    /// over-budget debit leaves the effective cost budget exhausted so the
2425    /// exhaustion is observed as soon as the mask is released. A nonbinding
2426    /// underlying cost dimension can still retain a positive balance.
2427    ///
2428    /// # Errors
2429    ///
2430    /// Returns an error when the debit cannot be admitted and cancellation is
2431    /// not currently masked.
2432    pub fn consume_cost(&self, cost: u64) -> Result<(), CancelledError> {
2433        if !self.request_scope_is_active() {
2434            return Err(CancelledError);
2435        }
2436        let _mask_transition = self
2437            .mask_transition
2438            .lock()
2439            .unwrap_or_else(std::sync::PoisonError::into_inner);
2440        let masked = self.framework_mask_depth.load(Ordering::SeqCst) > 0;
2441        let ambient = self.cx.budget();
2442        let now = self.cx.now();
2443        let mut state = self
2444            .budget_state
2445            .lock()
2446            .unwrap_or_else(std::sync::PoisonError::into_inner);
2447        let effective = self.apply_operation_deadline(state.effective(ambient));
2448        let enough_cost = effective
2449            .cost_quota
2450            .is_none_or(|remaining| remaining >= cost);
2451        let past_deadline = effective.is_past_deadline(now);
2452        let cancelled =
2453            self.request_cancellation.is_cancel_requested() || self.cx.is_cancel_requested();
2454
2455        if !masked && (cancelled || past_deadline || state.deferred_overrun || !enough_cost) {
2456            return Err(CancelledError);
2457        }
2458
2459        if !enough_cost {
2460            debug_assert!(masked);
2461            state.deferred_overrun = true;
2462        }
2463        state.ambient_cost_debits = state.ambient_cost_debits.saturating_add(cost);
2464        if let Some(budget) = state.ceiling.as_mut()
2465            && !budget.consume_cost(cost)
2466        {
2467            debug_assert!(masked);
2468            budget.cost_quota = Some(0);
2469        }
2470
2471        Ok(())
2472    }
2473
2474    /// Executes a closure with cancellation masked.
2475    ///
2476    /// While masked, `checkpoint()` will not return an error even if
2477    /// cancellation is pending. Use this for critical sections that
2478    /// must complete atomically.
2479    ///
2480    /// Masking is request-context-wide: this context and all of its clones
2481    /// share both the underlying [`Cx`] mask and the framework ceiling mask.
2482    /// Independently cancellable concurrent work therefore requires distinct
2483    /// runtime-owned child contexts rather than clones of one `McpContext`.
2484    ///
2485    /// This method masks only the synchronous execution of `f`. Passing an
2486    /// async block merely constructs a future while masked; polling that future
2487    /// after this method returns is not protected. Asynchronous critical
2488    /// sections require a runtime-owned structured cancellation scope.
2489    ///
2490    /// # Errors
2491    ///
2492    /// Returns [`CancelledError`] if this context's request lease has already
2493    /// closed or the framework mask depth cannot be incremented.
2494    ///
2495    /// # Example
2496    ///
2497    /// ```ignore
2498    /// // Commit transaction - must not be interrupted
2499    /// ctx.masked(|| db.commit_synchronously())?;
2500    /// ```
2501    pub fn masked<F, R>(&self, f: F) -> Result<R, CancelledError>
2502    where
2503        F: FnOnce() -> R,
2504    {
2505        if !self.request_scope_is_active() {
2506            return Err(CancelledError);
2507        }
2508        let entry_transition = self
2509            .mask_transition
2510            .lock()
2511            .unwrap_or_else(std::sync::PoisonError::into_inner);
2512        if self.framework_mask_depth.load(Ordering::SeqCst) >= MAX_MASK_DEPTH {
2513            return Err(CancelledError);
2514        }
2515        if self
2516            .framework_mask_depth
2517            .try_update(Ordering::SeqCst, Ordering::SeqCst, |depth| {
2518                depth.checked_add(1)
2519            })
2520            .is_err()
2521        {
2522            return Err(CancelledError);
2523        }
2524        let framework_mask = FrameworkMaskGuard {
2525            depth: &self.framework_mask_depth,
2526        };
2527        let masked_outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2528            self.cx.masked(|| {
2529                drop(entry_transition);
2530                let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
2531                let exit_transition = self
2532                    .mask_transition
2533                    .lock()
2534                    .unwrap_or_else(std::sync::PoisonError::into_inner);
2535                (outcome, exit_transition)
2536            })
2537        }));
2538        let (outcome, exit_transition) = match masked_outcome {
2539            Ok(result) => result,
2540            Err(_runtime_mask_failure) => {
2541                let exit_transition = self
2542                    .mask_transition
2543                    .lock()
2544                    .unwrap_or_else(std::sync::PoisonError::into_inner);
2545                drop(framework_mask);
2546                drop(exit_transition);
2547                return Err(CancelledError);
2548            }
2549        };
2550        drop(framework_mask);
2551        drop(exit_transition);
2552
2553        match outcome {
2554            Ok(result) => Ok(result),
2555            Err(payload) => std::panic::resume_unwind(payload),
2556        }
2557    }
2558
2559    /// Records a trace event for this request.
2560    ///
2561    /// Events are associated with the request's trace context and can be
2562    /// used for debugging and observability.
2563    pub fn trace(&self, message: &str) {
2564        if self.ensure_live().is_ok() {
2565            self.cx.trace(message);
2566        }
2567    }
2568
2569    /// Emits a debug `notifications/message` when the client asked for that floor.
2570    pub fn debug(&self, message: impl AsRef<str>) {
2571        self.log(McpLogLevel::Debug, message);
2572    }
2573
2574    /// Emits an info `notifications/message` when the client asked for that floor.
2575    pub fn info(&self, message: impl AsRef<str>) {
2576        self.log(McpLogLevel::Info, message);
2577    }
2578
2579    /// Emits a notice `notifications/message` when the client asked for that floor.
2580    pub fn notice(&self, message: impl AsRef<str>) {
2581        self.log(McpLogLevel::Notice, message);
2582    }
2583
2584    /// Emits a warning `notifications/message` when the client asked for that floor.
2585    pub fn warning(&self, message: impl AsRef<str>) {
2586        self.log(McpLogLevel::Warning, message);
2587    }
2588
2589    /// Emits an error `notifications/message` when the client asked for that floor.
2590    pub fn error(&self, message: impl AsRef<str>) {
2591        self.log(McpLogLevel::Error, message);
2592    }
2593
2594    /// Emits one MCP log notification if the client floor admits `level`.
2595    ///
2596    /// Missing floor, missing sender, or a cancelled request are silent
2597    /// no-ops so handlers can log without branching on transport wiring.
2598    pub fn log(&self, level: McpLogLevel, message: impl AsRef<str>) {
2599        self.log_data(
2600            level,
2601            serde_json::Value::String(message.as_ref().to_owned()),
2602        );
2603    }
2604
2605    /// Emits one MCP log notification with caller-owned JSON data.
2606    pub fn log_data(&self, level: McpLogLevel, data: serde_json::Value) {
2607        if self.ensure_live().is_err() {
2608            return;
2609        }
2610        let Some(min_level) = self.min_log_level else {
2611            return;
2612        };
2613        if level.rank() < min_level.rank() {
2614            return;
2615        }
2616        if let Some(sender) = self.log_sender.as_ref() {
2617            sender.send_log(level, Some("fastmcp"), data);
2618        }
2619    }
2620
2621    /// Notifies subscribers that `uri` changed.
2622    ///
2623    /// Returns `true` when a 2024 session subscriber received
2624    /// `notifications/resources/updated` or at least one modern
2625    /// `subscriptions/listen` stream accepted the event.
2626    pub fn notify_resource_updated(&self, uri: impl AsRef<str>) -> bool {
2627        if self.ensure_live().is_err() {
2628            return false;
2629        }
2630        let uri = uri.as_ref();
2631        let mut delivered = false;
2632        if self
2633            .resource_subscriptions
2634            .as_ref()
2635            .is_some_and(|uris| uris.contains(uri))
2636            && let Some(sender) = self.log_sender.as_ref()
2637        {
2638            sender.send_resource_updated(uri);
2639            delivered = true;
2640        }
2641        if let Some(publisher) = self.catalog_publisher.as_ref()
2642            && publisher.publish_resource_updated(uri)
2643        {
2644            delivered = true;
2645        }
2646        delivered
2647    }
2648
2649    /// Returns a reference to the underlying asupersync Cx.
2650    ///
2651    /// Use this when you need direct access to asupersync primitives,
2652    /// such as spawning tasks or using combinators. Direct Cx checkpoints and
2653    /// budget snapshots do not observe FastMCP's framework ceiling, cumulative
2654    /// cost ledger, or two-layer mask transition; request admission code must
2655    /// use [`checkpoint`](Self::checkpoint), [`consume_cost`](Self::consume_cost),
2656    /// and [`budget`](Self::budget) instead. Conversely, masking the raw `Cx`
2657    /// does not mask FastMCP admission checks: code that calls back into this
2658    /// context must use [`masked`](Self::masked). The raw handle also cannot be
2659    /// revoked when the FastMCP request lease closes, so it must not be retained
2660    /// or used as an independently owned request capability.
2661    #[must_use]
2662    pub fn cx(&self) -> &Cx {
2663        &self.cx
2664    }
2665
2666    /// Admits one final dual-era result as a four-valued MCP outcome.
2667    ///
2668    /// The result retains both its `Modern`/`Legacy` branch and the caller's
2669    /// exact terminal-reason type. This context performs its normal request
2670    /// liveness check before admitting a newly completed result, so ambient
2671    /// `Cx` cancellation, request-local cancellation, lease closure, and
2672    /// bounded framework admission continue to win without creating a runtime.
2673    #[must_use]
2674    pub fn final_result_outcome<TypedResult, LegacyResult, TerminalReason>(
2675        &self,
2676        result: crate::combinator::FinalRequestResult<TypedResult, LegacyResult, TerminalReason>,
2677    ) -> crate::McpOutcome<
2678        crate::combinator::FinalRequestResult<TypedResult, LegacyResult, TerminalReason>,
2679    > {
2680        if self.ensure_live().is_err() {
2681            return Outcome::Cancelled(self.final_result_cancellation_reason());
2682        }
2683        Outcome::Ok(result)
2684    }
2685
2686    /// Preserves an already-terminal request outcome while admitting an `Ok` final result.
2687    ///
2688    /// A supplied cancellation reason or panic payload is returned unchanged;
2689    /// only an `Ok` result is subject to the context's current liveness check.
2690    #[must_use]
2691    pub fn adapt_final_request_outcome<TypedResult, LegacyResult, TerminalReason>(
2692        &self,
2693        outcome: crate::McpOutcome<
2694            crate::combinator::FinalRequestResult<TypedResult, LegacyResult, TerminalReason>,
2695        >,
2696    ) -> crate::McpOutcome<
2697        crate::combinator::FinalRequestResult<TypedResult, LegacyResult, TerminalReason>,
2698    > {
2699        match outcome {
2700            Outcome::Ok(result) => self.final_result_outcome(result),
2701            Outcome::Err(error) => Outcome::Err(error),
2702            Outcome::Cancelled(reason) => Outcome::Cancelled(reason),
2703            Outcome::Panicked(payload) => Outcome::Panicked(payload),
2704        }
2705    }
2706
2707    fn final_result_cancellation_reason(&self) -> CancelReason {
2708        self.cx.cancel_reason().unwrap_or_else(|| {
2709            if self.request_cancellation.is_cancel_requested() {
2710                CancelReason::user("FastMCP request-local cancellation")
2711            } else if !self.request_scope_is_active() {
2712                CancelReason::user("FastMCP request lease closed")
2713            } else {
2714                CancelReason::user("FastMCP request liveness rejected final result")
2715            }
2716        })
2717    }
2718
2719    // ========================================================================
2720    // Session State Access
2721    // ========================================================================
2722
2723    /// Gets a value from session state by key.
2724    ///
2725    /// Returns `None` if:
2726    /// - Session state is not available (context created without state)
2727    /// - The key doesn't exist
2728    /// - Deserialization to type `T` fails
2729    ///
2730    /// # Example
2731    ///
2732    /// ```ignore
2733    /// async fn my_tool(ctx: &McpContext, args: MyArgs) -> McpResult<Value> {
2734    ///     // Get a counter from session state
2735    ///     let count: Option<i32> = ctx.get_state("counter");
2736    ///     let count = count.unwrap_or(0);
2737    ///     // ... use count ...
2738    ///     Ok(json!({"count": count}))
2739    /// }
2740    /// ```
2741    #[must_use]
2742    pub fn get_state<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
2743        if !self.request_scope_is_active() {
2744            return None;
2745        }
2746        self.state.as_ref()?.get(key)
2747    }
2748
2749    /// Returns the authentication context for this request, if available.
2750    #[must_use]
2751    pub fn auth(&self) -> Option<AuthContext> {
2752        if !self.request_scope_is_active() {
2753            return None;
2754        }
2755        self.auth
2756            .lock()
2757            .unwrap_or_else(std::sync::PoisonError::into_inner)
2758            .clone()
2759    }
2760
2761    /// Commits authentication context for this request if the slot is empty.
2762    ///
2763    /// The slot is write-once across all context clones. Authentication
2764    /// providers may use an isolated staging context, and the server commits
2765    /// the successful result to the shared request context. Middleware,
2766    /// handlers, and nested dispatch cannot replace that committed principal.
2767    /// Returns `false` if the request lease is closed or an identity has
2768    /// already been committed.
2769    pub fn set_auth(&self, auth: AuthContext) -> bool {
2770        if self.ensure_live().is_err() {
2771            return false;
2772        }
2773        let mut slot = self
2774            .auth
2775            .lock()
2776            .unwrap_or_else(std::sync::PoisonError::into_inner);
2777        if self.auth_state.load(Ordering::Acquire) != REQUEST_AUTH_UNCOMMITTED {
2778            return false;
2779        }
2780        *slot = Some(auth);
2781        self.auth_state
2782            .store(REQUEST_AUTH_AUTHENTICATED, Ordering::Release);
2783        true
2784    }
2785
2786    /// Commits this request as unauthenticated without exposing an empty
2787    /// [`AuthContext`] to handlers.
2788    ///
2789    /// This is a write-once internal admission marker. It prevents later
2790    /// middleware from forging handler-visible authentication while allowing
2791    /// cache middleware to distinguish admitted anonymous traffic from an
2792    /// authentication flow that has not completed.
2793    #[doc(hidden)]
2794    pub fn commit_anonymous_auth(&self) -> bool {
2795        if self.ensure_live().is_err() {
2796            return false;
2797        }
2798        let slot = self
2799            .auth
2800            .lock()
2801            .unwrap_or_else(std::sync::PoisonError::into_inner);
2802        if self.auth_state.load(Ordering::Acquire) != REQUEST_AUTH_UNCOMMITTED || slot.is_some() {
2803            return false;
2804        }
2805        self.auth_state
2806            .store(REQUEST_AUTH_ANONYMOUS, Ordering::Release);
2807        true
2808    }
2809
2810    /// Returns the committed cache authorization partition.
2811    ///
2812    /// The outer `Option` distinguishes incomplete admission from a committed
2813    /// request. The inner `Option` is `None` for anonymous admission and
2814    /// contains the complete handler-visible authenticated facts otherwise.
2815    #[doc(hidden)]
2816    #[must_use]
2817    pub fn cache_auth_partition(&self) -> Option<Option<AuthContext>> {
2818        if !self.request_scope_is_active() {
2819            return None;
2820        }
2821        let slot = self
2822            .auth
2823            .lock()
2824            .unwrap_or_else(std::sync::PoisonError::into_inner);
2825        match self.auth_state.load(Ordering::Acquire) {
2826            REQUEST_AUTH_ANONYMOUS => Some(None),
2827            REQUEST_AUTH_AUTHENTICATED => slot.clone().map(Some),
2828            _ => None,
2829        }
2830    }
2831
2832    /// Returns a cloned context with request-local auth attached.
2833    #[must_use]
2834    pub fn with_auth(self, auth: AuthContext) -> Self {
2835        let _ = self.set_auth(auth);
2836        self
2837    }
2838
2839    /// Returns a derived context with an isolated authentication staging slot.
2840    ///
2841    /// Only budget accounting, cancellation, masking, and request identity
2842    /// remain shared. Session state, nested dispatch, progress, sampling,
2843    /// elicitation, and roots are removed from the staging view so
2844    /// authentication code cannot exercise handler authority and a handler
2845    /// cannot use this method to forge a principal for nested dispatch.
2846    #[must_use]
2847    pub fn with_isolated_auth(mut self) -> Self {
2848        let already_committed = self.auth_state.load(Ordering::Acquire) != REQUEST_AUTH_UNCOMMITTED;
2849        if already_committed {
2850            return self;
2851        }
2852        self.auth = Arc::new(Mutex::new(None));
2853        self.auth_state = Arc::new(AtomicU8::new(REQUEST_AUTH_UNCOMMITTED));
2854        self.state = None;
2855        self.progress_reporter = None;
2856        self.sampling_sender = None;
2857        self.elicitation_sender = None;
2858        self.roots_provider = None;
2859        self.resource_reader = None;
2860        self.tool_caller = None;
2861        self.prompt_caller = None;
2862        self
2863    }
2864
2865    /// Sets a value in session state.
2866    ///
2867    /// The value persists across requests within the same session.
2868    /// Returns `true` if the value was successfully stored.
2869    /// Returns `false` if session state is not available or serialization fails.
2870    ///
2871    /// # Example
2872    ///
2873    /// ```ignore
2874    /// async fn my_tool(ctx: &McpContext, args: MyArgs) -> McpResult<Value> {
2875    ///     // Increment a counter in session state
2876    ///     let count: i32 = ctx.get_state("counter").unwrap_or(0);
2877    ///     ctx.set_state("counter", count + 1);
2878    ///     Ok(json!({"new_count": count + 1}))
2879    /// }
2880    /// ```
2881    pub fn set_state<T: serde::Serialize>(&self, key: impl Into<String>, value: T) -> bool {
2882        if self.ensure_live().is_err() {
2883            return false;
2884        }
2885        match &self.state {
2886            Some(state) => state.set(key, value),
2887            None => false,
2888        }
2889    }
2890
2891    /// Removes a value from session state.
2892    ///
2893    /// Returns the previous value if it existed, or `None` if:
2894    /// - Session state is not available
2895    /// - The key didn't exist
2896    pub fn remove_state(&self, key: &str) -> Option<serde_json::Value> {
2897        if self.ensure_live().is_err() {
2898            return None;
2899        }
2900        self.state.as_ref()?.remove(key)
2901    }
2902
2903    /// Checks if a key exists in session state.
2904    ///
2905    /// Returns `false` if session state is not available.
2906    #[must_use]
2907    pub fn has_state(&self, key: &str) -> bool {
2908        self.request_scope_is_active() && self.state.as_ref().is_some_and(|s| s.contains(key))
2909    }
2910
2911    /// Returns whether session state is available in this context.
2912    #[must_use]
2913    pub fn has_session_state(&self) -> bool {
2914        self.request_scope_is_active() && self.state.is_some()
2915    }
2916
2917    /// Returns whether attached session state is request-local, not durable.
2918    #[doc(hidden)]
2919    #[must_use]
2920    pub fn session_is_ephemeral(&self) -> bool {
2921        self.request_scope_is_active()
2922            && self.state.as_ref().is_some_and(SessionState::is_ephemeral)
2923    }
2924
2925    /// Returns the session state attached to this context, if any.
2926    ///
2927    /// Final dispatch uses this shared bag so a later inbound on the same
2928    /// modern connection still sees `disable_*` mutations from earlier
2929    /// requests. Cloning the returned value shares the underlying store.
2930    #[must_use]
2931    pub fn session_state(&self) -> Option<&SessionState> {
2932        self.state.as_ref()
2933    }
2934
2935    /// Returns the opaque cache partition and mutation revision for this
2936    /// request's session state.
2937    ///
2938    /// This is an internal cross-crate integration hook. It returns `None`
2939    /// when the request is no longer live or the state cannot provide a safe
2940    /// stable partition. Cache implementations must additionally partition by
2941    /// all response-relevant authenticated facts.
2942    #[doc(hidden)]
2943    #[must_use]
2944    pub fn session_cache_partition(&self) -> Option<([u8; 32], u64)> {
2945        if !self.request_scope_is_active() {
2946            return None;
2947        }
2948        self.state.as_ref()?.cache_partition()
2949    }
2950
2951    /// Captures the current session cache partition for this request.
2952    ///
2953    /// Repeated callers receive the same partition only while session state has
2954    /// not changed. This lets cache middleware prove that a response completed
2955    /// against the same state revision used for lookup.
2956    #[doc(hidden)]
2957    #[must_use]
2958    pub fn begin_session_cache_partition(&self) -> Option<([u8; 32], u64)> {
2959        let current = self.session_cache_partition()?;
2960        let mut admitted = self
2961            .cache_admission_partition
2962            .lock()
2963            .unwrap_or_else(std::sync::PoisonError::into_inner);
2964        match *admitted {
2965            None => {
2966                *admitted = Some(current);
2967                Some(current)
2968            }
2969            Some(existing) if existing == current => Some(existing),
2970            Some(_) => None,
2971        }
2972    }
2973
2974    /// Returns the admitted cache partition only if the state revision is
2975    /// unchanged at response completion.
2976    #[doc(hidden)]
2977    #[must_use]
2978    pub fn complete_session_cache_partition(&self) -> Option<([u8; 32], u64)> {
2979        if !self.request_scope_is_active() {
2980            return None;
2981        }
2982        let admitted = *self
2983            .cache_admission_partition
2984            .lock()
2985            .unwrap_or_else(std::sync::PoisonError::into_inner);
2986        let admitted = admitted?;
2987        (self.state.as_ref()?.cache_partition() == Some(admitted)).then_some(admitted)
2988    }
2989
2990    /// Marks that one middleware instance produced this request's response from
2991    /// a cache hit.
2992    #[doc(hidden)]
2993    pub fn mark_response_cache_hit(&self, cache_id: u64) -> bool {
2994        const MAX_CACHE_MIDDLEWARE_PER_REQUEST: usize = 64;
2995        if !self.request_scope_is_active() || cache_id == 0 {
2996            return false;
2997        }
2998        let mut hits = self
2999            .response_cache_hits
3000            .lock()
3001            .unwrap_or_else(std::sync::PoisonError::into_inner);
3002        if hits.contains(&cache_id) {
3003            return true;
3004        }
3005        if hits.len() >= MAX_CACHE_MIDDLEWARE_PER_REQUEST || hits.try_reserve(1).is_err() {
3006            return false;
3007        }
3008        hits.push(cache_id);
3009        true
3010    }
3011
3012    /// Returns whether a specific middleware instance produced this request's
3013    /// response from cache.
3014    #[doc(hidden)]
3015    #[must_use]
3016    pub fn response_was_cache_hit(&self, cache_id: u64) -> bool {
3017        self.request_scope_is_active()
3018            && cache_id != 0
3019            && self
3020                .response_cache_hits
3021                .lock()
3022                .unwrap_or_else(std::sync::PoisonError::into_inner)
3023                .contains(&cache_id)
3024    }
3025
3026    /// Returns whether any response-cache middleware served this request.
3027    #[doc(hidden)]
3028    #[must_use]
3029    pub fn response_was_served_from_cache(&self) -> bool {
3030        self.request_scope_is_active()
3031            && !self
3032                .response_cache_hits
3033                .lock()
3034                .unwrap_or_else(std::sync::PoisonError::into_inner)
3035                .is_empty()
3036    }
3037
3038    // ========================================================================
3039    // Capabilities Access
3040    // ========================================================================
3041
3042    /// Returns the client capability information, if available.
3043    ///
3044    /// Capabilities are set by the server after initialization and reflect
3045    /// what the connected client supports.
3046    #[must_use]
3047    pub fn client_capabilities(&self) -> Option<&ClientCapabilityInfo> {
3048        self.client_capabilities.as_ref()
3049    }
3050
3051    /// Returns the self-reported modern client Implementation, if advertised.
3052    ///
3053    /// This is request `_meta` identity, not authentication. A missing value
3054    /// means the peer did not send `io.modelcontextprotocol/clientInfo`.
3055    #[must_use]
3056    pub fn client_implementation(&self) -> Option<&ClientImplementationInfo> {
3057        self.client_implementation.as_ref()
3058    }
3059
3060    /// Returns the server capability information, if available.
3061    ///
3062    /// Reflects what capabilities this server advertises.
3063    #[must_use]
3064    pub fn server_capabilities(&self) -> Option<&ServerCapabilityInfo> {
3065        self.server_capabilities.as_ref()
3066    }
3067
3068    /// Returns whether the client supports sampling (LLM completions).
3069    ///
3070    /// This is a convenience method that checks the client capabilities.
3071    /// Returns `false` if capabilities are not yet available (before initialization).
3072    #[must_use]
3073    pub fn client_supports_sampling(&self) -> bool {
3074        self.client_capabilities
3075            .as_ref()
3076            .is_some_and(|c| c.sampling)
3077    }
3078
3079    /// Returns whether the client supports elicitation (user input requests).
3080    ///
3081    /// This is a convenience method that checks the client capabilities.
3082    /// Returns `false` if capabilities are not yet available.
3083    #[must_use]
3084    pub fn client_supports_elicitation(&self) -> bool {
3085        self.client_capabilities
3086            .as_ref()
3087            .is_some_and(|c| c.elicitation)
3088    }
3089
3090    /// Returns whether the client supports form-mode elicitation.
3091    #[must_use]
3092    pub fn client_supports_elicitation_form(&self) -> bool {
3093        self.client_capabilities
3094            .as_ref()
3095            .is_some_and(|c| c.elicitation_form)
3096    }
3097
3098    /// Returns whether the client supports URL-mode elicitation.
3099    #[must_use]
3100    pub fn client_supports_elicitation_url(&self) -> bool {
3101        self.client_capabilities
3102            .as_ref()
3103            .is_some_and(|c| c.elicitation_url)
3104    }
3105
3106    /// Returns whether the client supports roots listing.
3107    ///
3108    /// This is a convenience method that checks the client capabilities.
3109    /// Returns `false` if capabilities are not yet available.
3110    #[must_use]
3111    pub fn client_supports_roots(&self) -> bool {
3112        self.client_capabilities.as_ref().is_some_and(|c| c.roots)
3113    }
3114
3115    // ========================================================================
3116    // Dynamic Component Enable/Disable
3117    // ========================================================================
3118
3119    /// Session state key for disabled tools.
3120    const DISABLED_TOOLS_KEY: &'static str = "fastmcp.disabled_tools";
3121    /// Session state key for disabled resources.
3122    const DISABLED_RESOURCES_KEY: &'static str = "fastmcp.disabled_resources";
3123    /// Session state key for disabled prompts.
3124    const DISABLED_PROMPTS_KEY: &'static str = "fastmcp.disabled_prompts";
3125
3126    /// Disables a tool for this session.
3127    ///
3128    /// Disabled tools will not appear in `tools/list` responses and will return
3129    /// an error if called directly. This is useful for adapting available
3130    /// functionality based on user permissions, feature flags, or runtime conditions.
3131    ///
3132    /// Returns `true` if the operation succeeded, `false` if session state is unavailable.
3133    ///
3134    /// # Example
3135    ///
3136    /// ```ignore
3137    /// async fn my_tool(ctx: &McpContext) -> McpResult<String> {
3138    ///     // Disable the "admin_tool" for this session
3139    ///     ctx.disable_tool("admin_tool");
3140    ///     Ok("Admin tool disabled".to_string())
3141    /// }
3142    /// ```
3143    pub fn disable_tool(&self, name: impl Into<String>) -> bool {
3144        self.add_to_disabled_set(Self::DISABLED_TOOLS_KEY, name.into(), McpCatalogKind::Tools)
3145    }
3146
3147    /// Enables a previously disabled tool for this session.
3148    ///
3149    /// Returns `true` if the operation succeeded, `false` if session state is unavailable.
3150    pub fn enable_tool(&self, name: &str) -> bool {
3151        self.remove_from_disabled_set(Self::DISABLED_TOOLS_KEY, name, McpCatalogKind::Tools)
3152    }
3153
3154    /// Returns whether a tool is enabled (not disabled) for this session.
3155    ///
3156    /// Tools are enabled by default unless explicitly disabled.
3157    #[must_use]
3158    pub fn is_tool_enabled(&self, name: &str) -> bool {
3159        self.request_scope_is_active() && !self.is_in_disabled_set(Self::DISABLED_TOOLS_KEY, name)
3160    }
3161
3162    /// Disables a resource for this session.
3163    ///
3164    /// Disabled resources will not appear in `resources/list` responses and will
3165    /// return an error if read directly.
3166    ///
3167    /// Returns `true` if the operation succeeded, `false` if session state is unavailable.
3168    pub fn disable_resource(&self, uri: impl Into<String>) -> bool {
3169        self.add_to_disabled_set(
3170            Self::DISABLED_RESOURCES_KEY,
3171            uri.into(),
3172            McpCatalogKind::Resources,
3173        )
3174    }
3175
3176    /// Enables a previously disabled resource for this session.
3177    ///
3178    /// Returns `true` if the operation succeeded, `false` if session state is unavailable.
3179    pub fn enable_resource(&self, uri: &str) -> bool {
3180        self.remove_from_disabled_set(Self::DISABLED_RESOURCES_KEY, uri, McpCatalogKind::Resources)
3181    }
3182
3183    /// Returns whether a resource is enabled (not disabled) for this session.
3184    ///
3185    /// Resources are enabled by default unless explicitly disabled.
3186    #[must_use]
3187    pub fn is_resource_enabled(&self, uri: &str) -> bool {
3188        self.request_scope_is_active()
3189            && !self.is_in_disabled_set(Self::DISABLED_RESOURCES_KEY, uri)
3190    }
3191
3192    /// Disables a prompt for this session.
3193    ///
3194    /// Disabled prompts will not appear in `prompts/list` responses and will
3195    /// return an error if retrieved directly.
3196    ///
3197    /// Returns `true` if the operation succeeded, `false` if session state is unavailable.
3198    pub fn disable_prompt(&self, name: impl Into<String>) -> bool {
3199        self.add_to_disabled_set(
3200            Self::DISABLED_PROMPTS_KEY,
3201            name.into(),
3202            McpCatalogKind::Prompts,
3203        )
3204    }
3205
3206    /// Enables a previously disabled prompt for this session.
3207    ///
3208    /// Returns `true` if the operation succeeded, `false` if session state is unavailable.
3209    pub fn enable_prompt(&self, name: &str) -> bool {
3210        self.remove_from_disabled_set(Self::DISABLED_PROMPTS_KEY, name, McpCatalogKind::Prompts)
3211    }
3212
3213    /// Returns whether a prompt is enabled (not disabled) for this session.
3214    ///
3215    /// Prompts are enabled by default unless explicitly disabled.
3216    #[must_use]
3217    pub fn is_prompt_enabled(&self, name: &str) -> bool {
3218        self.request_scope_is_active() && !self.is_in_disabled_set(Self::DISABLED_PROMPTS_KEY, name)
3219    }
3220
3221    /// Returns the set of disabled tools for this session.
3222    #[must_use]
3223    pub fn disabled_tools(&self) -> std::collections::HashSet<String> {
3224        self.get_disabled_set(Self::DISABLED_TOOLS_KEY)
3225    }
3226
3227    /// Returns the set of disabled resources for this session.
3228    #[must_use]
3229    pub fn disabled_resources(&self) -> std::collections::HashSet<String> {
3230        self.get_disabled_set(Self::DISABLED_RESOURCES_KEY)
3231    }
3232
3233    /// Returns the set of disabled prompts for this session.
3234    #[must_use]
3235    pub fn disabled_prompts(&self) -> std::collections::HashSet<String> {
3236        self.get_disabled_set(Self::DISABLED_PROMPTS_KEY)
3237    }
3238
3239    // Helper: Add a name to a disabled set
3240    fn add_to_disabled_set(&self, key: &str, name: String, kind: McpCatalogKind) -> bool {
3241        if self.ensure_live().is_err() {
3242            return false;
3243        }
3244        let Some(state) = self.state.as_ref() else {
3245            return false;
3246        };
3247        let mut set: std::collections::HashSet<String> = state.get(key).unwrap_or_default();
3248        let changed = set.insert(name);
3249        let stored = state.set(key, set);
3250        if stored && changed {
3251            self.emit_catalog_changed(kind);
3252        }
3253        stored
3254    }
3255
3256    // Helper: Remove a name from a disabled set
3257    fn remove_from_disabled_set(&self, key: &str, name: &str, kind: McpCatalogKind) -> bool {
3258        if self.ensure_live().is_err() {
3259            return false;
3260        }
3261        let Some(state) = self.state.as_ref() else {
3262            return false;
3263        };
3264        let mut set: std::collections::HashSet<String> = state.get(key).unwrap_or_default();
3265        let changed = set.remove(name);
3266        let stored = state.set(key, set);
3267        if stored && changed {
3268            self.emit_catalog_changed(kind);
3269        }
3270        stored
3271    }
3272
3273    fn emit_catalog_changed(&self, kind: McpCatalogKind) {
3274        if let Some(sender) = self.log_sender.as_ref() {
3275            sender.send_catalog_changed(kind);
3276        }
3277        if let Some(publisher) = self.catalog_publisher.as_ref() {
3278            let _ = publisher.publish_catalog_changed(kind);
3279        }
3280    }
3281
3282    // Helper: Check if a name is in a disabled set
3283    fn is_in_disabled_set(&self, key: &str, name: &str) -> bool {
3284        if !self.request_scope_is_active() {
3285            return false;
3286        }
3287        let Some(state) = self.state.as_ref() else {
3288            return false;
3289        };
3290        let set: std::collections::HashSet<String> = state.get(key).unwrap_or_default();
3291        set.contains(name)
3292    }
3293
3294    // Helper: Get the full disabled set
3295    fn get_disabled_set(&self, key: &str) -> std::collections::HashSet<String> {
3296        if !self.request_scope_is_active() {
3297            return std::collections::HashSet::new();
3298        }
3299        self.state
3300            .as_ref()
3301            .and_then(|s| s.get(key))
3302            .unwrap_or_default()
3303    }
3304
3305    // ========================================================================
3306    // Client Roots
3307    // ========================================================================
3308
3309    /// Returns whether client roots are available in this context.
3310    #[must_use]
3311    pub fn can_list_roots(&self) -> bool {
3312        self.ensure_live().is_ok() && self.roots_provider.is_some()
3313    }
3314
3315    /// Lists the filesystem roots exposed by the connected client.
3316    ///
3317    /// # Errors
3318    ///
3319    /// Returns an error when the client did not advertise roots, the transport
3320    /// cannot complete the reverse request, or this request is cancelled.
3321    pub async fn list_roots(&self) -> crate::McpResult<Vec<ClientRoot>> {
3322        self.ensure_live()
3323            .map_err(|_| crate::McpError::request_cancelled())?;
3324        let provider = self.roots_provider.as_ref().ok_or_else(|| {
3325            crate::McpError::new(
3326                crate::McpErrorCode::InvalidRequest,
3327                "Roots not available: client does not support roots capability",
3328            )
3329        })?;
3330
3331        let roots = provider.list_roots().await?;
3332        self.ensure_live()
3333            .map_err(|_| crate::McpError::request_cancelled())?;
3334        Ok(roots)
3335    }
3336
3337    // ========================================================================
3338    // Sampling (LLM Completions)
3339    // ========================================================================
3340
3341    /// Returns whether sampling is available in this context.
3342    ///
3343    /// Sampling is available when the client has advertised sampling
3344    /// capability and a sampling sender has been configured.
3345    #[must_use]
3346    pub fn can_sample(&self) -> bool {
3347        self.ensure_live().is_ok() && self.sampling_sender.is_some()
3348    }
3349
3350    /// Requests an LLM completion from the client.
3351    ///
3352    /// This is a convenience method for simple text prompts. For more control
3353    /// over the request, use [`sample_with_request`](Self::sample_with_request).
3354    ///
3355    /// # Arguments
3356    ///
3357    /// * `prompt` - The prompt text to send (as a user message)
3358    /// * `max_tokens` - Maximum number of tokens to generate
3359    ///
3360    /// # Errors
3361    ///
3362    /// Returns an error if:
3363    /// - The client doesn't support sampling
3364    /// - The sampling request fails
3365    ///
3366    /// # Example
3367    ///
3368    /// ```ignore
3369    /// async fn my_tool(ctx: &McpContext, topic: String) -> McpResult<String> {
3370    ///     let response = ctx.sample(&format!("Write a haiku about {topic}"), 100).await?;
3371    ///     Ok(response.text)
3372    /// }
3373    /// ```
3374    pub async fn sample(
3375        &self,
3376        prompt: impl Into<String>,
3377        max_tokens: u32,
3378    ) -> crate::McpResult<SamplingResponse> {
3379        let request = SamplingRequest::prompt(prompt, max_tokens);
3380        self.sample_with_request(request).await
3381    }
3382
3383    /// Requests an LLM completion with full control over the request.
3384    ///
3385    /// # Arguments
3386    ///
3387    /// * `request` - The full sampling request parameters
3388    ///
3389    /// # Errors
3390    ///
3391    /// Returns an error if:
3392    /// - The client doesn't support sampling
3393    /// - The sampling request fails
3394    ///
3395    /// # Example
3396    ///
3397    /// ```ignore
3398    /// async fn my_tool(ctx: &McpContext) -> McpResult<String> {
3399    ///     let request = SamplingRequest::new(
3400    ///         vec![
3401    ///             SamplingRequestMessage::user("Hello!"),
3402    ///             SamplingRequestMessage::assistant("Hi! How can I help?"),
3403    ///             SamplingRequestMessage::user("Tell me a joke."),
3404    ///         ],
3405    ///         200,
3406    ///     )
3407    ///     .with_system_prompt("You are a helpful and funny assistant.")
3408    ///     .with_temperature(0.8);
3409    ///
3410    ///     let response = ctx.sample_with_request(request).await?;
3411    ///     Ok(response.text)
3412    /// }
3413    /// ```
3414    pub async fn sample_with_request(
3415        &self,
3416        request: SamplingRequest,
3417    ) -> crate::McpResult<SamplingResponse> {
3418        self.ensure_live()
3419            .map_err(|_| crate::McpError::request_cancelled())?;
3420        let sender = self.sampling_sender.as_ref().ok_or_else(|| {
3421            crate::McpError::new(
3422                crate::McpErrorCode::InvalidRequest,
3423                "Sampling not available: client does not support sampling capability",
3424            )
3425        })?;
3426
3427        let response = sender.create_message(request).await?;
3428        self.ensure_live()
3429            .map_err(|_| crate::McpError::request_cancelled())?;
3430        Ok(response)
3431    }
3432
3433    // ========================================================================
3434    // Elicitation (User Input Requests)
3435    // ========================================================================
3436
3437    /// Returns whether elicitation is available in this context.
3438    ///
3439    /// Elicitation is available when the client has advertised elicitation
3440    /// capability and an elicitation sender has been configured.
3441    #[must_use]
3442    pub fn can_elicit(&self) -> bool {
3443        self.ensure_live().is_ok() && self.elicitation_sender.is_some()
3444    }
3445
3446    /// Requests user input via a form.
3447    ///
3448    /// This presents a form to the user with fields defined by the JSON schema.
3449    /// The user can accept (submit the form), decline, or cancel.
3450    ///
3451    /// # Arguments
3452    ///
3453    /// * `message` - Message to display explaining what input is needed
3454    /// * `schema` - JSON Schema defining the form fields
3455    ///
3456    /// # Errors
3457    ///
3458    /// Returns an error if:
3459    /// - The client doesn't support elicitation
3460    /// - The elicitation request fails
3461    ///
3462    /// # Example
3463    ///
3464    /// ```ignore
3465    /// async fn my_tool(ctx: &McpContext) -> McpResult<String> {
3466    ///     let schema = serde_json::json!({
3467    ///         "type": "object",
3468    ///         "properties": {
3469    ///             "name": {"type": "string"},
3470    ///             "age": {"type": "integer"}
3471    ///         },
3472    ///         "required": ["name"]
3473    ///     });
3474    ///     let response = ctx.elicit_form("Please enter your details", schema).await?;
3475    ///     if response.is_accepted() {
3476    ///         let name = response.get_string("name").unwrap_or("Unknown");
3477    ///         Ok(format!("Hello, {name}!"))
3478    ///     } else {
3479    ///         Ok("User declined input".to_string())
3480    ///     }
3481    /// }
3482    /// ```
3483    pub async fn elicit_form(
3484        &self,
3485        message: impl Into<String>,
3486        schema: serde_json::Value,
3487    ) -> crate::McpResult<ElicitationResponse> {
3488        let request = ElicitationRequest::form(message, schema);
3489        self.elicit_with_request(request).await
3490    }
3491
3492    /// Requests user interaction via an external URL.
3493    ///
3494    /// This directs the user to an external URL for sensitive operations like
3495    /// OAuth flows, payment processing, or credential collection.
3496    ///
3497    /// # Arguments
3498    ///
3499    /// * `message` - Message to display explaining why the URL visit is needed
3500    /// * `url` - The URL the user should navigate to
3501    /// * `elicitation_id` - Unique ID for tracking this elicitation
3502    ///
3503    /// # Errors
3504    ///
3505    /// Returns an error if:
3506    /// - The client doesn't support elicitation
3507    /// - The elicitation request fails
3508    ///
3509    /// # Example
3510    ///
3511    /// ```ignore
3512    /// async fn my_tool(ctx: &McpContext) -> McpResult<String> {
3513    ///     let response = ctx.elicit_url(
3514    ///         "Please authenticate with your GitHub account",
3515    ///         "https://github.com/login/oauth/authorize?...",
3516    ///         "github-auth-12345",
3517    ///     ).await?;
3518    ///     if response.is_accepted() {
3519    ///         Ok("Authentication successful".to_string())
3520    ///     } else {
3521    ///         Ok("Authentication cancelled".to_string())
3522    ///     }
3523    /// }
3524    /// ```
3525    pub async fn elicit_url(
3526        &self,
3527        message: impl Into<String>,
3528        url: impl Into<String>,
3529        elicitation_id: impl Into<String>,
3530    ) -> crate::McpResult<ElicitationResponse> {
3531        let request = ElicitationRequest::url(message, url, elicitation_id);
3532        self.elicit_with_request(request).await
3533    }
3534
3535    /// Requests user input with full control over the request.
3536    ///
3537    /// # Arguments
3538    ///
3539    /// * `request` - The full elicitation request parameters
3540    ///
3541    /// # Errors
3542    ///
3543    /// Returns an error if:
3544    /// - The client doesn't support elicitation
3545    /// - The elicitation request fails
3546    pub async fn elicit_with_request(
3547        &self,
3548        request: ElicitationRequest,
3549    ) -> crate::McpResult<ElicitationResponse> {
3550        self.ensure_live()
3551            .map_err(|_| crate::McpError::request_cancelled())?;
3552        let sender = self.elicitation_sender.as_ref().ok_or_else(|| {
3553            crate::McpError::new(
3554                crate::McpErrorCode::InvalidRequest,
3555                "Elicitation not available: client does not support elicitation capability",
3556            )
3557        })?;
3558
3559        let response = sender.elicit(request).await?;
3560        self.ensure_live()
3561            .map_err(|_| crate::McpError::request_cancelled())?;
3562        Ok(response)
3563    }
3564
3565    // ========================================================================
3566    // Resource Reading (Cross-Component Access)
3567    // ========================================================================
3568
3569    /// Returns whether resource reading is available in this context.
3570    ///
3571    /// Resource reading is available when a resource reader (Router) has
3572    /// been attached to this context.
3573    #[must_use]
3574    pub fn can_read_resources(&self) -> bool {
3575        self.ensure_live().is_ok() && self.resource_reader.is_some()
3576    }
3577
3578    /// Returns the current resource read depth.
3579    ///
3580    /// This is used to track recursion when resources read other resources.
3581    #[must_use]
3582    pub fn resource_read_depth(&self) -> u32 {
3583        self.resource_read_depth
3584    }
3585
3586    /// Reads a resource by URI.
3587    ///
3588    /// This allows tools, resources, and prompts to read other resources
3589    /// configured on the same server. This enables composition and code reuse.
3590    ///
3591    /// # Arguments
3592    ///
3593    /// * `uri` - The resource URI to read
3594    ///
3595    /// # Errors
3596    ///
3597    /// Returns an error if:
3598    /// - No resource reader is available (context not configured for resource access)
3599    /// - The resource is not found
3600    /// - Maximum recursion depth is exceeded
3601    /// - The resource read fails
3602    ///
3603    /// # Example
3604    ///
3605    /// ```ignore
3606    /// #[tool]
3607    /// async fn process_config(ctx: &McpContext) -> Result<String, ToolError> {
3608    ///     let config = ctx.read_resource("config://app").await?;
3609    ///     let text = config.first_text()
3610    ///         .ok_or(ToolError::InvalidConfig)?;
3611    ///     Ok(format!("Config loaded: {}", text))
3612    /// }
3613    /// ```
3614    pub async fn read_resource(&self, uri: &str) -> crate::McpResult<ResourceReadResult> {
3615        self.ensure_live()
3616            .map_err(|_| crate::McpError::request_cancelled())?;
3617        // Check if we have a resource reader
3618        let reader = self.resource_reader.as_ref().ok_or_else(|| {
3619            crate::McpError::new(
3620                crate::McpErrorCode::InternalError,
3621                "Resource reading not available: no router attached to context",
3622            )
3623        })?;
3624
3625        // Use one effective nesting depth across all cross-component APIs so
3626        // alternating tool -> resource -> prompt cycles cannot reset a
3627        // type-specific counter.
3628        let nested_dispatch_depth = self.nested_dispatch_depth();
3629        if nested_dispatch_depth >= MAX_RESOURCE_READ_DEPTH {
3630            return Err(crate::McpError::new(
3631                crate::McpErrorCode::InternalError,
3632                format!(
3633                    "Maximum resource read depth ({}) exceeded; possible infinite recursion",
3634                    MAX_RESOURCE_READ_DEPTH
3635                ),
3636            ));
3637        }
3638
3639        // Read the resource with incremented depth
3640        let result = reader
3641            .read_resource(self, uri, nested_dispatch_depth + 1)
3642            .await?;
3643        self.ensure_live()
3644            .map_err(|_| crate::McpError::request_cancelled())?;
3645        Ok(result)
3646    }
3647
3648    /// Reads a resource and extracts the text content.
3649    ///
3650    /// This is a convenience method that reads a resource and returns
3651    /// the first text content item.
3652    ///
3653    /// # Errors
3654    ///
3655    /// Returns an error if:
3656    /// - The resource read fails
3657    /// - The resource has no text content
3658    ///
3659    /// # Example
3660    ///
3661    /// ```ignore
3662    /// let text = ctx.read_resource_text("file://readme.md").await?;
3663    /// println!("Content: {}", text);
3664    /// ```
3665    pub async fn read_resource_text(&self, uri: &str) -> crate::McpResult<String> {
3666        let result = self.read_resource(uri).await?;
3667        result.first_text().map(String::from).ok_or_else(|| {
3668            crate::McpError::new(
3669                crate::McpErrorCode::InternalError,
3670                format!("Resource '{}' has no text content", uri),
3671            )
3672        })
3673    }
3674
3675    /// Reads a resource and parses it as JSON.
3676    ///
3677    /// This is a convenience method that reads a resource and deserializes
3678    /// the text content as JSON.
3679    ///
3680    /// # Errors
3681    ///
3682    /// Returns an error if:
3683    /// - The resource read fails
3684    /// - The resource has no text content
3685    /// - JSON deserialization fails
3686    ///
3687    /// # Example
3688    ///
3689    /// ```ignore
3690    /// #[derive(Deserialize)]
3691    /// struct Config {
3692    ///     database_url: String,
3693    /// }
3694    ///
3695    /// let config: Config = ctx.read_resource_json("config://app").await?;
3696    /// println!("Database: {}", config.database_url);
3697    /// ```
3698    pub async fn read_resource_json<T: serde::de::DeserializeOwned>(
3699        &self,
3700        uri: &str,
3701    ) -> crate::McpResult<T> {
3702        let text = self.read_resource_text(uri).await?;
3703        serde_json::from_str(&text).map_err(|e| {
3704            crate::McpError::new(
3705                crate::McpErrorCode::InternalError,
3706                format!("Failed to parse resource '{}' as JSON: {}", uri, e),
3707            )
3708        })
3709    }
3710
3711    // ========================================================================
3712    // Tool Calling (Cross-Component Access)
3713    // ========================================================================
3714
3715    /// Returns whether tool calling is available in this context.
3716    ///
3717    /// Tool calling is available when a tool caller (Router) has
3718    /// been attached to this context.
3719    #[must_use]
3720    pub fn can_call_tools(&self) -> bool {
3721        self.ensure_live().is_ok() && self.tool_caller.is_some()
3722    }
3723
3724    /// Returns the current tool call depth.
3725    ///
3726    /// This is used to track recursion when tools call other tools.
3727    #[must_use]
3728    pub fn tool_call_depth(&self) -> u32 {
3729        self.tool_call_depth
3730    }
3731
3732    /// Calls a tool by name with the given arguments.
3733    ///
3734    /// This allows tools, resources, and prompts to call other tools
3735    /// configured on the same server. This enables composition and code reuse.
3736    ///
3737    /// # Arguments
3738    ///
3739    /// * `name` - The tool name to call
3740    /// * `args` - The arguments as a JSON value
3741    ///
3742    /// # Errors
3743    ///
3744    /// Returns an error if:
3745    /// - No tool caller is available (context not configured for tool access)
3746    /// - The tool is not found
3747    /// - Maximum recursion depth is exceeded
3748    /// - The tool execution fails
3749    ///
3750    /// # Example
3751    ///
3752    /// ```ignore
3753    /// #[tool]
3754    /// async fn double_add(ctx: &McpContext, a: i32, b: i32) -> Result<i32, ToolError> {
3755    ///     let sum: i32 = ctx.call_tool_json("add", json!({"a": a, "b": b})).await?;
3756    ///     Ok(sum * 2)
3757    /// }
3758    /// ```
3759    pub async fn call_tool(
3760        &self,
3761        name: &str,
3762        args: serde_json::Value,
3763    ) -> crate::McpResult<ToolCallResult> {
3764        self.ensure_live()
3765            .map_err(|_| crate::McpError::request_cancelled())?;
3766        // Check if we have a tool caller
3767        let caller = self.tool_caller.as_ref().ok_or_else(|| {
3768            crate::McpError::new(
3769                crate::McpErrorCode::InternalError,
3770                "Tool calling not available: no router attached to context",
3771            )
3772        })?;
3773
3774        // Share the effective depth with resource reads and prompt gets so
3775        // alternating cycles are bounded just like same-kind recursion.
3776        let nested_dispatch_depth = self.nested_dispatch_depth();
3777        if nested_dispatch_depth >= MAX_TOOL_CALL_DEPTH {
3778            return Err(crate::McpError::new(
3779                crate::McpErrorCode::InternalError,
3780                format!(
3781                    "Maximum tool call depth ({}) exceeded calling '{}'; possible infinite recursion",
3782                    MAX_TOOL_CALL_DEPTH, name
3783                ),
3784            ));
3785        }
3786
3787        // Call the tool with incremented depth
3788        let result = caller
3789            .call_tool(self, name, args, nested_dispatch_depth + 1)
3790            .await?;
3791        self.ensure_live()
3792            .map_err(|_| crate::McpError::request_cancelled())?;
3793        Ok(result)
3794    }
3795
3796    /// Calls a tool and extracts the text content.
3797    ///
3798    /// This is a convenience method that calls a tool and returns
3799    /// the first text content item.
3800    ///
3801    /// # Errors
3802    ///
3803    /// Returns an error if:
3804    /// - The tool call fails
3805    /// - The tool returns an error result
3806    /// - The tool has no text content
3807    ///
3808    /// # Example
3809    ///
3810    /// ```ignore
3811    /// let greeting = ctx.call_tool_text("greet", json!({"name": "World"})).await?;
3812    /// println!("Result: {}", greeting);
3813    /// ```
3814    pub async fn call_tool_text(
3815        &self,
3816        name: &str,
3817        args: serde_json::Value,
3818    ) -> crate::McpResult<String> {
3819        let result = self.call_tool(name, args).await?;
3820
3821        // Check if tool returned an error
3822        if result.is_error {
3823            let error_msg = result.first_text().unwrap_or("Tool returned an error");
3824            return Err(crate::McpError::new(
3825                crate::McpErrorCode::InternalError,
3826                format!("Tool '{}' failed: {}", name, error_msg),
3827            ));
3828        }
3829
3830        result.first_text().map(String::from).ok_or_else(|| {
3831            crate::McpError::new(
3832                crate::McpErrorCode::InternalError,
3833                format!("Tool '{}' returned no text content", name),
3834            )
3835        })
3836    }
3837
3838    /// Calls a tool and parses the result as JSON.
3839    ///
3840    /// This is a convenience method that calls a tool and deserializes
3841    /// the text content as JSON.
3842    ///
3843    /// # Errors
3844    ///
3845    /// Returns an error if:
3846    /// - The tool call fails
3847    /// - The tool returns an error result
3848    /// - The tool has no text content
3849    /// - JSON deserialization fails
3850    ///
3851    /// # Example
3852    ///
3853    /// ```ignore
3854    /// #[derive(Deserialize)]
3855    /// struct ComputeResult {
3856    ///     value: i64,
3857    /// }
3858    ///
3859    /// let result: ComputeResult = ctx.call_tool_json("compute", json!({"x": 5})).await?;
3860    /// println!("Result: {}", result.value);
3861    /// ```
3862    pub async fn call_tool_json<T: serde::de::DeserializeOwned>(
3863        &self,
3864        name: &str,
3865        args: serde_json::Value,
3866    ) -> crate::McpResult<T> {
3867        let text = self.call_tool_text(name, args).await?;
3868        serde_json::from_str(&text).map_err(|e| {
3869            crate::McpError::new(
3870                crate::McpErrorCode::InternalError,
3871                format!("Failed to parse tool '{}' result as JSON: {}", name, e),
3872            )
3873        })
3874    }
3875
3876    // ========================================================================
3877    // Prompt Getting (Cross-Component Access)
3878    // ========================================================================
3879
3880    /// Returns whether prompt getting is available in this context.
3881    #[must_use]
3882    pub fn can_get_prompts(&self) -> bool {
3883        self.ensure_live().is_ok() && self.prompt_caller.is_some()
3884    }
3885
3886    /// Returns the current prompt get depth.
3887    #[must_use]
3888    pub fn prompt_get_depth(&self) -> u32 {
3889        self.prompt_get_depth
3890    }
3891
3892    fn nested_dispatch_depth(&self) -> u32 {
3893        self.resource_read_depth
3894            .max(self.tool_call_depth)
3895            .max(self.prompt_get_depth)
3896    }
3897
3898    /// Gets a prompt by name with the given arguments.
3899    ///
3900    /// This allows tools, resources, and prompts to get other prompts
3901    /// configured on the same server.
3902    pub async fn get_prompt(
3903        &self,
3904        name: &str,
3905        arguments: std::collections::HashMap<String, String>,
3906    ) -> crate::McpResult<PromptGetResult> {
3907        self.ensure_live()
3908            .map_err(|_| crate::McpError::request_cancelled())?;
3909        let caller = self.prompt_caller.as_ref().ok_or_else(|| {
3910            crate::McpError::new(
3911                crate::McpErrorCode::InternalError,
3912                "Prompt getting not available: no router attached to context",
3913            )
3914        })?;
3915
3916        let nested_dispatch_depth = self.nested_dispatch_depth();
3917        if nested_dispatch_depth >= MAX_PROMPT_GET_DEPTH {
3918            return Err(crate::McpError::new(
3919                crate::McpErrorCode::InternalError,
3920                format!(
3921                    "Maximum prompt get depth ({}) exceeded getting '{}'; possible infinite recursion",
3922                    MAX_PROMPT_GET_DEPTH, name
3923                ),
3924            ));
3925        }
3926
3927        let result = caller
3928            .get_prompt(self, name, arguments, nested_dispatch_depth + 1)
3929            .await?;
3930        self.ensure_live()
3931            .map_err(|_| crate::McpError::request_cancelled())?;
3932        Ok(result)
3933    }
3934
3935    /// Gets a prompt and extracts the first text message.
3936    pub async fn get_prompt_text(
3937        &self,
3938        name: &str,
3939        arguments: std::collections::HashMap<String, String>,
3940    ) -> crate::McpResult<String> {
3941        let result = self.get_prompt(name, arguments).await?;
3942        result.first_text().map(String::from).ok_or_else(|| {
3943            crate::McpError::new(
3944                crate::McpErrorCode::InternalError,
3945                format!("Prompt '{}' returned no text content", name),
3946            )
3947        })
3948    }
3949
3950    // ========================================================================
3951    // Parallel Combinators
3952    // ========================================================================
3953
3954    /// Waits for all futures to complete and returns their results.
3955    ///
3956    /// This is the N-of-N combinator: all futures must complete before
3957    /// returning. Results are returned in the same order as input futures.
3958    ///
3959    /// # Example
3960    ///
3961    /// ```ignore
3962    /// let futures = vec![
3963    ///     Box::pin(fetch_user(1)),
3964    ///     Box::pin(fetch_user(2)),
3965    ///     Box::pin(fetch_user(3)),
3966    /// ];
3967    /// let users = ctx.join_all(futures).await?;
3968    /// ```
3969    pub async fn join_all<T: Send + 'static>(
3970        &self,
3971        futures: Vec<crate::combinator::BoxFuture<'_, T>>,
3972    ) -> crate::McpResult<Vec<T>> {
3973        self.ensure_live()
3974            .map_err(|_| crate::McpError::request_cancelled())?;
3975        let results = crate::combinator::join_all(&self.cx, futures).await;
3976        self.ensure_live()
3977            .map_err(|_| crate::McpError::request_cancelled())?;
3978        Ok(results)
3979    }
3980
3981    /// Races multiple futures, returning the first to complete.
3982    ///
3983    /// This is the 1-of-N combinator: the first future to complete wins,
3984    /// and all other supplied futures are dropped. Dropping a future does not
3985    /// cancel or drain work that it spawned independently; such work must live
3986    /// in a caller-owned structured scope with an explicit join obligation.
3987    ///
3988    /// # Example
3989    ///
3990    /// ```ignore
3991    /// let futures = vec![
3992    ///     Box::pin(fetch_from_primary()),
3993    ///     Box::pin(fetch_from_replica()),
3994    /// ];
3995    /// let result = ctx.race(futures).await?;
3996    /// ```
3997    pub async fn race<T: Send + 'static>(
3998        &self,
3999        futures: Vec<crate::combinator::BoxFuture<'_, T>>,
4000    ) -> crate::McpResult<T> {
4001        self.ensure_live()
4002            .map_err(|_| crate::McpError::request_cancelled())?;
4003        let result = crate::combinator::race(&self.cx, futures).await;
4004        self.ensure_live()
4005            .map_err(|_| crate::McpError::request_cancelled())?;
4006        result
4007    }
4008
4009    /// Waits for M of N futures to complete successfully.
4010    ///
4011    /// Returns when `required` futures have completed successfully.
4012    /// Remaining supplied futures are dropped. Independently spawned work is
4013    /// neither cancelled nor drained by dropping its parent future.
4014    ///
4015    /// # Example
4016    ///
4017    /// ```ignore
4018    /// let futures = vec![
4019    ///     Box::pin(write_to_replica(1)),
4020    ///     Box::pin(write_to_replica(2)),
4021    ///     Box::pin(write_to_replica(3)),
4022    /// ];
4023    /// let result = ctx.quorum(2, futures).await?;
4024    /// ```
4025    pub async fn quorum<T: Send + 'static>(
4026        &self,
4027        required: usize,
4028        futures: Vec<crate::combinator::BoxFuture<'_, crate::McpResult<T>>>,
4029    ) -> crate::McpResult<crate::combinator::QuorumResult<T>> {
4030        self.ensure_live()
4031            .map_err(|_| crate::McpError::request_cancelled())?;
4032        let result = crate::combinator::quorum(&self.cx, required, futures).await;
4033        self.ensure_live()
4034            .map_err(|_| crate::McpError::request_cancelled())?;
4035        result
4036    }
4037
4038    /// Races futures and returns the first successful result.
4039    ///
4040    /// Unlike `race` which returns the first to complete (success or failure),
4041    /// `first_ok` returns the first to complete successfully. Once a result is
4042    /// selected, the remaining supplied futures are dropped; independently
4043    /// spawned work is not cancelled or drained.
4044    ///
4045    /// # Example
4046    ///
4047    /// ```ignore
4048    /// let futures = vec![
4049    ///     Box::pin(try_primary()),
4050    ///     Box::pin(try_fallback()),
4051    /// ];
4052    /// let result = ctx.first_ok(futures).await?;
4053    /// ```
4054    pub async fn first_ok<T: Send + 'static>(
4055        &self,
4056        futures: Vec<crate::combinator::BoxFuture<'_, crate::McpResult<T>>>,
4057    ) -> crate::McpResult<T> {
4058        self.ensure_live()
4059            .map_err(|_| crate::McpError::request_cancelled())?;
4060        let result = crate::combinator::first_ok(&self.cx, futures).await;
4061        self.ensure_live()
4062            .map_err(|_| crate::McpError::request_cancelled())?;
4063        result
4064    }
4065}
4066
4067/// Error returned when a request has been cancelled.
4068///
4069/// This is returned by `checkpoint()` when the request should stop
4070/// processing. The server will convert this to an appropriate MCP
4071/// error response.
4072#[derive(Debug, Clone, Copy)]
4073pub struct CancelledError;
4074
4075impl std::fmt::Display for CancelledError {
4076    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4077        write!(f, "request cancelled")
4078    }
4079}
4080
4081impl std::error::Error for CancelledError {}
4082
4083/// Extension trait for converting MCP results to asupersync Outcome.
4084///
4085/// This bridges the MCP error model with asupersync's 4-valued outcome
4086/// (Ok, Err, Cancelled, Panicked).
4087pub trait IntoOutcome<T, E> {
4088    /// Converts this result into an asupersync Outcome.
4089    fn into_outcome(self) -> Outcome<T, E>;
4090}
4091
4092impl<T, E> IntoOutcome<T, E> for Result<T, E> {
4093    fn into_outcome(self) -> Outcome<T, E> {
4094        match self {
4095            Ok(v) => Outcome::Ok(v),
4096            Err(e) => Outcome::Err(e),
4097        }
4098    }
4099}
4100
4101impl<T, E> IntoOutcome<T, E> for Result<T, CancelledError>
4102where
4103    E: Default,
4104{
4105    fn into_outcome(self) -> Outcome<T, E> {
4106        match self {
4107            Ok(v) => Outcome::Ok(v),
4108            Err(CancelledError) => Outcome::Cancelled(CancelReason::user("request cancelled")),
4109        }
4110    }
4111}
4112
4113#[cfg(test)]
4114mod tests {
4115    use super::*;
4116
4117    #[test]
4118    fn test_mcp_context_creation() {
4119        let cx = Cx::for_testing();
4120        let ctx = McpContext::new(cx, 42);
4121
4122        assert_eq!(ctx.request_id(), 42);
4123    }
4124
4125    #[test]
4126    fn test_mcp_context_not_cancelled_initially() {
4127        let cx = Cx::for_testing();
4128        let ctx = McpContext::new(cx, 1);
4129
4130        assert!(!ctx.is_cancelled());
4131    }
4132
4133    #[test]
4134    fn test_mcp_context_checkpoint_success() {
4135        let cx = Cx::for_testing();
4136        let ctx = McpContext::new(cx, 1);
4137
4138        // Should succeed when not cancelled
4139        assert!(ctx.checkpoint().is_ok());
4140    }
4141
4142    #[test]
4143    fn test_mcp_context_checkpoint_cancelled() {
4144        let cx = Cx::for_testing();
4145        cx.set_cancel_requested(true);
4146        let ctx = McpContext::new(cx, 1);
4147
4148        // Should fail when cancelled
4149        assert!(ctx.checkpoint().is_err());
4150    }
4151
4152    #[test]
4153    fn request_local_cancellation_does_not_cancel_shared_ambient_context() {
4154        let cx = Cx::for_testing();
4155        let cancellation = McpRequestCancellation::new();
4156        let request =
4157            McpContext::new(cx.clone(), 1).with_request_cancellation(cancellation.clone());
4158        let sibling = McpContext::new(cx.clone(), 2);
4159
4160        cancellation.cancel();
4161
4162        assert!(request.ensure_live().is_err());
4163        assert!(request.checkpoint().is_err());
4164        assert!(sibling.ensure_live().is_ok());
4165        assert!(!cx.is_cancel_requested());
4166    }
4167
4168    #[test]
4169    fn context_exposes_its_request_local_cancellation_handle() {
4170        let cancellation = McpRequestCancellation::new();
4171        let context =
4172            McpContext::new(Cx::for_testing(), 1).with_request_cancellation(cancellation.clone());
4173
4174        let observed = context.request_cancellation();
4175        assert!(observed.cancel());
4176        assert!(cancellation.is_cancel_requested());
4177        assert!(context.is_cancelled());
4178    }
4179
4180    #[test]
4181    fn request_local_cancelled_future_registers_and_is_woken_without_polling() {
4182        use std::sync::atomic::AtomicBool;
4183
4184        struct WakeFlag(AtomicBool);
4185
4186        impl std::task::Wake for WakeFlag {
4187            fn wake(self: Arc<Self>) {
4188                self.0.store(true, Ordering::Release);
4189            }
4190        }
4191
4192        let cancellation = McpRequestCancellation::new();
4193        let wake_flag = Arc::new(WakeFlag(AtomicBool::new(false)));
4194        let waker = std::task::Waker::from(Arc::clone(&wake_flag));
4195        let mut task_cx = std::task::Context::from_waker(&waker);
4196        let mut future = Box::pin(cancellation.cancelled());
4197
4198        assert!(std::future::Future::poll(future.as_mut(), &mut task_cx).is_pending());
4199        assert!(cancellation.cancel());
4200        assert!(wake_flag.0.load(Ordering::Acquire));
4201        assert!(std::future::Future::poll(future.as_mut(), &mut task_cx).is_ready());
4202    }
4203
4204    #[test]
4205    fn request_local_cancelled_future_observes_preexisting_cancellation() {
4206        let cancellation = McpRequestCancellation::new();
4207        assert!(cancellation.cancel());
4208
4209        let mut future = Box::pin(cancellation.cancelled());
4210        let waker = std::task::Waker::noop();
4211        let mut task_cx = std::task::Context::from_waker(waker);
4212
4213        assert!(std::future::Future::poll(future.as_mut(), &mut task_cx).is_ready());
4214    }
4215
4216    #[test]
4217    fn request_terminal_future_is_woken_when_finalization_wins() {
4218        use std::sync::atomic::AtomicBool;
4219
4220        struct WakeFlag(AtomicBool);
4221
4222        impl std::task::Wake for WakeFlag {
4223            fn wake(self: Arc<Self>) {
4224                self.0.store(true, Ordering::Release);
4225            }
4226        }
4227
4228        let cancellation = McpRequestCancellation::new();
4229        let wake_flag = Arc::new(WakeFlag(AtomicBool::new(false)));
4230        let waker = std::task::Waker::from(Arc::clone(&wake_flag));
4231        let mut task_cx = std::task::Context::from_waker(&waker);
4232        let mut future = Box::pin(cancellation.terminated());
4233
4234        assert!(!cancellation.is_terminal());
4235        assert!(std::future::Future::poll(future.as_mut(), &mut task_cx).is_pending());
4236        assert!(cancellation.begin_finalization());
4237        assert!(cancellation.is_terminal());
4238        assert!(wake_flag.0.load(Ordering::Acquire));
4239        assert!(std::future::Future::poll(future.as_mut(), &mut task_cx).is_ready());
4240    }
4241
4242    #[test]
4243    fn request_local_cancellation_is_deferred_inside_framework_mask() {
4244        let cancellation = McpRequestCancellation::new();
4245        let ctx =
4246            McpContext::new(Cx::for_testing(), 1).with_request_cancellation(cancellation.clone());
4247
4248        let checkpoint = ctx
4249            .masked(|| {
4250                cancellation.cancel();
4251                ctx.checkpoint()
4252            })
4253            .expect("framework mask should be admitted");
4254
4255        assert!(checkpoint.is_ok());
4256        assert!(ctx.ensure_live().is_err());
4257    }
4258
4259    #[test]
4260    fn request_local_cancellation_stops_state_and_capability_effects() {
4261        let state = SessionState::new();
4262        assert!(state.set("existing", 1_u32));
4263        let cancellation = McpRequestCancellation::new();
4264        let ctx = McpContext::with_state(Cx::for_testing(), 1, state.clone())
4265            .with_sampling(Arc::new(NoOpSamplingSender))
4266            .with_elicitation(Arc::new(NoOpElicitationSender))
4267            .with_request_cancellation(cancellation.clone());
4268
4269        assert!(ctx.can_sample());
4270        assert!(ctx.can_elicit());
4271        assert!(cancellation.cancel());
4272
4273        assert!(!ctx.set_state("late", 2_u32));
4274        assert!(ctx.remove_state("existing").is_none());
4275        assert!(!ctx.disable_tool("late-tool"));
4276        assert!(!ctx.disable_resource("late://resource"));
4277        assert!(!ctx.disable_prompt("late-prompt"));
4278        assert!(!ctx.can_sample());
4279        assert!(!ctx.can_elicit());
4280        assert_eq!(state.get::<u32>("existing"), Some(1));
4281        assert!(!state.contains("late"));
4282    }
4283
4284    #[test]
4285    fn admitted_mask_allows_critical_state_commit_before_cancellation_surfaces() {
4286        let state = SessionState::new();
4287        let cancellation = McpRequestCancellation::new();
4288        let ctx = McpContext::with_state(Cx::for_testing(), 1, state.clone())
4289            .with_request_cancellation(cancellation.clone());
4290
4291        let committed = ctx
4292            .masked(|| {
4293                assert!(cancellation.cancel());
4294                ctx.set_state("critical-commit", true)
4295            })
4296            .expect("mask should be admitted before cancellation");
4297
4298        assert!(committed);
4299        assert_eq!(state.get::<bool>("critical-commit"), Some(true));
4300        assert!(ctx.ensure_live().is_err());
4301    }
4302
4303    #[test]
4304    fn active_request_clone_cannot_replace_cancellation_authority() {
4305        let original = McpRequestCancellation::new();
4306        let replacement = McpRequestCancellation::new();
4307        let root =
4308            McpContext::new(Cx::for_testing(), 1).with_request_cancellation(original.clone());
4309        let (scoped, _guard) = root
4310            .begin_request_scope()
4311            .expect("new context should activate one request lease");
4312        let attempted_escape = scoped
4313            .clone()
4314            .with_request_cancellation(replacement.clone());
4315
4316        assert!(original.cancel());
4317        assert!(attempted_escape.ensure_live().is_err());
4318        assert!(!replacement.is_cancel_requested());
4319    }
4320
4321    #[test]
4322    fn request_finalization_and_cancellation_have_one_atomic_winner() {
4323        let cancellation_wins = McpRequestCancellation::new();
4324        assert!(cancellation_wins.cancel());
4325        assert!(!cancellation_wins.begin_finalization());
4326        assert!(cancellation_wins.is_cancel_requested());
4327        assert!(cancellation_wins.is_terminal());
4328
4329        let finalization_wins = McpRequestCancellation::new();
4330        assert!(finalization_wins.begin_finalization());
4331        assert!(finalization_wins.is_finalizing());
4332        assert!(finalization_wins.is_terminal());
4333        assert!(!finalization_wins.cancel());
4334        assert!(!finalization_wins.is_cancel_requested());
4335    }
4336
4337    #[test]
4338    fn test_mcp_context_checkpoint_budget_exhausted() {
4339        let cx = Cx::for_testing_with_budget(Budget::ZERO);
4340        let ctx = McpContext::new(cx, 1);
4341
4342        // Should fail when budget is exhausted
4343        assert!(ctx.checkpoint().is_err());
4344    }
4345
4346    #[test]
4347    fn checkpoint_does_not_treat_zero_cost_as_poll_exhaustion() {
4348        let budget = Budget::new().with_poll_quota(2).with_cost_quota(0);
4349        let cx = Cx::for_testing_with_budget(budget);
4350        let ctx = McpContext::new(cx.clone(), 1);
4351
4352        assert!(ctx.checkpoint().is_ok());
4353        assert!(!cx.is_cancel_requested());
4354        assert_eq!(ctx.budget().cost_quota, Some(0));
4355    }
4356
4357    #[test]
4358    fn closed_request_lease_cannot_be_revived_or_use_framework_capabilities() {
4359        let state = SessionState::new();
4360        let root = McpContext::with_state(Cx::for_testing(), 1, state);
4361        let clone_created_before_scope = root.clone();
4362        let (scoped, guard) = root
4363            .begin_request_scope()
4364            .expect("new context should create one request lease");
4365        let escaped = scoped.clone();
4366        drop(guard);
4367
4368        assert!(escaped.ensure_live().is_err());
4369        assert!(escaped.checkpoint().is_err());
4370        assert!(escaped.consume_cost(0).is_err());
4371        assert!(escaped.masked(|| 42).is_err());
4372        assert!(!escaped.set_auth(AuthContext::with_subject("late")));
4373        assert!(!escaped.set_state("late", true));
4374        assert!(escaped.auth().is_none());
4375        assert!(!escaped.can_call_tools());
4376        assert!(!escaped.can_read_resources());
4377        assert!(clone_created_before_scope.ensure_live().is_err());
4378
4379        assert!(clone_created_before_scope.begin_request_scope().is_none());
4380    }
4381
4382    #[test]
4383    fn test_mcp_context_masked_section() {
4384        let cx = Cx::for_testing();
4385        let ctx = McpContext::new(cx, 1);
4386
4387        // masked() should execute the closure and return its value
4388        let result = ctx.masked(|| 42).expect("mask should be admitted");
4389        assert_eq!(result, 42);
4390    }
4391
4392    #[test]
4393    fn test_mcp_context_budget() {
4394        let cx = Cx::for_testing();
4395        let ctx = McpContext::new(cx, 1);
4396
4397        // Budget should be available
4398        let budget = ctx.budget();
4399        // For testing Cx, budget should not be exhausted
4400        assert!(!budget.is_exhausted());
4401    }
4402
4403    #[test]
4404    fn budget_ceiling_is_monotone_and_visible_to_checkpoints() {
4405        let ambient_deadline = wall_now().saturating_add_nanos(5_000_000_000);
4406        let tighter_deadline = ambient_deadline.saturating_sub_nanos(1_000_000_000);
4407        let later_deadline = ambient_deadline.saturating_add_nanos(1_000_000_000);
4408        let cx = Cx::for_testing_with_budget(Budget::new().with_deadline(ambient_deadline));
4409        let ctx = McpContext::new(cx, 1)
4410            .with_budget_ceiling(Budget::new().with_deadline(tighter_deadline))
4411            .with_budget_ceiling(Budget::new().with_deadline(later_deadline));
4412
4413        assert_eq!(ctx.budget().deadline, Some(tighter_deadline));
4414        assert!(ctx.checkpoint().is_ok());
4415    }
4416
4417    #[test]
4418    fn operation_deadline_tightens_child_without_leaking_to_parent() {
4419        let parent_deadline = wall_now().saturating_add_nanos(5_000_000_000);
4420        let child_deadline = parent_deadline.saturating_sub_nanos(1_000_000_000);
4421        let parent = McpContext::new(Cx::for_testing(), 1)
4422            .with_budget_ceiling(Budget::new().with_deadline(parent_deadline));
4423        let child = parent.clone().with_operation_deadline(Some(child_deadline));
4424        let grandchild = child.clone().with_operation_deadline(None);
4425
4426        assert_eq!(parent.budget().deadline, Some(parent_deadline));
4427        assert_eq!(child.budget().deadline, Some(child_deadline));
4428        assert_eq!(grandchild.budget().deadline, Some(child_deadline));
4429    }
4430
4431    #[test]
4432    fn framework_poll_ceiling_drains_across_clones_at_n_plus_one() {
4433        const LIMIT: u32 = 3;
4434
4435        let ctx = McpContext::new(Cx::for_testing(), 1)
4436            .with_budget_ceiling(Budget::new().with_poll_quota(LIMIT));
4437        let clone = ctx.clone();
4438
4439        for admitted in 0..LIMIT {
4440            let result = if admitted % 2 == 0 {
4441                ctx.checkpoint()
4442            } else {
4443                clone.checkpoint()
4444            };
4445            assert!(result.is_ok(), "checkpoint {} should fit", admitted + 1);
4446            let expected = LIMIT - admitted - 1;
4447            assert_eq!(ctx.budget().poll_quota, expected);
4448            assert_eq!(clone.budget().poll_quota, expected);
4449        }
4450
4451        assert!(clone.checkpoint().is_err(), "checkpoint N+1 must fail");
4452        assert_eq!(ctx.budget().poll_quota, 0);
4453        assert!(!ctx.cx().is_cancel_requested());
4454    }
4455
4456    #[test]
4457    fn ambient_poll_budget_drains_across_clones_without_mutating_cx() {
4458        const LIMIT: u32 = 3;
4459
4460        let cx = Cx::for_testing_with_budget(Budget::new().with_poll_quota(LIMIT));
4461        let ctx = McpContext::new(cx.clone(), 1);
4462        let clone = ctx.clone();
4463
4464        for admitted in 0..LIMIT {
4465            let result = if admitted % 2 == 0 {
4466                ctx.checkpoint()
4467            } else {
4468                clone.checkpoint()
4469            };
4470            assert!(
4471                result.is_ok(),
4472                "ambient checkpoint {} should fit",
4473                admitted + 1
4474            );
4475            assert_eq!(ctx.budget().poll_quota, LIMIT - admitted - 1);
4476        }
4477
4478        let debits_before_rejection = ctx
4479            .budget_state
4480            .lock()
4481            .unwrap_or_else(std::sync::PoisonError::into_inner)
4482            .ambient_poll_debits;
4483        assert!(
4484            clone.checkpoint().is_err(),
4485            "ambient checkpoint N+1 must fail"
4486        );
4487        assert_eq!(
4488            ctx.budget_state
4489                .lock()
4490                .unwrap_or_else(std::sync::PoisonError::into_inner)
4491                .ambient_poll_debits,
4492            debits_before_rejection,
4493            "a rejected checkpoint must not partially debit the ledger"
4494        );
4495        assert_eq!(ctx.budget().poll_quota, 0);
4496        assert_eq!(cx.budget().poll_quota, LIMIT);
4497        assert!(!cx.is_cancel_requested());
4498        assert!(ctx.ensure_live().is_ok());
4499    }
4500
4501    #[test]
4502    fn tighter_ambient_poll_limit_does_not_debit_looser_ceiling_on_rejection() {
4503        let cx = Cx::for_testing_with_budget(Budget::new().with_poll_quota(2));
4504        let ctx =
4505            McpContext::new(cx.clone(), 1).with_budget_ceiling(Budget::new().with_poll_quota(3));
4506
4507        assert!(ctx.checkpoint().is_ok());
4508        assert!(ctx.checkpoint().is_ok());
4509        assert!(ctx.checkpoint().is_err());
4510
4511        let state = *ctx
4512            .budget_state
4513            .lock()
4514            .unwrap_or_else(std::sync::PoisonError::into_inner);
4515        assert_eq!(state.ambient_poll_debits, 2);
4516        assert_eq!(state.ceiling.map(|budget| budget.poll_quota), Some(1));
4517        assert_eq!(cx.budget().poll_quota, 2);
4518    }
4519
4520    #[test]
4521    fn framework_cost_ceiling_drains_across_clones_at_n_plus_one() {
4522        const LIMIT: u64 = 3;
4523
4524        let ctx = McpContext::new(Cx::for_testing(), 1)
4525            .with_budget_ceiling(Budget::new().with_cost_quota(LIMIT));
4526        let clone = ctx.clone();
4527
4528        for admitted in 0..LIMIT {
4529            let result = if admitted % 2 == 0 {
4530                ctx.consume_cost(1)
4531            } else {
4532                clone.consume_cost(1)
4533            };
4534            assert!(result.is_ok(), "cost debit {} should fit", admitted + 1);
4535            let expected = Some(LIMIT - admitted - 1);
4536            assert_eq!(ctx.budget().cost_quota, expected);
4537            assert_eq!(clone.budget().cost_quota, expected);
4538        }
4539
4540        assert!(clone.consume_cost(1).is_err(), "cost debit N+1 must fail");
4541        assert_eq!(ctx.budget().cost_quota, Some(0));
4542        assert!(!ctx.cx().is_cancel_requested());
4543        assert!(
4544            ctx.ensure_live().is_ok(),
4545            "an exactly admitted final debit is not an overrun"
4546        );
4547    }
4548
4549    #[test]
4550    fn framework_poll_and_cost_debits_are_independent() {
4551        let ctx = McpContext::new(Cx::for_testing(), 1)
4552            .with_budget_ceiling(Budget::new().with_poll_quota(2).with_cost_quota(2));
4553
4554        assert!(ctx.checkpoint().is_ok());
4555        assert_eq!(ctx.budget().poll_quota, 1);
4556        assert_eq!(ctx.budget().cost_quota, Some(2));
4557
4558        assert!(ctx.consume_cost(1).is_ok());
4559        assert_eq!(ctx.budget().poll_quota, 1);
4560        assert_eq!(ctx.budget().cost_quota, Some(1));
4561    }
4562
4563    #[test]
4564    fn exact_poll_depletion_is_live_until_the_next_poll_admission() {
4565        let ctx = McpContext::new(Cx::for_testing(), 1)
4566            .with_budget_ceiling(Budget::new().with_poll_quota(1));
4567
4568        assert!(ctx.checkpoint().is_ok());
4569        assert_eq!(ctx.budget().poll_quota, 0);
4570        assert!(ctx.ensure_live().is_ok());
4571        assert!(ctx.checkpoint().is_err());
4572    }
4573
4574    #[test]
4575    fn zero_framework_quotas_fail_without_cancelling_ambient_context() {
4576        let poll_ctx = McpContext::new(Cx::for_testing(), 1)
4577            .with_budget_ceiling(Budget::new().with_poll_quota(0));
4578        let cost_ctx = McpContext::new(Cx::for_testing(), 2)
4579            .with_budget_ceiling(Budget::new().with_cost_quota(0));
4580
4581        assert!(poll_ctx.checkpoint().is_err());
4582        assert_eq!(poll_ctx.budget().poll_quota, 0);
4583        assert!(!poll_ctx.cx().is_cancel_requested());
4584
4585        assert!(cost_ctx.consume_cost(0).is_ok());
4586        assert!(cost_ctx.consume_cost(1).is_err());
4587        assert_eq!(cost_ctx.budget().cost_quota, Some(0));
4588        assert!(!cost_ctx.cx().is_cancel_requested());
4589    }
4590
4591    #[test]
4592    fn oversized_framework_cost_debit_is_atomic() {
4593        let ctx = McpContext::new(Cx::for_testing(), 1)
4594            .with_budget_ceiling(Budget::new().with_cost_quota(2));
4595
4596        assert!(ctx.consume_cost(3).is_err());
4597        assert_eq!(ctx.budget().cost_quota, Some(2));
4598        assert!(ctx.consume_cost(2).is_ok());
4599        assert_eq!(ctx.budget().cost_quota, Some(0));
4600        assert!(ctx.consume_cost(1).is_err());
4601    }
4602
4603    #[test]
4604    fn zero_ambient_cost_quota_prevents_framework_cost_debit() {
4605        let ambient = Budget::new().with_cost_quota(0);
4606        let ctx = McpContext::new(Cx::for_testing_with_budget(ambient), 1)
4607            .with_budget_ceiling(Budget::new().with_cost_quota(3));
4608
4609        assert!(ctx.consume_cost(1).is_err());
4610        assert_eq!(ctx.budget().cost_quota, Some(0));
4611    }
4612
4613    #[test]
4614    fn positive_ambient_cost_quota_drains_cumulatively_across_clones() {
4615        const LIMIT: u64 = 3;
4616        let ambient = Budget::new().with_cost_quota(LIMIT);
4617        let ctx = McpContext::new(Cx::for_testing_with_budget(ambient), 1);
4618        let clone = ctx.clone();
4619
4620        for admitted in 0..LIMIT {
4621            let result = if admitted % 2 == 0 {
4622                ctx.consume_cost(1)
4623            } else {
4624                clone.consume_cost(1)
4625            };
4626            assert!(result.is_ok(), "ambient debit {} should fit", admitted + 1);
4627            assert_eq!(ctx.budget().cost_quota, Some(LIMIT - admitted - 1));
4628        }
4629
4630        assert!(
4631            clone.consume_cost(1).is_err(),
4632            "ambient debit N+1 must fail"
4633        );
4634        assert_eq!(ctx.budget().cost_quota, Some(0));
4635        assert_eq!(
4636            ctx.cx().budget().cost_quota,
4637            Some(LIMIT),
4638            "request-local accounting must not mutate the caller-owned Cx"
4639        );
4640    }
4641
4642    #[test]
4643    fn rejected_cost_debit_does_not_record_an_ambient_checkpoint() {
4644        let cx = Cx::for_testing_with_budget(Budget::new().with_cost_quota(2));
4645        let ctx = McpContext::new(cx, 1);
4646        let before = ctx.cx().checkpoint_state().checkpoint_count;
4647
4648        assert!(ctx.consume_cost(3).is_err());
4649        assert_eq!(ctx.cx().checkpoint_state().checkpoint_count, before);
4650        assert_eq!(ctx.budget().cost_quota, Some(2));
4651    }
4652
4653    #[test]
4654    fn zero_cost_debit_observes_explicit_cancellation() {
4655        let cx = Cx::for_testing();
4656        cx.set_cancel_requested(true);
4657        let ctx = McpContext::new(cx, 1);
4658
4659        assert!(ctx.consume_cost(0).is_err());
4660    }
4661
4662    #[test]
4663    fn expired_request_ceiling_fails_without_cancelling_ambient_context() {
4664        let cx = Cx::for_testing();
4665        let ctx = McpContext::new(cx, 1)
4666            .with_budget_ceiling(Budget::new().with_deadline(asupersync::Time::ZERO));
4667
4668        assert!(ctx.is_cancelled());
4669        assert!(ctx.checkpoint().is_err());
4670        assert!(!ctx.cx().is_cancel_requested());
4671    }
4672
4673    #[test]
4674    fn framework_budget_ceiling_is_deferred_while_masked() {
4675        let ctx = McpContext::new(Cx::for_testing(), 1)
4676            .with_budget_ceiling(Budget::new().with_deadline(asupersync::Time::ZERO));
4677
4678        assert!(
4679            ctx.masked(|| ctx.checkpoint())
4680                .expect("mask should be admitted")
4681                .is_ok()
4682        );
4683        assert!(ctx.checkpoint().is_err());
4684    }
4685
4686    #[test]
4687    fn framework_poll_debits_continue_while_enforcement_is_masked() {
4688        let ctx = McpContext::new(Cx::for_testing(), 1)
4689            .with_budget_ceiling(Budget::new().with_poll_quota(1));
4690
4691        ctx.masked(|| {
4692            assert!(ctx.checkpoint().is_ok());
4693            assert_eq!(ctx.budget().poll_quota, 0);
4694            assert!(ctx.checkpoint().is_ok());
4695        })
4696        .expect("mask should be admitted");
4697
4698        assert!(ctx.checkpoint().is_err());
4699    }
4700
4701    #[test]
4702    fn masked_cost_overage_saturates_framework_ceiling() {
4703        let ctx = McpContext::new(Cx::for_testing(), 1)
4704            .with_budget_ceiling(Budget::new().with_cost_quota(2));
4705
4706        assert!(
4707            ctx.masked(|| ctx.consume_cost(3))
4708                .expect("mask should be admitted")
4709                .is_ok()
4710        );
4711        assert_eq!(ctx.budget().cost_quota, Some(0));
4712        assert!(ctx.ensure_live().is_err());
4713        assert!(ctx.consume_cost(1).is_err());
4714    }
4715
4716    #[test]
4717    fn masked_exact_cost_depletion_does_not_become_a_deferred_overrun() {
4718        let ctx = McpContext::new(Cx::for_testing(), 1)
4719            .with_budget_ceiling(Budget::new().with_cost_quota(2));
4720
4721        assert!(
4722            ctx.masked(|| ctx.consume_cost(2))
4723                .expect("mask should be admitted")
4724                .is_ok()
4725        );
4726        assert_eq!(ctx.budget().cost_quota, Some(0));
4727        assert!(ctx.ensure_live().is_ok());
4728        assert!(ctx.consume_cost(0).is_ok());
4729        assert!(ctx.consume_cost(1).is_err());
4730    }
4731
4732    #[test]
4733    fn masked_cost_overage_saturates_tighter_ambient_quota() {
4734        let ambient = Budget::new().with_cost_quota(2);
4735        let ctx = McpContext::new(Cx::for_testing_with_budget(ambient), 1)
4736            .with_budget_ceiling(Budget::new().with_cost_quota(10));
4737
4738        assert!(
4739            ctx.masked(|| ctx.consume_cost(3))
4740                .expect("mask should be admitted")
4741                .is_ok()
4742        );
4743        assert_eq!(ctx.budget().cost_quota, Some(0));
4744        assert_eq!(
4745            ctx.budget_state
4746                .lock()
4747                .unwrap_or_else(std::sync::PoisonError::into_inner)
4748                .ceiling
4749                .and_then(|budget| budget.cost_quota),
4750            Some(7),
4751            "the looser framework ceiling is still debited independently"
4752        );
4753        assert!(ctx.consume_cost(1).is_err());
4754    }
4755
4756    #[test]
4757    fn framework_mask_is_shared_with_clones_and_restored_after_exit() {
4758        let ctx = McpContext::new(Cx::for_testing(), 1)
4759            .with_budget_ceiling(Budget::new().with_poll_quota(0));
4760        let clone = ctx.clone();
4761
4762        assert!(
4763            ctx.masked(|| clone.checkpoint())
4764                .expect("mask should be admitted")
4765                .is_ok()
4766        );
4767        assert!(clone.checkpoint().is_err());
4768    }
4769
4770    #[test]
4771    fn framework_mask_depth_is_restored_after_unwind() {
4772        let ctx = McpContext::new(Cx::for_testing(), 1)
4773            .with_budget_ceiling(Budget::new().with_deadline(asupersync::Time::ZERO));
4774
4775        let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4776            let _ = ctx.masked(|| panic!("test-only masked-section panic"));
4777        }));
4778
4779        assert!(ctx.checkpoint().is_err());
4780        assert_eq!(ctx.framework_mask_depth.load(Ordering::SeqCst), 0);
4781    }
4782
4783    #[test]
4784    fn test_cancelled_error_display() {
4785        let err = CancelledError;
4786        assert_eq!(err.to_string(), "request cancelled");
4787    }
4788
4789    #[test]
4790    fn handler_log_respects_client_floor_and_missing_floor() {
4791        let captured = Arc::new(Mutex::new(Vec::new()));
4792        struct CaptureSender(Arc<Mutex<Vec<(McpLogLevel, String)>>>);
4793        impl NotificationSender for CaptureSender {
4794            fn send_progress(&self, _progress: f64, _total: Option<f64>, _message: Option<&str>) {}
4795            fn send_log(&self, level: McpLogLevel, _logger: Option<&str>, data: serde_json::Value) {
4796                self.0
4797                    .lock()
4798                    .unwrap_or_else(std::sync::PoisonError::into_inner)
4799                    .push((level, data.as_str().unwrap_or_default().to_owned()));
4800            }
4801        }
4802
4803        let silent = McpContext::new(Cx::for_testing(), 1)
4804            .with_log_sender(Arc::new(CaptureSender(Arc::clone(&captured))));
4805        silent.info("before-floor");
4806        assert!(captured.lock().expect("lock").is_empty());
4807
4808        let ctx = silent.with_min_log_level(Some(McpLogLevel::Info));
4809        assert_eq!(ctx.min_log_level(), Some(McpLogLevel::Info));
4810        ctx.debug("too-low");
4811        ctx.info("admitted");
4812        ctx.warning("also-admitted");
4813        let emitted = captured.lock().expect("lock").clone();
4814        assert_eq!(
4815            emitted,
4816            vec![
4817                (McpLogLevel::Info, "admitted".to_owned()),
4818                (McpLogLevel::Warning, "also-admitted".to_owned()),
4819            ]
4820        );
4821    }
4822
4823    #[test]
4824    fn catalog_change_emits_only_when_the_disabled_set_mutates() {
4825        let captured = Arc::new(Mutex::new(Vec::new()));
4826        struct CaptureSender(Arc<Mutex<Vec<McpCatalogKind>>>);
4827        impl NotificationSender for CaptureSender {
4828            fn send_progress(&self, _progress: f64, _total: Option<f64>, _message: Option<&str>) {}
4829            fn send_catalog_changed(&self, kind: McpCatalogKind) {
4830                self.0
4831                    .lock()
4832                    .unwrap_or_else(std::sync::PoisonError::into_inner)
4833                    .push(kind);
4834            }
4835        }
4836
4837        let ctx = McpContext::with_state(Cx::for_testing(), 1, SessionState::new())
4838            .with_log_sender(Arc::new(CaptureSender(Arc::clone(&captured))));
4839        assert!(ctx.disable_tool("admin"));
4840        assert!(ctx.disable_tool("admin"));
4841        assert!(ctx.enable_tool("admin"));
4842        assert!(ctx.enable_tool("admin"));
4843        assert!(ctx.disable_resource("file://secret"));
4844        assert!(ctx.disable_prompt("hidden"));
4845        assert_eq!(
4846            *captured.lock().expect("lock"),
4847            vec![
4848                McpCatalogKind::Tools,
4849                McpCatalogKind::Tools,
4850                McpCatalogKind::Resources,
4851                McpCatalogKind::Prompts,
4852            ]
4853        );
4854    }
4855
4856    #[test]
4857    fn catalog_publisher_receives_mutations_even_without_a_session_sender() {
4858        let captured = Arc::new(Mutex::new(Vec::new()));
4859        struct CapturePublisher(Arc<Mutex<Vec<McpCatalogKind>>>);
4860        impl CatalogChangePublisher for CapturePublisher {
4861            fn publish_catalog_changed(&self, kind: McpCatalogKind) -> bool {
4862                self.0
4863                    .lock()
4864                    .unwrap_or_else(std::sync::PoisonError::into_inner)
4865                    .push(kind);
4866                true
4867            }
4868            fn publish_resource_updated(&self, _uri: &str) -> bool {
4869                false
4870            }
4871        }
4872
4873        let ctx = McpContext::with_state(Cx::for_testing(), 1, SessionState::new())
4874            .with_catalog_publisher(Arc::new(CapturePublisher(Arc::clone(&captured))));
4875        assert!(ctx.disable_tool("admin"));
4876        assert!(ctx.disable_tool("admin"));
4877        assert_eq!(*captured.lock().expect("lock"), vec![McpCatalogKind::Tools]);
4878    }
4879
4880    #[test]
4881    fn notify_resource_updated_requires_a_live_subscription() {
4882        let captured = Arc::new(Mutex::new(Vec::new()));
4883        struct CaptureSender(Arc<Mutex<Vec<String>>>);
4884        impl NotificationSender for CaptureSender {
4885            fn send_progress(&self, _progress: f64, _total: Option<f64>, _message: Option<&str>) {}
4886            fn send_resource_updated(&self, uri: &str) {
4887                self.0
4888                    .lock()
4889                    .unwrap_or_else(std::sync::PoisonError::into_inner)
4890                    .push(uri.to_owned());
4891            }
4892        }
4893
4894        let ctx = McpContext::new(Cx::for_testing(), 1)
4895            .with_log_sender(Arc::new(CaptureSender(Arc::clone(&captured))))
4896            .with_resource_subscriptions(["file:///watched.txt"]);
4897        assert!(!ctx.notify_resource_updated("file:///other.txt"));
4898        assert!(ctx.notify_resource_updated("file:///watched.txt"));
4899        assert_eq!(
4900            *captured.lock().expect("lock"),
4901            vec!["file:///watched.txt".to_owned()]
4902        );
4903    }
4904
4905    #[test]
4906    fn test_into_outcome_ok() {
4907        let result: Result<i32, CancelledError> = Ok(42);
4908        let outcome: Outcome<i32, CancelledError> = result.into_outcome();
4909        assert!(matches!(outcome, Outcome::Ok(42)));
4910    }
4911
4912    #[test]
4913    fn test_into_outcome_cancelled() {
4914        let result: Result<i32, CancelledError> = Err(CancelledError);
4915        let outcome: Outcome<i32, ()> = result.into_outcome();
4916        assert!(matches!(outcome, Outcome::Cancelled(_)));
4917    }
4918
4919    #[test]
4920    fn test_mcp_context_no_progress_reporter_by_default() {
4921        let cx = Cx::for_testing();
4922        let ctx = McpContext::new(cx, 1);
4923        assert!(!ctx.has_progress_reporter());
4924    }
4925
4926    #[test]
4927    fn test_mcp_context_with_progress_reporter() {
4928        let cx = Cx::for_testing();
4929        let sender = Arc::new(NoOpNotificationSender);
4930        let reporter = ProgressReporter::new(sender);
4931        let ctx = McpContext::with_progress(cx, 1, reporter);
4932        assert!(ctx.has_progress_reporter());
4933    }
4934
4935    #[test]
4936    fn progress_reporter_builder_preserves_request_accounting_domain() {
4937        let ctx = McpContext::new(Cx::for_testing(), 1)
4938            .with_budget_ceiling(Budget::new().with_cost_quota(5));
4939        let reporter = ProgressReporter::new(Arc::new(NoOpNotificationSender));
4940        let derived = ctx.clone().with_progress_reporter(reporter);
4941
4942        assert!(derived.has_progress_reporter());
4943        assert!(!ctx.has_progress_reporter());
4944        assert!(ctx.consume_cost(3).is_ok());
4945        assert_eq!(derived.budget().cost_quota, Some(2));
4946    }
4947
4948    #[test]
4949    fn isolated_auth_stages_identity_without_handler_capabilities() {
4950        let root = McpContext::with_state(Cx::for_testing(), 1, SessionState::new())
4951            .with_budget_ceiling(Budget::new().with_cost_quota(2))
4952            .with_sampling(Arc::new(NoOpSamplingSender))
4953            .with_elicitation(Arc::new(NoOpElicitationSender))
4954            .with_roots_provider(Arc::new(FixedRootsProvider));
4955        let staged = root.clone().with_isolated_auth();
4956
4957        assert!(staged.auth().is_none());
4958        assert!(!staged.has_session_state());
4959        assert!(!staged.can_sample());
4960        assert!(!staged.can_elicit());
4961        assert!(!staged.can_list_roots());
4962        assert!(!staged.can_read_resources());
4963        assert!(!staged.can_call_tools());
4964        assert!(staged.set_auth(AuthContext::with_subject("tentative")));
4965        assert_eq!(
4966            staged.auth().and_then(|auth| auth.subject),
4967            Some("tentative".to_string())
4968        );
4969        assert_eq!(root.auth().and_then(|auth| auth.subject), None);
4970
4971        assert!(root.set_auth(AuthContext::with_subject("committed")));
4972        let attempted_reisolation = root.clone().with_isolated_auth();
4973        assert_eq!(
4974            attempted_reisolation.auth().and_then(|auth| auth.subject),
4975            Some("committed".to_string())
4976        );
4977
4978        assert!(staged.consume_cost(1).is_ok());
4979        assert_eq!(root.budget().cost_quota, Some(1));
4980    }
4981
4982    #[test]
4983    fn committed_anonymous_auth_is_hidden_and_write_once() {
4984        let ctx = McpContext::with_state(Cx::for_testing(), 1, SessionState::new());
4985
4986        assert!(ctx.commit_anonymous_auth());
4987        assert!(ctx.auth().is_none());
4988        assert!(matches!(ctx.cache_auth_partition(), Some(None)));
4989        assert!(!ctx.set_auth(AuthContext::with_subject("forged")));
4990        assert!(!ctx.commit_anonymous_auth());
4991
4992        let clone = ctx.clone();
4993        assert!(clone.auth().is_none());
4994        assert!(matches!(clone.cache_auth_partition(), Some(None)));
4995    }
4996
4997    #[test]
4998    fn authenticated_cache_partition_contains_committed_facts() {
4999        let ctx = McpContext::new(Cx::for_testing(), 1);
5000        assert!(ctx.set_auth(AuthContext::with_subject("alice")));
5001
5002        let Some(Some(auth)) = ctx.cache_auth_partition() else {
5003            panic!("authenticated admission must expose cache partition facts");
5004        };
5005        assert_eq!(auth.subject.as_deref(), Some("alice"));
5006    }
5007
5008    #[test]
5009    fn test_report_progress_without_reporter() {
5010        let cx = Cx::for_testing();
5011        let ctx = McpContext::new(cx, 1);
5012        // Should not panic when no reporter is set
5013        ctx.report_progress(0.5, Some("test"));
5014        ctx.report_progress_with_total(5.0, 10.0, None);
5015    }
5016
5017    #[test]
5018    fn test_report_progress_with_reporter() {
5019        use std::sync::atomic::{AtomicU32, Ordering};
5020
5021        struct CountingSender {
5022            count: AtomicU32,
5023        }
5024
5025        impl NotificationSender for CountingSender {
5026            fn send_progress(&self, _progress: f64, _total: Option<f64>, _message: Option<&str>) {
5027                self.count.fetch_add(1, Ordering::SeqCst);
5028            }
5029        }
5030
5031        let cx = Cx::for_testing();
5032        let sender = Arc::new(CountingSender {
5033            count: AtomicU32::new(0),
5034        });
5035        let reporter = ProgressReporter::new(sender.clone());
5036        let ctx = McpContext::with_progress(cx, 1, reporter);
5037
5038        ctx.report_progress(0.25, Some("step 1"));
5039        ctx.report_progress(0.5, None);
5040        ctx.report_progress_with_total(3.0, 4.0, Some("step 3"));
5041
5042        assert_eq!(sender.count.load(Ordering::SeqCst), 3);
5043    }
5044
5045    #[test]
5046    fn request_local_cancellation_suppresses_subsequent_progress() {
5047        use std::sync::atomic::{AtomicU32, Ordering};
5048
5049        struct CountingSender {
5050            count: AtomicU32,
5051        }
5052
5053        impl NotificationSender for CountingSender {
5054            fn send_progress(&self, _progress: f64, _total: Option<f64>, _message: Option<&str>) {
5055                self.count.fetch_add(1, Ordering::SeqCst);
5056            }
5057        }
5058
5059        let sender = Arc::new(CountingSender {
5060            count: AtomicU32::new(0),
5061        });
5062        let cancellation = McpRequestCancellation::new();
5063        let ctx =
5064            McpContext::with_progress(Cx::for_testing(), 1, ProgressReporter::new(sender.clone()))
5065                .with_request_cancellation(cancellation.clone());
5066
5067        ctx.report_progress(0.25, Some("before cancellation"));
5068        assert!(cancellation.cancel());
5069        ctx.report_progress(0.5, Some("after cancellation"));
5070
5071        assert_eq!(sender.count.load(Ordering::SeqCst), 1);
5072        assert!(!ctx.has_progress_reporter());
5073    }
5074
5075    #[test]
5076    fn test_progress_reporter_debug() {
5077        let sender = Arc::new(NoOpNotificationSender);
5078        let reporter = ProgressReporter::new(sender);
5079        let debug = format!("{reporter:?}");
5080        assert!(debug.contains("ProgressReporter"));
5081    }
5082
5083    #[test]
5084    fn test_noop_notification_sender() {
5085        let sender = NoOpNotificationSender;
5086        // Should not panic
5087        sender.send_progress(0.5, Some(1.0), Some("test"));
5088    }
5089
5090    // Session state tests
5091    #[test]
5092    fn test_mcp_context_no_session_state_by_default() {
5093        let cx = Cx::for_testing();
5094        let ctx = McpContext::new(cx, 1);
5095        assert!(!ctx.has_session_state());
5096    }
5097
5098    #[test]
5099    fn test_mcp_context_with_session_state() {
5100        let cx = Cx::for_testing();
5101        let state = SessionState::new();
5102        let ctx = McpContext::with_state(cx, 1, state);
5103        assert!(ctx.has_session_state());
5104    }
5105
5106    #[test]
5107    fn cache_admission_fails_if_session_state_changes_before_completion() {
5108        let state = SessionState::new();
5109        let ctx = McpContext::with_state(Cx::for_testing(), 1, state.clone());
5110        let admitted = ctx
5111            .begin_session_cache_partition()
5112            .expect("test platform must provide cache-partition entropy");
5113        assert_eq!(ctx.complete_session_cache_partition(), Some(admitted));
5114
5115        assert!(state.set("changed", true));
5116        assert!(ctx.complete_session_cache_partition().is_none());
5117        assert!(ctx.begin_session_cache_partition().is_none());
5118    }
5119
5120    #[test]
5121    fn response_cache_hit_markers_are_middleware_specific() {
5122        let ctx = McpContext::new(Cx::for_testing(), 1);
5123        assert!(ctx.mark_response_cache_hit(10));
5124        assert!(ctx.response_was_cache_hit(10));
5125        assert!(!ctx.response_was_cache_hit(11));
5126        assert!(!ctx.mark_response_cache_hit(0));
5127    }
5128
5129    #[test]
5130    fn test_mcp_context_get_set_state() {
5131        let cx = Cx::for_testing();
5132        let state = SessionState::new();
5133        let ctx = McpContext::with_state(cx, 1, state);
5134
5135        // Set a value
5136        assert!(ctx.set_state("counter", 42));
5137
5138        // Get the value back
5139        let value: Option<i32> = ctx.get_state("counter");
5140        assert_eq!(value, Some(42));
5141    }
5142
5143    #[test]
5144    fn test_mcp_context_state_not_available() {
5145        let cx = Cx::for_testing();
5146        let ctx = McpContext::new(cx, 1);
5147
5148        // set_state returns false when state is not available
5149        assert!(!ctx.set_state("key", "value"));
5150
5151        // get_state returns None when state is not available
5152        let value: Option<String> = ctx.get_state("key");
5153        assert!(value.is_none());
5154    }
5155
5156    #[test]
5157    fn test_mcp_context_has_state() {
5158        let cx = Cx::for_testing();
5159        let state = SessionState::new();
5160        let ctx = McpContext::with_state(cx, 1, state);
5161
5162        assert!(!ctx.has_state("missing"));
5163
5164        ctx.set_state("present", true);
5165        assert!(ctx.has_state("present"));
5166    }
5167
5168    #[test]
5169    fn test_mcp_context_remove_state() {
5170        let cx = Cx::for_testing();
5171        let state = SessionState::new();
5172        let ctx = McpContext::with_state(cx, 1, state);
5173
5174        ctx.set_state("key", "value");
5175        assert!(ctx.has_state("key"));
5176
5177        let removed = ctx.remove_state("key");
5178        assert!(removed.is_some());
5179        assert!(!ctx.has_state("key"));
5180    }
5181
5182    #[test]
5183    fn test_mcp_context_with_state_and_progress() {
5184        let cx = Cx::for_testing();
5185        let state = SessionState::new();
5186        let sender = Arc::new(NoOpNotificationSender);
5187        let reporter = ProgressReporter::new(sender);
5188
5189        let ctx = McpContext::with_state_and_progress(cx, 1, state, reporter);
5190
5191        assert!(ctx.has_session_state());
5192        assert!(ctx.has_progress_reporter());
5193    }
5194
5195    #[test]
5196    fn test_mcp_context_auth_is_request_local() {
5197        let cx = Cx::for_testing();
5198        let state = SessionState::new();
5199        let ctx = McpContext::with_state(cx, 1, state.clone());
5200
5201        assert!(ctx.set_auth(AuthContext::with_subject("alice")));
5202
5203        assert_eq!(
5204            ctx.auth().and_then(|auth| auth.subject),
5205            Some("alice".to_string())
5206        );
5207        assert!(
5208            state.is_empty(),
5209            "request auth must not be persisted into session state"
5210        );
5211    }
5212
5213    #[test]
5214    fn test_mcp_context_clones_share_request_auth() {
5215        let cx = Cx::for_testing();
5216        let ctx = McpContext::new(cx, 1);
5217        let cloned = ctx.clone();
5218
5219        assert!(cloned.set_auth(AuthContext::with_subject("bob")));
5220
5221        assert_eq!(
5222            ctx.auth().and_then(|auth| auth.subject),
5223            Some("bob".to_string())
5224        );
5225    }
5226
5227    #[test]
5228    fn committed_request_auth_is_write_once_across_clones() {
5229        let ctx =
5230            McpContext::new(Cx::for_testing(), 1).with_auth(AuthContext::with_subject("verified"));
5231        let clone = ctx.clone();
5232
5233        assert!(!clone.set_auth(AuthContext::with_subject("replacement")));
5234        assert_eq!(
5235            ctx.auth().and_then(|auth| auth.subject),
5236            Some("verified".to_string())
5237        );
5238    }
5239
5240    #[test]
5241    fn test_new_mcp_contexts_do_not_share_request_auth_even_with_same_cx() {
5242        let cx = Cx::for_testing();
5243        let state = SessionState::new();
5244        let first = McpContext::with_state(cx.clone(), 7, state.clone());
5245        let second = McpContext::with_state(cx, 7, state);
5246
5247        assert!(first.set_auth(AuthContext::with_subject("carol")));
5248
5249        assert!(second.auth().is_none());
5250    }
5251
5252    #[test]
5253    fn test_new_mcp_contexts_do_not_share_request_auth_across_requests() {
5254        let state = SessionState::new();
5255        let first = McpContext::with_state(Cx::for_testing(), 7, state.clone());
5256        let second = McpContext::with_state(Cx::for_testing(), 8, state);
5257
5258        assert!(first.set_auth(AuthContext::with_subject("dave")));
5259
5260        assert_eq!(
5261            first.auth().and_then(|auth| auth.subject),
5262            Some("dave".to_string())
5263        );
5264        assert!(second.auth().is_none());
5265    }
5266
5267    #[test]
5268    fn test_mcp_context_drop_does_not_leak_request_auth() {
5269        let cx = Cx::for_testing();
5270
5271        {
5272            let ctx = McpContext::new(cx.clone(), 9);
5273            assert!(ctx.set_auth(AuthContext::with_subject("erin")));
5274        }
5275
5276        assert!(
5277            McpContext::new(cx, 9).auth().is_none(),
5278            "fresh contexts must start without inherited request auth"
5279        );
5280    }
5281
5282    // ========================================================================
5283    // Dynamic Enable/Disable Tests
5284    // ========================================================================
5285
5286    #[test]
5287    fn test_mcp_context_tools_enabled_by_default() {
5288        let cx = Cx::for_testing();
5289        let state = SessionState::new();
5290        let ctx = McpContext::with_state(cx, 1, state);
5291
5292        assert!(ctx.is_tool_enabled("any_tool"));
5293        assert!(ctx.is_tool_enabled("another_tool"));
5294    }
5295
5296    #[test]
5297    fn test_mcp_context_disable_enable_tool() {
5298        let cx = Cx::for_testing();
5299        let state = SessionState::new();
5300        let ctx = McpContext::with_state(cx, 1, state);
5301
5302        // Tool is enabled by default
5303        assert!(ctx.is_tool_enabled("my_tool"));
5304
5305        // Disable the tool
5306        assert!(ctx.disable_tool("my_tool"));
5307        assert!(!ctx.is_tool_enabled("my_tool"));
5308        assert!(ctx.is_tool_enabled("other_tool"));
5309
5310        // Re-enable the tool
5311        assert!(ctx.enable_tool("my_tool"));
5312        assert!(ctx.is_tool_enabled("my_tool"));
5313    }
5314
5315    #[test]
5316    fn test_mcp_context_disable_enable_resource() {
5317        let cx = Cx::for_testing();
5318        let state = SessionState::new();
5319        let ctx = McpContext::with_state(cx, 1, state);
5320
5321        // Resource is enabled by default
5322        assert!(ctx.is_resource_enabled("file://secret"));
5323
5324        // Disable the resource
5325        assert!(ctx.disable_resource("file://secret"));
5326        assert!(!ctx.is_resource_enabled("file://secret"));
5327        assert!(ctx.is_resource_enabled("file://public"));
5328
5329        // Re-enable the resource
5330        assert!(ctx.enable_resource("file://secret"));
5331        assert!(ctx.is_resource_enabled("file://secret"));
5332    }
5333
5334    #[test]
5335    fn test_mcp_context_disable_enable_prompt() {
5336        let cx = Cx::for_testing();
5337        let state = SessionState::new();
5338        let ctx = McpContext::with_state(cx, 1, state);
5339
5340        // Prompt is enabled by default
5341        assert!(ctx.is_prompt_enabled("admin_prompt"));
5342
5343        // Disable the prompt
5344        assert!(ctx.disable_prompt("admin_prompt"));
5345        assert!(!ctx.is_prompt_enabled("admin_prompt"));
5346        assert!(ctx.is_prompt_enabled("user_prompt"));
5347
5348        // Re-enable the prompt
5349        assert!(ctx.enable_prompt("admin_prompt"));
5350        assert!(ctx.is_prompt_enabled("admin_prompt"));
5351    }
5352
5353    #[test]
5354    fn test_mcp_context_disable_multiple_tools() {
5355        let cx = Cx::for_testing();
5356        let state = SessionState::new();
5357        let ctx = McpContext::with_state(cx, 1, state);
5358
5359        ctx.disable_tool("tool1");
5360        ctx.disable_tool("tool2");
5361        ctx.disable_tool("tool3");
5362
5363        assert!(!ctx.is_tool_enabled("tool1"));
5364        assert!(!ctx.is_tool_enabled("tool2"));
5365        assert!(!ctx.is_tool_enabled("tool3"));
5366        assert!(ctx.is_tool_enabled("tool4"));
5367
5368        let disabled = ctx.disabled_tools();
5369        assert_eq!(disabled.len(), 3);
5370        assert!(disabled.contains("tool1"));
5371        assert!(disabled.contains("tool2"));
5372        assert!(disabled.contains("tool3"));
5373    }
5374
5375    #[test]
5376    fn test_mcp_context_disabled_sets_empty_by_default() {
5377        let cx = Cx::for_testing();
5378        let state = SessionState::new();
5379        let ctx = McpContext::with_state(cx, 1, state);
5380
5381        assert!(ctx.disabled_tools().is_empty());
5382        assert!(ctx.disabled_resources().is_empty());
5383        assert!(ctx.disabled_prompts().is_empty());
5384    }
5385
5386    #[test]
5387    fn test_mcp_context_enable_disable_no_state() {
5388        let cx = Cx::for_testing();
5389        let ctx = McpContext::new(cx, 1);
5390
5391        // Without session state, disable returns false
5392        assert!(!ctx.disable_tool("tool"));
5393        assert!(!ctx.enable_tool("tool"));
5394
5395        // But is_enabled returns true (default is enabled)
5396        assert!(ctx.is_tool_enabled("tool"));
5397    }
5398
5399    #[test]
5400    fn test_mcp_context_disabled_state_persists_across_contexts() {
5401        let state = SessionState::new();
5402
5403        // First context disables a tool
5404        {
5405            let cx = Cx::for_testing();
5406            let ctx = McpContext::with_state(cx, 1, state.clone());
5407            ctx.disable_tool("shared_tool");
5408        }
5409
5410        // Second context (same session state) sees the disabled tool
5411        {
5412            let cx = Cx::for_testing();
5413            let ctx = McpContext::with_state(cx, 2, state.clone());
5414            assert!(!ctx.is_tool_enabled("shared_tool"));
5415        }
5416    }
5417
5418    // ========================================================================
5419    // Capabilities Tests
5420    // ========================================================================
5421
5422    #[test]
5423    fn test_mcp_context_no_capabilities_by_default() {
5424        let cx = Cx::for_testing();
5425        let ctx = McpContext::new(cx, 1);
5426
5427        assert!(ctx.client_capabilities().is_none());
5428        assert!(ctx.server_capabilities().is_none());
5429        assert!(!ctx.client_supports_sampling());
5430        assert!(!ctx.client_supports_elicitation());
5431        assert!(!ctx.client_supports_roots());
5432    }
5433
5434    #[test]
5435    fn test_mcp_context_with_client_capabilities() {
5436        let cx = Cx::for_testing();
5437        let caps = ClientCapabilityInfo::new()
5438            .with_sampling()
5439            .with_elicitation(true, false)
5440            .with_roots(true);
5441
5442        let ctx = McpContext::new(cx, 1).with_client_capabilities(caps);
5443
5444        assert!(ctx.client_capabilities().is_some());
5445        assert!(ctx.client_supports_sampling());
5446        assert!(ctx.client_supports_elicitation());
5447        assert!(ctx.client_supports_elicitation_form());
5448        assert!(!ctx.client_supports_elicitation_url());
5449        assert!(ctx.client_supports_roots());
5450    }
5451
5452    #[test]
5453    fn test_mcp_context_with_client_implementation() {
5454        let cx = Cx::for_testing();
5455        let mut identity = ClientImplementationInfo::new("e2e-client", "1.0.0");
5456        identity.title = Some("Client Title".to_owned());
5457        let ctx = McpContext::new(cx, 1).with_client_implementation(identity);
5458        let observed = ctx
5459            .client_implementation()
5460            .expect("the attached identity must be retained");
5461        assert_eq!(observed.name, "e2e-client");
5462        assert_eq!(observed.title.as_deref(), Some("Client Title"));
5463        assert!(observed.has_extras());
5464        let bare = McpContext::new(Cx::for_testing(), 2);
5465        assert!(bare.client_implementation().is_none());
5466    }
5467
5468    #[test]
5469    fn test_mcp_context_with_server_capabilities() {
5470        let cx = Cx::for_testing();
5471        let caps = ServerCapabilityInfo::new()
5472            .with_tools()
5473            .with_resources(true)
5474            .with_prompts()
5475            .with_logging();
5476
5477        let ctx = McpContext::new(cx, 1).with_server_capabilities(caps);
5478
5479        let server_caps = ctx.server_capabilities().unwrap();
5480        assert!(server_caps.tools);
5481        assert!(server_caps.resources);
5482        assert!(server_caps.resources_subscribe);
5483        assert!(server_caps.prompts);
5484        assert!(server_caps.logging);
5485    }
5486
5487    #[test]
5488    fn test_client_capability_info_builders() {
5489        let caps = ClientCapabilityInfo::new();
5490        assert!(!caps.sampling);
5491        assert!(!caps.elicitation);
5492        assert!(!caps.roots);
5493
5494        let caps = caps.with_sampling();
5495        assert!(caps.sampling);
5496
5497        let caps = ClientCapabilityInfo::new().with_elicitation(true, true);
5498        assert!(caps.elicitation);
5499        assert!(caps.elicitation_form);
5500        assert!(caps.elicitation_url);
5501
5502        let caps = ClientCapabilityInfo::new().with_roots(false);
5503        assert!(caps.roots);
5504        assert!(!caps.roots_list_changed);
5505    }
5506
5507    #[test]
5508    fn test_server_capability_info_builders() {
5509        let caps = ServerCapabilityInfo::new();
5510        assert!(!caps.tools);
5511        assert!(!caps.resources);
5512        assert!(!caps.prompts);
5513        assert!(!caps.logging);
5514
5515        let caps = caps
5516            .with_tools()
5517            .with_resources(false)
5518            .with_prompts()
5519            .with_logging();
5520        assert!(caps.tools);
5521        assert!(caps.resources);
5522        assert!(!caps.resources_subscribe);
5523        assert!(caps.prompts);
5524        assert!(caps.logging);
5525    }
5526
5527    // ========================================================================
5528    // ResourceContentItem Tests
5529    // ========================================================================
5530
5531    #[test]
5532    fn test_resource_content_item_text() {
5533        let item = ResourceContentItem::text("test://uri", "hello");
5534        assert_eq!(item.uri, "test://uri");
5535        assert_eq!(item.mime_type.as_deref(), Some("text/plain"));
5536        assert_eq!(item.as_text(), Some("hello"));
5537        assert!(item.as_blob().is_none());
5538        assert!(item.is_text());
5539        assert!(!item.is_blob());
5540    }
5541
5542    #[test]
5543    fn test_resource_content_item_json() {
5544        let item = ResourceContentItem::json("data://config", r#"{"key":"val"}"#);
5545        assert_eq!(item.uri, "data://config");
5546        assert_eq!(item.mime_type.as_deref(), Some("application/json"));
5547        assert_eq!(item.as_text(), Some(r#"{"key":"val"}"#));
5548        assert!(item.is_text());
5549        assert!(!item.is_blob());
5550    }
5551
5552    #[test]
5553    fn test_resource_content_item_blob() {
5554        let item = ResourceContentItem::blob("binary://data", "application/octet-stream", "AQID");
5555        assert_eq!(item.uri, "binary://data");
5556        assert_eq!(item.mime_type.as_deref(), Some("application/octet-stream"));
5557        assert!(item.as_text().is_none());
5558        assert_eq!(item.as_blob(), Some("AQID"));
5559        assert!(!item.is_text());
5560        assert!(item.is_blob());
5561    }
5562
5563    // ========================================================================
5564    // ResourceReadResult Tests
5565    // ========================================================================
5566
5567    #[test]
5568    fn test_resource_read_result_text() {
5569        let result = ResourceReadResult::text("test://doc", "content");
5570        assert_eq!(result.first_text(), Some("content"));
5571        assert!(result.first_blob().is_none());
5572        assert_eq!(result.contents.len(), 1);
5573    }
5574
5575    #[test]
5576    fn test_resource_read_result_new_multiple() {
5577        let result = ResourceReadResult::new(vec![
5578            ResourceContentItem::text("a://1", "first"),
5579            ResourceContentItem::blob("b://2", "image/png", "base64data"),
5580        ]);
5581        assert_eq!(result.contents.len(), 2);
5582        // first_text returns the first item's text
5583        assert_eq!(result.first_text(), Some("first"));
5584        // first_blob returns None because the first item is text
5585        assert!(result.first_blob().is_none());
5586    }
5587
5588    #[test]
5589    fn test_resource_read_result_empty() {
5590        let result = ResourceReadResult::new(vec![]);
5591        assert!(result.first_text().is_none());
5592        assert!(result.first_blob().is_none());
5593    }
5594
5595    #[test]
5596    fn test_resource_read_result_blob_first() {
5597        let result = ResourceReadResult::new(vec![ResourceContentItem::blob(
5598            "b://1",
5599            "image/png",
5600            "data",
5601        )]);
5602        assert!(result.first_text().is_none());
5603        assert_eq!(result.first_blob(), Some("data"));
5604    }
5605
5606    // ========================================================================
5607    // ToolContentItem Tests
5608    // ========================================================================
5609
5610    #[test]
5611    fn test_tool_content_item_text() {
5612        let item = ToolContentItem::text("hello");
5613        assert_eq!(item.as_text(), Some("hello"));
5614        assert!(item.is_text());
5615    }
5616
5617    #[test]
5618    fn test_tool_content_item_image() {
5619        let item = ToolContentItem::Image {
5620            data: "base64img".to_string(),
5621            mime_type: "image/png".to_string(),
5622        };
5623        assert!(item.as_text().is_none());
5624        assert!(!item.is_text());
5625    }
5626
5627    #[test]
5628    fn test_tool_content_item_audio() {
5629        let item = ToolContentItem::Audio {
5630            data: "base64audio".to_string(),
5631            mime_type: "audio/wav".to_string(),
5632        };
5633        assert!(item.as_text().is_none());
5634        assert!(!item.is_text());
5635    }
5636
5637    #[test]
5638    fn test_tool_content_item_resource() {
5639        let item = ToolContentItem::Resource {
5640            uri: "file://test".to_string(),
5641            mime_type: Some("text/plain".to_string()),
5642            text: Some("embedded".to_string()),
5643            blob: None,
5644        };
5645        assert!(item.as_text().is_none());
5646        assert!(!item.is_text());
5647    }
5648
5649    // ========================================================================
5650    // ToolCallResult Tests
5651    // ========================================================================
5652
5653    #[test]
5654    fn test_tool_call_result_success() {
5655        let result = ToolCallResult::success(vec![
5656            ToolContentItem::text("item1"),
5657            ToolContentItem::text("item2"),
5658        ]);
5659        assert!(!result.is_error);
5660        assert_eq!(result.content.len(), 2);
5661        assert_eq!(result.first_text(), Some("item1"));
5662    }
5663
5664    #[test]
5665    fn test_tool_call_result_text() {
5666        let result = ToolCallResult::text("simple output");
5667        assert!(!result.is_error);
5668        assert_eq!(result.content.len(), 1);
5669        assert_eq!(result.first_text(), Some("simple output"));
5670    }
5671
5672    #[test]
5673    fn test_tool_call_result_error() {
5674        let result = ToolCallResult::error("something failed");
5675        assert!(result.is_error);
5676        assert_eq!(result.first_text(), Some("something failed"));
5677    }
5678
5679    #[test]
5680    fn test_tool_call_result_empty() {
5681        let result = ToolCallResult::success(vec![]);
5682        assert!(!result.is_error);
5683        assert!(result.first_text().is_none());
5684    }
5685
5686    // ========================================================================
5687    // ElicitationResponse Tests
5688    // ========================================================================
5689
5690    #[test]
5691    fn test_elicitation_response_accept() {
5692        let mut data = std::collections::HashMap::new();
5693        data.insert("name".to_string(), serde_json::json!("Alice"));
5694        data.insert("age".to_string(), serde_json::json!(30));
5695        data.insert("active".to_string(), serde_json::json!(true));
5696
5697        let resp = ElicitationResponse::accept(data);
5698        assert!(resp.is_accepted());
5699        assert!(!resp.is_declined());
5700        assert!(!resp.is_cancelled());
5701        assert_eq!(resp.get_string("name"), Some("Alice"));
5702        assert_eq!(resp.get_int("age"), Some(30));
5703        assert_eq!(resp.get_bool("active"), Some(true));
5704    }
5705
5706    #[test]
5707    fn test_elicitation_response_accept_url() {
5708        let resp = ElicitationResponse::accept_url();
5709        assert!(resp.is_accepted());
5710        assert!(resp.content.is_none());
5711        assert!(resp.get_string("anything").is_none());
5712    }
5713
5714    #[test]
5715    fn test_elicitation_response_decline() {
5716        let resp = ElicitationResponse::decline();
5717        assert!(!resp.is_accepted());
5718        assert!(resp.is_declined());
5719        assert!(!resp.is_cancelled());
5720        assert!(resp.get_string("key").is_none());
5721    }
5722
5723    #[test]
5724    fn test_elicitation_response_cancel() {
5725        let resp = ElicitationResponse::cancel();
5726        assert!(!resp.is_accepted());
5727        assert!(!resp.is_declined());
5728        assert!(resp.is_cancelled());
5729    }
5730
5731    #[test]
5732    fn test_elicitation_response_missing_key() {
5733        let mut data = std::collections::HashMap::new();
5734        data.insert("exists".to_string(), serde_json::json!("value"));
5735        let resp = ElicitationResponse::accept(data);
5736
5737        assert!(resp.get_string("missing").is_none());
5738        assert!(resp.get_bool("missing").is_none());
5739        assert!(resp.get_int("missing").is_none());
5740    }
5741
5742    #[test]
5743    fn test_elicitation_response_type_mismatch() {
5744        let mut data = std::collections::HashMap::new();
5745        data.insert("num".to_string(), serde_json::json!(42));
5746        let resp = ElicitationResponse::accept(data);
5747
5748        // get_string on a number returns None
5749        assert!(resp.get_string("num").is_none());
5750        // get_bool on a number returns None
5751        assert!(resp.get_bool("num").is_none());
5752        // get_int on a number returns Some
5753        assert_eq!(resp.get_int("num"), Some(42));
5754    }
5755
5756    // ========================================================================
5757    // Capability Check Tests (can_sample, can_elicit, etc.)
5758    // ========================================================================
5759
5760    #[test]
5761    fn test_can_sample_false_by_default() {
5762        let cx = Cx::for_testing();
5763        let ctx = McpContext::new(cx, 1);
5764        assert!(!ctx.can_sample());
5765    }
5766
5767    #[test]
5768    fn test_can_elicit_false_by_default() {
5769        let cx = Cx::for_testing();
5770        let ctx = McpContext::new(cx, 1);
5771        assert!(!ctx.can_elicit());
5772    }
5773
5774    #[test]
5775    fn test_can_read_resources_false_by_default() {
5776        let cx = Cx::for_testing();
5777        let ctx = McpContext::new(cx, 1);
5778        assert!(!ctx.can_read_resources());
5779    }
5780
5781    #[test]
5782    fn test_can_call_tools_false_by_default() {
5783        let cx = Cx::for_testing();
5784        let ctx = McpContext::new(cx, 1);
5785        assert!(!ctx.can_call_tools());
5786    }
5787
5788    #[test]
5789    fn test_resource_read_depth_default() {
5790        let cx = Cx::for_testing();
5791        let ctx = McpContext::new(cx, 1);
5792        assert_eq!(ctx.resource_read_depth(), 0);
5793    }
5794
5795    #[test]
5796    fn test_tool_call_depth_default() {
5797        let cx = Cx::for_testing();
5798        let ctx = McpContext::new(cx, 1);
5799        assert_eq!(ctx.tool_call_depth(), 0);
5800    }
5801
5802    // ========================================================================
5803    // Additional coverage tests (bd-3fcm)
5804    // ========================================================================
5805
5806    #[test]
5807    fn sampling_request_builder_chain() {
5808        let req = SamplingRequest::prompt("hello", 100)
5809            .with_system_prompt("You are helpful")
5810            .with_temperature(0.7)
5811            .with_stop_sequences(vec!["STOP".into()])
5812            .with_model_hints(vec!["gpt-4".into()]);
5813
5814        assert_eq!(req.messages.len(), 1);
5815        assert_eq!(req.max_tokens, 100);
5816        assert_eq!(req.system_prompt.as_deref(), Some("You are helpful"));
5817        assert_eq!(req.temperature, Some(0.7));
5818        assert_eq!(req.stop_sequences, vec!["STOP"]);
5819        assert_eq!(req.model_hints, vec!["gpt-4"]);
5820    }
5821
5822    #[test]
5823    fn sampling_request_message_roles() {
5824        let user = SamplingRequestMessage::user("hi");
5825        assert_eq!(user.role, SamplingRole::User);
5826        assert_eq!(user.text, "hi");
5827
5828        let asst = SamplingRequestMessage::assistant("hello");
5829        assert_eq!(asst.role, SamplingRole::Assistant);
5830        assert_eq!(asst.text, "hello");
5831    }
5832
5833    #[test]
5834    fn sampling_response_new_default_stop_reason() {
5835        let resp = SamplingResponse::new("output", "model-1");
5836        assert_eq!(resp.text, "output");
5837        assert_eq!(resp.model, "model-1");
5838        assert_eq!(resp.stop_reason, SamplingStopReason::EndTurn);
5839        assert_eq!(SamplingStopReason::default(), SamplingStopReason::EndTurn);
5840    }
5841
5842    #[test]
5843    fn sampling_stop_reason_round_trips_optional_open_wire_values() {
5844        let absent = SamplingStopReason::from_wire_value(None);
5845        assert_eq!(absent, SamplingStopReason::Unspecified);
5846        assert_eq!(absent.as_wire_value(), None);
5847
5848        let provider =
5849            SamplingStopReason::from_wire_value(Some("provider_safety_limit".to_owned()));
5850        assert_eq!(
5851            provider,
5852            SamplingStopReason::Other("provider_safety_limit".to_owned())
5853        );
5854        assert_eq!(provider.as_wire_value(), Some("provider_safety_limit"));
5855    }
5856
5857    #[test]
5858    fn noop_sampling_sender_returns_error() {
5859        let sender = NoOpSamplingSender;
5860        let req = SamplingRequest::prompt("test", 10);
5861        let result = crate::block_on(sender.create_message(req));
5862        assert!(result.is_err());
5863    }
5864
5865    #[test]
5866    fn noop_elicitation_sender_returns_error() {
5867        let sender = NoOpElicitationSender;
5868        let req = ElicitationRequest::form("msg", serde_json::json!({}));
5869        let result = crate::block_on(sender.elicit(req));
5870        assert!(result.is_err());
5871    }
5872
5873    #[test]
5874    fn elicitation_request_form_constructor() {
5875        let req = ElicitationRequest::form("Enter name", serde_json::json!({"type": "string"}));
5876        assert_eq!(req.mode, ElicitationMode::Form);
5877        assert_eq!(req.message, "Enter name");
5878        assert!(req.schema.is_some());
5879        assert!(req.url.is_none());
5880        assert!(req.elicitation_id.is_none());
5881    }
5882
5883    #[test]
5884    fn elicitation_request_url_constructor() {
5885        let req = ElicitationRequest::url("Login", "https://example.com", "id-1");
5886        assert_eq!(req.mode, ElicitationMode::Url);
5887        assert_eq!(req.message, "Login");
5888        assert_eq!(req.url.as_deref(), Some("https://example.com"));
5889        assert_eq!(req.elicitation_id.as_deref(), Some("id-1"));
5890        assert!(req.schema.is_none());
5891    }
5892
5893    #[test]
5894    fn mcp_context_with_sampling_enables_can_sample() {
5895        let cx = Cx::for_testing();
5896        let sender = Arc::new(NoOpSamplingSender);
5897        let ctx = McpContext::new(cx, 1).with_sampling(sender);
5898        assert!(ctx.can_sample());
5899    }
5900
5901    #[test]
5902    fn mcp_context_with_elicitation_enables_can_elicit() {
5903        let cx = Cx::for_testing();
5904        let sender = Arc::new(NoOpElicitationSender);
5905        let ctx = McpContext::new(cx, 1).with_elicitation(sender);
5906        assert!(ctx.can_elicit());
5907    }
5908
5909    struct FixedRootsProvider;
5910
5911    impl RootsProvider for FixedRootsProvider {
5912        fn list_roots(
5913            &self,
5914        ) -> std::pin::Pin<
5915            Box<dyn std::future::Future<Output = crate::McpResult<Vec<ClientRoot>>> + Send + '_>,
5916        > {
5917            Box::pin(async {
5918                Ok(vec![
5919                    ClientRoot::with_name("file:///workspace", "workspace"),
5920                    ClientRoot::new("file:///tmp"),
5921                ])
5922            })
5923        }
5924    }
5925
5926    #[test]
5927    fn mcp_context_roots_provider_returns_client_roots() {
5928        let ctx =
5929            McpContext::new(Cx::for_testing(), 1).with_roots_provider(Arc::new(FixedRootsProvider));
5930
5931        assert!(ctx.can_list_roots());
5932        let roots = crate::block_on(ctx.list_roots()).expect("configured roots provider succeeds");
5933        assert_eq!(
5934            roots,
5935            vec![
5936                ClientRoot::with_name("file:///workspace", "workspace"),
5937                ClientRoot::new("file:///tmp"),
5938            ]
5939        );
5940    }
5941
5942    #[test]
5943    fn mcp_context_without_roots_provider_rejects_without_authority() {
5944        let ctx = McpContext::new(Cx::for_testing(), 1);
5945
5946        assert!(!ctx.can_list_roots());
5947        let error = crate::block_on(ctx.list_roots())
5948            .expect_err("without only the roots provider, the context must reject the request");
5949        assert_eq!(error.code, crate::McpErrorCode::InvalidRequest);
5950        assert_eq!(
5951            error.message,
5952            "Roots not available: client does not support roots capability"
5953        );
5954    }
5955
5956    #[test]
5957    fn mcp_context_depth_setters() {
5958        let cx = Cx::for_testing();
5959        let ctx = McpContext::new(cx, 1)
5960            .with_resource_read_depth(3)
5961            .with_tool_call_depth(5);
5962        assert_eq!(ctx.resource_read_depth(), 3);
5963        assert_eq!(ctx.tool_call_depth(), 5);
5964
5965        let attempted_reset = ctx.with_resource_read_depth(0).with_tool_call_depth(0);
5966        assert_eq!(attempted_reset.resource_read_depth(), 3);
5967        assert_eq!(attempted_reset.tool_call_depth(), 5);
5968    }
5969
5970    #[test]
5971    fn mcp_context_debug_includes_request_id() {
5972        let cx = Cx::for_testing();
5973        let ctx = McpContext::new(cx, 99);
5974        let debug = format!("{ctx:?}");
5975        assert!(debug.contains("request_id: 99"));
5976    }
5977
5978    #[test]
5979    fn mcp_context_cx_and_trace() {
5980        let cx = Cx::for_testing();
5981        let ctx = McpContext::new(cx, 1);
5982        // cx() should return a reference without panic
5983        let _ = ctx.cx();
5984        // trace() should not panic
5985        ctx.trace("test event");
5986    }
5987
5988    #[test]
5989    fn final_result_outcome_preserves_dual_era_and_terminal_reason() {
5990        use crate::combinator::{DualEraFinalResult, FinalRequestResult};
5991
5992        let context = McpContext::new(Cx::for_testing(), 1);
5993        let modern = context.final_result_outcome(
5994            FinalRequestResult::<u64, String, &'static str>::modern("typed-final", 42),
5995        );
5996        let legacy =
5997            context.final_result_outcome(FinalRequestResult::<u64, String, &'static str>::legacy(
5998                "legacy-final",
5999                "legacy wire result".to_owned(),
6000            ));
6001
6002        let Outcome::Ok(modern) = modern else {
6003            panic!("live context admits the modern final result");
6004        };
6005        assert_eq!(modern.terminal_reason(), &"typed-final");
6006        assert_eq!(modern.result(), &DualEraFinalResult::Modern(42));
6007
6008        let Outcome::Ok(legacy) = legacy else {
6009            panic!("live context admits the legacy final result");
6010        };
6011        assert_eq!(legacy.terminal_reason(), &"legacy-final");
6012        assert_eq!(
6013            legacy.result(),
6014            &DualEraFinalResult::Legacy("legacy wire result".to_owned())
6015        );
6016    }
6017
6018    #[test]
6019    fn final_result_outcome_cancellation_negative_preserves_cx_reason() {
6020        use crate::combinator::FinalRequestResult;
6021        use asupersync::types::CancelKind;
6022
6023        let cx = Cx::for_testing();
6024        cx.cancel_with(CancelKind::Timeout, Some("final-result race"));
6025        let expected_reason = cx
6026            .cancel_reason()
6027            .expect("cancel_with records the caller-owned terminal reason");
6028        let context = McpContext::new(cx, 1);
6029
6030        let outcome = context.final_result_outcome(
6031            FinalRequestResult::<u64, String, &'static str>::modern("typed-final", 42),
6032        );
6033
6034        let Outcome::Cancelled(reason) = outcome else {
6035            panic!("changing only caller cancellation rejects the same final result");
6036        };
6037        assert_eq!(reason, expected_reason);
6038    }
6039
6040    #[test]
6041    fn final_result_outcome_panic_negative_preserves_payload() {
6042        use crate::combinator::FinalRequestResult;
6043        use asupersync::types::{CancelKind, PanicPayload};
6044
6045        type Final = FinalRequestResult<u64, String, &'static str>;
6046
6047        let cx = Cx::for_testing();
6048        cx.cancel_with(CancelKind::Timeout, Some("competing terminal state"));
6049        let context = McpContext::new(cx, 1);
6050        let payload = PanicPayload::new("final typed result panicked");
6051        let source: crate::McpOutcome<Final> = Outcome::Panicked(payload.clone());
6052
6053        let outcome = context.adapt_final_request_outcome(source);
6054
6055        let Outcome::Panicked(actual) = outcome else {
6056            panic!("changing only the source terminal state to panic preserves panic");
6057        };
6058        assert_eq!(actual, payload);
6059    }
6060}