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    /// Optional progress reporter for long-running operations.
1450    progress_reporter: Option<ProgressReporter>,
1451    /// Session state for per-session key-value storage.
1452    state: Option<SessionState>,
1453    /// Session cache partition captured when cache lookup is admitted.
1454    cache_admission_partition: Arc<Mutex<Option<([u8; 32], u64)>>>,
1455    /// Cache middleware instances that short-circuited response generation.
1456    response_cache_hits: Arc<Mutex<Vec<u64>>>,
1457    /// Request-scoped authentication context.
1458    auth: Arc<Mutex<Option<AuthContext>>>,
1459    /// Write-once authentication admission state, including committed anonymous
1460    /// requests whose handler-visible [`Self::auth`] value remains `None`.
1461    auth_state: Arc<AtomicU8>,
1462    /// Optional sampling sender for LLM completions.
1463    sampling_sender: Option<Arc<dyn SamplingSender>>,
1464    /// Optional elicitation sender for user input requests.
1465    elicitation_sender: Option<Arc<dyn ElicitationSender>>,
1466    /// Optional roots provider for filesystem boundaries exposed by the client.
1467    roots_provider: Option<Arc<dyn RootsProvider>>,
1468    /// Optional resource reader for cross-component access.
1469    resource_reader: Option<Arc<dyn ResourceReader>>,
1470    /// Current resource read depth (to prevent infinite recursion).
1471    resource_read_depth: u32,
1472    /// Optional tool caller for cross-component access.
1473    tool_caller: Option<Arc<dyn ToolCaller>>,
1474    /// Current tool call depth (to prevent infinite recursion).
1475    tool_call_depth: u32,
1476    /// Optional prompt caller for cross-component access.
1477    prompt_caller: Option<Arc<dyn PromptCaller>>,
1478    /// Current prompt get depth (to prevent infinite recursion).
1479    prompt_get_depth: u32,
1480    /// Client capability information.
1481    client_capabilities: Option<ClientCapabilityInfo>,
1482    /// Self-reported modern client Implementation identity, when advertised.
1483    client_implementation: Option<ClientImplementationInfo>,
1484    /// Server capability information.
1485    server_capabilities: Option<ServerCapabilityInfo>,
1486    /// Optional log sender for `notifications/message`.
1487    log_sender: Option<Arc<dyn NotificationSender>>,
1488    /// Minimum severity the connected client asked to receive.
1489    ///
1490    /// `None` means the client has not sent `logging/setLevel`; MCP forbids
1491    /// emitting log notifications until that floor exists.
1492    min_log_level: Option<McpLogLevel>,
1493    /// Resource URIs this session has subscribed to.
1494    resource_subscriptions: Option<Arc<std::collections::HashSet<String>>>,
1495    /// Optional publisher for modern `subscriptions/listen` catalog events.
1496    catalog_publisher: Option<Arc<dyn CatalogChangePublisher>>,
1497}
1498
1499impl std::fmt::Debug for McpContext {
1500    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1501        let budget_state = *self
1502            .budget_state
1503            .lock()
1504            .unwrap_or_else(std::sync::PoisonError::into_inner);
1505        f.debug_struct("McpContext")
1506            .field("cx", &self.cx)
1507            .field("budget_ceiling", &budget_state.ceiling)
1508            .field("ambient_poll_debits", &budget_state.ambient_poll_debits)
1509            .field("ambient_cost_debits", &budget_state.ambient_cost_debits)
1510            .field("deferred_overrun", &budget_state.deferred_overrun)
1511            .field(
1512                "framework_mask_depth",
1513                &self.framework_mask_depth.load(Ordering::Relaxed),
1514            )
1515            .field("operation_deadline", &self.operation_deadline)
1516            .field("request_lease_active", &self.request_scope_is_active())
1517            .field(
1518                "request_cancel_requested",
1519                &self.request_cancellation.is_cancel_requested(),
1520            )
1521            .field("request_id", &self.request_id)
1522            .field("progress_reporter", &self.progress_reporter)
1523            .field("state", &self.state.is_some())
1524            .field(
1525                "cache_admission_partition",
1526                &self
1527                    .cache_admission_partition
1528                    .lock()
1529                    .unwrap_or_else(std::sync::PoisonError::into_inner)
1530                    .is_some(),
1531            )
1532            .field(
1533                "response_cache_hit_count",
1534                &self
1535                    .response_cache_hits
1536                    .lock()
1537                    .unwrap_or_else(std::sync::PoisonError::into_inner)
1538                    .len(),
1539            )
1540            .field(
1541                "auth",
1542                &self
1543                    .auth
1544                    .lock()
1545                    .unwrap_or_else(std::sync::PoisonError::into_inner)
1546                    .is_some(),
1547            )
1548            .field(
1549                "auth_committed",
1550                &(self.auth_state.load(Ordering::Acquire) != REQUEST_AUTH_UNCOMMITTED),
1551            )
1552            .field("sampling_sender", &self.sampling_sender.is_some())
1553            .field("elicitation_sender", &self.elicitation_sender.is_some())
1554            .field("roots_provider", &self.roots_provider.is_some())
1555            .field("resource_reader", &self.resource_reader.is_some())
1556            .field("resource_read_depth", &self.resource_read_depth)
1557            .field("tool_caller", &self.tool_caller.is_some())
1558            .field("tool_call_depth", &self.tool_call_depth)
1559            .field("prompt_caller", &self.prompt_caller.is_some())
1560            .field("prompt_get_depth", &self.prompt_get_depth)
1561            .field("client_capabilities", &self.client_capabilities)
1562            .field("client_implementation", &self.client_implementation)
1563            .field("server_capabilities", &self.server_capabilities)
1564            .field("log_sender", &self.log_sender.is_some())
1565            .field("min_log_level", &self.min_log_level)
1566            .field(
1567                "resource_subscription_count",
1568                &self
1569                    .resource_subscriptions
1570                    .as_ref()
1571                    .map_or(0, |uris| uris.len()),
1572            )
1573            .field("catalog_publisher", &self.catalog_publisher.is_some())
1574            .finish()
1575    }
1576}
1577
1578#[derive(Clone, Copy, Debug, Default)]
1579struct FrameworkBudgetState {
1580    ceiling: Option<Budget>,
1581    /// Cumulative request-local poll units charged against the ambient Cx
1582    /// snapshot without mutating its clone-shared runtime budget.
1583    ambient_poll_debits: u32,
1584    /// Cumulative request-local cost charged against the ambient Cx snapshot.
1585    ///
1586    /// Asupersync 0.3.9 exposes the ambient budget as a read-only snapshot, so
1587    /// FastMCP cannot mutate the supplied Cx's internal quota. Keeping the
1588    /// cumulative debit here makes admission real and clone-shared without
1589    /// claiming ownership of, or cancelling, that ambient context.
1590    ambient_cost_debits: u64,
1591    /// A masked admission attempted to exceed a finite framework/ambient
1592    /// dimension. Exact depletion to zero is valid; only an actual overrun
1593    /// sets this terminal request-local condition.
1594    deferred_overrun: bool,
1595}
1596
1597impl FrameworkBudgetState {
1598    fn adjusted_ambient(self, mut ambient: Budget) -> Budget {
1599        if ambient.poll_quota != u32::MAX {
1600            ambient.poll_quota = ambient.poll_quota.saturating_sub(self.ambient_poll_debits);
1601        }
1602        if let Some(remaining) = ambient.cost_quota.as_mut() {
1603            *remaining = remaining.saturating_sub(self.ambient_cost_debits);
1604        }
1605        ambient
1606    }
1607
1608    fn effective(self, ambient: Budget) -> Budget {
1609        let ambient = self.adjusted_ambient(ambient);
1610        self.ceiling
1611            .map_or(ambient, |ceiling| ambient.meet(ceiling))
1612    }
1613}
1614
1615struct FrameworkMaskGuard<'a> {
1616    depth: &'a AtomicU32,
1617}
1618
1619/// RAII owner for a server-installed [`McpContext`] request lease.
1620///
1621/// This is an internal cross-crate integration type. Dropping it rejects new
1622/// FastMCP capability calls from every context clone in the request domain.
1623#[doc(hidden)]
1624pub struct McpContextLeaseGuard {
1625    lease: Arc<AtomicU8>,
1626}
1627
1628impl std::fmt::Debug for McpContextLeaseGuard {
1629    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1630        f.debug_struct("McpContextLeaseGuard")
1631            .field(
1632                "active",
1633                &(self.lease.load(Ordering::Acquire) == REQUEST_LEASE_ACTIVE),
1634            )
1635            .finish()
1636    }
1637}
1638
1639impl Drop for McpContextLeaseGuard {
1640    fn drop(&mut self) {
1641        self.lease.store(REQUEST_LEASE_CLOSED, Ordering::Release);
1642    }
1643}
1644
1645impl Drop for FrameworkMaskGuard<'_> {
1646    fn drop(&mut self) {
1647        self.depth.fetch_sub(1, Ordering::SeqCst);
1648    }
1649}
1650
1651impl McpContext {
1652    /// Creates a new MCP context from an asupersync Cx.
1653    ///
1654    /// This wraps the supplied context; it does not create or own a child
1655    /// region. Request-owned cancellation/drain must come from the caller's
1656    /// runtime lifecycle. This constructor establishes a new request-accounting
1657    /// domain even when `cx` is itself a clone. Same-request derivations must
1658    /// clone the resulting `McpContext` and use its consuming builders.
1659    #[must_use]
1660    pub fn new(cx: Cx, request_id: u64) -> Self {
1661        Self {
1662            cx,
1663            budget_state: Arc::new(Mutex::new(FrameworkBudgetState::default())),
1664            framework_mask_depth: Arc::new(AtomicU32::new(0)),
1665            mask_transition: Arc::new(Mutex::new(())),
1666            operation_deadline: None,
1667            request_lease: Arc::new(AtomicU8::new(REQUEST_LEASE_UNMANAGED)),
1668            request_cancellation: McpRequestCancellation::new(),
1669            request_id,
1670            progress_reporter: None,
1671            state: None,
1672            cache_admission_partition: Arc::new(Mutex::new(None)),
1673            response_cache_hits: Arc::new(Mutex::new(Vec::new())),
1674            auth: Arc::new(Mutex::new(None)),
1675            auth_state: Arc::new(AtomicU8::new(REQUEST_AUTH_UNCOMMITTED)),
1676            sampling_sender: None,
1677            elicitation_sender: None,
1678            roots_provider: None,
1679            resource_reader: None,
1680            resource_read_depth: 0,
1681            tool_caller: None,
1682            tool_call_depth: 0,
1683            prompt_caller: None,
1684            prompt_get_depth: 0,
1685            client_capabilities: None,
1686            client_implementation: None,
1687            server_capabilities: None,
1688            log_sender: None,
1689            min_log_level: None,
1690            resource_subscriptions: None,
1691            catalog_publisher: None,
1692        }
1693    }
1694
1695    /// Creates a new MCP context with session state.
1696    ///
1697    /// Use this constructor when session state should be accessible to handlers.
1698    /// It establishes a new request-accounting domain; clone an existing
1699    /// `McpContext` when deriving another context for the same request.
1700    #[must_use]
1701    pub fn with_state(cx: Cx, request_id: u64, state: SessionState) -> Self {
1702        Self {
1703            cx,
1704            budget_state: Arc::new(Mutex::new(FrameworkBudgetState::default())),
1705            framework_mask_depth: Arc::new(AtomicU32::new(0)),
1706            mask_transition: Arc::new(Mutex::new(())),
1707            operation_deadline: None,
1708            request_lease: Arc::new(AtomicU8::new(REQUEST_LEASE_UNMANAGED)),
1709            request_cancellation: McpRequestCancellation::new(),
1710            request_id,
1711            progress_reporter: None,
1712            state: Some(state),
1713            cache_admission_partition: Arc::new(Mutex::new(None)),
1714            response_cache_hits: Arc::new(Mutex::new(Vec::new())),
1715            auth: Arc::new(Mutex::new(None)),
1716            auth_state: Arc::new(AtomicU8::new(REQUEST_AUTH_UNCOMMITTED)),
1717            sampling_sender: None,
1718            elicitation_sender: None,
1719            roots_provider: None,
1720            resource_reader: None,
1721            resource_read_depth: 0,
1722            tool_caller: None,
1723            tool_call_depth: 0,
1724            prompt_caller: None,
1725            prompt_get_depth: 0,
1726            client_capabilities: None,
1727            client_implementation: None,
1728            server_capabilities: None,
1729            log_sender: None,
1730            min_log_level: None,
1731            resource_subscriptions: None,
1732            catalog_publisher: None,
1733        }
1734    }
1735
1736    /// Creates a new MCP context with progress reporting enabled.
1737    ///
1738    /// Use this constructor when the client has provided a progress token
1739    /// and expects progress notifications. It establishes a new
1740    /// request-accounting domain; use [`Self::with_progress_reporter`] on a
1741    /// clone when attaching progress reporting within an existing request.
1742    #[must_use]
1743    pub fn with_progress(cx: Cx, request_id: u64, reporter: ProgressReporter) -> Self {
1744        Self {
1745            cx,
1746            budget_state: Arc::new(Mutex::new(FrameworkBudgetState::default())),
1747            framework_mask_depth: Arc::new(AtomicU32::new(0)),
1748            mask_transition: Arc::new(Mutex::new(())),
1749            operation_deadline: None,
1750            request_lease: Arc::new(AtomicU8::new(REQUEST_LEASE_UNMANAGED)),
1751            request_cancellation: McpRequestCancellation::new(),
1752            request_id,
1753            progress_reporter: Some(reporter),
1754            state: None,
1755            cache_admission_partition: Arc::new(Mutex::new(None)),
1756            response_cache_hits: Arc::new(Mutex::new(Vec::new())),
1757            auth: Arc::new(Mutex::new(None)),
1758            auth_state: Arc::new(AtomicU8::new(REQUEST_AUTH_UNCOMMITTED)),
1759            sampling_sender: None,
1760            elicitation_sender: None,
1761            roots_provider: None,
1762            resource_reader: None,
1763            resource_read_depth: 0,
1764            tool_caller: None,
1765            tool_call_depth: 0,
1766            prompt_caller: None,
1767            prompt_get_depth: 0,
1768            client_capabilities: None,
1769            client_implementation: None,
1770            server_capabilities: None,
1771            log_sender: None,
1772            min_log_level: None,
1773            resource_subscriptions: None,
1774            catalog_publisher: None,
1775        }
1776    }
1777
1778    /// Creates a new MCP context with both state and progress reporting.
1779    ///
1780    /// This constructor establishes a new request-accounting domain. Clone an
1781    /// existing `McpContext` and apply consuming builders when deriving another
1782    /// context for the same request.
1783    #[must_use]
1784    pub fn with_state_and_progress(
1785        cx: Cx,
1786        request_id: u64,
1787        state: SessionState,
1788        reporter: ProgressReporter,
1789    ) -> Self {
1790        Self {
1791            cx,
1792            budget_state: Arc::new(Mutex::new(FrameworkBudgetState::default())),
1793            framework_mask_depth: Arc::new(AtomicU32::new(0)),
1794            mask_transition: Arc::new(Mutex::new(())),
1795            operation_deadline: None,
1796            request_lease: Arc::new(AtomicU8::new(REQUEST_LEASE_UNMANAGED)),
1797            request_cancellation: McpRequestCancellation::new(),
1798            request_id,
1799            progress_reporter: Some(reporter),
1800            state: Some(state),
1801            cache_admission_partition: Arc::new(Mutex::new(None)),
1802            response_cache_hits: Arc::new(Mutex::new(Vec::new())),
1803            auth: Arc::new(Mutex::new(None)),
1804            auth_state: Arc::new(AtomicU8::new(REQUEST_AUTH_UNCOMMITTED)),
1805            sampling_sender: None,
1806            elicitation_sender: None,
1807            roots_provider: None,
1808            resource_reader: None,
1809            resource_read_depth: 0,
1810            tool_caller: None,
1811            tool_call_depth: 0,
1812            prompt_caller: None,
1813            prompt_get_depth: 0,
1814            client_capabilities: None,
1815            client_implementation: None,
1816            server_capabilities: None,
1817            log_sender: None,
1818            min_log_level: None,
1819            resource_subscriptions: None,
1820            catalog_publisher: None,
1821        }
1822    }
1823
1824    /// Attaches a progress reporter without changing the request-accounting domain.
1825    ///
1826    /// This consuming builder preserves the shared budget, mask, authentication,
1827    /// and other request-scoped state inherited from the context being consumed.
1828    /// Use it on a clone when deriving a progress-enabled context for the same
1829    /// request.
1830    #[must_use]
1831    pub fn with_progress_reporter(mut self, reporter: ProgressReporter) -> Self {
1832        self.progress_reporter = Some(reporter);
1833        self
1834    }
1835
1836    /// Installs the sender used by [`Self::info`] and the other log helpers.
1837    #[must_use]
1838    pub fn with_log_sender(mut self, sender: Arc<dyn NotificationSender>) -> Self {
1839        self.log_sender = Some(sender);
1840        self
1841    }
1842
1843    /// Sets the client-selected minimum log level for this request.
1844    ///
1845    /// `None` keeps log notifications suppressed, matching MCP's rule that a
1846    /// server must not emit `notifications/message` until `logging/setLevel`.
1847    #[must_use]
1848    pub fn with_min_log_level(mut self, level: Option<McpLogLevel>) -> Self {
1849        self.min_log_level = level;
1850        self
1851    }
1852
1853    /// Returns the client-selected minimum log level for this request.
1854    ///
1855    /// `None` means the client has not opted into `notifications/message`.
1856    #[must_use]
1857    pub fn min_log_level(&self) -> Option<McpLogLevel> {
1858        self.min_log_level
1859    }
1860
1861    /// Records the resource URIs this session has subscribed to.
1862    #[must_use]
1863    pub fn with_resource_subscriptions(
1864        mut self,
1865        uris: impl IntoIterator<Item = impl Into<String>>,
1866    ) -> Self {
1867        self.resource_subscriptions = Some(Arc::new(uris.into_iter().map(Into::into).collect()));
1868        self
1869    }
1870
1871    /// Installs the modern `subscriptions/listen` catalog publisher.
1872    #[must_use]
1873    pub fn with_catalog_publisher(mut self, publisher: Arc<dyn CatalogChangePublisher>) -> Self {
1874        self.catalog_publisher = Some(publisher);
1875        self
1876    }
1877
1878    /// Sets the sampling sender for this context.
1879    ///
1880    /// This enables the `sample()` method to request LLM completions from
1881    /// the client.
1882    #[must_use]
1883    pub fn with_sampling(mut self, sender: Arc<dyn SamplingSender>) -> Self {
1884        self.sampling_sender = Some(sender);
1885        self
1886    }
1887
1888    /// Sets the elicitation sender for this context.
1889    ///
1890    /// This enables the `elicit()` methods to request user input from
1891    /// the client.
1892    #[must_use]
1893    pub fn with_elicitation(mut self, sender: Arc<dyn ElicitationSender>) -> Self {
1894        self.elicitation_sender = Some(sender);
1895        self
1896    }
1897
1898    /// Sets the roots provider for this context.
1899    ///
1900    /// This enables [`list_roots`](Self::list_roots) for the current request.
1901    #[must_use]
1902    pub fn with_roots_provider(mut self, provider: Arc<dyn RootsProvider>) -> Self {
1903        self.roots_provider = Some(provider);
1904        self
1905    }
1906
1907    /// Tightens the budget visible through this MCP context.
1908    ///
1909    /// The supplied ceiling is met with both the ambient [`Cx`] budget and
1910    /// any ceiling already installed on the context. Consequently,
1911    /// `Budget::INFINITE`, an absent deadline, or a later deadline cannot
1912    /// relax a tighter caller-owned limit. The ceiling and its remaining
1913    /// quotas are request-owned and shared by every clone of this context;
1914    /// tightening one clone is therefore visible to all of them.
1915    #[must_use]
1916    pub fn with_budget_ceiling(self, ceiling: Budget) -> Self {
1917        {
1918            let mut current = self
1919                .budget_state
1920                .lock()
1921                .unwrap_or_else(std::sync::PoisonError::into_inner);
1922            current.ceiling = Some(
1923                current
1924                    .ceiling
1925                    .map_or(ceiling, |budget| budget.meet(ceiling)),
1926            );
1927        }
1928        self
1929    }
1930
1931    /// Tightens only this derived operation's absolute deadline.
1932    ///
1933    /// Ordinary clones inherit the resulting deadline, but the context from
1934    /// which this consuming builder was derived is unchanged. This is the
1935    /// correct boundary for handler-local timeout metadata: nested work sees
1936    /// the tighter deadline while its parent request retains its own lifetime.
1937    /// `None` adds no deadline and can never relax an inherited one.
1938    #[must_use]
1939    pub fn with_operation_deadline(mut self, deadline: Option<Time>) -> Self {
1940        if let Some(deadline) = deadline {
1941            self.operation_deadline = Some(
1942                self.operation_deadline
1943                    .map_or(deadline, |current| current.min(deadline)),
1944            );
1945        }
1946        self
1947    }
1948
1949    /// Installs the server-created cooperative cancellation domain.
1950    ///
1951    /// This must be done before request dispatch begins. Ordinary context
1952    /// clones preserve the same domain.
1953    #[doc(hidden)]
1954    #[must_use]
1955    pub fn with_request_cancellation(mut self, cancellation: McpRequestCancellation) -> Self {
1956        // Request cancellation authority is installed exactly once, before
1957        // the server activates the request lease. A handler holding an active
1958        // clone cannot swap in a fresh token and escape peer cancellation.
1959        if self.request_lease.load(Ordering::Acquire) == REQUEST_LEASE_UNMANAGED {
1960            self.request_cancellation = cancellation;
1961        }
1962        self
1963    }
1964
1965    /// Installs a clone-shared request lease and returns the scoped context
1966    /// with its RAII owner.
1967    ///
1968    /// The server keeps the guard for exactly one dispatch. Once the guard is
1969    /// dropped, retained context clones fail liveness checks and FastMCP
1970    /// capability calls begun afterward are rejected. A context that already
1971    /// belongs to a request scope returns `None`, so an expired clone cannot
1972    /// mint fresh authority. This lease does not drain calls already in
1973    /// progress or revoke the caller-owned [`Cx`].
1974    #[doc(hidden)]
1975    #[must_use]
1976    pub fn begin_request_scope(self) -> Option<(Self, McpContextLeaseGuard)> {
1977        if self
1978            .request_lease
1979            .compare_exchange(
1980                REQUEST_LEASE_UNMANAGED,
1981                REQUEST_LEASE_ACTIVE,
1982                Ordering::AcqRel,
1983                Ordering::Acquire,
1984            )
1985            .is_err()
1986        {
1987            return None;
1988        }
1989        let guard = McpContextLeaseGuard {
1990            lease: Arc::clone(&self.request_lease),
1991        };
1992        Some((self, guard))
1993    }
1994
1995    /// Sets the resource reader for this context.
1996    ///
1997    /// This enables the `read_resource()` methods to read resources from
1998    /// within tool, resource, or prompt handlers.
1999    #[must_use]
2000    pub fn with_resource_reader(mut self, reader: Arc<dyn ResourceReader>) -> Self {
2001        self.resource_reader = Some(reader);
2002        self
2003    }
2004
2005    /// Sets the resource read depth for this context.
2006    ///
2007    /// This is used internally to track recursion depth when reading
2008    /// resources from within resource handlers.
2009    #[must_use]
2010    pub fn with_resource_read_depth(mut self, depth: u32) -> Self {
2011        self.resource_read_depth = self.resource_read_depth.max(depth);
2012        self
2013    }
2014
2015    /// Sets the tool caller for this context.
2016    ///
2017    /// This enables the `call_tool()` methods to call other tools from
2018    /// within tool, resource, or prompt handlers.
2019    #[must_use]
2020    pub fn with_tool_caller(mut self, caller: Arc<dyn ToolCaller>) -> Self {
2021        self.tool_caller = Some(caller);
2022        self
2023    }
2024
2025    /// Sets the tool call depth for this context.
2026    ///
2027    /// This is used internally to track recursion depth when calling
2028    /// tools from within tool handlers.
2029    #[must_use]
2030    pub fn with_tool_call_depth(mut self, depth: u32) -> Self {
2031        self.tool_call_depth = self.tool_call_depth.max(depth);
2032        self
2033    }
2034
2035    /// Sets the prompt caller for this context.
2036    ///
2037    /// This enables the `get_prompt()` methods to get other prompts from
2038    /// within tool, resource, or prompt handlers.
2039    #[must_use]
2040    pub fn with_prompt_caller(mut self, caller: Arc<dyn PromptCaller>) -> Self {
2041        self.prompt_caller = Some(caller);
2042        self
2043    }
2044
2045    /// Sets the prompt get depth for this context.
2046    ///
2047    /// This is used internally to track recursion depth when getting
2048    /// prompts from within handlers.
2049    #[must_use]
2050    pub fn with_prompt_get_depth(mut self, depth: u32) -> Self {
2051        self.prompt_get_depth = self.prompt_get_depth.max(depth);
2052        self
2053    }
2054
2055    /// Sets the client capability information for this context.
2056    ///
2057    /// This enables handlers to check what capabilities the connected
2058    /// client supports.
2059    #[must_use]
2060    pub fn with_client_capabilities(mut self, capabilities: ClientCapabilityInfo) -> Self {
2061        self.client_capabilities = Some(capabilities);
2062        self
2063    }
2064
2065    /// Attaches the self-reported modern client Implementation identity.
2066    #[must_use]
2067    pub fn with_client_implementation(mut self, identity: ClientImplementationInfo) -> Self {
2068        self.client_implementation = Some(identity);
2069        self
2070    }
2071
2072    /// Sets the server capability information for this context.
2073    ///
2074    /// This enables handlers to check what capabilities this server
2075    /// advertises.
2076    #[must_use]
2077    pub fn with_server_capabilities(mut self, capabilities: ServerCapabilityInfo) -> Self {
2078        self.server_capabilities = Some(capabilities);
2079        self
2080    }
2081
2082    /// Returns whether progress reporting is enabled for this context.
2083    #[must_use]
2084    pub fn has_progress_reporter(&self) -> bool {
2085        self.ensure_live().is_ok() && self.progress_reporter.is_some()
2086    }
2087
2088    /// Returns the progress marker installed for this request, when available.
2089    ///
2090    /// A reporter without a marker cannot establish ownership of upstream
2091    /// progress frames and therefore must not cause proxy forwarding.
2092    #[must_use]
2093    pub fn progress_marker(&self) -> Option<&serde_json::Value> {
2094        self.ensure_live()
2095            .ok()
2096            .and_then(|()| self.progress_reporter.as_ref()?.marker())
2097    }
2098
2099    /// Reports progress on the current operation.
2100    ///
2101    /// If progress reporting is not enabled (no progress token was provided),
2102    /// this method does nothing.
2103    ///
2104    /// # Arguments
2105    ///
2106    /// * `progress` - Current progress value (0.0 to 1.0 for fractional progress)
2107    /// * `message` - Optional message describing current status
2108    ///
2109    /// # Example
2110    ///
2111    /// ```ignore
2112    /// async fn process_files(ctx: &McpContext, files: &[File]) -> McpResult<()> {
2113    ///     for (i, file) in files.iter().enumerate() {
2114    ///         ctx.report_progress(i as f64 / files.len() as f64, Some("Processing files"));
2115    ///         process_file(file).await?;
2116    ///     }
2117    ///     ctx.report_progress(1.0, Some("Complete"));
2118    ///     Ok(())
2119    /// }
2120    /// ```
2121    pub fn report_progress(&self, progress: f64, message: Option<&str>) {
2122        if self.ensure_live().is_ok()
2123            && let Some(ref reporter) = self.progress_reporter
2124        {
2125            reporter.report(progress, message);
2126        }
2127    }
2128
2129    /// Reports progress with explicit total for determinate progress bars.
2130    ///
2131    /// If progress reporting is not enabled, this method does nothing.
2132    ///
2133    /// # Arguments
2134    ///
2135    /// * `progress` - Current progress value
2136    /// * `total` - Total expected value
2137    /// * `message` - Optional message describing current status
2138    ///
2139    /// # Example
2140    ///
2141    /// ```ignore
2142    /// async fn process_items(ctx: &McpContext, items: &[Item]) -> McpResult<()> {
2143    ///     let total = items.len() as f64;
2144    ///     for (i, item) in items.iter().enumerate() {
2145    ///         ctx.report_progress_with_total(i as f64, total, Some(&format!("Item {}", i)));
2146    ///         process_item(item).await?;
2147    ///     }
2148    ///     Ok(())
2149    /// }
2150    /// ```
2151    pub fn report_progress_with_total(&self, progress: f64, total: f64, message: Option<&str>) {
2152        if self.ensure_live().is_ok()
2153            && let Some(ref reporter) = self.progress_reporter
2154        {
2155            reporter.report_with_total(progress, total, message);
2156        }
2157    }
2158
2159    /// Reports final progress while retaining the caller's exact JSON-number
2160    /// lexemes.
2161    ///
2162    /// This is a no-op unless the current request installed a final-capable
2163    /// progress reporter. The legacy `f64` progress APIs remain unchanged.
2164    pub fn report_progress_exact(
2165        &self,
2166        progress: serde_json::Number,
2167        total: Option<serde_json::Number>,
2168        message: Option<&str>,
2169    ) {
2170        if self.ensure_live().is_ok()
2171            && let Some(ref reporter) = self.progress_reporter
2172        {
2173            reporter.report_exact(progress, total, message);
2174        }
2175    }
2176
2177    /// Returns the unique request identifier.
2178    ///
2179    /// This corresponds to the JSON-RPC request ID and is useful for
2180    /// logging and tracing across the request lifecycle.
2181    #[must_use]
2182    pub fn request_id(&self) -> u64 {
2183        self.request_id
2184    }
2185
2186    /// Returns the underlying region ID from asupersync.
2187    ///
2188    /// This is the region of the caller-supplied [`Cx`]. FastMCP does not
2189    /// currently create a request-owned child region, so this identifier must
2190    /// not be interpreted as proof that spawned work is scoped to, cancelled
2191    /// with, or drained before completion of this MCP request.
2192    #[must_use]
2193    pub fn region_id(&self) -> RegionId {
2194        self.cx.region_id()
2195    }
2196
2197    /// Returns the current task ID.
2198    #[must_use]
2199    pub fn task_id(&self) -> TaskId {
2200        self.cx.task_id()
2201    }
2202
2203    fn apply_operation_deadline(&self, budget: Budget) -> Budget {
2204        self.operation_deadline.map_or(budget, |deadline| {
2205            budget.meet(Budget::new().with_deadline(deadline))
2206        })
2207    }
2208
2209    fn request_scope_is_active(&self) -> bool {
2210        self.request_lease.load(Ordering::Acquire) != REQUEST_LEASE_CLOSED
2211    }
2212
2213    /// Returns the current budget.
2214    ///
2215    /// The budget is a remaining-balance snapshot. A zero poll or cost balance
2216    /// records exact depletion; it does not retroactively fail the operation
2217    /// that consumed the final unit. Use [`ensure_live`](Self::ensure_live) for
2218    /// terminal liveness and [`checkpoint`](Self::checkpoint) or
2219    /// [`consume_cost`](Self::consume_cost) for dimension-specific admission.
2220    #[must_use]
2221    pub fn budget(&self) -> Budget {
2222        let ambient = self.cx.budget();
2223        let state = *self
2224            .budget_state
2225            .lock()
2226            .unwrap_or_else(std::sync::PoisonError::into_inner);
2227        self.apply_operation_deadline(state.effective(ambient))
2228    }
2229
2230    /// Checks if cancellation has been requested.
2231    ///
2232    /// This includes client disconnection, timeout, or explicit cancellation.
2233    /// Handlers should check this periodically and exit early if true.
2234    #[must_use]
2235    pub fn is_cancelled(&self) -> bool {
2236        self.ensure_live().is_err()
2237    }
2238
2239    /// Returns the cooperative cancellation domain owned by this request.
2240    ///
2241    /// Long-running framework integrations may await this handle instead of
2242    /// polling [`Self::is_cancelled`].  Cloning the handle never grants a way
2243    /// to replace the request's cancellation authority; it observes the same
2244    /// request-local transition installed before dispatch.
2245    #[must_use]
2246    pub fn request_cancellation(&self) -> McpRequestCancellation {
2247        self.request_cancellation.clone()
2248    }
2249
2250    /// Checks terminal request liveness without charging a poll or cost unit.
2251    ///
2252    /// A finite quota that was exactly depleted is not itself a failed
2253    /// operation. The next dimension-specific admission fails when it asks for
2254    /// unavailable work. This method rejects only explicit cancellation, an
2255    /// expired effective deadline, or a real overrun deferred by
2256    /// [`masked`](Self::masked). Explicit cancellation includes both the
2257    /// caller-owned [`Cx`] signal and FastMCP's request-local cooperative
2258    /// signal; the latter never mutates the ambient context.
2259    ///
2260    /// # Errors
2261    ///
2262    /// Returns [`CancelledError`] when a terminal liveness condition is
2263    /// observable outside a cancellation mask.
2264    pub fn ensure_live(&self) -> Result<(), CancelledError> {
2265        if !self.request_scope_is_active() {
2266            return Err(CancelledError);
2267        }
2268        let _mask_transition = self
2269            .mask_transition
2270            .lock()
2271            .unwrap_or_else(std::sync::PoisonError::into_inner);
2272        if self.framework_mask_depth.load(Ordering::SeqCst) > 0 {
2273            return Ok(());
2274        }
2275
2276        let ambient = self.cx.budget();
2277        let now = self.cx.now();
2278        let state = *self
2279            .budget_state
2280            .lock()
2281            .unwrap_or_else(std::sync::PoisonError::into_inner);
2282        let effective = self.apply_operation_deadline(state.effective(ambient));
2283        if self.request_cancellation.is_cancel_requested()
2284            || self.cx.is_cancel_requested()
2285            || effective.is_past_deadline(now)
2286            || state.deferred_overrun
2287        {
2288            return Err(CancelledError);
2289        }
2290        Ok(())
2291    }
2292
2293    /// Cooperative cancellation checkpoint.
2294    ///
2295    /// Call this at natural suspension points in your handler to allow
2296    /// graceful cancellation. Returns `Err` if cancellation is pending.
2297    ///
2298    /// # Errors
2299    ///
2300    /// Returns an error if the request has been cancelled and cancellation
2301    /// is not currently masked. Each admitted checkpoint consumes one unit
2302    /// from both a finite framework-owned poll ceiling and a finite ambient
2303    /// [`Cx`] poll snapshot. FastMCP records ambient debits in a clone-shared
2304    /// request ledger; it does not mutate or cancel the caller-owned context.
2305    ///
2306    /// This method intentionally does not call [`Cx::checkpoint`]. In the
2307    /// pinned runtime that API treats a zero cost balance as aggregate budget
2308    /// exhaustion and mutates the clone-shared cancellation state, even though
2309    /// this operation admits only the poll dimension. FastMCP observes the
2310    /// supplied context's cancellation flag and deadline without poisoning a
2311    /// caller-owned context shared by other request domains.
2312    ///
2313    /// # Example
2314    ///
2315    /// ```ignore
2316    /// async fn process_items(ctx: &McpContext, items: Vec<Item>) -> McpResult<()> {
2317    ///     for item in items {
2318    ///         ctx.checkpoint()?;  // Allow cancellation between items
2319    ///         process_item(item).await?;
2320    ///     }
2321    ///     Ok(())
2322    /// }
2323    /// ```
2324    pub fn checkpoint(&self) -> Result<(), CancelledError> {
2325        if !self.request_scope_is_active() {
2326            return Err(CancelledError);
2327        }
2328        let _mask_transition = self
2329            .mask_transition
2330            .lock()
2331            .unwrap_or_else(std::sync::PoisonError::into_inner);
2332        let masked = self.framework_mask_depth.load(Ordering::SeqCst) > 0;
2333        let ambient = self.cx.budget();
2334        let now = self.cx.now();
2335        let mut state = self
2336            .budget_state
2337            .lock()
2338            .unwrap_or_else(std::sync::PoisonError::into_inner);
2339        let adjusted_ambient = state.adjusted_ambient(ambient);
2340        let effective = self.apply_operation_deadline(
2341            state
2342                .ceiling
2343                .map_or(adjusted_ambient, |ceiling| adjusted_ambient.meet(ceiling)),
2344        );
2345        let poll_unavailable = effective.poll_quota == 0;
2346        let past_deadline = effective.is_past_deadline(now);
2347        let cancelled =
2348            self.request_cancellation.is_cancel_requested() || self.cx.is_cancel_requested();
2349        let deferred_overrun = state.deferred_overrun;
2350
2351        if !masked && (cancelled || poll_unavailable || past_deadline || deferred_overrun) {
2352            return Err(CancelledError);
2353        }
2354
2355        if poll_unavailable {
2356            debug_assert!(masked);
2357            state.deferred_overrun = true;
2358        }
2359
2360        if adjusted_ambient.poll_quota != u32::MAX {
2361            state.ambient_poll_debits = state.ambient_poll_debits.saturating_add(1);
2362        }
2363
2364        if let Some(budget) = state.ceiling.as_mut()
2365            && budget.poll_quota != u32::MAX
2366        {
2367            if budget.consume_poll().is_none() {
2368                debug_assert!(masked);
2369                state.deferred_overrun = true;
2370            }
2371        }
2372
2373        Ok(())
2374    }
2375
2376    /// Debits abstract cost units from the request budget.
2377    ///
2378    /// Cost is application-defined and is deliberately separate from poll
2379    /// accounting: [`checkpoint`](Self::checkpoint) never guesses an
2380    /// operation's cost. A successful debit is visible through
2381    /// [`budget`](Self::budget) and every clone of this context. If the
2382    /// request is explicitly cancelled or expired, or the effective ambient/
2383    /// ceiling cost budget has fewer than `cost` units remaining, this returns
2384    /// [`CancelledError`] without a partial debit. Poll accounting remains the
2385    /// responsibility of [`checkpoint`](Self::checkpoint); this method does
2386    /// not acknowledge cancellation or consume a poll checkpoint.
2387    ///
2388    /// Asupersync exposes the supplied [`Cx`] budget as a read-only snapshot.
2389    /// FastMCP therefore records cumulative, clone-shared request-local debits
2390    /// and subtracts them from the current ambient snapshot without mutating or
2391    /// cancelling the caller-owned Cx. A framework cost ceiling, when present,
2392    /// is debited independently under the same lock. A zero-unit debit succeeds
2393    /// at a zero cost quota, but still observes explicit cancellation and an
2394    /// expired deadline.
2395    ///
2396    /// Inside [`masked`](Self::masked), enforcement remains deferred just as
2397    /// it is for checkpoints. Affordable debits are still recorded, while an
2398    /// over-budget debit leaves the effective cost budget exhausted so the
2399    /// exhaustion is observed as soon as the mask is released. A nonbinding
2400    /// underlying cost dimension can still retain a positive balance.
2401    ///
2402    /// # Errors
2403    ///
2404    /// Returns an error when the debit cannot be admitted and cancellation is
2405    /// not currently masked.
2406    pub fn consume_cost(&self, cost: u64) -> Result<(), CancelledError> {
2407        if !self.request_scope_is_active() {
2408            return Err(CancelledError);
2409        }
2410        let _mask_transition = self
2411            .mask_transition
2412            .lock()
2413            .unwrap_or_else(std::sync::PoisonError::into_inner);
2414        let masked = self.framework_mask_depth.load(Ordering::SeqCst) > 0;
2415        let ambient = self.cx.budget();
2416        let now = self.cx.now();
2417        let mut state = self
2418            .budget_state
2419            .lock()
2420            .unwrap_or_else(std::sync::PoisonError::into_inner);
2421        let effective = self.apply_operation_deadline(state.effective(ambient));
2422        let enough_cost = effective
2423            .cost_quota
2424            .is_none_or(|remaining| remaining >= cost);
2425        let past_deadline = effective.is_past_deadline(now);
2426        let cancelled =
2427            self.request_cancellation.is_cancel_requested() || self.cx.is_cancel_requested();
2428
2429        if !masked && (cancelled || past_deadline || state.deferred_overrun || !enough_cost) {
2430            return Err(CancelledError);
2431        }
2432
2433        if !enough_cost {
2434            debug_assert!(masked);
2435            state.deferred_overrun = true;
2436        }
2437        state.ambient_cost_debits = state.ambient_cost_debits.saturating_add(cost);
2438        if let Some(budget) = state.ceiling.as_mut()
2439            && !budget.consume_cost(cost)
2440        {
2441            debug_assert!(masked);
2442            budget.cost_quota = Some(0);
2443        }
2444
2445        Ok(())
2446    }
2447
2448    /// Executes a closure with cancellation masked.
2449    ///
2450    /// While masked, `checkpoint()` will not return an error even if
2451    /// cancellation is pending. Use this for critical sections that
2452    /// must complete atomically.
2453    ///
2454    /// Masking is request-context-wide: this context and all of its clones
2455    /// share both the underlying [`Cx`] mask and the framework ceiling mask.
2456    /// Independently cancellable concurrent work therefore requires distinct
2457    /// runtime-owned child contexts rather than clones of one `McpContext`.
2458    ///
2459    /// This method masks only the synchronous execution of `f`. Passing an
2460    /// async block merely constructs a future while masked; polling that future
2461    /// after this method returns is not protected. Asynchronous critical
2462    /// sections require a runtime-owned structured cancellation scope.
2463    ///
2464    /// # Errors
2465    ///
2466    /// Returns [`CancelledError`] if this context's request lease has already
2467    /// closed or the framework mask depth cannot be incremented.
2468    ///
2469    /// # Example
2470    ///
2471    /// ```ignore
2472    /// // Commit transaction - must not be interrupted
2473    /// ctx.masked(|| db.commit_synchronously())?;
2474    /// ```
2475    pub fn masked<F, R>(&self, f: F) -> Result<R, CancelledError>
2476    where
2477        F: FnOnce() -> R,
2478    {
2479        if !self.request_scope_is_active() {
2480            return Err(CancelledError);
2481        }
2482        let entry_transition = self
2483            .mask_transition
2484            .lock()
2485            .unwrap_or_else(std::sync::PoisonError::into_inner);
2486        if self.framework_mask_depth.load(Ordering::SeqCst) >= MAX_MASK_DEPTH {
2487            return Err(CancelledError);
2488        }
2489        if self
2490            .framework_mask_depth
2491            .try_update(Ordering::SeqCst, Ordering::SeqCst, |depth| {
2492                depth.checked_add(1)
2493            })
2494            .is_err()
2495        {
2496            return Err(CancelledError);
2497        }
2498        let framework_mask = FrameworkMaskGuard {
2499            depth: &self.framework_mask_depth,
2500        };
2501        let masked_outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2502            self.cx.masked(|| {
2503                drop(entry_transition);
2504                let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
2505                let exit_transition = self
2506                    .mask_transition
2507                    .lock()
2508                    .unwrap_or_else(std::sync::PoisonError::into_inner);
2509                (outcome, exit_transition)
2510            })
2511        }));
2512        let (outcome, exit_transition) = match masked_outcome {
2513            Ok(result) => result,
2514            Err(_runtime_mask_failure) => {
2515                let exit_transition = self
2516                    .mask_transition
2517                    .lock()
2518                    .unwrap_or_else(std::sync::PoisonError::into_inner);
2519                drop(framework_mask);
2520                drop(exit_transition);
2521                return Err(CancelledError);
2522            }
2523        };
2524        drop(framework_mask);
2525        drop(exit_transition);
2526
2527        match outcome {
2528            Ok(result) => Ok(result),
2529            Err(payload) => std::panic::resume_unwind(payload),
2530        }
2531    }
2532
2533    /// Records a trace event for this request.
2534    ///
2535    /// Events are associated with the request's trace context and can be
2536    /// used for debugging and observability.
2537    pub fn trace(&self, message: &str) {
2538        if self.ensure_live().is_ok() {
2539            self.cx.trace(message);
2540        }
2541    }
2542
2543    /// Emits a debug `notifications/message` when the client asked for that floor.
2544    pub fn debug(&self, message: impl AsRef<str>) {
2545        self.log(McpLogLevel::Debug, message);
2546    }
2547
2548    /// Emits an info `notifications/message` when the client asked for that floor.
2549    pub fn info(&self, message: impl AsRef<str>) {
2550        self.log(McpLogLevel::Info, message);
2551    }
2552
2553    /// Emits a notice `notifications/message` when the client asked for that floor.
2554    pub fn notice(&self, message: impl AsRef<str>) {
2555        self.log(McpLogLevel::Notice, message);
2556    }
2557
2558    /// Emits a warning `notifications/message` when the client asked for that floor.
2559    pub fn warning(&self, message: impl AsRef<str>) {
2560        self.log(McpLogLevel::Warning, message);
2561    }
2562
2563    /// Emits an error `notifications/message` when the client asked for that floor.
2564    pub fn error(&self, message: impl AsRef<str>) {
2565        self.log(McpLogLevel::Error, message);
2566    }
2567
2568    /// Emits one MCP log notification if the client floor admits `level`.
2569    ///
2570    /// Missing floor, missing sender, or a cancelled request are silent
2571    /// no-ops so handlers can log without branching on transport wiring.
2572    pub fn log(&self, level: McpLogLevel, message: impl AsRef<str>) {
2573        self.log_data(
2574            level,
2575            serde_json::Value::String(message.as_ref().to_owned()),
2576        );
2577    }
2578
2579    /// Emits one MCP log notification with caller-owned JSON data.
2580    pub fn log_data(&self, level: McpLogLevel, data: serde_json::Value) {
2581        if self.ensure_live().is_err() {
2582            return;
2583        }
2584        let Some(min_level) = self.min_log_level else {
2585            return;
2586        };
2587        if level.rank() < min_level.rank() {
2588            return;
2589        }
2590        if let Some(sender) = self.log_sender.as_ref() {
2591            sender.send_log(level, Some("fastmcp"), data);
2592        }
2593    }
2594
2595    /// Notifies subscribers that `uri` changed.
2596    ///
2597    /// Returns `true` when a 2024 session subscriber received
2598    /// `notifications/resources/updated` or at least one modern
2599    /// `subscriptions/listen` stream accepted the event.
2600    pub fn notify_resource_updated(&self, uri: impl AsRef<str>) -> bool {
2601        if self.ensure_live().is_err() {
2602            return false;
2603        }
2604        let uri = uri.as_ref();
2605        let mut delivered = false;
2606        if self
2607            .resource_subscriptions
2608            .as_ref()
2609            .is_some_and(|uris| uris.contains(uri))
2610            && let Some(sender) = self.log_sender.as_ref()
2611        {
2612            sender.send_resource_updated(uri);
2613            delivered = true;
2614        }
2615        if let Some(publisher) = self.catalog_publisher.as_ref()
2616            && publisher.publish_resource_updated(uri)
2617        {
2618            delivered = true;
2619        }
2620        delivered
2621    }
2622
2623    /// Returns a reference to the underlying asupersync Cx.
2624    ///
2625    /// Use this when you need direct access to asupersync primitives,
2626    /// such as spawning tasks or using combinators. Direct Cx checkpoints and
2627    /// budget snapshots do not observe FastMCP's framework ceiling, cumulative
2628    /// cost ledger, or two-layer mask transition; request admission code must
2629    /// use [`checkpoint`](Self::checkpoint), [`consume_cost`](Self::consume_cost),
2630    /// and [`budget`](Self::budget) instead. Conversely, masking the raw `Cx`
2631    /// does not mask FastMCP admission checks: code that calls back into this
2632    /// context must use [`masked`](Self::masked). The raw handle also cannot be
2633    /// revoked when the FastMCP request lease closes, so it must not be retained
2634    /// or used as an independently owned request capability.
2635    #[must_use]
2636    pub fn cx(&self) -> &Cx {
2637        &self.cx
2638    }
2639
2640    /// Admits one final dual-era result as a four-valued MCP outcome.
2641    ///
2642    /// The result retains both its `Modern`/`Legacy` branch and the caller's
2643    /// exact terminal-reason type. This context performs its normal request
2644    /// liveness check before admitting a newly completed result, so ambient
2645    /// `Cx` cancellation, request-local cancellation, lease closure, and
2646    /// bounded framework admission continue to win without creating a runtime.
2647    #[must_use]
2648    pub fn final_result_outcome<TypedResult, LegacyResult, TerminalReason>(
2649        &self,
2650        result: crate::combinator::FinalRequestResult<TypedResult, LegacyResult, TerminalReason>,
2651    ) -> crate::McpOutcome<
2652        crate::combinator::FinalRequestResult<TypedResult, LegacyResult, TerminalReason>,
2653    > {
2654        if self.ensure_live().is_err() {
2655            return Outcome::Cancelled(self.final_result_cancellation_reason());
2656        }
2657        Outcome::Ok(result)
2658    }
2659
2660    /// Preserves an already-terminal request outcome while admitting an `Ok` final result.
2661    ///
2662    /// A supplied cancellation reason or panic payload is returned unchanged;
2663    /// only an `Ok` result is subject to the context's current liveness check.
2664    #[must_use]
2665    pub fn adapt_final_request_outcome<TypedResult, LegacyResult, TerminalReason>(
2666        &self,
2667        outcome: crate::McpOutcome<
2668            crate::combinator::FinalRequestResult<TypedResult, LegacyResult, TerminalReason>,
2669        >,
2670    ) -> crate::McpOutcome<
2671        crate::combinator::FinalRequestResult<TypedResult, LegacyResult, TerminalReason>,
2672    > {
2673        match outcome {
2674            Outcome::Ok(result) => self.final_result_outcome(result),
2675            Outcome::Err(error) => Outcome::Err(error),
2676            Outcome::Cancelled(reason) => Outcome::Cancelled(reason),
2677            Outcome::Panicked(payload) => Outcome::Panicked(payload),
2678        }
2679    }
2680
2681    fn final_result_cancellation_reason(&self) -> CancelReason {
2682        self.cx.cancel_reason().unwrap_or_else(|| {
2683            if self.request_cancellation.is_cancel_requested() {
2684                CancelReason::user("FastMCP request-local cancellation")
2685            } else if !self.request_scope_is_active() {
2686                CancelReason::user("FastMCP request lease closed")
2687            } else {
2688                CancelReason::user("FastMCP request liveness rejected final result")
2689            }
2690        })
2691    }
2692
2693    // ========================================================================
2694    // Session State Access
2695    // ========================================================================
2696
2697    /// Gets a value from session state by key.
2698    ///
2699    /// Returns `None` if:
2700    /// - Session state is not available (context created without state)
2701    /// - The key doesn't exist
2702    /// - Deserialization to type `T` fails
2703    ///
2704    /// # Example
2705    ///
2706    /// ```ignore
2707    /// async fn my_tool(ctx: &McpContext, args: MyArgs) -> McpResult<Value> {
2708    ///     // Get a counter from session state
2709    ///     let count: Option<i32> = ctx.get_state("counter");
2710    ///     let count = count.unwrap_or(0);
2711    ///     // ... use count ...
2712    ///     Ok(json!({"count": count}))
2713    /// }
2714    /// ```
2715    #[must_use]
2716    pub fn get_state<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
2717        if !self.request_scope_is_active() {
2718            return None;
2719        }
2720        self.state.as_ref()?.get(key)
2721    }
2722
2723    /// Returns the authentication context for this request, if available.
2724    #[must_use]
2725    pub fn auth(&self) -> Option<AuthContext> {
2726        if !self.request_scope_is_active() {
2727            return None;
2728        }
2729        self.auth
2730            .lock()
2731            .unwrap_or_else(std::sync::PoisonError::into_inner)
2732            .clone()
2733    }
2734
2735    /// Commits authentication context for this request if the slot is empty.
2736    ///
2737    /// The slot is write-once across all context clones. Authentication
2738    /// providers may use an isolated staging context, and the server commits
2739    /// the successful result to the shared request context. Middleware,
2740    /// handlers, and nested dispatch cannot replace that committed principal.
2741    /// Returns `false` if the request lease is closed or an identity has
2742    /// already been committed.
2743    pub fn set_auth(&self, auth: AuthContext) -> bool {
2744        if self.ensure_live().is_err() {
2745            return false;
2746        }
2747        let mut slot = self
2748            .auth
2749            .lock()
2750            .unwrap_or_else(std::sync::PoisonError::into_inner);
2751        if self.auth_state.load(Ordering::Acquire) != REQUEST_AUTH_UNCOMMITTED {
2752            return false;
2753        }
2754        *slot = Some(auth);
2755        self.auth_state
2756            .store(REQUEST_AUTH_AUTHENTICATED, Ordering::Release);
2757        true
2758    }
2759
2760    /// Commits this request as unauthenticated without exposing an empty
2761    /// [`AuthContext`] to handlers.
2762    ///
2763    /// This is a write-once internal admission marker. It prevents later
2764    /// middleware from forging handler-visible authentication while allowing
2765    /// cache middleware to distinguish admitted anonymous traffic from an
2766    /// authentication flow that has not completed.
2767    #[doc(hidden)]
2768    pub fn commit_anonymous_auth(&self) -> bool {
2769        if self.ensure_live().is_err() {
2770            return false;
2771        }
2772        let slot = self
2773            .auth
2774            .lock()
2775            .unwrap_or_else(std::sync::PoisonError::into_inner);
2776        if self.auth_state.load(Ordering::Acquire) != REQUEST_AUTH_UNCOMMITTED || slot.is_some() {
2777            return false;
2778        }
2779        self.auth_state
2780            .store(REQUEST_AUTH_ANONYMOUS, Ordering::Release);
2781        true
2782    }
2783
2784    /// Returns the committed cache authorization partition.
2785    ///
2786    /// The outer `Option` distinguishes incomplete admission from a committed
2787    /// request. The inner `Option` is `None` for anonymous admission and
2788    /// contains the complete handler-visible authenticated facts otherwise.
2789    #[doc(hidden)]
2790    #[must_use]
2791    pub fn cache_auth_partition(&self) -> Option<Option<AuthContext>> {
2792        if !self.request_scope_is_active() {
2793            return None;
2794        }
2795        let slot = self
2796            .auth
2797            .lock()
2798            .unwrap_or_else(std::sync::PoisonError::into_inner);
2799        match self.auth_state.load(Ordering::Acquire) {
2800            REQUEST_AUTH_ANONYMOUS => Some(None),
2801            REQUEST_AUTH_AUTHENTICATED => slot.clone().map(Some),
2802            _ => None,
2803        }
2804    }
2805
2806    /// Returns a cloned context with request-local auth attached.
2807    #[must_use]
2808    pub fn with_auth(self, auth: AuthContext) -> Self {
2809        let _ = self.set_auth(auth);
2810        self
2811    }
2812
2813    /// Returns a derived context with an isolated authentication staging slot.
2814    ///
2815    /// Only budget accounting, cancellation, masking, and request identity
2816    /// remain shared. Session state, nested dispatch, progress, sampling,
2817    /// elicitation, and roots are removed from the staging view so
2818    /// authentication code cannot exercise handler authority and a handler
2819    /// cannot use this method to forge a principal for nested dispatch.
2820    #[must_use]
2821    pub fn with_isolated_auth(mut self) -> Self {
2822        let already_committed = self.auth_state.load(Ordering::Acquire) != REQUEST_AUTH_UNCOMMITTED;
2823        if already_committed {
2824            return self;
2825        }
2826        self.auth = Arc::new(Mutex::new(None));
2827        self.auth_state = Arc::new(AtomicU8::new(REQUEST_AUTH_UNCOMMITTED));
2828        self.state = None;
2829        self.progress_reporter = None;
2830        self.sampling_sender = None;
2831        self.elicitation_sender = None;
2832        self.roots_provider = None;
2833        self.resource_reader = None;
2834        self.tool_caller = None;
2835        self.prompt_caller = None;
2836        self
2837    }
2838
2839    /// Sets a value in session state.
2840    ///
2841    /// The value persists across requests within the same session.
2842    /// Returns `true` if the value was successfully stored.
2843    /// Returns `false` if session state is not available or serialization fails.
2844    ///
2845    /// # Example
2846    ///
2847    /// ```ignore
2848    /// async fn my_tool(ctx: &McpContext, args: MyArgs) -> McpResult<Value> {
2849    ///     // Increment a counter in session state
2850    ///     let count: i32 = ctx.get_state("counter").unwrap_or(0);
2851    ///     ctx.set_state("counter", count + 1);
2852    ///     Ok(json!({"new_count": count + 1}))
2853    /// }
2854    /// ```
2855    pub fn set_state<T: serde::Serialize>(&self, key: impl Into<String>, value: T) -> bool {
2856        if self.ensure_live().is_err() {
2857            return false;
2858        }
2859        match &self.state {
2860            Some(state) => state.set(key, value),
2861            None => false,
2862        }
2863    }
2864
2865    /// Removes a value from session state.
2866    ///
2867    /// Returns the previous value if it existed, or `None` if:
2868    /// - Session state is not available
2869    /// - The key didn't exist
2870    pub fn remove_state(&self, key: &str) -> Option<serde_json::Value> {
2871        if self.ensure_live().is_err() {
2872            return None;
2873        }
2874        self.state.as_ref()?.remove(key)
2875    }
2876
2877    /// Checks if a key exists in session state.
2878    ///
2879    /// Returns `false` if session state is not available.
2880    #[must_use]
2881    pub fn has_state(&self, key: &str) -> bool {
2882        self.request_scope_is_active() && self.state.as_ref().is_some_and(|s| s.contains(key))
2883    }
2884
2885    /// Returns whether session state is available in this context.
2886    #[must_use]
2887    pub fn has_session_state(&self) -> bool {
2888        self.request_scope_is_active() && self.state.is_some()
2889    }
2890
2891    /// Returns whether attached session state is request-local, not durable.
2892    #[doc(hidden)]
2893    #[must_use]
2894    pub fn session_is_ephemeral(&self) -> bool {
2895        self.request_scope_is_active()
2896            && self.state.as_ref().is_some_and(SessionState::is_ephemeral)
2897    }
2898
2899    /// Returns the session state attached to this context, if any.
2900    ///
2901    /// Final dispatch uses this shared bag so a later inbound on the same
2902    /// modern connection still sees `disable_*` mutations from earlier
2903    /// requests. Cloning the returned value shares the underlying store.
2904    #[must_use]
2905    pub fn session_state(&self) -> Option<&SessionState> {
2906        self.state.as_ref()
2907    }
2908
2909    /// Returns the opaque cache partition and mutation revision for this
2910    /// request's session state.
2911    ///
2912    /// This is an internal cross-crate integration hook. It returns `None`
2913    /// when the request is no longer live or the state cannot provide a safe
2914    /// stable partition. Cache implementations must additionally partition by
2915    /// all response-relevant authenticated facts.
2916    #[doc(hidden)]
2917    #[must_use]
2918    pub fn session_cache_partition(&self) -> Option<([u8; 32], u64)> {
2919        if !self.request_scope_is_active() {
2920            return None;
2921        }
2922        self.state.as_ref()?.cache_partition()
2923    }
2924
2925    /// Captures the current session cache partition for this request.
2926    ///
2927    /// Repeated callers receive the same partition only while session state has
2928    /// not changed. This lets cache middleware prove that a response completed
2929    /// against the same state revision used for lookup.
2930    #[doc(hidden)]
2931    #[must_use]
2932    pub fn begin_session_cache_partition(&self) -> Option<([u8; 32], u64)> {
2933        let current = self.session_cache_partition()?;
2934        let mut admitted = self
2935            .cache_admission_partition
2936            .lock()
2937            .unwrap_or_else(std::sync::PoisonError::into_inner);
2938        match *admitted {
2939            None => {
2940                *admitted = Some(current);
2941                Some(current)
2942            }
2943            Some(existing) if existing == current => Some(existing),
2944            Some(_) => None,
2945        }
2946    }
2947
2948    /// Returns the admitted cache partition only if the state revision is
2949    /// unchanged at response completion.
2950    #[doc(hidden)]
2951    #[must_use]
2952    pub fn complete_session_cache_partition(&self) -> Option<([u8; 32], u64)> {
2953        if !self.request_scope_is_active() {
2954            return None;
2955        }
2956        let admitted = *self
2957            .cache_admission_partition
2958            .lock()
2959            .unwrap_or_else(std::sync::PoisonError::into_inner);
2960        let admitted = admitted?;
2961        (self.state.as_ref()?.cache_partition() == Some(admitted)).then_some(admitted)
2962    }
2963
2964    /// Marks that one middleware instance produced this request's response from
2965    /// a cache hit.
2966    #[doc(hidden)]
2967    pub fn mark_response_cache_hit(&self, cache_id: u64) -> bool {
2968        const MAX_CACHE_MIDDLEWARE_PER_REQUEST: usize = 64;
2969        if !self.request_scope_is_active() || cache_id == 0 {
2970            return false;
2971        }
2972        let mut hits = self
2973            .response_cache_hits
2974            .lock()
2975            .unwrap_or_else(std::sync::PoisonError::into_inner);
2976        if hits.contains(&cache_id) {
2977            return true;
2978        }
2979        if hits.len() >= MAX_CACHE_MIDDLEWARE_PER_REQUEST || hits.try_reserve(1).is_err() {
2980            return false;
2981        }
2982        hits.push(cache_id);
2983        true
2984    }
2985
2986    /// Returns whether a specific middleware instance produced this request's
2987    /// response from cache.
2988    #[doc(hidden)]
2989    #[must_use]
2990    pub fn response_was_cache_hit(&self, cache_id: u64) -> bool {
2991        self.request_scope_is_active()
2992            && cache_id != 0
2993            && self
2994                .response_cache_hits
2995                .lock()
2996                .unwrap_or_else(std::sync::PoisonError::into_inner)
2997                .contains(&cache_id)
2998    }
2999
3000    /// Returns whether any response-cache middleware served this request.
3001    #[doc(hidden)]
3002    #[must_use]
3003    pub fn response_was_served_from_cache(&self) -> bool {
3004        self.request_scope_is_active()
3005            && !self
3006                .response_cache_hits
3007                .lock()
3008                .unwrap_or_else(std::sync::PoisonError::into_inner)
3009                .is_empty()
3010    }
3011
3012    // ========================================================================
3013    // Capabilities Access
3014    // ========================================================================
3015
3016    /// Returns the client capability information, if available.
3017    ///
3018    /// Capabilities are set by the server after initialization and reflect
3019    /// what the connected client supports.
3020    #[must_use]
3021    pub fn client_capabilities(&self) -> Option<&ClientCapabilityInfo> {
3022        self.client_capabilities.as_ref()
3023    }
3024
3025    /// Returns the self-reported modern client Implementation, if advertised.
3026    ///
3027    /// This is request `_meta` identity, not authentication. A missing value
3028    /// means the peer did not send `io.modelcontextprotocol/clientInfo`.
3029    #[must_use]
3030    pub fn client_implementation(&self) -> Option<&ClientImplementationInfo> {
3031        self.client_implementation.as_ref()
3032    }
3033
3034    /// Returns the server capability information, if available.
3035    ///
3036    /// Reflects what capabilities this server advertises.
3037    #[must_use]
3038    pub fn server_capabilities(&self) -> Option<&ServerCapabilityInfo> {
3039        self.server_capabilities.as_ref()
3040    }
3041
3042    /// Returns whether the client supports sampling (LLM completions).
3043    ///
3044    /// This is a convenience method that checks the client capabilities.
3045    /// Returns `false` if capabilities are not yet available (before initialization).
3046    #[must_use]
3047    pub fn client_supports_sampling(&self) -> bool {
3048        self.client_capabilities
3049            .as_ref()
3050            .is_some_and(|c| c.sampling)
3051    }
3052
3053    /// Returns whether the client supports elicitation (user input requests).
3054    ///
3055    /// This is a convenience method that checks the client capabilities.
3056    /// Returns `false` if capabilities are not yet available.
3057    #[must_use]
3058    pub fn client_supports_elicitation(&self) -> bool {
3059        self.client_capabilities
3060            .as_ref()
3061            .is_some_and(|c| c.elicitation)
3062    }
3063
3064    /// Returns whether the client supports form-mode elicitation.
3065    #[must_use]
3066    pub fn client_supports_elicitation_form(&self) -> bool {
3067        self.client_capabilities
3068            .as_ref()
3069            .is_some_and(|c| c.elicitation_form)
3070    }
3071
3072    /// Returns whether the client supports URL-mode elicitation.
3073    #[must_use]
3074    pub fn client_supports_elicitation_url(&self) -> bool {
3075        self.client_capabilities
3076            .as_ref()
3077            .is_some_and(|c| c.elicitation_url)
3078    }
3079
3080    /// Returns whether the client supports roots listing.
3081    ///
3082    /// This is a convenience method that checks the client capabilities.
3083    /// Returns `false` if capabilities are not yet available.
3084    #[must_use]
3085    pub fn client_supports_roots(&self) -> bool {
3086        self.client_capabilities.as_ref().is_some_and(|c| c.roots)
3087    }
3088
3089    // ========================================================================
3090    // Dynamic Component Enable/Disable
3091    // ========================================================================
3092
3093    /// Session state key for disabled tools.
3094    const DISABLED_TOOLS_KEY: &'static str = "fastmcp.disabled_tools";
3095    /// Session state key for disabled resources.
3096    const DISABLED_RESOURCES_KEY: &'static str = "fastmcp.disabled_resources";
3097    /// Session state key for disabled prompts.
3098    const DISABLED_PROMPTS_KEY: &'static str = "fastmcp.disabled_prompts";
3099
3100    /// Disables a tool for this session.
3101    ///
3102    /// Disabled tools will not appear in `tools/list` responses and will return
3103    /// an error if called directly. This is useful for adapting available
3104    /// functionality based on user permissions, feature flags, or runtime conditions.
3105    ///
3106    /// Returns `true` if the operation succeeded, `false` if session state is unavailable.
3107    ///
3108    /// # Example
3109    ///
3110    /// ```ignore
3111    /// async fn my_tool(ctx: &McpContext) -> McpResult<String> {
3112    ///     // Disable the "admin_tool" for this session
3113    ///     ctx.disable_tool("admin_tool");
3114    ///     Ok("Admin tool disabled".to_string())
3115    /// }
3116    /// ```
3117    pub fn disable_tool(&self, name: impl Into<String>) -> bool {
3118        self.add_to_disabled_set(Self::DISABLED_TOOLS_KEY, name.into(), McpCatalogKind::Tools)
3119    }
3120
3121    /// Enables a previously disabled tool for this session.
3122    ///
3123    /// Returns `true` if the operation succeeded, `false` if session state is unavailable.
3124    pub fn enable_tool(&self, name: &str) -> bool {
3125        self.remove_from_disabled_set(Self::DISABLED_TOOLS_KEY, name, McpCatalogKind::Tools)
3126    }
3127
3128    /// Returns whether a tool is enabled (not disabled) for this session.
3129    ///
3130    /// Tools are enabled by default unless explicitly disabled.
3131    #[must_use]
3132    pub fn is_tool_enabled(&self, name: &str) -> bool {
3133        self.request_scope_is_active() && !self.is_in_disabled_set(Self::DISABLED_TOOLS_KEY, name)
3134    }
3135
3136    /// Disables a resource for this session.
3137    ///
3138    /// Disabled resources will not appear in `resources/list` responses and will
3139    /// return an error if read directly.
3140    ///
3141    /// Returns `true` if the operation succeeded, `false` if session state is unavailable.
3142    pub fn disable_resource(&self, uri: impl Into<String>) -> bool {
3143        self.add_to_disabled_set(
3144            Self::DISABLED_RESOURCES_KEY,
3145            uri.into(),
3146            McpCatalogKind::Resources,
3147        )
3148    }
3149
3150    /// Enables a previously disabled resource for this session.
3151    ///
3152    /// Returns `true` if the operation succeeded, `false` if session state is unavailable.
3153    pub fn enable_resource(&self, uri: &str) -> bool {
3154        self.remove_from_disabled_set(Self::DISABLED_RESOURCES_KEY, uri, McpCatalogKind::Resources)
3155    }
3156
3157    /// Returns whether a resource is enabled (not disabled) for this session.
3158    ///
3159    /// Resources are enabled by default unless explicitly disabled.
3160    #[must_use]
3161    pub fn is_resource_enabled(&self, uri: &str) -> bool {
3162        self.request_scope_is_active()
3163            && !self.is_in_disabled_set(Self::DISABLED_RESOURCES_KEY, uri)
3164    }
3165
3166    /// Disables a prompt for this session.
3167    ///
3168    /// Disabled prompts will not appear in `prompts/list` responses and will
3169    /// return an error if retrieved directly.
3170    ///
3171    /// Returns `true` if the operation succeeded, `false` if session state is unavailable.
3172    pub fn disable_prompt(&self, name: impl Into<String>) -> bool {
3173        self.add_to_disabled_set(
3174            Self::DISABLED_PROMPTS_KEY,
3175            name.into(),
3176            McpCatalogKind::Prompts,
3177        )
3178    }
3179
3180    /// Enables a previously disabled prompt for this session.
3181    ///
3182    /// Returns `true` if the operation succeeded, `false` if session state is unavailable.
3183    pub fn enable_prompt(&self, name: &str) -> bool {
3184        self.remove_from_disabled_set(Self::DISABLED_PROMPTS_KEY, name, McpCatalogKind::Prompts)
3185    }
3186
3187    /// Returns whether a prompt is enabled (not disabled) for this session.
3188    ///
3189    /// Prompts are enabled by default unless explicitly disabled.
3190    #[must_use]
3191    pub fn is_prompt_enabled(&self, name: &str) -> bool {
3192        self.request_scope_is_active() && !self.is_in_disabled_set(Self::DISABLED_PROMPTS_KEY, name)
3193    }
3194
3195    /// Returns the set of disabled tools for this session.
3196    #[must_use]
3197    pub fn disabled_tools(&self) -> std::collections::HashSet<String> {
3198        self.get_disabled_set(Self::DISABLED_TOOLS_KEY)
3199    }
3200
3201    /// Returns the set of disabled resources for this session.
3202    #[must_use]
3203    pub fn disabled_resources(&self) -> std::collections::HashSet<String> {
3204        self.get_disabled_set(Self::DISABLED_RESOURCES_KEY)
3205    }
3206
3207    /// Returns the set of disabled prompts for this session.
3208    #[must_use]
3209    pub fn disabled_prompts(&self) -> std::collections::HashSet<String> {
3210        self.get_disabled_set(Self::DISABLED_PROMPTS_KEY)
3211    }
3212
3213    // Helper: Add a name to a disabled set
3214    fn add_to_disabled_set(&self, key: &str, name: String, kind: McpCatalogKind) -> bool {
3215        if self.ensure_live().is_err() {
3216            return false;
3217        }
3218        let Some(state) = self.state.as_ref() else {
3219            return false;
3220        };
3221        let mut set: std::collections::HashSet<String> = state.get(key).unwrap_or_default();
3222        let changed = set.insert(name);
3223        let stored = state.set(key, set);
3224        if stored && changed {
3225            self.emit_catalog_changed(kind);
3226        }
3227        stored
3228    }
3229
3230    // Helper: Remove a name from a disabled set
3231    fn remove_from_disabled_set(&self, key: &str, name: &str, kind: McpCatalogKind) -> bool {
3232        if self.ensure_live().is_err() {
3233            return false;
3234        }
3235        let Some(state) = self.state.as_ref() else {
3236            return false;
3237        };
3238        let mut set: std::collections::HashSet<String> = state.get(key).unwrap_or_default();
3239        let changed = set.remove(name);
3240        let stored = state.set(key, set);
3241        if stored && changed {
3242            self.emit_catalog_changed(kind);
3243        }
3244        stored
3245    }
3246
3247    fn emit_catalog_changed(&self, kind: McpCatalogKind) {
3248        if let Some(sender) = self.log_sender.as_ref() {
3249            sender.send_catalog_changed(kind);
3250        }
3251        if let Some(publisher) = self.catalog_publisher.as_ref() {
3252            let _ = publisher.publish_catalog_changed(kind);
3253        }
3254    }
3255
3256    // Helper: Check if a name is in a disabled set
3257    fn is_in_disabled_set(&self, key: &str, name: &str) -> bool {
3258        if !self.request_scope_is_active() {
3259            return false;
3260        }
3261        let Some(state) = self.state.as_ref() else {
3262            return false;
3263        };
3264        let set: std::collections::HashSet<String> = state.get(key).unwrap_or_default();
3265        set.contains(name)
3266    }
3267
3268    // Helper: Get the full disabled set
3269    fn get_disabled_set(&self, key: &str) -> std::collections::HashSet<String> {
3270        if !self.request_scope_is_active() {
3271            return std::collections::HashSet::new();
3272        }
3273        self.state
3274            .as_ref()
3275            .and_then(|s| s.get(key))
3276            .unwrap_or_default()
3277    }
3278
3279    // ========================================================================
3280    // Client Roots
3281    // ========================================================================
3282
3283    /// Returns whether client roots are available in this context.
3284    #[must_use]
3285    pub fn can_list_roots(&self) -> bool {
3286        self.ensure_live().is_ok() && self.roots_provider.is_some()
3287    }
3288
3289    /// Lists the filesystem roots exposed by the connected client.
3290    ///
3291    /// # Errors
3292    ///
3293    /// Returns an error when the client did not advertise roots, the transport
3294    /// cannot complete the reverse request, or this request is cancelled.
3295    pub async fn list_roots(&self) -> crate::McpResult<Vec<ClientRoot>> {
3296        self.ensure_live()
3297            .map_err(|_| crate::McpError::request_cancelled())?;
3298        let provider = self.roots_provider.as_ref().ok_or_else(|| {
3299            crate::McpError::new(
3300                crate::McpErrorCode::InvalidRequest,
3301                "Roots not available: client does not support roots capability",
3302            )
3303        })?;
3304
3305        let roots = provider.list_roots().await?;
3306        self.ensure_live()
3307            .map_err(|_| crate::McpError::request_cancelled())?;
3308        Ok(roots)
3309    }
3310
3311    // ========================================================================
3312    // Sampling (LLM Completions)
3313    // ========================================================================
3314
3315    /// Returns whether sampling is available in this context.
3316    ///
3317    /// Sampling is available when the client has advertised sampling
3318    /// capability and a sampling sender has been configured.
3319    #[must_use]
3320    pub fn can_sample(&self) -> bool {
3321        self.ensure_live().is_ok() && self.sampling_sender.is_some()
3322    }
3323
3324    /// Requests an LLM completion from the client.
3325    ///
3326    /// This is a convenience method for simple text prompts. For more control
3327    /// over the request, use [`sample_with_request`](Self::sample_with_request).
3328    ///
3329    /// # Arguments
3330    ///
3331    /// * `prompt` - The prompt text to send (as a user message)
3332    /// * `max_tokens` - Maximum number of tokens to generate
3333    ///
3334    /// # Errors
3335    ///
3336    /// Returns an error if:
3337    /// - The client doesn't support sampling
3338    /// - The sampling request fails
3339    ///
3340    /// # Example
3341    ///
3342    /// ```ignore
3343    /// async fn my_tool(ctx: &McpContext, topic: String) -> McpResult<String> {
3344    ///     let response = ctx.sample(&format!("Write a haiku about {topic}"), 100).await?;
3345    ///     Ok(response.text)
3346    /// }
3347    /// ```
3348    pub async fn sample(
3349        &self,
3350        prompt: impl Into<String>,
3351        max_tokens: u32,
3352    ) -> crate::McpResult<SamplingResponse> {
3353        let request = SamplingRequest::prompt(prompt, max_tokens);
3354        self.sample_with_request(request).await
3355    }
3356
3357    /// Requests an LLM completion with full control over the request.
3358    ///
3359    /// # Arguments
3360    ///
3361    /// * `request` - The full sampling request parameters
3362    ///
3363    /// # Errors
3364    ///
3365    /// Returns an error if:
3366    /// - The client doesn't support sampling
3367    /// - The sampling request fails
3368    ///
3369    /// # Example
3370    ///
3371    /// ```ignore
3372    /// async fn my_tool(ctx: &McpContext) -> McpResult<String> {
3373    ///     let request = SamplingRequest::new(
3374    ///         vec![
3375    ///             SamplingRequestMessage::user("Hello!"),
3376    ///             SamplingRequestMessage::assistant("Hi! How can I help?"),
3377    ///             SamplingRequestMessage::user("Tell me a joke."),
3378    ///         ],
3379    ///         200,
3380    ///     )
3381    ///     .with_system_prompt("You are a helpful and funny assistant.")
3382    ///     .with_temperature(0.8);
3383    ///
3384    ///     let response = ctx.sample_with_request(request).await?;
3385    ///     Ok(response.text)
3386    /// }
3387    /// ```
3388    pub async fn sample_with_request(
3389        &self,
3390        request: SamplingRequest,
3391    ) -> crate::McpResult<SamplingResponse> {
3392        self.ensure_live()
3393            .map_err(|_| crate::McpError::request_cancelled())?;
3394        let sender = self.sampling_sender.as_ref().ok_or_else(|| {
3395            crate::McpError::new(
3396                crate::McpErrorCode::InvalidRequest,
3397                "Sampling not available: client does not support sampling capability",
3398            )
3399        })?;
3400
3401        let response = sender.create_message(request).await?;
3402        self.ensure_live()
3403            .map_err(|_| crate::McpError::request_cancelled())?;
3404        Ok(response)
3405    }
3406
3407    // ========================================================================
3408    // Elicitation (User Input Requests)
3409    // ========================================================================
3410
3411    /// Returns whether elicitation is available in this context.
3412    ///
3413    /// Elicitation is available when the client has advertised elicitation
3414    /// capability and an elicitation sender has been configured.
3415    #[must_use]
3416    pub fn can_elicit(&self) -> bool {
3417        self.ensure_live().is_ok() && self.elicitation_sender.is_some()
3418    }
3419
3420    /// Requests user input via a form.
3421    ///
3422    /// This presents a form to the user with fields defined by the JSON schema.
3423    /// The user can accept (submit the form), decline, or cancel.
3424    ///
3425    /// # Arguments
3426    ///
3427    /// * `message` - Message to display explaining what input is needed
3428    /// * `schema` - JSON Schema defining the form fields
3429    ///
3430    /// # Errors
3431    ///
3432    /// Returns an error if:
3433    /// - The client doesn't support elicitation
3434    /// - The elicitation request fails
3435    ///
3436    /// # Example
3437    ///
3438    /// ```ignore
3439    /// async fn my_tool(ctx: &McpContext) -> McpResult<String> {
3440    ///     let schema = serde_json::json!({
3441    ///         "type": "object",
3442    ///         "properties": {
3443    ///             "name": {"type": "string"},
3444    ///             "age": {"type": "integer"}
3445    ///         },
3446    ///         "required": ["name"]
3447    ///     });
3448    ///     let response = ctx.elicit_form("Please enter your details", schema).await?;
3449    ///     if response.is_accepted() {
3450    ///         let name = response.get_string("name").unwrap_or("Unknown");
3451    ///         Ok(format!("Hello, {name}!"))
3452    ///     } else {
3453    ///         Ok("User declined input".to_string())
3454    ///     }
3455    /// }
3456    /// ```
3457    pub async fn elicit_form(
3458        &self,
3459        message: impl Into<String>,
3460        schema: serde_json::Value,
3461    ) -> crate::McpResult<ElicitationResponse> {
3462        let request = ElicitationRequest::form(message, schema);
3463        self.elicit_with_request(request).await
3464    }
3465
3466    /// Requests user interaction via an external URL.
3467    ///
3468    /// This directs the user to an external URL for sensitive operations like
3469    /// OAuth flows, payment processing, or credential collection.
3470    ///
3471    /// # Arguments
3472    ///
3473    /// * `message` - Message to display explaining why the URL visit is needed
3474    /// * `url` - The URL the user should navigate to
3475    /// * `elicitation_id` - Unique ID for tracking this elicitation
3476    ///
3477    /// # Errors
3478    ///
3479    /// Returns an error if:
3480    /// - The client doesn't support elicitation
3481    /// - The elicitation request fails
3482    ///
3483    /// # Example
3484    ///
3485    /// ```ignore
3486    /// async fn my_tool(ctx: &McpContext) -> McpResult<String> {
3487    ///     let response = ctx.elicit_url(
3488    ///         "Please authenticate with your GitHub account",
3489    ///         "https://github.com/login/oauth/authorize?...",
3490    ///         "github-auth-12345",
3491    ///     ).await?;
3492    ///     if response.is_accepted() {
3493    ///         Ok("Authentication successful".to_string())
3494    ///     } else {
3495    ///         Ok("Authentication cancelled".to_string())
3496    ///     }
3497    /// }
3498    /// ```
3499    pub async fn elicit_url(
3500        &self,
3501        message: impl Into<String>,
3502        url: impl Into<String>,
3503        elicitation_id: impl Into<String>,
3504    ) -> crate::McpResult<ElicitationResponse> {
3505        let request = ElicitationRequest::url(message, url, elicitation_id);
3506        self.elicit_with_request(request).await
3507    }
3508
3509    /// Requests user input with full control over the request.
3510    ///
3511    /// # Arguments
3512    ///
3513    /// * `request` - The full elicitation request parameters
3514    ///
3515    /// # Errors
3516    ///
3517    /// Returns an error if:
3518    /// - The client doesn't support elicitation
3519    /// - The elicitation request fails
3520    pub async fn elicit_with_request(
3521        &self,
3522        request: ElicitationRequest,
3523    ) -> crate::McpResult<ElicitationResponse> {
3524        self.ensure_live()
3525            .map_err(|_| crate::McpError::request_cancelled())?;
3526        let sender = self.elicitation_sender.as_ref().ok_or_else(|| {
3527            crate::McpError::new(
3528                crate::McpErrorCode::InvalidRequest,
3529                "Elicitation not available: client does not support elicitation capability",
3530            )
3531        })?;
3532
3533        let response = sender.elicit(request).await?;
3534        self.ensure_live()
3535            .map_err(|_| crate::McpError::request_cancelled())?;
3536        Ok(response)
3537    }
3538
3539    // ========================================================================
3540    // Resource Reading (Cross-Component Access)
3541    // ========================================================================
3542
3543    /// Returns whether resource reading is available in this context.
3544    ///
3545    /// Resource reading is available when a resource reader (Router) has
3546    /// been attached to this context.
3547    #[must_use]
3548    pub fn can_read_resources(&self) -> bool {
3549        self.ensure_live().is_ok() && self.resource_reader.is_some()
3550    }
3551
3552    /// Returns the current resource read depth.
3553    ///
3554    /// This is used to track recursion when resources read other resources.
3555    #[must_use]
3556    pub fn resource_read_depth(&self) -> u32 {
3557        self.resource_read_depth
3558    }
3559
3560    /// Reads a resource by URI.
3561    ///
3562    /// This allows tools, resources, and prompts to read other resources
3563    /// configured on the same server. This enables composition and code reuse.
3564    ///
3565    /// # Arguments
3566    ///
3567    /// * `uri` - The resource URI to read
3568    ///
3569    /// # Errors
3570    ///
3571    /// Returns an error if:
3572    /// - No resource reader is available (context not configured for resource access)
3573    /// - The resource is not found
3574    /// - Maximum recursion depth is exceeded
3575    /// - The resource read fails
3576    ///
3577    /// # Example
3578    ///
3579    /// ```ignore
3580    /// #[tool]
3581    /// async fn process_config(ctx: &McpContext) -> Result<String, ToolError> {
3582    ///     let config = ctx.read_resource("config://app").await?;
3583    ///     let text = config.first_text()
3584    ///         .ok_or(ToolError::InvalidConfig)?;
3585    ///     Ok(format!("Config loaded: {}", text))
3586    /// }
3587    /// ```
3588    pub async fn read_resource(&self, uri: &str) -> crate::McpResult<ResourceReadResult> {
3589        self.ensure_live()
3590            .map_err(|_| crate::McpError::request_cancelled())?;
3591        // Check if we have a resource reader
3592        let reader = self.resource_reader.as_ref().ok_or_else(|| {
3593            crate::McpError::new(
3594                crate::McpErrorCode::InternalError,
3595                "Resource reading not available: no router attached to context",
3596            )
3597        })?;
3598
3599        // Use one effective nesting depth across all cross-component APIs so
3600        // alternating tool -> resource -> prompt cycles cannot reset a
3601        // type-specific counter.
3602        let nested_dispatch_depth = self.nested_dispatch_depth();
3603        if nested_dispatch_depth >= MAX_RESOURCE_READ_DEPTH {
3604            return Err(crate::McpError::new(
3605                crate::McpErrorCode::InternalError,
3606                format!(
3607                    "Maximum resource read depth ({}) exceeded; possible infinite recursion",
3608                    MAX_RESOURCE_READ_DEPTH
3609                ),
3610            ));
3611        }
3612
3613        // Read the resource with incremented depth
3614        let result = reader
3615            .read_resource(self, uri, nested_dispatch_depth + 1)
3616            .await?;
3617        self.ensure_live()
3618            .map_err(|_| crate::McpError::request_cancelled())?;
3619        Ok(result)
3620    }
3621
3622    /// Reads a resource and extracts the text content.
3623    ///
3624    /// This is a convenience method that reads a resource and returns
3625    /// the first text content item.
3626    ///
3627    /// # Errors
3628    ///
3629    /// Returns an error if:
3630    /// - The resource read fails
3631    /// - The resource has no text content
3632    ///
3633    /// # Example
3634    ///
3635    /// ```ignore
3636    /// let text = ctx.read_resource_text("file://readme.md").await?;
3637    /// println!("Content: {}", text);
3638    /// ```
3639    pub async fn read_resource_text(&self, uri: &str) -> crate::McpResult<String> {
3640        let result = self.read_resource(uri).await?;
3641        result.first_text().map(String::from).ok_or_else(|| {
3642            crate::McpError::new(
3643                crate::McpErrorCode::InternalError,
3644                format!("Resource '{}' has no text content", uri),
3645            )
3646        })
3647    }
3648
3649    /// Reads a resource and parses it as JSON.
3650    ///
3651    /// This is a convenience method that reads a resource and deserializes
3652    /// the text content as JSON.
3653    ///
3654    /// # Errors
3655    ///
3656    /// Returns an error if:
3657    /// - The resource read fails
3658    /// - The resource has no text content
3659    /// - JSON deserialization fails
3660    ///
3661    /// # Example
3662    ///
3663    /// ```ignore
3664    /// #[derive(Deserialize)]
3665    /// struct Config {
3666    ///     database_url: String,
3667    /// }
3668    ///
3669    /// let config: Config = ctx.read_resource_json("config://app").await?;
3670    /// println!("Database: {}", config.database_url);
3671    /// ```
3672    pub async fn read_resource_json<T: serde::de::DeserializeOwned>(
3673        &self,
3674        uri: &str,
3675    ) -> crate::McpResult<T> {
3676        let text = self.read_resource_text(uri).await?;
3677        serde_json::from_str(&text).map_err(|e| {
3678            crate::McpError::new(
3679                crate::McpErrorCode::InternalError,
3680                format!("Failed to parse resource '{}' as JSON: {}", uri, e),
3681            )
3682        })
3683    }
3684
3685    // ========================================================================
3686    // Tool Calling (Cross-Component Access)
3687    // ========================================================================
3688
3689    /// Returns whether tool calling is available in this context.
3690    ///
3691    /// Tool calling is available when a tool caller (Router) has
3692    /// been attached to this context.
3693    #[must_use]
3694    pub fn can_call_tools(&self) -> bool {
3695        self.ensure_live().is_ok() && self.tool_caller.is_some()
3696    }
3697
3698    /// Returns the current tool call depth.
3699    ///
3700    /// This is used to track recursion when tools call other tools.
3701    #[must_use]
3702    pub fn tool_call_depth(&self) -> u32 {
3703        self.tool_call_depth
3704    }
3705
3706    /// Calls a tool by name with the given arguments.
3707    ///
3708    /// This allows tools, resources, and prompts to call other tools
3709    /// configured on the same server. This enables composition and code reuse.
3710    ///
3711    /// # Arguments
3712    ///
3713    /// * `name` - The tool name to call
3714    /// * `args` - The arguments as a JSON value
3715    ///
3716    /// # Errors
3717    ///
3718    /// Returns an error if:
3719    /// - No tool caller is available (context not configured for tool access)
3720    /// - The tool is not found
3721    /// - Maximum recursion depth is exceeded
3722    /// - The tool execution fails
3723    ///
3724    /// # Example
3725    ///
3726    /// ```ignore
3727    /// #[tool]
3728    /// async fn double_add(ctx: &McpContext, a: i32, b: i32) -> Result<i32, ToolError> {
3729    ///     let sum: i32 = ctx.call_tool_json("add", json!({"a": a, "b": b})).await?;
3730    ///     Ok(sum * 2)
3731    /// }
3732    /// ```
3733    pub async fn call_tool(
3734        &self,
3735        name: &str,
3736        args: serde_json::Value,
3737    ) -> crate::McpResult<ToolCallResult> {
3738        self.ensure_live()
3739            .map_err(|_| crate::McpError::request_cancelled())?;
3740        // Check if we have a tool caller
3741        let caller = self.tool_caller.as_ref().ok_or_else(|| {
3742            crate::McpError::new(
3743                crate::McpErrorCode::InternalError,
3744                "Tool calling not available: no router attached to context",
3745            )
3746        })?;
3747
3748        // Share the effective depth with resource reads and prompt gets so
3749        // alternating cycles are bounded just like same-kind recursion.
3750        let nested_dispatch_depth = self.nested_dispatch_depth();
3751        if nested_dispatch_depth >= MAX_TOOL_CALL_DEPTH {
3752            return Err(crate::McpError::new(
3753                crate::McpErrorCode::InternalError,
3754                format!(
3755                    "Maximum tool call depth ({}) exceeded calling '{}'; possible infinite recursion",
3756                    MAX_TOOL_CALL_DEPTH, name
3757                ),
3758            ));
3759        }
3760
3761        // Call the tool with incremented depth
3762        let result = caller
3763            .call_tool(self, name, args, nested_dispatch_depth + 1)
3764            .await?;
3765        self.ensure_live()
3766            .map_err(|_| crate::McpError::request_cancelled())?;
3767        Ok(result)
3768    }
3769
3770    /// Calls a tool and extracts the text content.
3771    ///
3772    /// This is a convenience method that calls a tool and returns
3773    /// the first text content item.
3774    ///
3775    /// # Errors
3776    ///
3777    /// Returns an error if:
3778    /// - The tool call fails
3779    /// - The tool returns an error result
3780    /// - The tool has no text content
3781    ///
3782    /// # Example
3783    ///
3784    /// ```ignore
3785    /// let greeting = ctx.call_tool_text("greet", json!({"name": "World"})).await?;
3786    /// println!("Result: {}", greeting);
3787    /// ```
3788    pub async fn call_tool_text(
3789        &self,
3790        name: &str,
3791        args: serde_json::Value,
3792    ) -> crate::McpResult<String> {
3793        let result = self.call_tool(name, args).await?;
3794
3795        // Check if tool returned an error
3796        if result.is_error {
3797            let error_msg = result.first_text().unwrap_or("Tool returned an error");
3798            return Err(crate::McpError::new(
3799                crate::McpErrorCode::InternalError,
3800                format!("Tool '{}' failed: {}", name, error_msg),
3801            ));
3802        }
3803
3804        result.first_text().map(String::from).ok_or_else(|| {
3805            crate::McpError::new(
3806                crate::McpErrorCode::InternalError,
3807                format!("Tool '{}' returned no text content", name),
3808            )
3809        })
3810    }
3811
3812    /// Calls a tool and parses the result as JSON.
3813    ///
3814    /// This is a convenience method that calls a tool and deserializes
3815    /// the text content as JSON.
3816    ///
3817    /// # Errors
3818    ///
3819    /// Returns an error if:
3820    /// - The tool call fails
3821    /// - The tool returns an error result
3822    /// - The tool has no text content
3823    /// - JSON deserialization fails
3824    ///
3825    /// # Example
3826    ///
3827    /// ```ignore
3828    /// #[derive(Deserialize)]
3829    /// struct ComputeResult {
3830    ///     value: i64,
3831    /// }
3832    ///
3833    /// let result: ComputeResult = ctx.call_tool_json("compute", json!({"x": 5})).await?;
3834    /// println!("Result: {}", result.value);
3835    /// ```
3836    pub async fn call_tool_json<T: serde::de::DeserializeOwned>(
3837        &self,
3838        name: &str,
3839        args: serde_json::Value,
3840    ) -> crate::McpResult<T> {
3841        let text = self.call_tool_text(name, args).await?;
3842        serde_json::from_str(&text).map_err(|e| {
3843            crate::McpError::new(
3844                crate::McpErrorCode::InternalError,
3845                format!("Failed to parse tool '{}' result as JSON: {}", name, e),
3846            )
3847        })
3848    }
3849
3850    // ========================================================================
3851    // Prompt Getting (Cross-Component Access)
3852    // ========================================================================
3853
3854    /// Returns whether prompt getting is available in this context.
3855    #[must_use]
3856    pub fn can_get_prompts(&self) -> bool {
3857        self.ensure_live().is_ok() && self.prompt_caller.is_some()
3858    }
3859
3860    /// Returns the current prompt get depth.
3861    #[must_use]
3862    pub fn prompt_get_depth(&self) -> u32 {
3863        self.prompt_get_depth
3864    }
3865
3866    fn nested_dispatch_depth(&self) -> u32 {
3867        self.resource_read_depth
3868            .max(self.tool_call_depth)
3869            .max(self.prompt_get_depth)
3870    }
3871
3872    /// Gets a prompt by name with the given arguments.
3873    ///
3874    /// This allows tools, resources, and prompts to get other prompts
3875    /// configured on the same server.
3876    pub async fn get_prompt(
3877        &self,
3878        name: &str,
3879        arguments: std::collections::HashMap<String, String>,
3880    ) -> crate::McpResult<PromptGetResult> {
3881        self.ensure_live()
3882            .map_err(|_| crate::McpError::request_cancelled())?;
3883        let caller = self.prompt_caller.as_ref().ok_or_else(|| {
3884            crate::McpError::new(
3885                crate::McpErrorCode::InternalError,
3886                "Prompt getting not available: no router attached to context",
3887            )
3888        })?;
3889
3890        let nested_dispatch_depth = self.nested_dispatch_depth();
3891        if nested_dispatch_depth >= MAX_PROMPT_GET_DEPTH {
3892            return Err(crate::McpError::new(
3893                crate::McpErrorCode::InternalError,
3894                format!(
3895                    "Maximum prompt get depth ({}) exceeded getting '{}'; possible infinite recursion",
3896                    MAX_PROMPT_GET_DEPTH, name
3897                ),
3898            ));
3899        }
3900
3901        let result = caller
3902            .get_prompt(self, name, arguments, nested_dispatch_depth + 1)
3903            .await?;
3904        self.ensure_live()
3905            .map_err(|_| crate::McpError::request_cancelled())?;
3906        Ok(result)
3907    }
3908
3909    /// Gets a prompt and extracts the first text message.
3910    pub async fn get_prompt_text(
3911        &self,
3912        name: &str,
3913        arguments: std::collections::HashMap<String, String>,
3914    ) -> crate::McpResult<String> {
3915        let result = self.get_prompt(name, arguments).await?;
3916        result.first_text().map(String::from).ok_or_else(|| {
3917            crate::McpError::new(
3918                crate::McpErrorCode::InternalError,
3919                format!("Prompt '{}' returned no text content", name),
3920            )
3921        })
3922    }
3923
3924    // ========================================================================
3925    // Parallel Combinators
3926    // ========================================================================
3927
3928    /// Waits for all futures to complete and returns their results.
3929    ///
3930    /// This is the N-of-N combinator: all futures must complete before
3931    /// returning. Results are returned in the same order as input futures.
3932    ///
3933    /// # Example
3934    ///
3935    /// ```ignore
3936    /// let futures = vec![
3937    ///     Box::pin(fetch_user(1)),
3938    ///     Box::pin(fetch_user(2)),
3939    ///     Box::pin(fetch_user(3)),
3940    /// ];
3941    /// let users = ctx.join_all(futures).await?;
3942    /// ```
3943    pub async fn join_all<T: Send + 'static>(
3944        &self,
3945        futures: Vec<crate::combinator::BoxFuture<'_, T>>,
3946    ) -> crate::McpResult<Vec<T>> {
3947        self.ensure_live()
3948            .map_err(|_| crate::McpError::request_cancelled())?;
3949        let results = crate::combinator::join_all(&self.cx, futures).await;
3950        self.ensure_live()
3951            .map_err(|_| crate::McpError::request_cancelled())?;
3952        Ok(results)
3953    }
3954
3955    /// Races multiple futures, returning the first to complete.
3956    ///
3957    /// This is the 1-of-N combinator: the first future to complete wins,
3958    /// and all other supplied futures are dropped. Dropping a future does not
3959    /// cancel or drain work that it spawned independently; such work must live
3960    /// in a caller-owned structured scope with an explicit join obligation.
3961    ///
3962    /// # Example
3963    ///
3964    /// ```ignore
3965    /// let futures = vec![
3966    ///     Box::pin(fetch_from_primary()),
3967    ///     Box::pin(fetch_from_replica()),
3968    /// ];
3969    /// let result = ctx.race(futures).await?;
3970    /// ```
3971    pub async fn race<T: Send + 'static>(
3972        &self,
3973        futures: Vec<crate::combinator::BoxFuture<'_, T>>,
3974    ) -> crate::McpResult<T> {
3975        self.ensure_live()
3976            .map_err(|_| crate::McpError::request_cancelled())?;
3977        let result = crate::combinator::race(&self.cx, futures).await;
3978        self.ensure_live()
3979            .map_err(|_| crate::McpError::request_cancelled())?;
3980        result
3981    }
3982
3983    /// Waits for M of N futures to complete successfully.
3984    ///
3985    /// Returns when `required` futures have completed successfully.
3986    /// Remaining supplied futures are dropped. Independently spawned work is
3987    /// neither cancelled nor drained by dropping its parent future.
3988    ///
3989    /// # Example
3990    ///
3991    /// ```ignore
3992    /// let futures = vec![
3993    ///     Box::pin(write_to_replica(1)),
3994    ///     Box::pin(write_to_replica(2)),
3995    ///     Box::pin(write_to_replica(3)),
3996    /// ];
3997    /// let result = ctx.quorum(2, futures).await?;
3998    /// ```
3999    pub async fn quorum<T: Send + 'static>(
4000        &self,
4001        required: usize,
4002        futures: Vec<crate::combinator::BoxFuture<'_, crate::McpResult<T>>>,
4003    ) -> crate::McpResult<crate::combinator::QuorumResult<T>> {
4004        self.ensure_live()
4005            .map_err(|_| crate::McpError::request_cancelled())?;
4006        let result = crate::combinator::quorum(&self.cx, required, futures).await;
4007        self.ensure_live()
4008            .map_err(|_| crate::McpError::request_cancelled())?;
4009        result
4010    }
4011
4012    /// Races futures and returns the first successful result.
4013    ///
4014    /// Unlike `race` which returns the first to complete (success or failure),
4015    /// `first_ok` returns the first to complete successfully. Once a result is
4016    /// selected, the remaining supplied futures are dropped; independently
4017    /// spawned work is not cancelled or drained.
4018    ///
4019    /// # Example
4020    ///
4021    /// ```ignore
4022    /// let futures = vec![
4023    ///     Box::pin(try_primary()),
4024    ///     Box::pin(try_fallback()),
4025    /// ];
4026    /// let result = ctx.first_ok(futures).await?;
4027    /// ```
4028    pub async fn first_ok<T: Send + 'static>(
4029        &self,
4030        futures: Vec<crate::combinator::BoxFuture<'_, crate::McpResult<T>>>,
4031    ) -> crate::McpResult<T> {
4032        self.ensure_live()
4033            .map_err(|_| crate::McpError::request_cancelled())?;
4034        let result = crate::combinator::first_ok(&self.cx, futures).await;
4035        self.ensure_live()
4036            .map_err(|_| crate::McpError::request_cancelled())?;
4037        result
4038    }
4039}
4040
4041/// Error returned when a request has been cancelled.
4042///
4043/// This is returned by `checkpoint()` when the request should stop
4044/// processing. The server will convert this to an appropriate MCP
4045/// error response.
4046#[derive(Debug, Clone, Copy)]
4047pub struct CancelledError;
4048
4049impl std::fmt::Display for CancelledError {
4050    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4051        write!(f, "request cancelled")
4052    }
4053}
4054
4055impl std::error::Error for CancelledError {}
4056
4057/// Extension trait for converting MCP results to asupersync Outcome.
4058///
4059/// This bridges the MCP error model with asupersync's 4-valued outcome
4060/// (Ok, Err, Cancelled, Panicked).
4061pub trait IntoOutcome<T, E> {
4062    /// Converts this result into an asupersync Outcome.
4063    fn into_outcome(self) -> Outcome<T, E>;
4064}
4065
4066impl<T, E> IntoOutcome<T, E> for Result<T, E> {
4067    fn into_outcome(self) -> Outcome<T, E> {
4068        match self {
4069            Ok(v) => Outcome::Ok(v),
4070            Err(e) => Outcome::Err(e),
4071        }
4072    }
4073}
4074
4075impl<T, E> IntoOutcome<T, E> for Result<T, CancelledError>
4076where
4077    E: Default,
4078{
4079    fn into_outcome(self) -> Outcome<T, E> {
4080        match self {
4081            Ok(v) => Outcome::Ok(v),
4082            Err(CancelledError) => Outcome::Cancelled(CancelReason::user("request cancelled")),
4083        }
4084    }
4085}
4086
4087#[cfg(test)]
4088mod tests {
4089    use super::*;
4090
4091    #[test]
4092    fn test_mcp_context_creation() {
4093        let cx = Cx::for_testing();
4094        let ctx = McpContext::new(cx, 42);
4095
4096        assert_eq!(ctx.request_id(), 42);
4097    }
4098
4099    #[test]
4100    fn test_mcp_context_not_cancelled_initially() {
4101        let cx = Cx::for_testing();
4102        let ctx = McpContext::new(cx, 1);
4103
4104        assert!(!ctx.is_cancelled());
4105    }
4106
4107    #[test]
4108    fn test_mcp_context_checkpoint_success() {
4109        let cx = Cx::for_testing();
4110        let ctx = McpContext::new(cx, 1);
4111
4112        // Should succeed when not cancelled
4113        assert!(ctx.checkpoint().is_ok());
4114    }
4115
4116    #[test]
4117    fn test_mcp_context_checkpoint_cancelled() {
4118        let cx = Cx::for_testing();
4119        cx.set_cancel_requested(true);
4120        let ctx = McpContext::new(cx, 1);
4121
4122        // Should fail when cancelled
4123        assert!(ctx.checkpoint().is_err());
4124    }
4125
4126    #[test]
4127    fn request_local_cancellation_does_not_cancel_shared_ambient_context() {
4128        let cx = Cx::for_testing();
4129        let cancellation = McpRequestCancellation::new();
4130        let request =
4131            McpContext::new(cx.clone(), 1).with_request_cancellation(cancellation.clone());
4132        let sibling = McpContext::new(cx.clone(), 2);
4133
4134        cancellation.cancel();
4135
4136        assert!(request.ensure_live().is_err());
4137        assert!(request.checkpoint().is_err());
4138        assert!(sibling.ensure_live().is_ok());
4139        assert!(!cx.is_cancel_requested());
4140    }
4141
4142    #[test]
4143    fn context_exposes_its_request_local_cancellation_handle() {
4144        let cancellation = McpRequestCancellation::new();
4145        let context =
4146            McpContext::new(Cx::for_testing(), 1).with_request_cancellation(cancellation.clone());
4147
4148        let observed = context.request_cancellation();
4149        assert!(observed.cancel());
4150        assert!(cancellation.is_cancel_requested());
4151        assert!(context.is_cancelled());
4152    }
4153
4154    #[test]
4155    fn request_local_cancelled_future_registers_and_is_woken_without_polling() {
4156        use std::sync::atomic::AtomicBool;
4157
4158        struct WakeFlag(AtomicBool);
4159
4160        impl std::task::Wake for WakeFlag {
4161            fn wake(self: Arc<Self>) {
4162                self.0.store(true, Ordering::Release);
4163            }
4164        }
4165
4166        let cancellation = McpRequestCancellation::new();
4167        let wake_flag = Arc::new(WakeFlag(AtomicBool::new(false)));
4168        let waker = std::task::Waker::from(Arc::clone(&wake_flag));
4169        let mut task_cx = std::task::Context::from_waker(&waker);
4170        let mut future = Box::pin(cancellation.cancelled());
4171
4172        assert!(std::future::Future::poll(future.as_mut(), &mut task_cx).is_pending());
4173        assert!(cancellation.cancel());
4174        assert!(wake_flag.0.load(Ordering::Acquire));
4175        assert!(std::future::Future::poll(future.as_mut(), &mut task_cx).is_ready());
4176    }
4177
4178    #[test]
4179    fn request_local_cancelled_future_observes_preexisting_cancellation() {
4180        let cancellation = McpRequestCancellation::new();
4181        assert!(cancellation.cancel());
4182
4183        let mut future = Box::pin(cancellation.cancelled());
4184        let waker = std::task::Waker::noop();
4185        let mut task_cx = std::task::Context::from_waker(waker);
4186
4187        assert!(std::future::Future::poll(future.as_mut(), &mut task_cx).is_ready());
4188    }
4189
4190    #[test]
4191    fn request_terminal_future_is_woken_when_finalization_wins() {
4192        use std::sync::atomic::AtomicBool;
4193
4194        struct WakeFlag(AtomicBool);
4195
4196        impl std::task::Wake for WakeFlag {
4197            fn wake(self: Arc<Self>) {
4198                self.0.store(true, Ordering::Release);
4199            }
4200        }
4201
4202        let cancellation = McpRequestCancellation::new();
4203        let wake_flag = Arc::new(WakeFlag(AtomicBool::new(false)));
4204        let waker = std::task::Waker::from(Arc::clone(&wake_flag));
4205        let mut task_cx = std::task::Context::from_waker(&waker);
4206        let mut future = Box::pin(cancellation.terminated());
4207
4208        assert!(!cancellation.is_terminal());
4209        assert!(std::future::Future::poll(future.as_mut(), &mut task_cx).is_pending());
4210        assert!(cancellation.begin_finalization());
4211        assert!(cancellation.is_terminal());
4212        assert!(wake_flag.0.load(Ordering::Acquire));
4213        assert!(std::future::Future::poll(future.as_mut(), &mut task_cx).is_ready());
4214    }
4215
4216    #[test]
4217    fn request_local_cancellation_is_deferred_inside_framework_mask() {
4218        let cancellation = McpRequestCancellation::new();
4219        let ctx =
4220            McpContext::new(Cx::for_testing(), 1).with_request_cancellation(cancellation.clone());
4221
4222        let checkpoint = ctx
4223            .masked(|| {
4224                cancellation.cancel();
4225                ctx.checkpoint()
4226            })
4227            .expect("framework mask should be admitted");
4228
4229        assert!(checkpoint.is_ok());
4230        assert!(ctx.ensure_live().is_err());
4231    }
4232
4233    #[test]
4234    fn request_local_cancellation_stops_state_and_capability_effects() {
4235        let state = SessionState::new();
4236        assert!(state.set("existing", 1_u32));
4237        let cancellation = McpRequestCancellation::new();
4238        let ctx = McpContext::with_state(Cx::for_testing(), 1, state.clone())
4239            .with_sampling(Arc::new(NoOpSamplingSender))
4240            .with_elicitation(Arc::new(NoOpElicitationSender))
4241            .with_request_cancellation(cancellation.clone());
4242
4243        assert!(ctx.can_sample());
4244        assert!(ctx.can_elicit());
4245        assert!(cancellation.cancel());
4246
4247        assert!(!ctx.set_state("late", 2_u32));
4248        assert!(ctx.remove_state("existing").is_none());
4249        assert!(!ctx.disable_tool("late-tool"));
4250        assert!(!ctx.disable_resource("late://resource"));
4251        assert!(!ctx.disable_prompt("late-prompt"));
4252        assert!(!ctx.can_sample());
4253        assert!(!ctx.can_elicit());
4254        assert_eq!(state.get::<u32>("existing"), Some(1));
4255        assert!(!state.contains("late"));
4256    }
4257
4258    #[test]
4259    fn admitted_mask_allows_critical_state_commit_before_cancellation_surfaces() {
4260        let state = SessionState::new();
4261        let cancellation = McpRequestCancellation::new();
4262        let ctx = McpContext::with_state(Cx::for_testing(), 1, state.clone())
4263            .with_request_cancellation(cancellation.clone());
4264
4265        let committed = ctx
4266            .masked(|| {
4267                assert!(cancellation.cancel());
4268                ctx.set_state("critical-commit", true)
4269            })
4270            .expect("mask should be admitted before cancellation");
4271
4272        assert!(committed);
4273        assert_eq!(state.get::<bool>("critical-commit"), Some(true));
4274        assert!(ctx.ensure_live().is_err());
4275    }
4276
4277    #[test]
4278    fn active_request_clone_cannot_replace_cancellation_authority() {
4279        let original = McpRequestCancellation::new();
4280        let replacement = McpRequestCancellation::new();
4281        let root =
4282            McpContext::new(Cx::for_testing(), 1).with_request_cancellation(original.clone());
4283        let (scoped, _guard) = root
4284            .begin_request_scope()
4285            .expect("new context should activate one request lease");
4286        let attempted_escape = scoped
4287            .clone()
4288            .with_request_cancellation(replacement.clone());
4289
4290        assert!(original.cancel());
4291        assert!(attempted_escape.ensure_live().is_err());
4292        assert!(!replacement.is_cancel_requested());
4293    }
4294
4295    #[test]
4296    fn request_finalization_and_cancellation_have_one_atomic_winner() {
4297        let cancellation_wins = McpRequestCancellation::new();
4298        assert!(cancellation_wins.cancel());
4299        assert!(!cancellation_wins.begin_finalization());
4300        assert!(cancellation_wins.is_cancel_requested());
4301        assert!(cancellation_wins.is_terminal());
4302
4303        let finalization_wins = McpRequestCancellation::new();
4304        assert!(finalization_wins.begin_finalization());
4305        assert!(finalization_wins.is_finalizing());
4306        assert!(finalization_wins.is_terminal());
4307        assert!(!finalization_wins.cancel());
4308        assert!(!finalization_wins.is_cancel_requested());
4309    }
4310
4311    #[test]
4312    fn test_mcp_context_checkpoint_budget_exhausted() {
4313        let cx = Cx::for_testing_with_budget(Budget::ZERO);
4314        let ctx = McpContext::new(cx, 1);
4315
4316        // Should fail when budget is exhausted
4317        assert!(ctx.checkpoint().is_err());
4318    }
4319
4320    #[test]
4321    fn checkpoint_does_not_treat_zero_cost_as_poll_exhaustion() {
4322        let budget = Budget::new().with_poll_quota(2).with_cost_quota(0);
4323        let cx = Cx::for_testing_with_budget(budget);
4324        let ctx = McpContext::new(cx.clone(), 1);
4325
4326        assert!(ctx.checkpoint().is_ok());
4327        assert!(!cx.is_cancel_requested());
4328        assert_eq!(ctx.budget().cost_quota, Some(0));
4329    }
4330
4331    #[test]
4332    fn closed_request_lease_cannot_be_revived_or_use_framework_capabilities() {
4333        let state = SessionState::new();
4334        let root = McpContext::with_state(Cx::for_testing(), 1, state);
4335        let clone_created_before_scope = root.clone();
4336        let (scoped, guard) = root
4337            .begin_request_scope()
4338            .expect("new context should create one request lease");
4339        let escaped = scoped.clone();
4340        drop(guard);
4341
4342        assert!(escaped.ensure_live().is_err());
4343        assert!(escaped.checkpoint().is_err());
4344        assert!(escaped.consume_cost(0).is_err());
4345        assert!(escaped.masked(|| 42).is_err());
4346        assert!(!escaped.set_auth(AuthContext::with_subject("late")));
4347        assert!(!escaped.set_state("late", true));
4348        assert!(escaped.auth().is_none());
4349        assert!(!escaped.can_call_tools());
4350        assert!(!escaped.can_read_resources());
4351        assert!(clone_created_before_scope.ensure_live().is_err());
4352
4353        assert!(clone_created_before_scope.begin_request_scope().is_none());
4354    }
4355
4356    #[test]
4357    fn test_mcp_context_masked_section() {
4358        let cx = Cx::for_testing();
4359        let ctx = McpContext::new(cx, 1);
4360
4361        // masked() should execute the closure and return its value
4362        let result = ctx.masked(|| 42).expect("mask should be admitted");
4363        assert_eq!(result, 42);
4364    }
4365
4366    #[test]
4367    fn test_mcp_context_budget() {
4368        let cx = Cx::for_testing();
4369        let ctx = McpContext::new(cx, 1);
4370
4371        // Budget should be available
4372        let budget = ctx.budget();
4373        // For testing Cx, budget should not be exhausted
4374        assert!(!budget.is_exhausted());
4375    }
4376
4377    #[test]
4378    fn budget_ceiling_is_monotone_and_visible_to_checkpoints() {
4379        let ambient_deadline = wall_now().saturating_add_nanos(5_000_000_000);
4380        let tighter_deadline = ambient_deadline.saturating_sub_nanos(1_000_000_000);
4381        let later_deadline = ambient_deadline.saturating_add_nanos(1_000_000_000);
4382        let cx = Cx::for_testing_with_budget(Budget::new().with_deadline(ambient_deadline));
4383        let ctx = McpContext::new(cx, 1)
4384            .with_budget_ceiling(Budget::new().with_deadline(tighter_deadline))
4385            .with_budget_ceiling(Budget::new().with_deadline(later_deadline));
4386
4387        assert_eq!(ctx.budget().deadline, Some(tighter_deadline));
4388        assert!(ctx.checkpoint().is_ok());
4389    }
4390
4391    #[test]
4392    fn operation_deadline_tightens_child_without_leaking_to_parent() {
4393        let parent_deadline = wall_now().saturating_add_nanos(5_000_000_000);
4394        let child_deadline = parent_deadline.saturating_sub_nanos(1_000_000_000);
4395        let parent = McpContext::new(Cx::for_testing(), 1)
4396            .with_budget_ceiling(Budget::new().with_deadline(parent_deadline));
4397        let child = parent.clone().with_operation_deadline(Some(child_deadline));
4398        let grandchild = child.clone().with_operation_deadline(None);
4399
4400        assert_eq!(parent.budget().deadline, Some(parent_deadline));
4401        assert_eq!(child.budget().deadline, Some(child_deadline));
4402        assert_eq!(grandchild.budget().deadline, Some(child_deadline));
4403    }
4404
4405    #[test]
4406    fn framework_poll_ceiling_drains_across_clones_at_n_plus_one() {
4407        const LIMIT: u32 = 3;
4408
4409        let ctx = McpContext::new(Cx::for_testing(), 1)
4410            .with_budget_ceiling(Budget::new().with_poll_quota(LIMIT));
4411        let clone = ctx.clone();
4412
4413        for admitted in 0..LIMIT {
4414            let result = if admitted % 2 == 0 {
4415                ctx.checkpoint()
4416            } else {
4417                clone.checkpoint()
4418            };
4419            assert!(result.is_ok(), "checkpoint {} should fit", admitted + 1);
4420            let expected = LIMIT - admitted - 1;
4421            assert_eq!(ctx.budget().poll_quota, expected);
4422            assert_eq!(clone.budget().poll_quota, expected);
4423        }
4424
4425        assert!(clone.checkpoint().is_err(), "checkpoint N+1 must fail");
4426        assert_eq!(ctx.budget().poll_quota, 0);
4427        assert!(!ctx.cx().is_cancel_requested());
4428    }
4429
4430    #[test]
4431    fn ambient_poll_budget_drains_across_clones_without_mutating_cx() {
4432        const LIMIT: u32 = 3;
4433
4434        let cx = Cx::for_testing_with_budget(Budget::new().with_poll_quota(LIMIT));
4435        let ctx = McpContext::new(cx.clone(), 1);
4436        let clone = ctx.clone();
4437
4438        for admitted in 0..LIMIT {
4439            let result = if admitted % 2 == 0 {
4440                ctx.checkpoint()
4441            } else {
4442                clone.checkpoint()
4443            };
4444            assert!(
4445                result.is_ok(),
4446                "ambient checkpoint {} should fit",
4447                admitted + 1
4448            );
4449            assert_eq!(ctx.budget().poll_quota, LIMIT - admitted - 1);
4450        }
4451
4452        let debits_before_rejection = ctx
4453            .budget_state
4454            .lock()
4455            .unwrap_or_else(std::sync::PoisonError::into_inner)
4456            .ambient_poll_debits;
4457        assert!(
4458            clone.checkpoint().is_err(),
4459            "ambient checkpoint N+1 must fail"
4460        );
4461        assert_eq!(
4462            ctx.budget_state
4463                .lock()
4464                .unwrap_or_else(std::sync::PoisonError::into_inner)
4465                .ambient_poll_debits,
4466            debits_before_rejection,
4467            "a rejected checkpoint must not partially debit the ledger"
4468        );
4469        assert_eq!(ctx.budget().poll_quota, 0);
4470        assert_eq!(cx.budget().poll_quota, LIMIT);
4471        assert!(!cx.is_cancel_requested());
4472        assert!(ctx.ensure_live().is_ok());
4473    }
4474
4475    #[test]
4476    fn tighter_ambient_poll_limit_does_not_debit_looser_ceiling_on_rejection() {
4477        let cx = Cx::for_testing_with_budget(Budget::new().with_poll_quota(2));
4478        let ctx =
4479            McpContext::new(cx.clone(), 1).with_budget_ceiling(Budget::new().with_poll_quota(3));
4480
4481        assert!(ctx.checkpoint().is_ok());
4482        assert!(ctx.checkpoint().is_ok());
4483        assert!(ctx.checkpoint().is_err());
4484
4485        let state = *ctx
4486            .budget_state
4487            .lock()
4488            .unwrap_or_else(std::sync::PoisonError::into_inner);
4489        assert_eq!(state.ambient_poll_debits, 2);
4490        assert_eq!(state.ceiling.map(|budget| budget.poll_quota), Some(1));
4491        assert_eq!(cx.budget().poll_quota, 2);
4492    }
4493
4494    #[test]
4495    fn framework_cost_ceiling_drains_across_clones_at_n_plus_one() {
4496        const LIMIT: u64 = 3;
4497
4498        let ctx = McpContext::new(Cx::for_testing(), 1)
4499            .with_budget_ceiling(Budget::new().with_cost_quota(LIMIT));
4500        let clone = ctx.clone();
4501
4502        for admitted in 0..LIMIT {
4503            let result = if admitted % 2 == 0 {
4504                ctx.consume_cost(1)
4505            } else {
4506                clone.consume_cost(1)
4507            };
4508            assert!(result.is_ok(), "cost debit {} should fit", admitted + 1);
4509            let expected = Some(LIMIT - admitted - 1);
4510            assert_eq!(ctx.budget().cost_quota, expected);
4511            assert_eq!(clone.budget().cost_quota, expected);
4512        }
4513
4514        assert!(clone.consume_cost(1).is_err(), "cost debit N+1 must fail");
4515        assert_eq!(ctx.budget().cost_quota, Some(0));
4516        assert!(!ctx.cx().is_cancel_requested());
4517        assert!(
4518            ctx.ensure_live().is_ok(),
4519            "an exactly admitted final debit is not an overrun"
4520        );
4521    }
4522
4523    #[test]
4524    fn framework_poll_and_cost_debits_are_independent() {
4525        let ctx = McpContext::new(Cx::for_testing(), 1)
4526            .with_budget_ceiling(Budget::new().with_poll_quota(2).with_cost_quota(2));
4527
4528        assert!(ctx.checkpoint().is_ok());
4529        assert_eq!(ctx.budget().poll_quota, 1);
4530        assert_eq!(ctx.budget().cost_quota, Some(2));
4531
4532        assert!(ctx.consume_cost(1).is_ok());
4533        assert_eq!(ctx.budget().poll_quota, 1);
4534        assert_eq!(ctx.budget().cost_quota, Some(1));
4535    }
4536
4537    #[test]
4538    fn exact_poll_depletion_is_live_until_the_next_poll_admission() {
4539        let ctx = McpContext::new(Cx::for_testing(), 1)
4540            .with_budget_ceiling(Budget::new().with_poll_quota(1));
4541
4542        assert!(ctx.checkpoint().is_ok());
4543        assert_eq!(ctx.budget().poll_quota, 0);
4544        assert!(ctx.ensure_live().is_ok());
4545        assert!(ctx.checkpoint().is_err());
4546    }
4547
4548    #[test]
4549    fn zero_framework_quotas_fail_without_cancelling_ambient_context() {
4550        let poll_ctx = McpContext::new(Cx::for_testing(), 1)
4551            .with_budget_ceiling(Budget::new().with_poll_quota(0));
4552        let cost_ctx = McpContext::new(Cx::for_testing(), 2)
4553            .with_budget_ceiling(Budget::new().with_cost_quota(0));
4554
4555        assert!(poll_ctx.checkpoint().is_err());
4556        assert_eq!(poll_ctx.budget().poll_quota, 0);
4557        assert!(!poll_ctx.cx().is_cancel_requested());
4558
4559        assert!(cost_ctx.consume_cost(0).is_ok());
4560        assert!(cost_ctx.consume_cost(1).is_err());
4561        assert_eq!(cost_ctx.budget().cost_quota, Some(0));
4562        assert!(!cost_ctx.cx().is_cancel_requested());
4563    }
4564
4565    #[test]
4566    fn oversized_framework_cost_debit_is_atomic() {
4567        let ctx = McpContext::new(Cx::for_testing(), 1)
4568            .with_budget_ceiling(Budget::new().with_cost_quota(2));
4569
4570        assert!(ctx.consume_cost(3).is_err());
4571        assert_eq!(ctx.budget().cost_quota, Some(2));
4572        assert!(ctx.consume_cost(2).is_ok());
4573        assert_eq!(ctx.budget().cost_quota, Some(0));
4574        assert!(ctx.consume_cost(1).is_err());
4575    }
4576
4577    #[test]
4578    fn zero_ambient_cost_quota_prevents_framework_cost_debit() {
4579        let ambient = Budget::new().with_cost_quota(0);
4580        let ctx = McpContext::new(Cx::for_testing_with_budget(ambient), 1)
4581            .with_budget_ceiling(Budget::new().with_cost_quota(3));
4582
4583        assert!(ctx.consume_cost(1).is_err());
4584        assert_eq!(ctx.budget().cost_quota, Some(0));
4585    }
4586
4587    #[test]
4588    fn positive_ambient_cost_quota_drains_cumulatively_across_clones() {
4589        const LIMIT: u64 = 3;
4590        let ambient = Budget::new().with_cost_quota(LIMIT);
4591        let ctx = McpContext::new(Cx::for_testing_with_budget(ambient), 1);
4592        let clone = ctx.clone();
4593
4594        for admitted in 0..LIMIT {
4595            let result = if admitted % 2 == 0 {
4596                ctx.consume_cost(1)
4597            } else {
4598                clone.consume_cost(1)
4599            };
4600            assert!(result.is_ok(), "ambient debit {} should fit", admitted + 1);
4601            assert_eq!(ctx.budget().cost_quota, Some(LIMIT - admitted - 1));
4602        }
4603
4604        assert!(
4605            clone.consume_cost(1).is_err(),
4606            "ambient debit N+1 must fail"
4607        );
4608        assert_eq!(ctx.budget().cost_quota, Some(0));
4609        assert_eq!(
4610            ctx.cx().budget().cost_quota,
4611            Some(LIMIT),
4612            "request-local accounting must not mutate the caller-owned Cx"
4613        );
4614    }
4615
4616    #[test]
4617    fn rejected_cost_debit_does_not_record_an_ambient_checkpoint() {
4618        let cx = Cx::for_testing_with_budget(Budget::new().with_cost_quota(2));
4619        let ctx = McpContext::new(cx, 1);
4620        let before = ctx.cx().checkpoint_state().checkpoint_count;
4621
4622        assert!(ctx.consume_cost(3).is_err());
4623        assert_eq!(ctx.cx().checkpoint_state().checkpoint_count, before);
4624        assert_eq!(ctx.budget().cost_quota, Some(2));
4625    }
4626
4627    #[test]
4628    fn zero_cost_debit_observes_explicit_cancellation() {
4629        let cx = Cx::for_testing();
4630        cx.set_cancel_requested(true);
4631        let ctx = McpContext::new(cx, 1);
4632
4633        assert!(ctx.consume_cost(0).is_err());
4634    }
4635
4636    #[test]
4637    fn expired_request_ceiling_fails_without_cancelling_ambient_context() {
4638        let cx = Cx::for_testing();
4639        let ctx = McpContext::new(cx, 1)
4640            .with_budget_ceiling(Budget::new().with_deadline(asupersync::Time::ZERO));
4641
4642        assert!(ctx.is_cancelled());
4643        assert!(ctx.checkpoint().is_err());
4644        assert!(!ctx.cx().is_cancel_requested());
4645    }
4646
4647    #[test]
4648    fn framework_budget_ceiling_is_deferred_while_masked() {
4649        let ctx = McpContext::new(Cx::for_testing(), 1)
4650            .with_budget_ceiling(Budget::new().with_deadline(asupersync::Time::ZERO));
4651
4652        assert!(
4653            ctx.masked(|| ctx.checkpoint())
4654                .expect("mask should be admitted")
4655                .is_ok()
4656        );
4657        assert!(ctx.checkpoint().is_err());
4658    }
4659
4660    #[test]
4661    fn framework_poll_debits_continue_while_enforcement_is_masked() {
4662        let ctx = McpContext::new(Cx::for_testing(), 1)
4663            .with_budget_ceiling(Budget::new().with_poll_quota(1));
4664
4665        ctx.masked(|| {
4666            assert!(ctx.checkpoint().is_ok());
4667            assert_eq!(ctx.budget().poll_quota, 0);
4668            assert!(ctx.checkpoint().is_ok());
4669        })
4670        .expect("mask should be admitted");
4671
4672        assert!(ctx.checkpoint().is_err());
4673    }
4674
4675    #[test]
4676    fn masked_cost_overage_saturates_framework_ceiling() {
4677        let ctx = McpContext::new(Cx::for_testing(), 1)
4678            .with_budget_ceiling(Budget::new().with_cost_quota(2));
4679
4680        assert!(
4681            ctx.masked(|| ctx.consume_cost(3))
4682                .expect("mask should be admitted")
4683                .is_ok()
4684        );
4685        assert_eq!(ctx.budget().cost_quota, Some(0));
4686        assert!(ctx.ensure_live().is_err());
4687        assert!(ctx.consume_cost(1).is_err());
4688    }
4689
4690    #[test]
4691    fn masked_exact_cost_depletion_does_not_become_a_deferred_overrun() {
4692        let ctx = McpContext::new(Cx::for_testing(), 1)
4693            .with_budget_ceiling(Budget::new().with_cost_quota(2));
4694
4695        assert!(
4696            ctx.masked(|| ctx.consume_cost(2))
4697                .expect("mask should be admitted")
4698                .is_ok()
4699        );
4700        assert_eq!(ctx.budget().cost_quota, Some(0));
4701        assert!(ctx.ensure_live().is_ok());
4702        assert!(ctx.consume_cost(0).is_ok());
4703        assert!(ctx.consume_cost(1).is_err());
4704    }
4705
4706    #[test]
4707    fn masked_cost_overage_saturates_tighter_ambient_quota() {
4708        let ambient = Budget::new().with_cost_quota(2);
4709        let ctx = McpContext::new(Cx::for_testing_with_budget(ambient), 1)
4710            .with_budget_ceiling(Budget::new().with_cost_quota(10));
4711
4712        assert!(
4713            ctx.masked(|| ctx.consume_cost(3))
4714                .expect("mask should be admitted")
4715                .is_ok()
4716        );
4717        assert_eq!(ctx.budget().cost_quota, Some(0));
4718        assert_eq!(
4719            ctx.budget_state
4720                .lock()
4721                .unwrap_or_else(std::sync::PoisonError::into_inner)
4722                .ceiling
4723                .and_then(|budget| budget.cost_quota),
4724            Some(7),
4725            "the looser framework ceiling is still debited independently"
4726        );
4727        assert!(ctx.consume_cost(1).is_err());
4728    }
4729
4730    #[test]
4731    fn framework_mask_is_shared_with_clones_and_restored_after_exit() {
4732        let ctx = McpContext::new(Cx::for_testing(), 1)
4733            .with_budget_ceiling(Budget::new().with_poll_quota(0));
4734        let clone = ctx.clone();
4735
4736        assert!(
4737            ctx.masked(|| clone.checkpoint())
4738                .expect("mask should be admitted")
4739                .is_ok()
4740        );
4741        assert!(clone.checkpoint().is_err());
4742    }
4743
4744    #[test]
4745    fn framework_mask_depth_is_restored_after_unwind() {
4746        let ctx = McpContext::new(Cx::for_testing(), 1)
4747            .with_budget_ceiling(Budget::new().with_deadline(asupersync::Time::ZERO));
4748
4749        let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4750            let _ = ctx.masked(|| panic!("test-only masked-section panic"));
4751        }));
4752
4753        assert!(ctx.checkpoint().is_err());
4754        assert_eq!(ctx.framework_mask_depth.load(Ordering::SeqCst), 0);
4755    }
4756
4757    #[test]
4758    fn test_cancelled_error_display() {
4759        let err = CancelledError;
4760        assert_eq!(err.to_string(), "request cancelled");
4761    }
4762
4763    #[test]
4764    fn handler_log_respects_client_floor_and_missing_floor() {
4765        let captured = Arc::new(Mutex::new(Vec::new()));
4766        struct CaptureSender(Arc<Mutex<Vec<(McpLogLevel, String)>>>);
4767        impl NotificationSender for CaptureSender {
4768            fn send_progress(&self, _progress: f64, _total: Option<f64>, _message: Option<&str>) {}
4769            fn send_log(&self, level: McpLogLevel, _logger: Option<&str>, data: serde_json::Value) {
4770                self.0
4771                    .lock()
4772                    .unwrap_or_else(std::sync::PoisonError::into_inner)
4773                    .push((level, data.as_str().unwrap_or_default().to_owned()));
4774            }
4775        }
4776
4777        let silent = McpContext::new(Cx::for_testing(), 1)
4778            .with_log_sender(Arc::new(CaptureSender(Arc::clone(&captured))));
4779        silent.info("before-floor");
4780        assert!(captured.lock().expect("lock").is_empty());
4781
4782        let ctx = silent.with_min_log_level(Some(McpLogLevel::Info));
4783        assert_eq!(ctx.min_log_level(), Some(McpLogLevel::Info));
4784        ctx.debug("too-low");
4785        ctx.info("admitted");
4786        ctx.warning("also-admitted");
4787        let emitted = captured.lock().expect("lock").clone();
4788        assert_eq!(
4789            emitted,
4790            vec![
4791                (McpLogLevel::Info, "admitted".to_owned()),
4792                (McpLogLevel::Warning, "also-admitted".to_owned()),
4793            ]
4794        );
4795    }
4796
4797    #[test]
4798    fn catalog_change_emits_only_when_the_disabled_set_mutates() {
4799        let captured = Arc::new(Mutex::new(Vec::new()));
4800        struct CaptureSender(Arc<Mutex<Vec<McpCatalogKind>>>);
4801        impl NotificationSender for CaptureSender {
4802            fn send_progress(&self, _progress: f64, _total: Option<f64>, _message: Option<&str>) {}
4803            fn send_catalog_changed(&self, kind: McpCatalogKind) {
4804                self.0
4805                    .lock()
4806                    .unwrap_or_else(std::sync::PoisonError::into_inner)
4807                    .push(kind);
4808            }
4809        }
4810
4811        let ctx = McpContext::with_state(Cx::for_testing(), 1, SessionState::new())
4812            .with_log_sender(Arc::new(CaptureSender(Arc::clone(&captured))));
4813        assert!(ctx.disable_tool("admin"));
4814        assert!(ctx.disable_tool("admin"));
4815        assert!(ctx.enable_tool("admin"));
4816        assert!(ctx.enable_tool("admin"));
4817        assert!(ctx.disable_resource("file://secret"));
4818        assert!(ctx.disable_prompt("hidden"));
4819        assert_eq!(
4820            *captured.lock().expect("lock"),
4821            vec![
4822                McpCatalogKind::Tools,
4823                McpCatalogKind::Tools,
4824                McpCatalogKind::Resources,
4825                McpCatalogKind::Prompts,
4826            ]
4827        );
4828    }
4829
4830    #[test]
4831    fn catalog_publisher_receives_mutations_even_without_a_session_sender() {
4832        let captured = Arc::new(Mutex::new(Vec::new()));
4833        struct CapturePublisher(Arc<Mutex<Vec<McpCatalogKind>>>);
4834        impl CatalogChangePublisher for CapturePublisher {
4835            fn publish_catalog_changed(&self, kind: McpCatalogKind) -> bool {
4836                self.0
4837                    .lock()
4838                    .unwrap_or_else(std::sync::PoisonError::into_inner)
4839                    .push(kind);
4840                true
4841            }
4842            fn publish_resource_updated(&self, _uri: &str) -> bool {
4843                false
4844            }
4845        }
4846
4847        let ctx = McpContext::with_state(Cx::for_testing(), 1, SessionState::new())
4848            .with_catalog_publisher(Arc::new(CapturePublisher(Arc::clone(&captured))));
4849        assert!(ctx.disable_tool("admin"));
4850        assert!(ctx.disable_tool("admin"));
4851        assert_eq!(*captured.lock().expect("lock"), vec![McpCatalogKind::Tools]);
4852    }
4853
4854    #[test]
4855    fn notify_resource_updated_requires_a_live_subscription() {
4856        let captured = Arc::new(Mutex::new(Vec::new()));
4857        struct CaptureSender(Arc<Mutex<Vec<String>>>);
4858        impl NotificationSender for CaptureSender {
4859            fn send_progress(&self, _progress: f64, _total: Option<f64>, _message: Option<&str>) {}
4860            fn send_resource_updated(&self, uri: &str) {
4861                self.0
4862                    .lock()
4863                    .unwrap_or_else(std::sync::PoisonError::into_inner)
4864                    .push(uri.to_owned());
4865            }
4866        }
4867
4868        let ctx = McpContext::new(Cx::for_testing(), 1)
4869            .with_log_sender(Arc::new(CaptureSender(Arc::clone(&captured))))
4870            .with_resource_subscriptions(["file:///watched.txt"]);
4871        assert!(!ctx.notify_resource_updated("file:///other.txt"));
4872        assert!(ctx.notify_resource_updated("file:///watched.txt"));
4873        assert_eq!(
4874            *captured.lock().expect("lock"),
4875            vec!["file:///watched.txt".to_owned()]
4876        );
4877    }
4878
4879    #[test]
4880    fn test_into_outcome_ok() {
4881        let result: Result<i32, CancelledError> = Ok(42);
4882        let outcome: Outcome<i32, CancelledError> = result.into_outcome();
4883        assert!(matches!(outcome, Outcome::Ok(42)));
4884    }
4885
4886    #[test]
4887    fn test_into_outcome_cancelled() {
4888        let result: Result<i32, CancelledError> = Err(CancelledError);
4889        let outcome: Outcome<i32, ()> = result.into_outcome();
4890        assert!(matches!(outcome, Outcome::Cancelled(_)));
4891    }
4892
4893    #[test]
4894    fn test_mcp_context_no_progress_reporter_by_default() {
4895        let cx = Cx::for_testing();
4896        let ctx = McpContext::new(cx, 1);
4897        assert!(!ctx.has_progress_reporter());
4898    }
4899
4900    #[test]
4901    fn test_mcp_context_with_progress_reporter() {
4902        let cx = Cx::for_testing();
4903        let sender = Arc::new(NoOpNotificationSender);
4904        let reporter = ProgressReporter::new(sender);
4905        let ctx = McpContext::with_progress(cx, 1, reporter);
4906        assert!(ctx.has_progress_reporter());
4907    }
4908
4909    #[test]
4910    fn progress_reporter_builder_preserves_request_accounting_domain() {
4911        let ctx = McpContext::new(Cx::for_testing(), 1)
4912            .with_budget_ceiling(Budget::new().with_cost_quota(5));
4913        let reporter = ProgressReporter::new(Arc::new(NoOpNotificationSender));
4914        let derived = ctx.clone().with_progress_reporter(reporter);
4915
4916        assert!(derived.has_progress_reporter());
4917        assert!(!ctx.has_progress_reporter());
4918        assert!(ctx.consume_cost(3).is_ok());
4919        assert_eq!(derived.budget().cost_quota, Some(2));
4920    }
4921
4922    #[test]
4923    fn isolated_auth_stages_identity_without_handler_capabilities() {
4924        let root = McpContext::with_state(Cx::for_testing(), 1, SessionState::new())
4925            .with_budget_ceiling(Budget::new().with_cost_quota(2))
4926            .with_sampling(Arc::new(NoOpSamplingSender))
4927            .with_elicitation(Arc::new(NoOpElicitationSender))
4928            .with_roots_provider(Arc::new(FixedRootsProvider));
4929        let staged = root.clone().with_isolated_auth();
4930
4931        assert!(staged.auth().is_none());
4932        assert!(!staged.has_session_state());
4933        assert!(!staged.can_sample());
4934        assert!(!staged.can_elicit());
4935        assert!(!staged.can_list_roots());
4936        assert!(!staged.can_read_resources());
4937        assert!(!staged.can_call_tools());
4938        assert!(staged.set_auth(AuthContext::with_subject("tentative")));
4939        assert_eq!(
4940            staged.auth().and_then(|auth| auth.subject),
4941            Some("tentative".to_string())
4942        );
4943        assert_eq!(root.auth().and_then(|auth| auth.subject), None);
4944
4945        assert!(root.set_auth(AuthContext::with_subject("committed")));
4946        let attempted_reisolation = root.clone().with_isolated_auth();
4947        assert_eq!(
4948            attempted_reisolation.auth().and_then(|auth| auth.subject),
4949            Some("committed".to_string())
4950        );
4951
4952        assert!(staged.consume_cost(1).is_ok());
4953        assert_eq!(root.budget().cost_quota, Some(1));
4954    }
4955
4956    #[test]
4957    fn committed_anonymous_auth_is_hidden_and_write_once() {
4958        let ctx = McpContext::with_state(Cx::for_testing(), 1, SessionState::new());
4959
4960        assert!(ctx.commit_anonymous_auth());
4961        assert!(ctx.auth().is_none());
4962        assert!(matches!(ctx.cache_auth_partition(), Some(None)));
4963        assert!(!ctx.set_auth(AuthContext::with_subject("forged")));
4964        assert!(!ctx.commit_anonymous_auth());
4965
4966        let clone = ctx.clone();
4967        assert!(clone.auth().is_none());
4968        assert!(matches!(clone.cache_auth_partition(), Some(None)));
4969    }
4970
4971    #[test]
4972    fn authenticated_cache_partition_contains_committed_facts() {
4973        let ctx = McpContext::new(Cx::for_testing(), 1);
4974        assert!(ctx.set_auth(AuthContext::with_subject("alice")));
4975
4976        let Some(Some(auth)) = ctx.cache_auth_partition() else {
4977            panic!("authenticated admission must expose cache partition facts");
4978        };
4979        assert_eq!(auth.subject.as_deref(), Some("alice"));
4980    }
4981
4982    #[test]
4983    fn test_report_progress_without_reporter() {
4984        let cx = Cx::for_testing();
4985        let ctx = McpContext::new(cx, 1);
4986        // Should not panic when no reporter is set
4987        ctx.report_progress(0.5, Some("test"));
4988        ctx.report_progress_with_total(5.0, 10.0, None);
4989    }
4990
4991    #[test]
4992    fn test_report_progress_with_reporter() {
4993        use std::sync::atomic::{AtomicU32, Ordering};
4994
4995        struct CountingSender {
4996            count: AtomicU32,
4997        }
4998
4999        impl NotificationSender for CountingSender {
5000            fn send_progress(&self, _progress: f64, _total: Option<f64>, _message: Option<&str>) {
5001                self.count.fetch_add(1, Ordering::SeqCst);
5002            }
5003        }
5004
5005        let cx = Cx::for_testing();
5006        let sender = Arc::new(CountingSender {
5007            count: AtomicU32::new(0),
5008        });
5009        let reporter = ProgressReporter::new(sender.clone());
5010        let ctx = McpContext::with_progress(cx, 1, reporter);
5011
5012        ctx.report_progress(0.25, Some("step 1"));
5013        ctx.report_progress(0.5, None);
5014        ctx.report_progress_with_total(3.0, 4.0, Some("step 3"));
5015
5016        assert_eq!(sender.count.load(Ordering::SeqCst), 3);
5017    }
5018
5019    #[test]
5020    fn request_local_cancellation_suppresses_subsequent_progress() {
5021        use std::sync::atomic::{AtomicU32, Ordering};
5022
5023        struct CountingSender {
5024            count: AtomicU32,
5025        }
5026
5027        impl NotificationSender for CountingSender {
5028            fn send_progress(&self, _progress: f64, _total: Option<f64>, _message: Option<&str>) {
5029                self.count.fetch_add(1, Ordering::SeqCst);
5030            }
5031        }
5032
5033        let sender = Arc::new(CountingSender {
5034            count: AtomicU32::new(0),
5035        });
5036        let cancellation = McpRequestCancellation::new();
5037        let ctx =
5038            McpContext::with_progress(Cx::for_testing(), 1, ProgressReporter::new(sender.clone()))
5039                .with_request_cancellation(cancellation.clone());
5040
5041        ctx.report_progress(0.25, Some("before cancellation"));
5042        assert!(cancellation.cancel());
5043        ctx.report_progress(0.5, Some("after cancellation"));
5044
5045        assert_eq!(sender.count.load(Ordering::SeqCst), 1);
5046        assert!(!ctx.has_progress_reporter());
5047    }
5048
5049    #[test]
5050    fn test_progress_reporter_debug() {
5051        let sender = Arc::new(NoOpNotificationSender);
5052        let reporter = ProgressReporter::new(sender);
5053        let debug = format!("{reporter:?}");
5054        assert!(debug.contains("ProgressReporter"));
5055    }
5056
5057    #[test]
5058    fn test_noop_notification_sender() {
5059        let sender = NoOpNotificationSender;
5060        // Should not panic
5061        sender.send_progress(0.5, Some(1.0), Some("test"));
5062    }
5063
5064    // Session state tests
5065    #[test]
5066    fn test_mcp_context_no_session_state_by_default() {
5067        let cx = Cx::for_testing();
5068        let ctx = McpContext::new(cx, 1);
5069        assert!(!ctx.has_session_state());
5070    }
5071
5072    #[test]
5073    fn test_mcp_context_with_session_state() {
5074        let cx = Cx::for_testing();
5075        let state = SessionState::new();
5076        let ctx = McpContext::with_state(cx, 1, state);
5077        assert!(ctx.has_session_state());
5078    }
5079
5080    #[test]
5081    fn cache_admission_fails_if_session_state_changes_before_completion() {
5082        let state = SessionState::new();
5083        let ctx = McpContext::with_state(Cx::for_testing(), 1, state.clone());
5084        let admitted = ctx
5085            .begin_session_cache_partition()
5086            .expect("test platform must provide cache-partition entropy");
5087        assert_eq!(ctx.complete_session_cache_partition(), Some(admitted));
5088
5089        assert!(state.set("changed", true));
5090        assert!(ctx.complete_session_cache_partition().is_none());
5091        assert!(ctx.begin_session_cache_partition().is_none());
5092    }
5093
5094    #[test]
5095    fn response_cache_hit_markers_are_middleware_specific() {
5096        let ctx = McpContext::new(Cx::for_testing(), 1);
5097        assert!(ctx.mark_response_cache_hit(10));
5098        assert!(ctx.response_was_cache_hit(10));
5099        assert!(!ctx.response_was_cache_hit(11));
5100        assert!(!ctx.mark_response_cache_hit(0));
5101    }
5102
5103    #[test]
5104    fn test_mcp_context_get_set_state() {
5105        let cx = Cx::for_testing();
5106        let state = SessionState::new();
5107        let ctx = McpContext::with_state(cx, 1, state);
5108
5109        // Set a value
5110        assert!(ctx.set_state("counter", 42));
5111
5112        // Get the value back
5113        let value: Option<i32> = ctx.get_state("counter");
5114        assert_eq!(value, Some(42));
5115    }
5116
5117    #[test]
5118    fn test_mcp_context_state_not_available() {
5119        let cx = Cx::for_testing();
5120        let ctx = McpContext::new(cx, 1);
5121
5122        // set_state returns false when state is not available
5123        assert!(!ctx.set_state("key", "value"));
5124
5125        // get_state returns None when state is not available
5126        let value: Option<String> = ctx.get_state("key");
5127        assert!(value.is_none());
5128    }
5129
5130    #[test]
5131    fn test_mcp_context_has_state() {
5132        let cx = Cx::for_testing();
5133        let state = SessionState::new();
5134        let ctx = McpContext::with_state(cx, 1, state);
5135
5136        assert!(!ctx.has_state("missing"));
5137
5138        ctx.set_state("present", true);
5139        assert!(ctx.has_state("present"));
5140    }
5141
5142    #[test]
5143    fn test_mcp_context_remove_state() {
5144        let cx = Cx::for_testing();
5145        let state = SessionState::new();
5146        let ctx = McpContext::with_state(cx, 1, state);
5147
5148        ctx.set_state("key", "value");
5149        assert!(ctx.has_state("key"));
5150
5151        let removed = ctx.remove_state("key");
5152        assert!(removed.is_some());
5153        assert!(!ctx.has_state("key"));
5154    }
5155
5156    #[test]
5157    fn test_mcp_context_with_state_and_progress() {
5158        let cx = Cx::for_testing();
5159        let state = SessionState::new();
5160        let sender = Arc::new(NoOpNotificationSender);
5161        let reporter = ProgressReporter::new(sender);
5162
5163        let ctx = McpContext::with_state_and_progress(cx, 1, state, reporter);
5164
5165        assert!(ctx.has_session_state());
5166        assert!(ctx.has_progress_reporter());
5167    }
5168
5169    #[test]
5170    fn test_mcp_context_auth_is_request_local() {
5171        let cx = Cx::for_testing();
5172        let state = SessionState::new();
5173        let ctx = McpContext::with_state(cx, 1, state.clone());
5174
5175        assert!(ctx.set_auth(AuthContext::with_subject("alice")));
5176
5177        assert_eq!(
5178            ctx.auth().and_then(|auth| auth.subject),
5179            Some("alice".to_string())
5180        );
5181        assert!(
5182            state.is_empty(),
5183            "request auth must not be persisted into session state"
5184        );
5185    }
5186
5187    #[test]
5188    fn test_mcp_context_clones_share_request_auth() {
5189        let cx = Cx::for_testing();
5190        let ctx = McpContext::new(cx, 1);
5191        let cloned = ctx.clone();
5192
5193        assert!(cloned.set_auth(AuthContext::with_subject("bob")));
5194
5195        assert_eq!(
5196            ctx.auth().and_then(|auth| auth.subject),
5197            Some("bob".to_string())
5198        );
5199    }
5200
5201    #[test]
5202    fn committed_request_auth_is_write_once_across_clones() {
5203        let ctx =
5204            McpContext::new(Cx::for_testing(), 1).with_auth(AuthContext::with_subject("verified"));
5205        let clone = ctx.clone();
5206
5207        assert!(!clone.set_auth(AuthContext::with_subject("replacement")));
5208        assert_eq!(
5209            ctx.auth().and_then(|auth| auth.subject),
5210            Some("verified".to_string())
5211        );
5212    }
5213
5214    #[test]
5215    fn test_new_mcp_contexts_do_not_share_request_auth_even_with_same_cx() {
5216        let cx = Cx::for_testing();
5217        let state = SessionState::new();
5218        let first = McpContext::with_state(cx.clone(), 7, state.clone());
5219        let second = McpContext::with_state(cx, 7, state);
5220
5221        assert!(first.set_auth(AuthContext::with_subject("carol")));
5222
5223        assert!(second.auth().is_none());
5224    }
5225
5226    #[test]
5227    fn test_new_mcp_contexts_do_not_share_request_auth_across_requests() {
5228        let state = SessionState::new();
5229        let first = McpContext::with_state(Cx::for_testing(), 7, state.clone());
5230        let second = McpContext::with_state(Cx::for_testing(), 8, state);
5231
5232        assert!(first.set_auth(AuthContext::with_subject("dave")));
5233
5234        assert_eq!(
5235            first.auth().and_then(|auth| auth.subject),
5236            Some("dave".to_string())
5237        );
5238        assert!(second.auth().is_none());
5239    }
5240
5241    #[test]
5242    fn test_mcp_context_drop_does_not_leak_request_auth() {
5243        let cx = Cx::for_testing();
5244
5245        {
5246            let ctx = McpContext::new(cx.clone(), 9);
5247            assert!(ctx.set_auth(AuthContext::with_subject("erin")));
5248        }
5249
5250        assert!(
5251            McpContext::new(cx, 9).auth().is_none(),
5252            "fresh contexts must start without inherited request auth"
5253        );
5254    }
5255
5256    // ========================================================================
5257    // Dynamic Enable/Disable Tests
5258    // ========================================================================
5259
5260    #[test]
5261    fn test_mcp_context_tools_enabled_by_default() {
5262        let cx = Cx::for_testing();
5263        let state = SessionState::new();
5264        let ctx = McpContext::with_state(cx, 1, state);
5265
5266        assert!(ctx.is_tool_enabled("any_tool"));
5267        assert!(ctx.is_tool_enabled("another_tool"));
5268    }
5269
5270    #[test]
5271    fn test_mcp_context_disable_enable_tool() {
5272        let cx = Cx::for_testing();
5273        let state = SessionState::new();
5274        let ctx = McpContext::with_state(cx, 1, state);
5275
5276        // Tool is enabled by default
5277        assert!(ctx.is_tool_enabled("my_tool"));
5278
5279        // Disable the tool
5280        assert!(ctx.disable_tool("my_tool"));
5281        assert!(!ctx.is_tool_enabled("my_tool"));
5282        assert!(ctx.is_tool_enabled("other_tool"));
5283
5284        // Re-enable the tool
5285        assert!(ctx.enable_tool("my_tool"));
5286        assert!(ctx.is_tool_enabled("my_tool"));
5287    }
5288
5289    #[test]
5290    fn test_mcp_context_disable_enable_resource() {
5291        let cx = Cx::for_testing();
5292        let state = SessionState::new();
5293        let ctx = McpContext::with_state(cx, 1, state);
5294
5295        // Resource is enabled by default
5296        assert!(ctx.is_resource_enabled("file://secret"));
5297
5298        // Disable the resource
5299        assert!(ctx.disable_resource("file://secret"));
5300        assert!(!ctx.is_resource_enabled("file://secret"));
5301        assert!(ctx.is_resource_enabled("file://public"));
5302
5303        // Re-enable the resource
5304        assert!(ctx.enable_resource("file://secret"));
5305        assert!(ctx.is_resource_enabled("file://secret"));
5306    }
5307
5308    #[test]
5309    fn test_mcp_context_disable_enable_prompt() {
5310        let cx = Cx::for_testing();
5311        let state = SessionState::new();
5312        let ctx = McpContext::with_state(cx, 1, state);
5313
5314        // Prompt is enabled by default
5315        assert!(ctx.is_prompt_enabled("admin_prompt"));
5316
5317        // Disable the prompt
5318        assert!(ctx.disable_prompt("admin_prompt"));
5319        assert!(!ctx.is_prompt_enabled("admin_prompt"));
5320        assert!(ctx.is_prompt_enabled("user_prompt"));
5321
5322        // Re-enable the prompt
5323        assert!(ctx.enable_prompt("admin_prompt"));
5324        assert!(ctx.is_prompt_enabled("admin_prompt"));
5325    }
5326
5327    #[test]
5328    fn test_mcp_context_disable_multiple_tools() {
5329        let cx = Cx::for_testing();
5330        let state = SessionState::new();
5331        let ctx = McpContext::with_state(cx, 1, state);
5332
5333        ctx.disable_tool("tool1");
5334        ctx.disable_tool("tool2");
5335        ctx.disable_tool("tool3");
5336
5337        assert!(!ctx.is_tool_enabled("tool1"));
5338        assert!(!ctx.is_tool_enabled("tool2"));
5339        assert!(!ctx.is_tool_enabled("tool3"));
5340        assert!(ctx.is_tool_enabled("tool4"));
5341
5342        let disabled = ctx.disabled_tools();
5343        assert_eq!(disabled.len(), 3);
5344        assert!(disabled.contains("tool1"));
5345        assert!(disabled.contains("tool2"));
5346        assert!(disabled.contains("tool3"));
5347    }
5348
5349    #[test]
5350    fn test_mcp_context_disabled_sets_empty_by_default() {
5351        let cx = Cx::for_testing();
5352        let state = SessionState::new();
5353        let ctx = McpContext::with_state(cx, 1, state);
5354
5355        assert!(ctx.disabled_tools().is_empty());
5356        assert!(ctx.disabled_resources().is_empty());
5357        assert!(ctx.disabled_prompts().is_empty());
5358    }
5359
5360    #[test]
5361    fn test_mcp_context_enable_disable_no_state() {
5362        let cx = Cx::for_testing();
5363        let ctx = McpContext::new(cx, 1);
5364
5365        // Without session state, disable returns false
5366        assert!(!ctx.disable_tool("tool"));
5367        assert!(!ctx.enable_tool("tool"));
5368
5369        // But is_enabled returns true (default is enabled)
5370        assert!(ctx.is_tool_enabled("tool"));
5371    }
5372
5373    #[test]
5374    fn test_mcp_context_disabled_state_persists_across_contexts() {
5375        let state = SessionState::new();
5376
5377        // First context disables a tool
5378        {
5379            let cx = Cx::for_testing();
5380            let ctx = McpContext::with_state(cx, 1, state.clone());
5381            ctx.disable_tool("shared_tool");
5382        }
5383
5384        // Second context (same session state) sees the disabled tool
5385        {
5386            let cx = Cx::for_testing();
5387            let ctx = McpContext::with_state(cx, 2, state.clone());
5388            assert!(!ctx.is_tool_enabled("shared_tool"));
5389        }
5390    }
5391
5392    // ========================================================================
5393    // Capabilities Tests
5394    // ========================================================================
5395
5396    #[test]
5397    fn test_mcp_context_no_capabilities_by_default() {
5398        let cx = Cx::for_testing();
5399        let ctx = McpContext::new(cx, 1);
5400
5401        assert!(ctx.client_capabilities().is_none());
5402        assert!(ctx.server_capabilities().is_none());
5403        assert!(!ctx.client_supports_sampling());
5404        assert!(!ctx.client_supports_elicitation());
5405        assert!(!ctx.client_supports_roots());
5406    }
5407
5408    #[test]
5409    fn test_mcp_context_with_client_capabilities() {
5410        let cx = Cx::for_testing();
5411        let caps = ClientCapabilityInfo::new()
5412            .with_sampling()
5413            .with_elicitation(true, false)
5414            .with_roots(true);
5415
5416        let ctx = McpContext::new(cx, 1).with_client_capabilities(caps);
5417
5418        assert!(ctx.client_capabilities().is_some());
5419        assert!(ctx.client_supports_sampling());
5420        assert!(ctx.client_supports_elicitation());
5421        assert!(ctx.client_supports_elicitation_form());
5422        assert!(!ctx.client_supports_elicitation_url());
5423        assert!(ctx.client_supports_roots());
5424    }
5425
5426    #[test]
5427    fn test_mcp_context_with_client_implementation() {
5428        let cx = Cx::for_testing();
5429        let mut identity = ClientImplementationInfo::new("e2e-client", "1.0.0");
5430        identity.title = Some("Client Title".to_owned());
5431        let ctx = McpContext::new(cx, 1).with_client_implementation(identity);
5432        let observed = ctx
5433            .client_implementation()
5434            .expect("the attached identity must be retained");
5435        assert_eq!(observed.name, "e2e-client");
5436        assert_eq!(observed.title.as_deref(), Some("Client Title"));
5437        assert!(observed.has_extras());
5438        let bare = McpContext::new(Cx::for_testing(), 2);
5439        assert!(bare.client_implementation().is_none());
5440    }
5441
5442    #[test]
5443    fn test_mcp_context_with_server_capabilities() {
5444        let cx = Cx::for_testing();
5445        let caps = ServerCapabilityInfo::new()
5446            .with_tools()
5447            .with_resources(true)
5448            .with_prompts()
5449            .with_logging();
5450
5451        let ctx = McpContext::new(cx, 1).with_server_capabilities(caps);
5452
5453        let server_caps = ctx.server_capabilities().unwrap();
5454        assert!(server_caps.tools);
5455        assert!(server_caps.resources);
5456        assert!(server_caps.resources_subscribe);
5457        assert!(server_caps.prompts);
5458        assert!(server_caps.logging);
5459    }
5460
5461    #[test]
5462    fn test_client_capability_info_builders() {
5463        let caps = ClientCapabilityInfo::new();
5464        assert!(!caps.sampling);
5465        assert!(!caps.elicitation);
5466        assert!(!caps.roots);
5467
5468        let caps = caps.with_sampling();
5469        assert!(caps.sampling);
5470
5471        let caps = ClientCapabilityInfo::new().with_elicitation(true, true);
5472        assert!(caps.elicitation);
5473        assert!(caps.elicitation_form);
5474        assert!(caps.elicitation_url);
5475
5476        let caps = ClientCapabilityInfo::new().with_roots(false);
5477        assert!(caps.roots);
5478        assert!(!caps.roots_list_changed);
5479    }
5480
5481    #[test]
5482    fn test_server_capability_info_builders() {
5483        let caps = ServerCapabilityInfo::new();
5484        assert!(!caps.tools);
5485        assert!(!caps.resources);
5486        assert!(!caps.prompts);
5487        assert!(!caps.logging);
5488
5489        let caps = caps
5490            .with_tools()
5491            .with_resources(false)
5492            .with_prompts()
5493            .with_logging();
5494        assert!(caps.tools);
5495        assert!(caps.resources);
5496        assert!(!caps.resources_subscribe);
5497        assert!(caps.prompts);
5498        assert!(caps.logging);
5499    }
5500
5501    // ========================================================================
5502    // ResourceContentItem Tests
5503    // ========================================================================
5504
5505    #[test]
5506    fn test_resource_content_item_text() {
5507        let item = ResourceContentItem::text("test://uri", "hello");
5508        assert_eq!(item.uri, "test://uri");
5509        assert_eq!(item.mime_type.as_deref(), Some("text/plain"));
5510        assert_eq!(item.as_text(), Some("hello"));
5511        assert!(item.as_blob().is_none());
5512        assert!(item.is_text());
5513        assert!(!item.is_blob());
5514    }
5515
5516    #[test]
5517    fn test_resource_content_item_json() {
5518        let item = ResourceContentItem::json("data://config", r#"{"key":"val"}"#);
5519        assert_eq!(item.uri, "data://config");
5520        assert_eq!(item.mime_type.as_deref(), Some("application/json"));
5521        assert_eq!(item.as_text(), Some(r#"{"key":"val"}"#));
5522        assert!(item.is_text());
5523        assert!(!item.is_blob());
5524    }
5525
5526    #[test]
5527    fn test_resource_content_item_blob() {
5528        let item = ResourceContentItem::blob("binary://data", "application/octet-stream", "AQID");
5529        assert_eq!(item.uri, "binary://data");
5530        assert_eq!(item.mime_type.as_deref(), Some("application/octet-stream"));
5531        assert!(item.as_text().is_none());
5532        assert_eq!(item.as_blob(), Some("AQID"));
5533        assert!(!item.is_text());
5534        assert!(item.is_blob());
5535    }
5536
5537    // ========================================================================
5538    // ResourceReadResult Tests
5539    // ========================================================================
5540
5541    #[test]
5542    fn test_resource_read_result_text() {
5543        let result = ResourceReadResult::text("test://doc", "content");
5544        assert_eq!(result.first_text(), Some("content"));
5545        assert!(result.first_blob().is_none());
5546        assert_eq!(result.contents.len(), 1);
5547    }
5548
5549    #[test]
5550    fn test_resource_read_result_new_multiple() {
5551        let result = ResourceReadResult::new(vec![
5552            ResourceContentItem::text("a://1", "first"),
5553            ResourceContentItem::blob("b://2", "image/png", "base64data"),
5554        ]);
5555        assert_eq!(result.contents.len(), 2);
5556        // first_text returns the first item's text
5557        assert_eq!(result.first_text(), Some("first"));
5558        // first_blob returns None because the first item is text
5559        assert!(result.first_blob().is_none());
5560    }
5561
5562    #[test]
5563    fn test_resource_read_result_empty() {
5564        let result = ResourceReadResult::new(vec![]);
5565        assert!(result.first_text().is_none());
5566        assert!(result.first_blob().is_none());
5567    }
5568
5569    #[test]
5570    fn test_resource_read_result_blob_first() {
5571        let result = ResourceReadResult::new(vec![ResourceContentItem::blob(
5572            "b://1",
5573            "image/png",
5574            "data",
5575        )]);
5576        assert!(result.first_text().is_none());
5577        assert_eq!(result.first_blob(), Some("data"));
5578    }
5579
5580    // ========================================================================
5581    // ToolContentItem Tests
5582    // ========================================================================
5583
5584    #[test]
5585    fn test_tool_content_item_text() {
5586        let item = ToolContentItem::text("hello");
5587        assert_eq!(item.as_text(), Some("hello"));
5588        assert!(item.is_text());
5589    }
5590
5591    #[test]
5592    fn test_tool_content_item_image() {
5593        let item = ToolContentItem::Image {
5594            data: "base64img".to_string(),
5595            mime_type: "image/png".to_string(),
5596        };
5597        assert!(item.as_text().is_none());
5598        assert!(!item.is_text());
5599    }
5600
5601    #[test]
5602    fn test_tool_content_item_audio() {
5603        let item = ToolContentItem::Audio {
5604            data: "base64audio".to_string(),
5605            mime_type: "audio/wav".to_string(),
5606        };
5607        assert!(item.as_text().is_none());
5608        assert!(!item.is_text());
5609    }
5610
5611    #[test]
5612    fn test_tool_content_item_resource() {
5613        let item = ToolContentItem::Resource {
5614            uri: "file://test".to_string(),
5615            mime_type: Some("text/plain".to_string()),
5616            text: Some("embedded".to_string()),
5617            blob: None,
5618        };
5619        assert!(item.as_text().is_none());
5620        assert!(!item.is_text());
5621    }
5622
5623    // ========================================================================
5624    // ToolCallResult Tests
5625    // ========================================================================
5626
5627    #[test]
5628    fn test_tool_call_result_success() {
5629        let result = ToolCallResult::success(vec![
5630            ToolContentItem::text("item1"),
5631            ToolContentItem::text("item2"),
5632        ]);
5633        assert!(!result.is_error);
5634        assert_eq!(result.content.len(), 2);
5635        assert_eq!(result.first_text(), Some("item1"));
5636    }
5637
5638    #[test]
5639    fn test_tool_call_result_text() {
5640        let result = ToolCallResult::text("simple output");
5641        assert!(!result.is_error);
5642        assert_eq!(result.content.len(), 1);
5643        assert_eq!(result.first_text(), Some("simple output"));
5644    }
5645
5646    #[test]
5647    fn test_tool_call_result_error() {
5648        let result = ToolCallResult::error("something failed");
5649        assert!(result.is_error);
5650        assert_eq!(result.first_text(), Some("something failed"));
5651    }
5652
5653    #[test]
5654    fn test_tool_call_result_empty() {
5655        let result = ToolCallResult::success(vec![]);
5656        assert!(!result.is_error);
5657        assert!(result.first_text().is_none());
5658    }
5659
5660    // ========================================================================
5661    // ElicitationResponse Tests
5662    // ========================================================================
5663
5664    #[test]
5665    fn test_elicitation_response_accept() {
5666        let mut data = std::collections::HashMap::new();
5667        data.insert("name".to_string(), serde_json::json!("Alice"));
5668        data.insert("age".to_string(), serde_json::json!(30));
5669        data.insert("active".to_string(), serde_json::json!(true));
5670
5671        let resp = ElicitationResponse::accept(data);
5672        assert!(resp.is_accepted());
5673        assert!(!resp.is_declined());
5674        assert!(!resp.is_cancelled());
5675        assert_eq!(resp.get_string("name"), Some("Alice"));
5676        assert_eq!(resp.get_int("age"), Some(30));
5677        assert_eq!(resp.get_bool("active"), Some(true));
5678    }
5679
5680    #[test]
5681    fn test_elicitation_response_accept_url() {
5682        let resp = ElicitationResponse::accept_url();
5683        assert!(resp.is_accepted());
5684        assert!(resp.content.is_none());
5685        assert!(resp.get_string("anything").is_none());
5686    }
5687
5688    #[test]
5689    fn test_elicitation_response_decline() {
5690        let resp = ElicitationResponse::decline();
5691        assert!(!resp.is_accepted());
5692        assert!(resp.is_declined());
5693        assert!(!resp.is_cancelled());
5694        assert!(resp.get_string("key").is_none());
5695    }
5696
5697    #[test]
5698    fn test_elicitation_response_cancel() {
5699        let resp = ElicitationResponse::cancel();
5700        assert!(!resp.is_accepted());
5701        assert!(!resp.is_declined());
5702        assert!(resp.is_cancelled());
5703    }
5704
5705    #[test]
5706    fn test_elicitation_response_missing_key() {
5707        let mut data = std::collections::HashMap::new();
5708        data.insert("exists".to_string(), serde_json::json!("value"));
5709        let resp = ElicitationResponse::accept(data);
5710
5711        assert!(resp.get_string("missing").is_none());
5712        assert!(resp.get_bool("missing").is_none());
5713        assert!(resp.get_int("missing").is_none());
5714    }
5715
5716    #[test]
5717    fn test_elicitation_response_type_mismatch() {
5718        let mut data = std::collections::HashMap::new();
5719        data.insert("num".to_string(), serde_json::json!(42));
5720        let resp = ElicitationResponse::accept(data);
5721
5722        // get_string on a number returns None
5723        assert!(resp.get_string("num").is_none());
5724        // get_bool on a number returns None
5725        assert!(resp.get_bool("num").is_none());
5726        // get_int on a number returns Some
5727        assert_eq!(resp.get_int("num"), Some(42));
5728    }
5729
5730    // ========================================================================
5731    // Capability Check Tests (can_sample, can_elicit, etc.)
5732    // ========================================================================
5733
5734    #[test]
5735    fn test_can_sample_false_by_default() {
5736        let cx = Cx::for_testing();
5737        let ctx = McpContext::new(cx, 1);
5738        assert!(!ctx.can_sample());
5739    }
5740
5741    #[test]
5742    fn test_can_elicit_false_by_default() {
5743        let cx = Cx::for_testing();
5744        let ctx = McpContext::new(cx, 1);
5745        assert!(!ctx.can_elicit());
5746    }
5747
5748    #[test]
5749    fn test_can_read_resources_false_by_default() {
5750        let cx = Cx::for_testing();
5751        let ctx = McpContext::new(cx, 1);
5752        assert!(!ctx.can_read_resources());
5753    }
5754
5755    #[test]
5756    fn test_can_call_tools_false_by_default() {
5757        let cx = Cx::for_testing();
5758        let ctx = McpContext::new(cx, 1);
5759        assert!(!ctx.can_call_tools());
5760    }
5761
5762    #[test]
5763    fn test_resource_read_depth_default() {
5764        let cx = Cx::for_testing();
5765        let ctx = McpContext::new(cx, 1);
5766        assert_eq!(ctx.resource_read_depth(), 0);
5767    }
5768
5769    #[test]
5770    fn test_tool_call_depth_default() {
5771        let cx = Cx::for_testing();
5772        let ctx = McpContext::new(cx, 1);
5773        assert_eq!(ctx.tool_call_depth(), 0);
5774    }
5775
5776    // ========================================================================
5777    // Additional coverage tests (bd-3fcm)
5778    // ========================================================================
5779
5780    #[test]
5781    fn sampling_request_builder_chain() {
5782        let req = SamplingRequest::prompt("hello", 100)
5783            .with_system_prompt("You are helpful")
5784            .with_temperature(0.7)
5785            .with_stop_sequences(vec!["STOP".into()])
5786            .with_model_hints(vec!["gpt-4".into()]);
5787
5788        assert_eq!(req.messages.len(), 1);
5789        assert_eq!(req.max_tokens, 100);
5790        assert_eq!(req.system_prompt.as_deref(), Some("You are helpful"));
5791        assert_eq!(req.temperature, Some(0.7));
5792        assert_eq!(req.stop_sequences, vec!["STOP"]);
5793        assert_eq!(req.model_hints, vec!["gpt-4"]);
5794    }
5795
5796    #[test]
5797    fn sampling_request_message_roles() {
5798        let user = SamplingRequestMessage::user("hi");
5799        assert_eq!(user.role, SamplingRole::User);
5800        assert_eq!(user.text, "hi");
5801
5802        let asst = SamplingRequestMessage::assistant("hello");
5803        assert_eq!(asst.role, SamplingRole::Assistant);
5804        assert_eq!(asst.text, "hello");
5805    }
5806
5807    #[test]
5808    fn sampling_response_new_default_stop_reason() {
5809        let resp = SamplingResponse::new("output", "model-1");
5810        assert_eq!(resp.text, "output");
5811        assert_eq!(resp.model, "model-1");
5812        assert_eq!(resp.stop_reason, SamplingStopReason::EndTurn);
5813        assert_eq!(SamplingStopReason::default(), SamplingStopReason::EndTurn);
5814    }
5815
5816    #[test]
5817    fn sampling_stop_reason_round_trips_optional_open_wire_values() {
5818        let absent = SamplingStopReason::from_wire_value(None);
5819        assert_eq!(absent, SamplingStopReason::Unspecified);
5820        assert_eq!(absent.as_wire_value(), None);
5821
5822        let provider =
5823            SamplingStopReason::from_wire_value(Some("provider_safety_limit".to_owned()));
5824        assert_eq!(
5825            provider,
5826            SamplingStopReason::Other("provider_safety_limit".to_owned())
5827        );
5828        assert_eq!(provider.as_wire_value(), Some("provider_safety_limit"));
5829    }
5830
5831    #[test]
5832    fn noop_sampling_sender_returns_error() {
5833        let sender = NoOpSamplingSender;
5834        let req = SamplingRequest::prompt("test", 10);
5835        let result = crate::block_on(sender.create_message(req));
5836        assert!(result.is_err());
5837    }
5838
5839    #[test]
5840    fn noop_elicitation_sender_returns_error() {
5841        let sender = NoOpElicitationSender;
5842        let req = ElicitationRequest::form("msg", serde_json::json!({}));
5843        let result = crate::block_on(sender.elicit(req));
5844        assert!(result.is_err());
5845    }
5846
5847    #[test]
5848    fn elicitation_request_form_constructor() {
5849        let req = ElicitationRequest::form("Enter name", serde_json::json!({"type": "string"}));
5850        assert_eq!(req.mode, ElicitationMode::Form);
5851        assert_eq!(req.message, "Enter name");
5852        assert!(req.schema.is_some());
5853        assert!(req.url.is_none());
5854        assert!(req.elicitation_id.is_none());
5855    }
5856
5857    #[test]
5858    fn elicitation_request_url_constructor() {
5859        let req = ElicitationRequest::url("Login", "https://example.com", "id-1");
5860        assert_eq!(req.mode, ElicitationMode::Url);
5861        assert_eq!(req.message, "Login");
5862        assert_eq!(req.url.as_deref(), Some("https://example.com"));
5863        assert_eq!(req.elicitation_id.as_deref(), Some("id-1"));
5864        assert!(req.schema.is_none());
5865    }
5866
5867    #[test]
5868    fn mcp_context_with_sampling_enables_can_sample() {
5869        let cx = Cx::for_testing();
5870        let sender = Arc::new(NoOpSamplingSender);
5871        let ctx = McpContext::new(cx, 1).with_sampling(sender);
5872        assert!(ctx.can_sample());
5873    }
5874
5875    #[test]
5876    fn mcp_context_with_elicitation_enables_can_elicit() {
5877        let cx = Cx::for_testing();
5878        let sender = Arc::new(NoOpElicitationSender);
5879        let ctx = McpContext::new(cx, 1).with_elicitation(sender);
5880        assert!(ctx.can_elicit());
5881    }
5882
5883    struct FixedRootsProvider;
5884
5885    impl RootsProvider for FixedRootsProvider {
5886        fn list_roots(
5887            &self,
5888        ) -> std::pin::Pin<
5889            Box<dyn std::future::Future<Output = crate::McpResult<Vec<ClientRoot>>> + Send + '_>,
5890        > {
5891            Box::pin(async {
5892                Ok(vec![
5893                    ClientRoot::with_name("file:///workspace", "workspace"),
5894                    ClientRoot::new("file:///tmp"),
5895                ])
5896            })
5897        }
5898    }
5899
5900    #[test]
5901    fn mcp_context_roots_provider_returns_client_roots() {
5902        let ctx =
5903            McpContext::new(Cx::for_testing(), 1).with_roots_provider(Arc::new(FixedRootsProvider));
5904
5905        assert!(ctx.can_list_roots());
5906        let roots = crate::block_on(ctx.list_roots()).expect("configured roots provider succeeds");
5907        assert_eq!(
5908            roots,
5909            vec![
5910                ClientRoot::with_name("file:///workspace", "workspace"),
5911                ClientRoot::new("file:///tmp"),
5912            ]
5913        );
5914    }
5915
5916    #[test]
5917    fn mcp_context_without_roots_provider_rejects_without_authority() {
5918        let ctx = McpContext::new(Cx::for_testing(), 1);
5919
5920        assert!(!ctx.can_list_roots());
5921        let error = crate::block_on(ctx.list_roots())
5922            .expect_err("without only the roots provider, the context must reject the request");
5923        assert_eq!(error.code, crate::McpErrorCode::InvalidRequest);
5924        assert_eq!(
5925            error.message,
5926            "Roots not available: client does not support roots capability"
5927        );
5928    }
5929
5930    #[test]
5931    fn mcp_context_depth_setters() {
5932        let cx = Cx::for_testing();
5933        let ctx = McpContext::new(cx, 1)
5934            .with_resource_read_depth(3)
5935            .with_tool_call_depth(5);
5936        assert_eq!(ctx.resource_read_depth(), 3);
5937        assert_eq!(ctx.tool_call_depth(), 5);
5938
5939        let attempted_reset = ctx.with_resource_read_depth(0).with_tool_call_depth(0);
5940        assert_eq!(attempted_reset.resource_read_depth(), 3);
5941        assert_eq!(attempted_reset.tool_call_depth(), 5);
5942    }
5943
5944    #[test]
5945    fn mcp_context_debug_includes_request_id() {
5946        let cx = Cx::for_testing();
5947        let ctx = McpContext::new(cx, 99);
5948        let debug = format!("{ctx:?}");
5949        assert!(debug.contains("request_id: 99"));
5950    }
5951
5952    #[test]
5953    fn mcp_context_cx_and_trace() {
5954        let cx = Cx::for_testing();
5955        let ctx = McpContext::new(cx, 1);
5956        // cx() should return a reference without panic
5957        let _ = ctx.cx();
5958        // trace() should not panic
5959        ctx.trace("test event");
5960    }
5961
5962    #[test]
5963    fn final_result_outcome_preserves_dual_era_and_terminal_reason() {
5964        use crate::combinator::{DualEraFinalResult, FinalRequestResult};
5965
5966        let context = McpContext::new(Cx::for_testing(), 1);
5967        let modern = context.final_result_outcome(
5968            FinalRequestResult::<u64, String, &'static str>::modern("typed-final", 42),
5969        );
5970        let legacy =
5971            context.final_result_outcome(FinalRequestResult::<u64, String, &'static str>::legacy(
5972                "legacy-final",
5973                "legacy wire result".to_owned(),
5974            ));
5975
5976        let Outcome::Ok(modern) = modern else {
5977            panic!("live context admits the modern final result");
5978        };
5979        assert_eq!(modern.terminal_reason(), &"typed-final");
5980        assert_eq!(modern.result(), &DualEraFinalResult::Modern(42));
5981
5982        let Outcome::Ok(legacy) = legacy else {
5983            panic!("live context admits the legacy final result");
5984        };
5985        assert_eq!(legacy.terminal_reason(), &"legacy-final");
5986        assert_eq!(
5987            legacy.result(),
5988            &DualEraFinalResult::Legacy("legacy wire result".to_owned())
5989        );
5990    }
5991
5992    #[test]
5993    fn final_result_outcome_cancellation_negative_preserves_cx_reason() {
5994        use crate::combinator::FinalRequestResult;
5995        use asupersync::types::CancelKind;
5996
5997        let cx = Cx::for_testing();
5998        cx.cancel_with(CancelKind::Timeout, Some("final-result race"));
5999        let expected_reason = cx
6000            .cancel_reason()
6001            .expect("cancel_with records the caller-owned terminal reason");
6002        let context = McpContext::new(cx, 1);
6003
6004        let outcome = context.final_result_outcome(
6005            FinalRequestResult::<u64, String, &'static str>::modern("typed-final", 42),
6006        );
6007
6008        let Outcome::Cancelled(reason) = outcome else {
6009            panic!("changing only caller cancellation rejects the same final result");
6010        };
6011        assert_eq!(reason, expected_reason);
6012    }
6013
6014    #[test]
6015    fn final_result_outcome_panic_negative_preserves_payload() {
6016        use crate::combinator::FinalRequestResult;
6017        use asupersync::types::{CancelKind, PanicPayload};
6018
6019        type Final = FinalRequestResult<u64, String, &'static str>;
6020
6021        let cx = Cx::for_testing();
6022        cx.cancel_with(CancelKind::Timeout, Some("competing terminal state"));
6023        let context = McpContext::new(cx, 1);
6024        let payload = PanicPayload::new("final typed result panicked");
6025        let source: crate::McpOutcome<Final> = Outcome::Panicked(payload.clone());
6026
6027        let outcome = context.adapt_final_request_outcome(source);
6028
6029        let Outcome::Panicked(actual) = outcome else {
6030            panic!("changing only the source terminal state to panic preserves panic");
6031        };
6032        assert_eq!(actual, payload);
6033    }
6034}