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::Arc;
9
10use asupersync::types::CancelReason;
11use asupersync::{Budget, Cx, Outcome, RegionId, TaskId};
12
13use crate::{AUTH_STATE_KEY, AuthContext, SessionState};
14
15// ============================================================================
16// Notification Sender
17// ============================================================================
18
19/// Trait for sending notifications back to the client.
20///
21/// This is implemented by the server's transport layer to allow handlers
22/// to send progress updates and other notifications during execution.
23pub trait NotificationSender: Send + Sync {
24    /// Sends a progress notification to the client.
25    ///
26    /// # Arguments
27    ///
28    /// * `progress` - Current progress value
29    /// * `total` - Optional total for determinate progress
30    /// * `message` - Optional message describing current status
31    fn send_progress(&self, progress: f64, total: Option<f64>, message: Option<&str>);
32}
33
34// ============================================================================
35// Sampling Sender
36// ============================================================================
37
38/// Trait for sending sampling requests to the client.
39///
40/// Sampling allows the server to request LLM completions from the client.
41/// This enables agentic workflows where tools can leverage the client's
42/// LLM capabilities.
43pub trait SamplingSender: Send + Sync {
44    /// Sends a sampling/createMessage request to the client.
45    ///
46    /// # Arguments
47    ///
48    /// * `request` - The sampling request parameters
49    ///
50    /// # Returns
51    ///
52    /// The sampling response from the client, or an error if sampling failed
53    /// or the client doesn't support sampling.
54    fn create_message(
55        &self,
56        request: SamplingRequest,
57    ) -> std::pin::Pin<
58        Box<dyn std::future::Future<Output = crate::McpResult<SamplingResponse>> + Send + '_>,
59    >;
60}
61
62/// Parameters for a sampling request.
63#[derive(Debug, Clone)]
64pub struct SamplingRequest {
65    /// Conversation messages.
66    pub messages: Vec<SamplingRequestMessage>,
67    /// Maximum tokens to generate.
68    pub max_tokens: u32,
69    /// Optional system prompt.
70    pub system_prompt: Option<String>,
71    /// Sampling temperature (0.0 to 2.0).
72    pub temperature: Option<f64>,
73    /// Stop sequences to end generation.
74    pub stop_sequences: Vec<String>,
75    /// Model hints for preference.
76    pub model_hints: Vec<String>,
77}
78
79impl SamplingRequest {
80    /// Creates a new sampling request with the given messages and max tokens.
81    #[must_use]
82    pub fn new(messages: Vec<SamplingRequestMessage>, max_tokens: u32) -> Self {
83        Self {
84            messages,
85            max_tokens,
86            system_prompt: None,
87            temperature: None,
88            stop_sequences: Vec::new(),
89            model_hints: Vec::new(),
90        }
91    }
92
93    /// Creates a simple user prompt request.
94    #[must_use]
95    pub fn prompt(text: impl Into<String>, max_tokens: u32) -> Self {
96        Self::new(vec![SamplingRequestMessage::user(text)], max_tokens)
97    }
98
99    /// Sets the system prompt.
100    #[must_use]
101    pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
102        self.system_prompt = Some(prompt.into());
103        self
104    }
105
106    /// Sets the temperature.
107    #[must_use]
108    pub fn with_temperature(mut self, temp: f64) -> Self {
109        self.temperature = Some(temp);
110        self
111    }
112
113    /// Adds stop sequences.
114    #[must_use]
115    pub fn with_stop_sequences(mut self, sequences: Vec<String>) -> Self {
116        self.stop_sequences = sequences;
117        self
118    }
119
120    /// Adds model hints.
121    #[must_use]
122    pub fn with_model_hints(mut self, hints: Vec<String>) -> Self {
123        self.model_hints = hints;
124        self
125    }
126}
127
128/// A message in a sampling request.
129#[derive(Debug, Clone)]
130pub struct SamplingRequestMessage {
131    /// Message role.
132    pub role: SamplingRole,
133    /// Message text content.
134    pub text: String,
135}
136
137impl SamplingRequestMessage {
138    /// Creates a user message.
139    #[must_use]
140    pub fn user(text: impl Into<String>) -> Self {
141        Self {
142            role: SamplingRole::User,
143            text: text.into(),
144        }
145    }
146
147    /// Creates an assistant message.
148    #[must_use]
149    pub fn assistant(text: impl Into<String>) -> Self {
150        Self {
151            role: SamplingRole::Assistant,
152            text: text.into(),
153        }
154    }
155}
156
157/// Role in a sampling message.
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159pub enum SamplingRole {
160    /// User message.
161    User,
162    /// Assistant message.
163    Assistant,
164}
165
166/// Response from a sampling request.
167#[derive(Debug, Clone)]
168pub struct SamplingResponse {
169    /// Generated text content.
170    pub text: String,
171    /// Model that was used.
172    pub model: String,
173    /// Reason generation stopped.
174    pub stop_reason: SamplingStopReason,
175}
176
177impl SamplingResponse {
178    /// Creates a new sampling response.
179    #[must_use]
180    pub fn new(text: impl Into<String>, model: impl Into<String>) -> Self {
181        Self {
182            text: text.into(),
183            model: model.into(),
184            stop_reason: SamplingStopReason::EndTurn,
185        }
186    }
187}
188
189/// Stop reason for sampling.
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
191pub enum SamplingStopReason {
192    /// End of natural turn.
193    #[default]
194    EndTurn,
195    /// Hit stop sequence.
196    StopSequence,
197    /// Hit max tokens limit.
198    MaxTokens,
199}
200
201/// A no-op sampling sender that always returns an error.
202///
203/// Used when the client doesn't support sampling.
204#[derive(Debug, Clone, Copy, Default)]
205pub struct NoOpSamplingSender;
206
207impl SamplingSender for NoOpSamplingSender {
208    fn create_message(
209        &self,
210        _request: SamplingRequest,
211    ) -> std::pin::Pin<
212        Box<dyn std::future::Future<Output = crate::McpResult<SamplingResponse>> + Send + '_>,
213    > {
214        Box::pin(async {
215            Err(crate::McpError::new(
216                crate::McpErrorCode::InvalidRequest,
217                "Sampling not supported: client does not have sampling capability",
218            ))
219        })
220    }
221}
222
223// ============================================================================
224// Elicitation Sender
225// ============================================================================
226
227/// Trait for sending elicitation requests to the client.
228///
229/// Elicitation allows the server to request user input from the client.
230/// This enables interactive workflows where tools can prompt users for
231/// additional information.
232pub trait ElicitationSender: Send + Sync {
233    /// Sends an elicitation/create request to the client.
234    ///
235    /// # Arguments
236    ///
237    /// * `request` - The elicitation request parameters
238    ///
239    /// # Returns
240    ///
241    /// The elicitation response from the client, or an error if elicitation
242    /// failed or the client doesn't support elicitation.
243    fn elicit(
244        &self,
245        request: ElicitationRequest,
246    ) -> std::pin::Pin<
247        Box<dyn std::future::Future<Output = crate::McpResult<ElicitationResponse>> + Send + '_>,
248    >;
249}
250
251/// Parameters for an elicitation request.
252#[derive(Debug, Clone)]
253pub struct ElicitationRequest {
254    /// Mode of elicitation (form or URL).
255    pub mode: ElicitationMode,
256    /// Message to present to the user.
257    pub message: String,
258    /// For form mode: JSON Schema for the expected response.
259    pub schema: Option<serde_json::Value>,
260    /// For URL mode: URL to navigate to.
261    pub url: Option<String>,
262    /// For URL mode: Unique elicitation ID.
263    pub elicitation_id: Option<String>,
264}
265
266impl ElicitationRequest {
267    /// Creates a form mode elicitation request.
268    #[must_use]
269    pub fn form(message: impl Into<String>, schema: serde_json::Value) -> Self {
270        Self {
271            mode: ElicitationMode::Form,
272            message: message.into(),
273            schema: Some(schema),
274            url: None,
275            elicitation_id: None,
276        }
277    }
278
279    /// Creates a URL mode elicitation request.
280    #[must_use]
281    pub fn url(
282        message: impl Into<String>,
283        url: impl Into<String>,
284        elicitation_id: impl Into<String>,
285    ) -> Self {
286        Self {
287            mode: ElicitationMode::Url,
288            message: message.into(),
289            schema: None,
290            url: Some(url.into()),
291            elicitation_id: Some(elicitation_id.into()),
292        }
293    }
294}
295
296/// Mode of elicitation.
297#[derive(Debug, Clone, Copy, PartialEq, Eq)]
298pub enum ElicitationMode {
299    /// Form mode - collect user input via in-band form.
300    Form,
301    /// URL mode - redirect user to external URL.
302    Url,
303}
304
305/// Response from an elicitation request.
306#[derive(Debug, Clone)]
307pub struct ElicitationResponse {
308    /// User's action (accept, decline, cancel).
309    pub action: ElicitationAction,
310    /// Form data (only present when action is Accept and mode is Form).
311    pub content: Option<std::collections::HashMap<String, serde_json::Value>>,
312}
313
314impl ElicitationResponse {
315    /// Creates an accepted response with form data.
316    #[must_use]
317    pub fn accept(content: std::collections::HashMap<String, serde_json::Value>) -> Self {
318        Self {
319            action: ElicitationAction::Accept,
320            content: Some(content),
321        }
322    }
323
324    /// Creates an accepted response for URL mode (no content).
325    #[must_use]
326    pub fn accept_url() -> Self {
327        Self {
328            action: ElicitationAction::Accept,
329            content: None,
330        }
331    }
332
333    /// Creates a declined response.
334    #[must_use]
335    pub fn decline() -> Self {
336        Self {
337            action: ElicitationAction::Decline,
338            content: None,
339        }
340    }
341
342    /// Creates a cancelled response.
343    #[must_use]
344    pub fn cancel() -> Self {
345        Self {
346            action: ElicitationAction::Cancel,
347            content: None,
348        }
349    }
350
351    /// Returns true if the user accepted.
352    #[must_use]
353    pub fn is_accepted(&self) -> bool {
354        matches!(self.action, ElicitationAction::Accept)
355    }
356
357    /// Returns true if the user declined.
358    #[must_use]
359    pub fn is_declined(&self) -> bool {
360        matches!(self.action, ElicitationAction::Decline)
361    }
362
363    /// Returns true if the user cancelled.
364    #[must_use]
365    pub fn is_cancelled(&self) -> bool {
366        matches!(self.action, ElicitationAction::Cancel)
367    }
368
369    /// Gets a string value from the form content.
370    #[must_use]
371    pub fn get_string(&self, key: &str) -> Option<&str> {
372        self.content.as_ref()?.get(key)?.as_str()
373    }
374
375    /// Gets a boolean value from the form content.
376    #[must_use]
377    pub fn get_bool(&self, key: &str) -> Option<bool> {
378        self.content.as_ref()?.get(key)?.as_bool()
379    }
380
381    /// Gets an integer value from the form content.
382    #[must_use]
383    pub fn get_int(&self, key: &str) -> Option<i64> {
384        self.content.as_ref()?.get(key)?.as_i64()
385    }
386}
387
388/// Action taken by the user in response to elicitation.
389#[derive(Debug, Clone, Copy, PartialEq, Eq)]
390pub enum ElicitationAction {
391    /// User accepted/submitted the form.
392    Accept,
393    /// User explicitly declined.
394    Decline,
395    /// User dismissed without choice.
396    Cancel,
397}
398
399/// A no-op elicitation sender that always returns an error.
400///
401/// Used when the client doesn't support elicitation.
402#[derive(Debug, Clone, Copy, Default)]
403pub struct NoOpElicitationSender;
404
405impl ElicitationSender for NoOpElicitationSender {
406    fn elicit(
407        &self,
408        _request: ElicitationRequest,
409    ) -> std::pin::Pin<
410        Box<dyn std::future::Future<Output = crate::McpResult<ElicitationResponse>> + Send + '_>,
411    > {
412        Box::pin(async {
413            Err(crate::McpError::new(
414                crate::McpErrorCode::InvalidRequest,
415                "Elicitation not supported: client does not have elicitation capability",
416            ))
417        })
418    }
419}
420
421// ============================================================================
422// Resource Reader (Cross-Component Access)
423// ============================================================================
424
425/// Maximum depth for nested resource reads to prevent infinite recursion.
426pub const MAX_RESOURCE_READ_DEPTH: u32 = 10;
427
428/// A single item of resource content.
429///
430/// Mirrors the protocol's ResourceContent but lives in core to avoid
431/// circular dependencies.
432#[derive(Debug, Clone)]
433pub struct ResourceContentItem {
434    /// Resource URI.
435    pub uri: String,
436    /// MIME type.
437    pub mime_type: Option<String>,
438    /// Text content (if text).
439    pub text: Option<String>,
440    /// Binary content (if blob, base64-encoded).
441    pub blob: Option<String>,
442}
443
444impl ResourceContentItem {
445    /// Creates a text resource content item.
446    #[must_use]
447    pub fn text(uri: impl Into<String>, text: impl Into<String>) -> Self {
448        Self {
449            uri: uri.into(),
450            mime_type: Some("text/plain".to_string()),
451            text: Some(text.into()),
452            blob: None,
453        }
454    }
455
456    /// Creates a JSON resource content item.
457    #[must_use]
458    pub fn json(uri: impl Into<String>, text: impl Into<String>) -> Self {
459        Self {
460            uri: uri.into(),
461            mime_type: Some("application/json".to_string()),
462            text: Some(text.into()),
463            blob: None,
464        }
465    }
466
467    /// Creates a binary resource content item.
468    #[must_use]
469    pub fn blob(
470        uri: impl Into<String>,
471        mime_type: impl Into<String>,
472        blob: impl Into<String>,
473    ) -> Self {
474        Self {
475            uri: uri.into(),
476            mime_type: Some(mime_type.into()),
477            text: None,
478            blob: Some(blob.into()),
479        }
480    }
481
482    /// Returns the text content, if present.
483    #[must_use]
484    pub fn as_text(&self) -> Option<&str> {
485        self.text.as_deref()
486    }
487
488    /// Returns the blob content, if present.
489    #[must_use]
490    pub fn as_blob(&self) -> Option<&str> {
491        self.blob.as_deref()
492    }
493
494    /// Returns true if this is a text resource.
495    #[must_use]
496    pub fn is_text(&self) -> bool {
497        self.text.is_some()
498    }
499
500    /// Returns true if this is a blob resource.
501    #[must_use]
502    pub fn is_blob(&self) -> bool {
503        self.blob.is_some()
504    }
505}
506
507/// Result of reading a resource.
508#[derive(Debug, Clone)]
509pub struct ResourceReadResult {
510    /// The content items.
511    pub contents: Vec<ResourceContentItem>,
512}
513
514impl ResourceReadResult {
515    /// Creates a new resource read result with the given contents.
516    #[must_use]
517    pub fn new(contents: Vec<ResourceContentItem>) -> Self {
518        Self { contents }
519    }
520
521    /// Creates a single-item text result.
522    #[must_use]
523    pub fn text(uri: impl Into<String>, text: impl Into<String>) -> Self {
524        Self {
525            contents: vec![ResourceContentItem::text(uri, text)],
526        }
527    }
528
529    /// Returns the first text content, if present.
530    #[must_use]
531    pub fn first_text(&self) -> Option<&str> {
532        self.contents.first().and_then(|c| c.as_text())
533    }
534
535    /// Returns the first blob content, if present.
536    #[must_use]
537    pub fn first_blob(&self) -> Option<&str> {
538        self.contents.first().and_then(|c| c.as_blob())
539    }
540}
541
542/// Trait for reading resources from within handlers.
543///
544/// This trait is implemented by the server's Router to allow tools,
545/// resources, and prompts to read other resources. It enables
546/// cross-component composition and code reuse.
547///
548/// The trait uses boxed futures to avoid complex lifetime issues
549/// with async traits.
550pub trait ResourceReader: Send + Sync {
551    /// Reads a resource by URI.
552    ///
553    /// # Arguments
554    ///
555    /// * `cx` - The asupersync context
556    /// * `uri` - The resource URI to read
557    /// * `depth` - Current recursion depth (to prevent infinite loops)
558    ///
559    /// # Returns
560    ///
561    /// The resource contents, or an error if the resource doesn't exist
562    /// or reading fails.
563    fn read_resource(
564        &self,
565        cx: &Cx,
566        uri: &str,
567        depth: u32,
568    ) -> Pin<Box<dyn Future<Output = crate::McpResult<ResourceReadResult>> + Send + '_>>;
569}
570
571// ============================================================================
572// Tool Caller (Cross-Component Access)
573// ============================================================================
574
575/// Maximum depth for nested tool calls to prevent infinite recursion.
576pub const MAX_TOOL_CALL_DEPTH: u32 = 10;
577
578/// A single item of content returned from a tool call.
579///
580/// Mirrors the protocol's Content type but lives in core to avoid
581/// circular dependencies.
582#[derive(Debug, Clone)]
583pub enum ToolContentItem {
584    /// Text content.
585    Text {
586        /// The text content.
587        text: String,
588    },
589    /// Image content (base64-encoded).
590    Image {
591        /// Base64-encoded image data.
592        data: String,
593        /// MIME type of the image.
594        mime_type: String,
595    },
596    /// Embedded resource reference.
597    Resource {
598        /// Resource URI.
599        uri: String,
600        /// MIME type.
601        mime_type: Option<String>,
602        /// Text content.
603        text: Option<String>,
604    },
605}
606
607impl ToolContentItem {
608    /// Creates a text content item.
609    #[must_use]
610    pub fn text(text: impl Into<String>) -> Self {
611        Self::Text { text: text.into() }
612    }
613
614    /// Returns the text content, if this is a text item.
615    #[must_use]
616    pub fn as_text(&self) -> Option<&str> {
617        match self {
618            Self::Text { text } => Some(text),
619            _ => None,
620        }
621    }
622
623    /// Returns true if this is a text content item.
624    #[must_use]
625    pub fn is_text(&self) -> bool {
626        matches!(self, Self::Text { .. })
627    }
628}
629
630/// Result of calling a tool.
631#[derive(Debug, Clone)]
632pub struct ToolCallResult {
633    /// The content items returned by the tool.
634    pub content: Vec<ToolContentItem>,
635    /// Whether the tool returned an error.
636    pub is_error: bool,
637}
638
639impl ToolCallResult {
640    /// Creates a successful tool result with the given content.
641    #[must_use]
642    pub fn success(content: Vec<ToolContentItem>) -> Self {
643        Self {
644            content,
645            is_error: false,
646        }
647    }
648
649    /// Creates a successful tool result with a single text item.
650    #[must_use]
651    pub fn text(text: impl Into<String>) -> Self {
652        Self {
653            content: vec![ToolContentItem::text(text)],
654            is_error: false,
655        }
656    }
657
658    /// Creates an error tool result.
659    #[must_use]
660    pub fn error(message: impl Into<String>) -> Self {
661        Self {
662            content: vec![ToolContentItem::text(message)],
663            is_error: true,
664        }
665    }
666
667    /// Returns the first text content, if present.
668    #[must_use]
669    pub fn first_text(&self) -> Option<&str> {
670        self.content.first().and_then(|c| c.as_text())
671    }
672}
673
674/// Trait for calling tools from within handlers.
675///
676/// This trait is implemented by the server's Router to allow tools,
677/// resources, and prompts to call other tools. It enables
678/// cross-component composition and code reuse.
679///
680/// The trait uses boxed futures to avoid complex lifetime issues
681/// with async traits.
682pub trait ToolCaller: Send + Sync {
683    /// Calls a tool by name with the given arguments.
684    ///
685    /// # Arguments
686    ///
687    /// * `cx` - The asupersync context
688    /// * `name` - The tool name to call
689    /// * `args` - The arguments as a JSON value
690    /// * `depth` - Current recursion depth (to prevent infinite loops)
691    ///
692    /// # Returns
693    ///
694    /// The tool result, or an error if the tool doesn't exist
695    /// or execution fails.
696    fn call_tool(
697        &self,
698        cx: &Cx,
699        name: &str,
700        args: serde_json::Value,
701        depth: u32,
702    ) -> Pin<Box<dyn Future<Output = crate::McpResult<ToolCallResult>> + Send + '_>>;
703}
704
705// ============================================================================
706// Capabilities Info
707// ============================================================================
708
709/// Client capability information accessible from handlers.
710///
711/// This provides a simplified view of what capabilities the connected client
712/// supports. Use this to adapt handler behavior based on client capabilities.
713#[derive(Debug, Clone, Default)]
714pub struct ClientCapabilityInfo {
715    /// Whether the client supports sampling (LLM completions).
716    pub sampling: bool,
717    /// Whether the client supports elicitation (user input requests).
718    pub elicitation: bool,
719    /// Whether the client supports form-mode elicitation.
720    pub elicitation_form: bool,
721    /// Whether the client supports URL-mode elicitation.
722    pub elicitation_url: bool,
723    /// Whether the client supports roots listing.
724    pub roots: bool,
725    /// Whether the client wants list_changed notifications for roots.
726    pub roots_list_changed: bool,
727}
728
729impl ClientCapabilityInfo {
730    /// Creates a new empty capability info (no capabilities).
731    #[must_use]
732    pub fn new() -> Self {
733        Self::default()
734    }
735
736    /// Creates capability info with sampling enabled.
737    #[must_use]
738    pub fn with_sampling(mut self) -> Self {
739        self.sampling = true;
740        self
741    }
742
743    /// Creates capability info with elicitation enabled.
744    #[must_use]
745    pub fn with_elicitation(mut self, form: bool, url: bool) -> Self {
746        self.elicitation = form || url;
747        self.elicitation_form = form;
748        self.elicitation_url = url;
749        self
750    }
751
752    /// Creates capability info with roots enabled.
753    #[must_use]
754    pub fn with_roots(mut self, list_changed: bool) -> Self {
755        self.roots = true;
756        self.roots_list_changed = list_changed;
757        self
758    }
759}
760
761/// Server capability information accessible from handlers.
762///
763/// This provides a simplified view of what capabilities this server advertises.
764#[derive(Debug, Clone, Default)]
765pub struct ServerCapabilityInfo {
766    /// Whether the server supports tools.
767    pub tools: bool,
768    /// Whether the server supports resources.
769    pub resources: bool,
770    /// Whether resources support subscriptions.
771    pub resources_subscribe: bool,
772    /// Whether the server supports prompts.
773    pub prompts: bool,
774    /// Whether the server supports logging.
775    pub logging: bool,
776}
777
778impl ServerCapabilityInfo {
779    /// Creates a new empty server capability info.
780    #[must_use]
781    pub fn new() -> Self {
782        Self::default()
783    }
784
785    /// Creates capability info with tools enabled.
786    #[must_use]
787    pub fn with_tools(mut self) -> Self {
788        self.tools = true;
789        self
790    }
791
792    /// Creates capability info with resources enabled.
793    #[must_use]
794    pub fn with_resources(mut self, subscribe: bool) -> Self {
795        self.resources = true;
796        self.resources_subscribe = subscribe;
797        self
798    }
799
800    /// Creates capability info with prompts enabled.
801    #[must_use]
802    pub fn with_prompts(mut self) -> Self {
803        self.prompts = true;
804        self
805    }
806
807    /// Creates capability info with logging enabled.
808    #[must_use]
809    pub fn with_logging(mut self) -> Self {
810        self.logging = true;
811        self
812    }
813}
814
815/// A no-op notification sender used when progress reporting is disabled.
816#[derive(Debug, Clone, Copy, Default)]
817pub struct NoOpNotificationSender;
818
819impl NotificationSender for NoOpNotificationSender {
820    fn send_progress(&self, _progress: f64, _total: Option<f64>, _message: Option<&str>) {
821        // No-op: progress reporting disabled
822    }
823}
824
825/// Progress reporter that wraps a notification sender with a progress token.
826///
827/// This is the concrete type stored in McpContext that handles sending
828/// progress notifications with the correct token.
829#[derive(Clone)]
830pub struct ProgressReporter {
831    sender: Arc<dyn NotificationSender>,
832}
833
834impl ProgressReporter {
835    /// Creates a new progress reporter with the given sender.
836    pub fn new(sender: Arc<dyn NotificationSender>) -> Self {
837        Self { sender }
838    }
839
840    /// Reports progress to the client.
841    ///
842    /// # Arguments
843    ///
844    /// * `progress` - Current progress value (0.0 to 1.0 for fractional, or absolute)
845    /// * `message` - Optional message describing current status
846    pub fn report(&self, progress: f64, message: Option<&str>) {
847        self.sender.send_progress(progress, None, message);
848    }
849
850    /// Reports progress with a total for determinate progress bars.
851    ///
852    /// # Arguments
853    ///
854    /// * `progress` - Current progress value
855    /// * `total` - Total expected value
856    /// * `message` - Optional message describing current status
857    pub fn report_with_total(&self, progress: f64, total: f64, message: Option<&str>) {
858        self.sender.send_progress(progress, Some(total), message);
859    }
860}
861
862impl std::fmt::Debug for ProgressReporter {
863    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
864        f.debug_struct("ProgressReporter").finish_non_exhaustive()
865    }
866}
867
868/// MCP context that wraps asupersync's capability context.
869///
870/// `McpContext` provides access to:
871/// - Request-scoped identity (request ID, trace context)
872/// - Cancellation checkpoints for cancel-safe handlers
873/// - Budget/deadline awareness for timeout enforcement
874/// - Region-scoped spawning for background work
875/// - Sampling capability for LLM completions (if client supports it)
876/// - Elicitation capability for user input requests (if client supports it)
877/// - Cross-component resource reading (if router is attached)
878///
879/// # Example
880///
881/// ```ignore
882/// async fn my_tool(ctx: &McpContext, args: MyArgs) -> McpResult<Value> {
883///     // Check for client disconnect
884///     ctx.checkpoint()?;
885///
886///     // Do work with budget awareness
887///     let remaining = ctx.budget();
888///
889///     // Request an LLM completion (if available)
890///     let response = ctx.sample("Write a haiku about Rust", 100).await?;
891///
892///     // Request user input (if available)
893///     let input = ctx.elicit_form("Enter your name", schema).await?;
894///
895///     // Read a resource from within a tool
896///     let config = ctx.read_resource("config://app").await?;
897///
898///     // Call another tool from within a tool
899///     let result = ctx.call_tool("other_tool", json!({"arg": "value"})).await?;
900///
901///     // Return result
902///     Ok(json!({"result": response.text}))
903/// }
904/// ```
905#[derive(Clone)]
906pub struct McpContext {
907    /// The underlying capability context.
908    cx: Cx,
909    /// Unique request identifier for tracing (from JSON-RPC id).
910    request_id: u64,
911    /// Optional progress reporter for long-running operations.
912    progress_reporter: Option<ProgressReporter>,
913    /// Session state for per-session key-value storage.
914    state: Option<SessionState>,
915    /// Optional sampling sender for LLM completions.
916    sampling_sender: Option<Arc<dyn SamplingSender>>,
917    /// Optional elicitation sender for user input requests.
918    elicitation_sender: Option<Arc<dyn ElicitationSender>>,
919    /// Optional resource reader for cross-component access.
920    resource_reader: Option<Arc<dyn ResourceReader>>,
921    /// Current resource read depth (to prevent infinite recursion).
922    resource_read_depth: u32,
923    /// Optional tool caller for cross-component access.
924    tool_caller: Option<Arc<dyn ToolCaller>>,
925    /// Current tool call depth (to prevent infinite recursion).
926    tool_call_depth: u32,
927    /// Client capability information.
928    client_capabilities: Option<ClientCapabilityInfo>,
929    /// Server capability information.
930    server_capabilities: Option<ServerCapabilityInfo>,
931}
932
933impl std::fmt::Debug for McpContext {
934    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
935        f.debug_struct("McpContext")
936            .field("cx", &self.cx)
937            .field("request_id", &self.request_id)
938            .field("progress_reporter", &self.progress_reporter)
939            .field("state", &self.state.is_some())
940            .field("sampling_sender", &self.sampling_sender.is_some())
941            .field("elicitation_sender", &self.elicitation_sender.is_some())
942            .field("resource_reader", &self.resource_reader.is_some())
943            .field("resource_read_depth", &self.resource_read_depth)
944            .field("tool_caller", &self.tool_caller.is_some())
945            .field("tool_call_depth", &self.tool_call_depth)
946            .field("client_capabilities", &self.client_capabilities)
947            .field("server_capabilities", &self.server_capabilities)
948            .finish()
949    }
950}
951
952impl McpContext {
953    /// Creates a new MCP context from an asupersync Cx.
954    ///
955    /// This is typically called by the server when processing a new request,
956    /// creating a new region for the request lifecycle.
957    #[must_use]
958    pub fn new(cx: Cx, request_id: u64) -> Self {
959        Self {
960            cx,
961            request_id,
962            progress_reporter: None,
963            state: None,
964            sampling_sender: None,
965            elicitation_sender: None,
966            resource_reader: None,
967            resource_read_depth: 0,
968            tool_caller: None,
969            tool_call_depth: 0,
970            client_capabilities: None,
971            server_capabilities: None,
972        }
973    }
974
975    /// Creates a new MCP context with session state.
976    ///
977    /// Use this constructor when session state should be accessible to handlers.
978    #[must_use]
979    pub fn with_state(cx: Cx, request_id: u64, state: SessionState) -> Self {
980        Self {
981            cx,
982            request_id,
983            progress_reporter: None,
984            state: Some(state),
985            sampling_sender: None,
986            elicitation_sender: None,
987            resource_reader: None,
988            resource_read_depth: 0,
989            tool_caller: None,
990            tool_call_depth: 0,
991            client_capabilities: None,
992            server_capabilities: None,
993        }
994    }
995
996    /// Creates a new MCP context with progress reporting enabled.
997    ///
998    /// Use this constructor when the client has provided a progress token
999    /// and expects progress notifications.
1000    #[must_use]
1001    pub fn with_progress(cx: Cx, request_id: u64, reporter: ProgressReporter) -> Self {
1002        Self {
1003            cx,
1004            request_id,
1005            progress_reporter: Some(reporter),
1006            state: None,
1007            sampling_sender: None,
1008            elicitation_sender: None,
1009            resource_reader: None,
1010            resource_read_depth: 0,
1011            tool_caller: None,
1012            tool_call_depth: 0,
1013            client_capabilities: None,
1014            server_capabilities: None,
1015        }
1016    }
1017
1018    /// Creates a new MCP context with both state and progress reporting.
1019    #[must_use]
1020    pub fn with_state_and_progress(
1021        cx: Cx,
1022        request_id: u64,
1023        state: SessionState,
1024        reporter: ProgressReporter,
1025    ) -> Self {
1026        Self {
1027            cx,
1028            request_id,
1029            progress_reporter: Some(reporter),
1030            state: Some(state),
1031            sampling_sender: None,
1032            elicitation_sender: None,
1033            resource_reader: None,
1034            resource_read_depth: 0,
1035            tool_caller: None,
1036            tool_call_depth: 0,
1037            client_capabilities: None,
1038            server_capabilities: None,
1039        }
1040    }
1041
1042    /// Sets the sampling sender for this context.
1043    ///
1044    /// This enables the `sample()` method to request LLM completions from
1045    /// the client.
1046    #[must_use]
1047    pub fn with_sampling(mut self, sender: Arc<dyn SamplingSender>) -> Self {
1048        self.sampling_sender = Some(sender);
1049        self
1050    }
1051
1052    /// Sets the elicitation sender for this context.
1053    ///
1054    /// This enables the `elicit()` methods to request user input from
1055    /// the client.
1056    #[must_use]
1057    pub fn with_elicitation(mut self, sender: Arc<dyn ElicitationSender>) -> Self {
1058        self.elicitation_sender = Some(sender);
1059        self
1060    }
1061
1062    /// Sets the resource reader for this context.
1063    ///
1064    /// This enables the `read_resource()` methods to read resources from
1065    /// within tool, resource, or prompt handlers.
1066    #[must_use]
1067    pub fn with_resource_reader(mut self, reader: Arc<dyn ResourceReader>) -> Self {
1068        self.resource_reader = Some(reader);
1069        self
1070    }
1071
1072    /// Sets the resource read depth for this context.
1073    ///
1074    /// This is used internally to track recursion depth when reading
1075    /// resources from within resource handlers.
1076    #[must_use]
1077    pub fn with_resource_read_depth(mut self, depth: u32) -> Self {
1078        self.resource_read_depth = depth;
1079        self
1080    }
1081
1082    /// Sets the tool caller for this context.
1083    ///
1084    /// This enables the `call_tool()` methods to call other tools from
1085    /// within tool, resource, or prompt handlers.
1086    #[must_use]
1087    pub fn with_tool_caller(mut self, caller: Arc<dyn ToolCaller>) -> Self {
1088        self.tool_caller = Some(caller);
1089        self
1090    }
1091
1092    /// Sets the tool call depth for this context.
1093    ///
1094    /// This is used internally to track recursion depth when calling
1095    /// tools from within tool handlers.
1096    #[must_use]
1097    pub fn with_tool_call_depth(mut self, depth: u32) -> Self {
1098        self.tool_call_depth = depth;
1099        self
1100    }
1101
1102    /// Sets the client capability information for this context.
1103    ///
1104    /// This enables handlers to check what capabilities the connected
1105    /// client supports.
1106    #[must_use]
1107    pub fn with_client_capabilities(mut self, capabilities: ClientCapabilityInfo) -> Self {
1108        self.client_capabilities = Some(capabilities);
1109        self
1110    }
1111
1112    /// Sets the server capability information for this context.
1113    ///
1114    /// This enables handlers to check what capabilities this server
1115    /// advertises.
1116    #[must_use]
1117    pub fn with_server_capabilities(mut self, capabilities: ServerCapabilityInfo) -> Self {
1118        self.server_capabilities = Some(capabilities);
1119        self
1120    }
1121
1122    /// Returns whether progress reporting is enabled for this context.
1123    #[must_use]
1124    pub fn has_progress_reporter(&self) -> bool {
1125        self.progress_reporter.is_some()
1126    }
1127
1128    /// Reports progress on the current operation.
1129    ///
1130    /// If progress reporting is not enabled (no progress token was provided),
1131    /// this method does nothing.
1132    ///
1133    /// # Arguments
1134    ///
1135    /// * `progress` - Current progress value (0.0 to 1.0 for fractional progress)
1136    /// * `message` - Optional message describing current status
1137    ///
1138    /// # Example
1139    ///
1140    /// ```ignore
1141    /// async fn process_files(ctx: &McpContext, files: &[File]) -> McpResult<()> {
1142    ///     for (i, file) in files.iter().enumerate() {
1143    ///         ctx.report_progress(i as f64 / files.len() as f64, Some("Processing files"));
1144    ///         process_file(file).await?;
1145    ///     }
1146    ///     ctx.report_progress(1.0, Some("Complete"));
1147    ///     Ok(())
1148    /// }
1149    /// ```
1150    pub fn report_progress(&self, progress: f64, message: Option<&str>) {
1151        if let Some(ref reporter) = self.progress_reporter {
1152            reporter.report(progress, message);
1153        }
1154    }
1155
1156    /// Reports progress with explicit total for determinate progress bars.
1157    ///
1158    /// If progress reporting is not enabled, this method does nothing.
1159    ///
1160    /// # Arguments
1161    ///
1162    /// * `progress` - Current progress value
1163    /// * `total` - Total expected value
1164    /// * `message` - Optional message describing current status
1165    ///
1166    /// # Example
1167    ///
1168    /// ```ignore
1169    /// async fn process_items(ctx: &McpContext, items: &[Item]) -> McpResult<()> {
1170    ///     let total = items.len() as f64;
1171    ///     for (i, item) in items.iter().enumerate() {
1172    ///         ctx.report_progress_with_total(i as f64, total, Some(&format!("Item {}", i)));
1173    ///         process_item(item).await?;
1174    ///     }
1175    ///     Ok(())
1176    /// }
1177    /// ```
1178    pub fn report_progress_with_total(&self, progress: f64, total: f64, message: Option<&str>) {
1179        if let Some(ref reporter) = self.progress_reporter {
1180            reporter.report_with_total(progress, total, message);
1181        }
1182    }
1183
1184    /// Returns the unique request identifier.
1185    ///
1186    /// This corresponds to the JSON-RPC request ID and is useful for
1187    /// logging and tracing across the request lifecycle.
1188    #[must_use]
1189    pub fn request_id(&self) -> u64 {
1190        self.request_id
1191    }
1192
1193    /// Returns the underlying region ID from asupersync.
1194    ///
1195    /// The region represents the request's lifecycle scope - all spawned
1196    /// tasks belong to this region and will be cleaned up when the
1197    /// request completes or is cancelled.
1198    #[must_use]
1199    pub fn region_id(&self) -> RegionId {
1200        self.cx.region_id()
1201    }
1202
1203    /// Returns the current task ID.
1204    #[must_use]
1205    pub fn task_id(&self) -> TaskId {
1206        self.cx.task_id()
1207    }
1208
1209    /// Returns the current budget.
1210    ///
1211    /// The budget represents the remaining computational resources (time, polls)
1212    /// available for this request. When exhausted, the request should be
1213    /// cancelled gracefully.
1214    #[must_use]
1215    pub fn budget(&self) -> Budget {
1216        self.cx.budget()
1217    }
1218
1219    /// Checks if cancellation has been requested.
1220    ///
1221    /// This includes client disconnection, timeout, or explicit cancellation.
1222    /// Handlers should check this periodically and exit early if true.
1223    #[must_use]
1224    pub fn is_cancelled(&self) -> bool {
1225        self.cx.is_cancel_requested() || self.cx.budget().is_exhausted()
1226    }
1227
1228    /// Cooperative cancellation checkpoint.
1229    ///
1230    /// Call this at natural suspension points in your handler to allow
1231    /// graceful cancellation. Returns `Err` if cancellation is pending.
1232    ///
1233    /// # Errors
1234    ///
1235    /// Returns an error if the request has been cancelled and cancellation
1236    /// is not currently masked.
1237    ///
1238    /// # Example
1239    ///
1240    /// ```ignore
1241    /// async fn process_items(ctx: &McpContext, items: Vec<Item>) -> McpResult<()> {
1242    ///     for item in items {
1243    ///         ctx.checkpoint()?;  // Allow cancellation between items
1244    ///         process_item(item).await?;
1245    ///     }
1246    ///     Ok(())
1247    /// }
1248    /// ```
1249    pub fn checkpoint(&self) -> Result<(), CancelledError> {
1250        self.cx.checkpoint().map_err(|_| CancelledError)?;
1251        if self.cx.budget().is_exhausted() {
1252            return Err(CancelledError);
1253        }
1254        Ok(())
1255    }
1256
1257    /// Executes a closure with cancellation masked.
1258    ///
1259    /// While masked, `checkpoint()` will not return an error even if
1260    /// cancellation is pending. Use this for critical sections that
1261    /// must complete atomically.
1262    ///
1263    /// # Example
1264    ///
1265    /// ```ignore
1266    /// // Commit transaction - must not be interrupted
1267    /// ctx.masked(|| {
1268    ///     db.commit().await?;
1269    ///     Ok(())
1270    /// })
1271    /// ```
1272    pub fn masked<F, R>(&self, f: F) -> R
1273    where
1274        F: FnOnce() -> R,
1275    {
1276        self.cx.masked(f)
1277    }
1278
1279    /// Records a trace event for this request.
1280    ///
1281    /// Events are associated with the request's trace context and can be
1282    /// used for debugging and observability.
1283    pub fn trace(&self, message: &str) {
1284        self.cx.trace(message);
1285    }
1286
1287    /// Returns a reference to the underlying asupersync Cx.
1288    ///
1289    /// Use this when you need direct access to asupersync primitives,
1290    /// such as spawning tasks or using combinators.
1291    #[must_use]
1292    pub fn cx(&self) -> &Cx {
1293        &self.cx
1294    }
1295
1296    // ========================================================================
1297    // Session State Access
1298    // ========================================================================
1299
1300    /// Gets a value from session state by key.
1301    ///
1302    /// Returns `None` if:
1303    /// - Session state is not available (context created without state)
1304    /// - The key doesn't exist
1305    /// - Deserialization to type `T` fails
1306    ///
1307    /// # Example
1308    ///
1309    /// ```ignore
1310    /// async fn my_tool(ctx: &McpContext, args: MyArgs) -> McpResult<Value> {
1311    ///     // Get a counter from session state
1312    ///     let count: Option<i32> = ctx.get_state("counter");
1313    ///     let count = count.unwrap_or(0);
1314    ///     // ... use count ...
1315    ///     Ok(json!({"count": count}))
1316    /// }
1317    /// ```
1318    #[must_use]
1319    pub fn get_state<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
1320        self.state.as_ref()?.get(key)
1321    }
1322
1323    /// Returns the authentication context for this request, if available.
1324    #[must_use]
1325    pub fn auth(&self) -> Option<AuthContext> {
1326        self.state.as_ref()?.get(AUTH_STATE_KEY)
1327    }
1328
1329    /// Stores authentication context into session state.
1330    ///
1331    /// Returns `false` if session state is unavailable or serialization fails.
1332    pub fn set_auth(&self, auth: AuthContext) -> bool {
1333        let Some(state) = self.state.as_ref() else {
1334            return false;
1335        };
1336        state.set(AUTH_STATE_KEY, auth)
1337    }
1338
1339    /// Sets a value in session state.
1340    ///
1341    /// The value persists across requests within the same session.
1342    /// Returns `true` if the value was successfully stored.
1343    /// Returns `false` if session state is not available or serialization fails.
1344    ///
1345    /// # Example
1346    ///
1347    /// ```ignore
1348    /// async fn my_tool(ctx: &McpContext, args: MyArgs) -> McpResult<Value> {
1349    ///     // Increment a counter in session state
1350    ///     let count: i32 = ctx.get_state("counter").unwrap_or(0);
1351    ///     ctx.set_state("counter", count + 1);
1352    ///     Ok(json!({"new_count": count + 1}))
1353    /// }
1354    /// ```
1355    pub fn set_state<T: serde::Serialize>(&self, key: impl Into<String>, value: T) -> bool {
1356        match &self.state {
1357            Some(state) => state.set(key, value),
1358            None => false,
1359        }
1360    }
1361
1362    /// Removes a value from session state.
1363    ///
1364    /// Returns the previous value if it existed, or `None` if:
1365    /// - Session state is not available
1366    /// - The key didn't exist
1367    pub fn remove_state(&self, key: &str) -> Option<serde_json::Value> {
1368        self.state.as_ref()?.remove(key)
1369    }
1370
1371    /// Checks if a key exists in session state.
1372    ///
1373    /// Returns `false` if session state is not available.
1374    #[must_use]
1375    pub fn has_state(&self, key: &str) -> bool {
1376        self.state.as_ref().is_some_and(|s| s.contains(key))
1377    }
1378
1379    /// Returns whether session state is available in this context.
1380    #[must_use]
1381    pub fn has_session_state(&self) -> bool {
1382        self.state.is_some()
1383    }
1384
1385    // ========================================================================
1386    // Capabilities Access
1387    // ========================================================================
1388
1389    /// Returns the client capability information, if available.
1390    ///
1391    /// Capabilities are set by the server after initialization and reflect
1392    /// what the connected client supports.
1393    #[must_use]
1394    pub fn client_capabilities(&self) -> Option<&ClientCapabilityInfo> {
1395        self.client_capabilities.as_ref()
1396    }
1397
1398    /// Returns the server capability information, if available.
1399    ///
1400    /// Reflects what capabilities this server advertises.
1401    #[must_use]
1402    pub fn server_capabilities(&self) -> Option<&ServerCapabilityInfo> {
1403        self.server_capabilities.as_ref()
1404    }
1405
1406    /// Returns whether the client supports sampling (LLM completions).
1407    ///
1408    /// This is a convenience method that checks the client capabilities.
1409    /// Returns `false` if capabilities are not yet available (before initialization).
1410    #[must_use]
1411    pub fn client_supports_sampling(&self) -> bool {
1412        self.client_capabilities
1413            .as_ref()
1414            .is_some_and(|c| c.sampling)
1415    }
1416
1417    /// Returns whether the client supports elicitation (user input requests).
1418    ///
1419    /// This is a convenience method that checks the client capabilities.
1420    /// Returns `false` if capabilities are not yet available.
1421    #[must_use]
1422    pub fn client_supports_elicitation(&self) -> bool {
1423        self.client_capabilities
1424            .as_ref()
1425            .is_some_and(|c| c.elicitation)
1426    }
1427
1428    /// Returns whether the client supports form-mode elicitation.
1429    #[must_use]
1430    pub fn client_supports_elicitation_form(&self) -> bool {
1431        self.client_capabilities
1432            .as_ref()
1433            .is_some_and(|c| c.elicitation_form)
1434    }
1435
1436    /// Returns whether the client supports URL-mode elicitation.
1437    #[must_use]
1438    pub fn client_supports_elicitation_url(&self) -> bool {
1439        self.client_capabilities
1440            .as_ref()
1441            .is_some_and(|c| c.elicitation_url)
1442    }
1443
1444    /// Returns whether the client supports roots listing.
1445    ///
1446    /// This is a convenience method that checks the client capabilities.
1447    /// Returns `false` if capabilities are not yet available.
1448    #[must_use]
1449    pub fn client_supports_roots(&self) -> bool {
1450        self.client_capabilities.as_ref().is_some_and(|c| c.roots)
1451    }
1452
1453    // ========================================================================
1454    // Dynamic Component Enable/Disable
1455    // ========================================================================
1456
1457    /// Session state key for disabled tools.
1458    const DISABLED_TOOLS_KEY: &'static str = "fastmcp.disabled_tools";
1459    /// Session state key for disabled resources.
1460    const DISABLED_RESOURCES_KEY: &'static str = "fastmcp.disabled_resources";
1461    /// Session state key for disabled prompts.
1462    const DISABLED_PROMPTS_KEY: &'static str = "fastmcp.disabled_prompts";
1463
1464    /// Disables a tool for this session.
1465    ///
1466    /// Disabled tools will not appear in `tools/list` responses and will return
1467    /// an error if called directly. This is useful for adapting available
1468    /// functionality based on user permissions, feature flags, or runtime conditions.
1469    ///
1470    /// Returns `true` if the operation succeeded, `false` if session state is unavailable.
1471    ///
1472    /// # Example
1473    ///
1474    /// ```ignore
1475    /// async fn my_tool(ctx: &McpContext) -> McpResult<String> {
1476    ///     // Disable the "admin_tool" for this session
1477    ///     ctx.disable_tool("admin_tool");
1478    ///     Ok("Admin tool disabled".to_string())
1479    /// }
1480    /// ```
1481    pub fn disable_tool(&self, name: impl Into<String>) -> bool {
1482        self.add_to_disabled_set(Self::DISABLED_TOOLS_KEY, name.into())
1483    }
1484
1485    /// Enables a previously disabled tool for this session.
1486    ///
1487    /// Returns `true` if the operation succeeded, `false` if session state is unavailable.
1488    pub fn enable_tool(&self, name: &str) -> bool {
1489        self.remove_from_disabled_set(Self::DISABLED_TOOLS_KEY, name)
1490    }
1491
1492    /// Returns whether a tool is enabled (not disabled) for this session.
1493    ///
1494    /// Tools are enabled by default unless explicitly disabled.
1495    #[must_use]
1496    pub fn is_tool_enabled(&self, name: &str) -> bool {
1497        !self.is_in_disabled_set(Self::DISABLED_TOOLS_KEY, name)
1498    }
1499
1500    /// Disables a resource for this session.
1501    ///
1502    /// Disabled resources will not appear in `resources/list` responses and will
1503    /// return an error if read directly.
1504    ///
1505    /// Returns `true` if the operation succeeded, `false` if session state is unavailable.
1506    pub fn disable_resource(&self, uri: impl Into<String>) -> bool {
1507        self.add_to_disabled_set(Self::DISABLED_RESOURCES_KEY, uri.into())
1508    }
1509
1510    /// Enables a previously disabled resource for this session.
1511    ///
1512    /// Returns `true` if the operation succeeded, `false` if session state is unavailable.
1513    pub fn enable_resource(&self, uri: &str) -> bool {
1514        self.remove_from_disabled_set(Self::DISABLED_RESOURCES_KEY, uri)
1515    }
1516
1517    /// Returns whether a resource is enabled (not disabled) for this session.
1518    ///
1519    /// Resources are enabled by default unless explicitly disabled.
1520    #[must_use]
1521    pub fn is_resource_enabled(&self, uri: &str) -> bool {
1522        !self.is_in_disabled_set(Self::DISABLED_RESOURCES_KEY, uri)
1523    }
1524
1525    /// Disables a prompt for this session.
1526    ///
1527    /// Disabled prompts will not appear in `prompts/list` responses and will
1528    /// return an error if retrieved directly.
1529    ///
1530    /// Returns `true` if the operation succeeded, `false` if session state is unavailable.
1531    pub fn disable_prompt(&self, name: impl Into<String>) -> bool {
1532        self.add_to_disabled_set(Self::DISABLED_PROMPTS_KEY, name.into())
1533    }
1534
1535    /// Enables a previously disabled prompt for this session.
1536    ///
1537    /// Returns `true` if the operation succeeded, `false` if session state is unavailable.
1538    pub fn enable_prompt(&self, name: &str) -> bool {
1539        self.remove_from_disabled_set(Self::DISABLED_PROMPTS_KEY, name)
1540    }
1541
1542    /// Returns whether a prompt is enabled (not disabled) for this session.
1543    ///
1544    /// Prompts are enabled by default unless explicitly disabled.
1545    #[must_use]
1546    pub fn is_prompt_enabled(&self, name: &str) -> bool {
1547        !self.is_in_disabled_set(Self::DISABLED_PROMPTS_KEY, name)
1548    }
1549
1550    /// Returns the set of disabled tools for this session.
1551    #[must_use]
1552    pub fn disabled_tools(&self) -> std::collections::HashSet<String> {
1553        self.get_disabled_set(Self::DISABLED_TOOLS_KEY)
1554    }
1555
1556    /// Returns the set of disabled resources for this session.
1557    #[must_use]
1558    pub fn disabled_resources(&self) -> std::collections::HashSet<String> {
1559        self.get_disabled_set(Self::DISABLED_RESOURCES_KEY)
1560    }
1561
1562    /// Returns the set of disabled prompts for this session.
1563    #[must_use]
1564    pub fn disabled_prompts(&self) -> std::collections::HashSet<String> {
1565        self.get_disabled_set(Self::DISABLED_PROMPTS_KEY)
1566    }
1567
1568    // Helper: Add a name to a disabled set
1569    fn add_to_disabled_set(&self, key: &str, name: String) -> bool {
1570        let Some(state) = self.state.as_ref() else {
1571            return false;
1572        };
1573        let mut set: std::collections::HashSet<String> = state.get(key).unwrap_or_default();
1574        set.insert(name);
1575        state.set(key, set)
1576    }
1577
1578    // Helper: Remove a name from a disabled set
1579    fn remove_from_disabled_set(&self, key: &str, name: &str) -> bool {
1580        let Some(state) = self.state.as_ref() else {
1581            return false;
1582        };
1583        let mut set: std::collections::HashSet<String> = state.get(key).unwrap_or_default();
1584        set.remove(name);
1585        state.set(key, set)
1586    }
1587
1588    // Helper: Check if a name is in a disabled set
1589    fn is_in_disabled_set(&self, key: &str, name: &str) -> bool {
1590        let Some(state) = self.state.as_ref() else {
1591            return false;
1592        };
1593        let set: std::collections::HashSet<String> = state.get(key).unwrap_or_default();
1594        set.contains(name)
1595    }
1596
1597    // Helper: Get the full disabled set
1598    fn get_disabled_set(&self, key: &str) -> std::collections::HashSet<String> {
1599        self.state
1600            .as_ref()
1601            .and_then(|s| s.get(key))
1602            .unwrap_or_default()
1603    }
1604
1605    // ========================================================================
1606    // Sampling (LLM Completions)
1607    // ========================================================================
1608
1609    /// Returns whether sampling is available in this context.
1610    ///
1611    /// Sampling is available when the client has advertised sampling
1612    /// capability and a sampling sender has been configured.
1613    #[must_use]
1614    pub fn can_sample(&self) -> bool {
1615        self.sampling_sender.is_some()
1616    }
1617
1618    /// Requests an LLM completion from the client.
1619    ///
1620    /// This is a convenience method for simple text prompts. For more control
1621    /// over the request, use [`sample_with_request`](Self::sample_with_request).
1622    ///
1623    /// # Arguments
1624    ///
1625    /// * `prompt` - The prompt text to send (as a user message)
1626    /// * `max_tokens` - Maximum number of tokens to generate
1627    ///
1628    /// # Errors
1629    ///
1630    /// Returns an error if:
1631    /// - The client doesn't support sampling
1632    /// - The sampling request fails
1633    ///
1634    /// # Example
1635    ///
1636    /// ```ignore
1637    /// async fn my_tool(ctx: &McpContext, topic: String) -> McpResult<String> {
1638    ///     let response = ctx.sample(&format!("Write a haiku about {topic}"), 100).await?;
1639    ///     Ok(response.text)
1640    /// }
1641    /// ```
1642    pub async fn sample(
1643        &self,
1644        prompt: impl Into<String>,
1645        max_tokens: u32,
1646    ) -> crate::McpResult<SamplingResponse> {
1647        let request = SamplingRequest::prompt(prompt, max_tokens);
1648        self.sample_with_request(request).await
1649    }
1650
1651    /// Requests an LLM completion with full control over the request.
1652    ///
1653    /// # Arguments
1654    ///
1655    /// * `request` - The full sampling request parameters
1656    ///
1657    /// # Errors
1658    ///
1659    /// Returns an error if:
1660    /// - The client doesn't support sampling
1661    /// - The sampling request fails
1662    ///
1663    /// # Example
1664    ///
1665    /// ```ignore
1666    /// async fn my_tool(ctx: &McpContext) -> McpResult<String> {
1667    ///     let request = SamplingRequest::new(
1668    ///         vec![
1669    ///             SamplingRequestMessage::user("Hello!"),
1670    ///             SamplingRequestMessage::assistant("Hi! How can I help?"),
1671    ///             SamplingRequestMessage::user("Tell me a joke."),
1672    ///         ],
1673    ///         200,
1674    ///     )
1675    ///     .with_system_prompt("You are a helpful and funny assistant.")
1676    ///     .with_temperature(0.8);
1677    ///
1678    ///     let response = ctx.sample_with_request(request).await?;
1679    ///     Ok(response.text)
1680    /// }
1681    /// ```
1682    pub async fn sample_with_request(
1683        &self,
1684        request: SamplingRequest,
1685    ) -> crate::McpResult<SamplingResponse> {
1686        let sender = self.sampling_sender.as_ref().ok_or_else(|| {
1687            crate::McpError::new(
1688                crate::McpErrorCode::InvalidRequest,
1689                "Sampling not available: client does not support sampling capability",
1690            )
1691        })?;
1692
1693        sender.create_message(request).await
1694    }
1695
1696    // ========================================================================
1697    // Elicitation (User Input Requests)
1698    // ========================================================================
1699
1700    /// Returns whether elicitation is available in this context.
1701    ///
1702    /// Elicitation is available when the client has advertised elicitation
1703    /// capability and an elicitation sender has been configured.
1704    #[must_use]
1705    pub fn can_elicit(&self) -> bool {
1706        self.elicitation_sender.is_some()
1707    }
1708
1709    /// Requests user input via a form.
1710    ///
1711    /// This presents a form to the user with fields defined by the JSON schema.
1712    /// The user can accept (submit the form), decline, or cancel.
1713    ///
1714    /// # Arguments
1715    ///
1716    /// * `message` - Message to display explaining what input is needed
1717    /// * `schema` - JSON Schema defining the form fields
1718    ///
1719    /// # Errors
1720    ///
1721    /// Returns an error if:
1722    /// - The client doesn't support elicitation
1723    /// - The elicitation request fails
1724    ///
1725    /// # Example
1726    ///
1727    /// ```ignore
1728    /// async fn my_tool(ctx: &McpContext) -> McpResult<String> {
1729    ///     let schema = serde_json::json!({
1730    ///         "type": "object",
1731    ///         "properties": {
1732    ///             "name": {"type": "string"},
1733    ///             "age": {"type": "integer"}
1734    ///         },
1735    ///         "required": ["name"]
1736    ///     });
1737    ///     let response = ctx.elicit_form("Please enter your details", schema).await?;
1738    ///     if response.is_accepted() {
1739    ///         let name = response.get_string("name").unwrap_or("Unknown");
1740    ///         Ok(format!("Hello, {name}!"))
1741    ///     } else {
1742    ///         Ok("User declined input".to_string())
1743    ///     }
1744    /// }
1745    /// ```
1746    pub async fn elicit_form(
1747        &self,
1748        message: impl Into<String>,
1749        schema: serde_json::Value,
1750    ) -> crate::McpResult<ElicitationResponse> {
1751        let request = ElicitationRequest::form(message, schema);
1752        self.elicit_with_request(request).await
1753    }
1754
1755    /// Requests user interaction via an external URL.
1756    ///
1757    /// This directs the user to an external URL for sensitive operations like
1758    /// OAuth flows, payment processing, or credential collection.
1759    ///
1760    /// # Arguments
1761    ///
1762    /// * `message` - Message to display explaining why the URL visit is needed
1763    /// * `url` - The URL the user should navigate to
1764    /// * `elicitation_id` - Unique ID for tracking this elicitation
1765    ///
1766    /// # Errors
1767    ///
1768    /// Returns an error if:
1769    /// - The client doesn't support elicitation
1770    /// - The elicitation request fails
1771    ///
1772    /// # Example
1773    ///
1774    /// ```ignore
1775    /// async fn my_tool(ctx: &McpContext) -> McpResult<String> {
1776    ///     let response = ctx.elicit_url(
1777    ///         "Please authenticate with your GitHub account",
1778    ///         "https://github.com/login/oauth/authorize?...",
1779    ///         "github-auth-12345",
1780    ///     ).await?;
1781    ///     if response.is_accepted() {
1782    ///         Ok("Authentication successful".to_string())
1783    ///     } else {
1784    ///         Ok("Authentication cancelled".to_string())
1785    ///     }
1786    /// }
1787    /// ```
1788    pub async fn elicit_url(
1789        &self,
1790        message: impl Into<String>,
1791        url: impl Into<String>,
1792        elicitation_id: impl Into<String>,
1793    ) -> crate::McpResult<ElicitationResponse> {
1794        let request = ElicitationRequest::url(message, url, elicitation_id);
1795        self.elicit_with_request(request).await
1796    }
1797
1798    /// Requests user input with full control over the request.
1799    ///
1800    /// # Arguments
1801    ///
1802    /// * `request` - The full elicitation request parameters
1803    ///
1804    /// # Errors
1805    ///
1806    /// Returns an error if:
1807    /// - The client doesn't support elicitation
1808    /// - The elicitation request fails
1809    pub async fn elicit_with_request(
1810        &self,
1811        request: ElicitationRequest,
1812    ) -> crate::McpResult<ElicitationResponse> {
1813        let sender = self.elicitation_sender.as_ref().ok_or_else(|| {
1814            crate::McpError::new(
1815                crate::McpErrorCode::InvalidRequest,
1816                "Elicitation not available: client does not support elicitation capability",
1817            )
1818        })?;
1819
1820        sender.elicit(request).await
1821    }
1822
1823    // ========================================================================
1824    // Resource Reading (Cross-Component Access)
1825    // ========================================================================
1826
1827    /// Returns whether resource reading is available in this context.
1828    ///
1829    /// Resource reading is available when a resource reader (Router) has
1830    /// been attached to this context.
1831    #[must_use]
1832    pub fn can_read_resources(&self) -> bool {
1833        self.resource_reader.is_some()
1834    }
1835
1836    /// Returns the current resource read depth.
1837    ///
1838    /// This is used to track recursion when resources read other resources.
1839    #[must_use]
1840    pub fn resource_read_depth(&self) -> u32 {
1841        self.resource_read_depth
1842    }
1843
1844    /// Reads a resource by URI.
1845    ///
1846    /// This allows tools, resources, and prompts to read other resources
1847    /// configured on the same server. This enables composition and code reuse.
1848    ///
1849    /// # Arguments
1850    ///
1851    /// * `uri` - The resource URI to read
1852    ///
1853    /// # Errors
1854    ///
1855    /// Returns an error if:
1856    /// - No resource reader is available (context not configured for resource access)
1857    /// - The resource is not found
1858    /// - Maximum recursion depth is exceeded
1859    /// - The resource read fails
1860    ///
1861    /// # Example
1862    ///
1863    /// ```ignore
1864    /// #[tool]
1865    /// async fn process_config(ctx: &McpContext) -> Result<String, ToolError> {
1866    ///     let config = ctx.read_resource("config://app").await?;
1867    ///     let text = config.first_text()
1868    ///         .ok_or(ToolError::InvalidConfig)?;
1869    ///     Ok(format!("Config loaded: {}", text))
1870    /// }
1871    /// ```
1872    pub async fn read_resource(&self, uri: &str) -> crate::McpResult<ResourceReadResult> {
1873        // Check if we have a resource reader
1874        let reader = self.resource_reader.as_ref().ok_or_else(|| {
1875            crate::McpError::new(
1876                crate::McpErrorCode::InternalError,
1877                "Resource reading not available: no router attached to context",
1878            )
1879        })?;
1880
1881        // Check recursion depth
1882        if self.resource_read_depth >= MAX_RESOURCE_READ_DEPTH {
1883            return Err(crate::McpError::new(
1884                crate::McpErrorCode::InternalError,
1885                format!(
1886                    "Maximum resource read depth ({}) exceeded; possible infinite recursion",
1887                    MAX_RESOURCE_READ_DEPTH
1888                ),
1889            ));
1890        }
1891
1892        // Read the resource with incremented depth
1893        reader
1894            .read_resource(&self.cx, uri, self.resource_read_depth + 1)
1895            .await
1896    }
1897
1898    /// Reads a resource and extracts the text content.
1899    ///
1900    /// This is a convenience method that reads a resource and returns
1901    /// the first text content item.
1902    ///
1903    /// # Errors
1904    ///
1905    /// Returns an error if:
1906    /// - The resource read fails
1907    /// - The resource has no text content
1908    ///
1909    /// # Example
1910    ///
1911    /// ```ignore
1912    /// let text = ctx.read_resource_text("file://readme.md").await?;
1913    /// println!("Content: {}", text);
1914    /// ```
1915    pub async fn read_resource_text(&self, uri: &str) -> crate::McpResult<String> {
1916        let result = self.read_resource(uri).await?;
1917        result.first_text().map(String::from).ok_or_else(|| {
1918            crate::McpError::new(
1919                crate::McpErrorCode::InternalError,
1920                format!("Resource '{}' has no text content", uri),
1921            )
1922        })
1923    }
1924
1925    /// Reads a resource and parses it as JSON.
1926    ///
1927    /// This is a convenience method that reads a resource and deserializes
1928    /// the text content as JSON.
1929    ///
1930    /// # Errors
1931    ///
1932    /// Returns an error if:
1933    /// - The resource read fails
1934    /// - The resource has no text content
1935    /// - JSON deserialization fails
1936    ///
1937    /// # Example
1938    ///
1939    /// ```ignore
1940    /// #[derive(Deserialize)]
1941    /// struct Config {
1942    ///     database_url: String,
1943    /// }
1944    ///
1945    /// let config: Config = ctx.read_resource_json("config://app").await?;
1946    /// println!("Database: {}", config.database_url);
1947    /// ```
1948    pub async fn read_resource_json<T: serde::de::DeserializeOwned>(
1949        &self,
1950        uri: &str,
1951    ) -> crate::McpResult<T> {
1952        let text = self.read_resource_text(uri).await?;
1953        serde_json::from_str(&text).map_err(|e| {
1954            crate::McpError::new(
1955                crate::McpErrorCode::InternalError,
1956                format!("Failed to parse resource '{}' as JSON: {}", uri, e),
1957            )
1958        })
1959    }
1960
1961    // ========================================================================
1962    // Tool Calling (Cross-Component Access)
1963    // ========================================================================
1964
1965    /// Returns whether tool calling is available in this context.
1966    ///
1967    /// Tool calling is available when a tool caller (Router) has
1968    /// been attached to this context.
1969    #[must_use]
1970    pub fn can_call_tools(&self) -> bool {
1971        self.tool_caller.is_some()
1972    }
1973
1974    /// Returns the current tool call depth.
1975    ///
1976    /// This is used to track recursion when tools call other tools.
1977    #[must_use]
1978    pub fn tool_call_depth(&self) -> u32 {
1979        self.tool_call_depth
1980    }
1981
1982    /// Calls a tool by name with the given arguments.
1983    ///
1984    /// This allows tools, resources, and prompts to call other tools
1985    /// configured on the same server. This enables composition and code reuse.
1986    ///
1987    /// # Arguments
1988    ///
1989    /// * `name` - The tool name to call
1990    /// * `args` - The arguments as a JSON value
1991    ///
1992    /// # Errors
1993    ///
1994    /// Returns an error if:
1995    /// - No tool caller is available (context not configured for tool access)
1996    /// - The tool is not found
1997    /// - Maximum recursion depth is exceeded
1998    /// - The tool execution fails
1999    ///
2000    /// # Example
2001    ///
2002    /// ```ignore
2003    /// #[tool]
2004    /// async fn double_add(ctx: &McpContext, a: i32, b: i32) -> Result<i32, ToolError> {
2005    ///     let sum: i32 = ctx.call_tool_json("add", json!({"a": a, "b": b})).await?;
2006    ///     Ok(sum * 2)
2007    /// }
2008    /// ```
2009    pub async fn call_tool(
2010        &self,
2011        name: &str,
2012        args: serde_json::Value,
2013    ) -> crate::McpResult<ToolCallResult> {
2014        // Check if we have a tool caller
2015        let caller = self.tool_caller.as_ref().ok_or_else(|| {
2016            crate::McpError::new(
2017                crate::McpErrorCode::InternalError,
2018                "Tool calling not available: no router attached to context",
2019            )
2020        })?;
2021
2022        // Check recursion depth
2023        if self.tool_call_depth >= MAX_TOOL_CALL_DEPTH {
2024            return Err(crate::McpError::new(
2025                crate::McpErrorCode::InternalError,
2026                format!(
2027                    "Maximum tool call depth ({}) exceeded calling '{}'; possible infinite recursion",
2028                    MAX_TOOL_CALL_DEPTH, name
2029                ),
2030            ));
2031        }
2032
2033        // Call the tool with incremented depth
2034        caller
2035            .call_tool(&self.cx, name, args, self.tool_call_depth + 1)
2036            .await
2037    }
2038
2039    /// Calls a tool and extracts the text content.
2040    ///
2041    /// This is a convenience method that calls a tool and returns
2042    /// the first text content item.
2043    ///
2044    /// # Errors
2045    ///
2046    /// Returns an error if:
2047    /// - The tool call fails
2048    /// - The tool returns an error result
2049    /// - The tool has no text content
2050    ///
2051    /// # Example
2052    ///
2053    /// ```ignore
2054    /// let greeting = ctx.call_tool_text("greet", json!({"name": "World"})).await?;
2055    /// println!("Result: {}", greeting);
2056    /// ```
2057    pub async fn call_tool_text(
2058        &self,
2059        name: &str,
2060        args: serde_json::Value,
2061    ) -> crate::McpResult<String> {
2062        let result = self.call_tool(name, args).await?;
2063
2064        // Check if tool returned an error
2065        if result.is_error {
2066            let error_msg = result.first_text().unwrap_or("Tool returned an error");
2067            return Err(crate::McpError::new(
2068                crate::McpErrorCode::InternalError,
2069                format!("Tool '{}' failed: {}", name, error_msg),
2070            ));
2071        }
2072
2073        result.first_text().map(String::from).ok_or_else(|| {
2074            crate::McpError::new(
2075                crate::McpErrorCode::InternalError,
2076                format!("Tool '{}' returned no text content", name),
2077            )
2078        })
2079    }
2080
2081    /// Calls a tool and parses the result as JSON.
2082    ///
2083    /// This is a convenience method that calls a tool and deserializes
2084    /// the text content as JSON.
2085    ///
2086    /// # Errors
2087    ///
2088    /// Returns an error if:
2089    /// - The tool call fails
2090    /// - The tool returns an error result
2091    /// - The tool has no text content
2092    /// - JSON deserialization fails
2093    ///
2094    /// # Example
2095    ///
2096    /// ```ignore
2097    /// #[derive(Deserialize)]
2098    /// struct ComputeResult {
2099    ///     value: i64,
2100    /// }
2101    ///
2102    /// let result: ComputeResult = ctx.call_tool_json("compute", json!({"x": 5})).await?;
2103    /// println!("Result: {}", result.value);
2104    /// ```
2105    pub async fn call_tool_json<T: serde::de::DeserializeOwned>(
2106        &self,
2107        name: &str,
2108        args: serde_json::Value,
2109    ) -> crate::McpResult<T> {
2110        let text = self.call_tool_text(name, args).await?;
2111        serde_json::from_str(&text).map_err(|e| {
2112            crate::McpError::new(
2113                crate::McpErrorCode::InternalError,
2114                format!("Failed to parse tool '{}' result as JSON: {}", name, e),
2115            )
2116        })
2117    }
2118
2119    // ========================================================================
2120    // Parallel Combinators
2121    // ========================================================================
2122
2123    /// Waits for all futures to complete and returns their results.
2124    ///
2125    /// This is the N-of-N combinator: all futures must complete before
2126    /// returning. Results are returned in the same order as input futures.
2127    ///
2128    /// # Example
2129    ///
2130    /// ```ignore
2131    /// let futures = vec![
2132    ///     Box::pin(fetch_user(1)),
2133    ///     Box::pin(fetch_user(2)),
2134    ///     Box::pin(fetch_user(3)),
2135    /// ];
2136    /// let users = ctx.join_all(futures).await;
2137    /// ```
2138    pub async fn join_all<T: Send + 'static>(
2139        &self,
2140        futures: Vec<crate::combinator::BoxFuture<'_, T>>,
2141    ) -> Vec<T> {
2142        crate::combinator::join_all(&self.cx, futures).await
2143    }
2144
2145    /// Races multiple futures, returning the first to complete.
2146    ///
2147    /// This is the 1-of-N combinator: the first future to complete wins,
2148    /// and all others are cancelled and drained.
2149    ///
2150    /// # Example
2151    ///
2152    /// ```ignore
2153    /// let futures = vec![
2154    ///     Box::pin(fetch_from_primary()),
2155    ///     Box::pin(fetch_from_replica()),
2156    /// ];
2157    /// let result = ctx.race(futures).await?;
2158    /// ```
2159    pub async fn race<T: Send + 'static>(
2160        &self,
2161        futures: Vec<crate::combinator::BoxFuture<'_, T>>,
2162    ) -> crate::McpResult<T> {
2163        crate::combinator::race(&self.cx, futures).await
2164    }
2165
2166    /// Waits for M of N futures to complete successfully.
2167    ///
2168    /// Returns when `required` futures have completed successfully.
2169    /// Remaining futures are cancelled.
2170    ///
2171    /// # Example
2172    ///
2173    /// ```ignore
2174    /// let futures = vec![
2175    ///     Box::pin(write_to_replica(1)),
2176    ///     Box::pin(write_to_replica(2)),
2177    ///     Box::pin(write_to_replica(3)),
2178    /// ];
2179    /// let result = ctx.quorum(2, futures).await?;
2180    /// ```
2181    pub async fn quorum<T: Send + 'static>(
2182        &self,
2183        required: usize,
2184        futures: Vec<crate::combinator::BoxFuture<'_, crate::McpResult<T>>>,
2185    ) -> crate::McpResult<crate::combinator::QuorumResult<T>> {
2186        crate::combinator::quorum(&self.cx, required, futures).await
2187    }
2188
2189    /// Races futures and returns the first successful result.
2190    ///
2191    /// Unlike `race` which returns the first to complete (success or failure),
2192    /// `first_ok` returns the first to complete successfully.
2193    ///
2194    /// # Example
2195    ///
2196    /// ```ignore
2197    /// let futures = vec![
2198    ///     Box::pin(try_primary()),
2199    ///     Box::pin(try_fallback()),
2200    /// ];
2201    /// let result = ctx.first_ok(futures).await?;
2202    /// ```
2203    pub async fn first_ok<T: Send + 'static>(
2204        &self,
2205        futures: Vec<crate::combinator::BoxFuture<'_, crate::McpResult<T>>>,
2206    ) -> crate::McpResult<T> {
2207        crate::combinator::first_ok(&self.cx, futures).await
2208    }
2209}
2210
2211/// Error returned when a request has been cancelled.
2212///
2213/// This is returned by `checkpoint()` when the request should stop
2214/// processing. The server will convert this to an appropriate MCP
2215/// error response.
2216#[derive(Debug, Clone, Copy)]
2217pub struct CancelledError;
2218
2219impl std::fmt::Display for CancelledError {
2220    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2221        write!(f, "request cancelled")
2222    }
2223}
2224
2225impl std::error::Error for CancelledError {}
2226
2227/// Extension trait for converting MCP results to asupersync Outcome.
2228///
2229/// This bridges the MCP error model with asupersync's 4-valued outcome
2230/// (Ok, Err, Cancelled, Panicked).
2231pub trait IntoOutcome<T, E> {
2232    /// Converts this result into an asupersync Outcome.
2233    fn into_outcome(self) -> Outcome<T, E>;
2234}
2235
2236impl<T, E> IntoOutcome<T, E> for Result<T, E> {
2237    fn into_outcome(self) -> Outcome<T, E> {
2238        match self {
2239            Ok(v) => Outcome::Ok(v),
2240            Err(e) => Outcome::Err(e),
2241        }
2242    }
2243}
2244
2245impl<T, E> IntoOutcome<T, E> for Result<T, CancelledError>
2246where
2247    E: Default,
2248{
2249    fn into_outcome(self) -> Outcome<T, E> {
2250        match self {
2251            Ok(v) => Outcome::Ok(v),
2252            Err(CancelledError) => Outcome::Cancelled(CancelReason::user("request cancelled")),
2253        }
2254    }
2255}
2256
2257#[cfg(test)]
2258mod tests {
2259    use super::*;
2260
2261    #[test]
2262    fn test_mcp_context_creation() {
2263        let cx = Cx::for_testing();
2264        let ctx = McpContext::new(cx, 42);
2265
2266        assert_eq!(ctx.request_id(), 42);
2267    }
2268
2269    #[test]
2270    fn test_mcp_context_not_cancelled_initially() {
2271        let cx = Cx::for_testing();
2272        let ctx = McpContext::new(cx, 1);
2273
2274        assert!(!ctx.is_cancelled());
2275    }
2276
2277    #[test]
2278    fn test_mcp_context_checkpoint_success() {
2279        let cx = Cx::for_testing();
2280        let ctx = McpContext::new(cx, 1);
2281
2282        // Should succeed when not cancelled
2283        assert!(ctx.checkpoint().is_ok());
2284    }
2285
2286    #[test]
2287    fn test_mcp_context_checkpoint_cancelled() {
2288        let cx = Cx::for_testing();
2289        cx.set_cancel_requested(true);
2290        let ctx = McpContext::new(cx, 1);
2291
2292        // Should fail when cancelled
2293        assert!(ctx.checkpoint().is_err());
2294    }
2295
2296    #[test]
2297    fn test_mcp_context_checkpoint_budget_exhausted() {
2298        let cx = Cx::for_testing_with_budget(Budget::ZERO);
2299        let ctx = McpContext::new(cx, 1);
2300
2301        // Should fail when budget is exhausted
2302        assert!(ctx.checkpoint().is_err());
2303    }
2304
2305    #[test]
2306    fn test_mcp_context_masked_section() {
2307        let cx = Cx::for_testing();
2308        let ctx = McpContext::new(cx, 1);
2309
2310        // masked() should execute the closure and return its value
2311        let result = ctx.masked(|| 42);
2312        assert_eq!(result, 42);
2313    }
2314
2315    #[test]
2316    fn test_mcp_context_budget() {
2317        let cx = Cx::for_testing();
2318        let ctx = McpContext::new(cx, 1);
2319
2320        // Budget should be available
2321        let budget = ctx.budget();
2322        // For testing Cx, budget should not be exhausted
2323        assert!(!budget.is_exhausted());
2324    }
2325
2326    #[test]
2327    fn test_cancelled_error_display() {
2328        let err = CancelledError;
2329        assert_eq!(err.to_string(), "request cancelled");
2330    }
2331
2332    #[test]
2333    fn test_into_outcome_ok() {
2334        let result: Result<i32, CancelledError> = Ok(42);
2335        let outcome: Outcome<i32, CancelledError> = result.into_outcome();
2336        assert!(matches!(outcome, Outcome::Ok(42)));
2337    }
2338
2339    #[test]
2340    fn test_into_outcome_cancelled() {
2341        let result: Result<i32, CancelledError> = Err(CancelledError);
2342        let outcome: Outcome<i32, ()> = result.into_outcome();
2343        assert!(matches!(outcome, Outcome::Cancelled(_)));
2344    }
2345
2346    #[test]
2347    fn test_mcp_context_no_progress_reporter_by_default() {
2348        let cx = Cx::for_testing();
2349        let ctx = McpContext::new(cx, 1);
2350        assert!(!ctx.has_progress_reporter());
2351    }
2352
2353    #[test]
2354    fn test_mcp_context_with_progress_reporter() {
2355        let cx = Cx::for_testing();
2356        let sender = Arc::new(NoOpNotificationSender);
2357        let reporter = ProgressReporter::new(sender);
2358        let ctx = McpContext::with_progress(cx, 1, reporter);
2359        assert!(ctx.has_progress_reporter());
2360    }
2361
2362    #[test]
2363    fn test_report_progress_without_reporter() {
2364        let cx = Cx::for_testing();
2365        let ctx = McpContext::new(cx, 1);
2366        // Should not panic when no reporter is set
2367        ctx.report_progress(0.5, Some("test"));
2368        ctx.report_progress_with_total(5.0, 10.0, None);
2369    }
2370
2371    #[test]
2372    fn test_report_progress_with_reporter() {
2373        use std::sync::atomic::{AtomicU32, Ordering};
2374
2375        struct CountingSender {
2376            count: AtomicU32,
2377        }
2378
2379        impl NotificationSender for CountingSender {
2380            fn send_progress(&self, _progress: f64, _total: Option<f64>, _message: Option<&str>) {
2381                self.count.fetch_add(1, Ordering::SeqCst);
2382            }
2383        }
2384
2385        let cx = Cx::for_testing();
2386        let sender = Arc::new(CountingSender {
2387            count: AtomicU32::new(0),
2388        });
2389        let reporter = ProgressReporter::new(sender.clone());
2390        let ctx = McpContext::with_progress(cx, 1, reporter);
2391
2392        ctx.report_progress(0.25, Some("step 1"));
2393        ctx.report_progress(0.5, None);
2394        ctx.report_progress_with_total(3.0, 4.0, Some("step 3"));
2395
2396        assert_eq!(sender.count.load(Ordering::SeqCst), 3);
2397    }
2398
2399    #[test]
2400    fn test_progress_reporter_debug() {
2401        let sender = Arc::new(NoOpNotificationSender);
2402        let reporter = ProgressReporter::new(sender);
2403        let debug = format!("{reporter:?}");
2404        assert!(debug.contains("ProgressReporter"));
2405    }
2406
2407    #[test]
2408    fn test_noop_notification_sender() {
2409        let sender = NoOpNotificationSender;
2410        // Should not panic
2411        sender.send_progress(0.5, Some(1.0), Some("test"));
2412    }
2413
2414    // Session state tests
2415    #[test]
2416    fn test_mcp_context_no_session_state_by_default() {
2417        let cx = Cx::for_testing();
2418        let ctx = McpContext::new(cx, 1);
2419        assert!(!ctx.has_session_state());
2420    }
2421
2422    #[test]
2423    fn test_mcp_context_with_session_state() {
2424        let cx = Cx::for_testing();
2425        let state = SessionState::new();
2426        let ctx = McpContext::with_state(cx, 1, state);
2427        assert!(ctx.has_session_state());
2428    }
2429
2430    #[test]
2431    fn test_mcp_context_get_set_state() {
2432        let cx = Cx::for_testing();
2433        let state = SessionState::new();
2434        let ctx = McpContext::with_state(cx, 1, state);
2435
2436        // Set a value
2437        assert!(ctx.set_state("counter", 42));
2438
2439        // Get the value back
2440        let value: Option<i32> = ctx.get_state("counter");
2441        assert_eq!(value, Some(42));
2442    }
2443
2444    #[test]
2445    fn test_mcp_context_state_not_available() {
2446        let cx = Cx::for_testing();
2447        let ctx = McpContext::new(cx, 1);
2448
2449        // set_state returns false when state is not available
2450        assert!(!ctx.set_state("key", "value"));
2451
2452        // get_state returns None when state is not available
2453        let value: Option<String> = ctx.get_state("key");
2454        assert!(value.is_none());
2455    }
2456
2457    #[test]
2458    fn test_mcp_context_has_state() {
2459        let cx = Cx::for_testing();
2460        let state = SessionState::new();
2461        let ctx = McpContext::with_state(cx, 1, state);
2462
2463        assert!(!ctx.has_state("missing"));
2464
2465        ctx.set_state("present", true);
2466        assert!(ctx.has_state("present"));
2467    }
2468
2469    #[test]
2470    fn test_mcp_context_remove_state() {
2471        let cx = Cx::for_testing();
2472        let state = SessionState::new();
2473        let ctx = McpContext::with_state(cx, 1, state);
2474
2475        ctx.set_state("key", "value");
2476        assert!(ctx.has_state("key"));
2477
2478        let removed = ctx.remove_state("key");
2479        assert!(removed.is_some());
2480        assert!(!ctx.has_state("key"));
2481    }
2482
2483    #[test]
2484    fn test_mcp_context_with_state_and_progress() {
2485        let cx = Cx::for_testing();
2486        let state = SessionState::new();
2487        let sender = Arc::new(NoOpNotificationSender);
2488        let reporter = ProgressReporter::new(sender);
2489
2490        let ctx = McpContext::with_state_and_progress(cx, 1, state, reporter);
2491
2492        assert!(ctx.has_session_state());
2493        assert!(ctx.has_progress_reporter());
2494    }
2495
2496    // ========================================================================
2497    // Dynamic Enable/Disable Tests
2498    // ========================================================================
2499
2500    #[test]
2501    fn test_mcp_context_tools_enabled_by_default() {
2502        let cx = Cx::for_testing();
2503        let state = SessionState::new();
2504        let ctx = McpContext::with_state(cx, 1, state);
2505
2506        assert!(ctx.is_tool_enabled("any_tool"));
2507        assert!(ctx.is_tool_enabled("another_tool"));
2508    }
2509
2510    #[test]
2511    fn test_mcp_context_disable_enable_tool() {
2512        let cx = Cx::for_testing();
2513        let state = SessionState::new();
2514        let ctx = McpContext::with_state(cx, 1, state);
2515
2516        // Tool is enabled by default
2517        assert!(ctx.is_tool_enabled("my_tool"));
2518
2519        // Disable the tool
2520        assert!(ctx.disable_tool("my_tool"));
2521        assert!(!ctx.is_tool_enabled("my_tool"));
2522        assert!(ctx.is_tool_enabled("other_tool"));
2523
2524        // Re-enable the tool
2525        assert!(ctx.enable_tool("my_tool"));
2526        assert!(ctx.is_tool_enabled("my_tool"));
2527    }
2528
2529    #[test]
2530    fn test_mcp_context_disable_enable_resource() {
2531        let cx = Cx::for_testing();
2532        let state = SessionState::new();
2533        let ctx = McpContext::with_state(cx, 1, state);
2534
2535        // Resource is enabled by default
2536        assert!(ctx.is_resource_enabled("file://secret"));
2537
2538        // Disable the resource
2539        assert!(ctx.disable_resource("file://secret"));
2540        assert!(!ctx.is_resource_enabled("file://secret"));
2541        assert!(ctx.is_resource_enabled("file://public"));
2542
2543        // Re-enable the resource
2544        assert!(ctx.enable_resource("file://secret"));
2545        assert!(ctx.is_resource_enabled("file://secret"));
2546    }
2547
2548    #[test]
2549    fn test_mcp_context_disable_enable_prompt() {
2550        let cx = Cx::for_testing();
2551        let state = SessionState::new();
2552        let ctx = McpContext::with_state(cx, 1, state);
2553
2554        // Prompt is enabled by default
2555        assert!(ctx.is_prompt_enabled("admin_prompt"));
2556
2557        // Disable the prompt
2558        assert!(ctx.disable_prompt("admin_prompt"));
2559        assert!(!ctx.is_prompt_enabled("admin_prompt"));
2560        assert!(ctx.is_prompt_enabled("user_prompt"));
2561
2562        // Re-enable the prompt
2563        assert!(ctx.enable_prompt("admin_prompt"));
2564        assert!(ctx.is_prompt_enabled("admin_prompt"));
2565    }
2566
2567    #[test]
2568    fn test_mcp_context_disable_multiple_tools() {
2569        let cx = Cx::for_testing();
2570        let state = SessionState::new();
2571        let ctx = McpContext::with_state(cx, 1, state);
2572
2573        ctx.disable_tool("tool1");
2574        ctx.disable_tool("tool2");
2575        ctx.disable_tool("tool3");
2576
2577        assert!(!ctx.is_tool_enabled("tool1"));
2578        assert!(!ctx.is_tool_enabled("tool2"));
2579        assert!(!ctx.is_tool_enabled("tool3"));
2580        assert!(ctx.is_tool_enabled("tool4"));
2581
2582        let disabled = ctx.disabled_tools();
2583        assert_eq!(disabled.len(), 3);
2584        assert!(disabled.contains("tool1"));
2585        assert!(disabled.contains("tool2"));
2586        assert!(disabled.contains("tool3"));
2587    }
2588
2589    #[test]
2590    fn test_mcp_context_disabled_sets_empty_by_default() {
2591        let cx = Cx::for_testing();
2592        let state = SessionState::new();
2593        let ctx = McpContext::with_state(cx, 1, state);
2594
2595        assert!(ctx.disabled_tools().is_empty());
2596        assert!(ctx.disabled_resources().is_empty());
2597        assert!(ctx.disabled_prompts().is_empty());
2598    }
2599
2600    #[test]
2601    fn test_mcp_context_enable_disable_no_state() {
2602        let cx = Cx::for_testing();
2603        let ctx = McpContext::new(cx, 1);
2604
2605        // Without session state, disable returns false
2606        assert!(!ctx.disable_tool("tool"));
2607        assert!(!ctx.enable_tool("tool"));
2608
2609        // But is_enabled returns true (default is enabled)
2610        assert!(ctx.is_tool_enabled("tool"));
2611    }
2612
2613    #[test]
2614    fn test_mcp_context_disabled_state_persists_across_contexts() {
2615        let state = SessionState::new();
2616
2617        // First context disables a tool
2618        {
2619            let cx = Cx::for_testing();
2620            let ctx = McpContext::with_state(cx, 1, state.clone());
2621            ctx.disable_tool("shared_tool");
2622        }
2623
2624        // Second context (same session state) sees the disabled tool
2625        {
2626            let cx = Cx::for_testing();
2627            let ctx = McpContext::with_state(cx, 2, state.clone());
2628            assert!(!ctx.is_tool_enabled("shared_tool"));
2629        }
2630    }
2631
2632    // ========================================================================
2633    // Capabilities Tests
2634    // ========================================================================
2635
2636    #[test]
2637    fn test_mcp_context_no_capabilities_by_default() {
2638        let cx = Cx::for_testing();
2639        let ctx = McpContext::new(cx, 1);
2640
2641        assert!(ctx.client_capabilities().is_none());
2642        assert!(ctx.server_capabilities().is_none());
2643        assert!(!ctx.client_supports_sampling());
2644        assert!(!ctx.client_supports_elicitation());
2645        assert!(!ctx.client_supports_roots());
2646    }
2647
2648    #[test]
2649    fn test_mcp_context_with_client_capabilities() {
2650        let cx = Cx::for_testing();
2651        let caps = ClientCapabilityInfo::new()
2652            .with_sampling()
2653            .with_elicitation(true, false)
2654            .with_roots(true);
2655
2656        let ctx = McpContext::new(cx, 1).with_client_capabilities(caps);
2657
2658        assert!(ctx.client_capabilities().is_some());
2659        assert!(ctx.client_supports_sampling());
2660        assert!(ctx.client_supports_elicitation());
2661        assert!(ctx.client_supports_elicitation_form());
2662        assert!(!ctx.client_supports_elicitation_url());
2663        assert!(ctx.client_supports_roots());
2664    }
2665
2666    #[test]
2667    fn test_mcp_context_with_server_capabilities() {
2668        let cx = Cx::for_testing();
2669        let caps = ServerCapabilityInfo::new()
2670            .with_tools()
2671            .with_resources(true)
2672            .with_prompts()
2673            .with_logging();
2674
2675        let ctx = McpContext::new(cx, 1).with_server_capabilities(caps);
2676
2677        let server_caps = ctx.server_capabilities().unwrap();
2678        assert!(server_caps.tools);
2679        assert!(server_caps.resources);
2680        assert!(server_caps.resources_subscribe);
2681        assert!(server_caps.prompts);
2682        assert!(server_caps.logging);
2683    }
2684
2685    #[test]
2686    fn test_client_capability_info_builders() {
2687        let caps = ClientCapabilityInfo::new();
2688        assert!(!caps.sampling);
2689        assert!(!caps.elicitation);
2690        assert!(!caps.roots);
2691
2692        let caps = caps.with_sampling();
2693        assert!(caps.sampling);
2694
2695        let caps = ClientCapabilityInfo::new().with_elicitation(true, true);
2696        assert!(caps.elicitation);
2697        assert!(caps.elicitation_form);
2698        assert!(caps.elicitation_url);
2699
2700        let caps = ClientCapabilityInfo::new().with_roots(false);
2701        assert!(caps.roots);
2702        assert!(!caps.roots_list_changed);
2703    }
2704
2705    #[test]
2706    fn test_server_capability_info_builders() {
2707        let caps = ServerCapabilityInfo::new();
2708        assert!(!caps.tools);
2709        assert!(!caps.resources);
2710        assert!(!caps.prompts);
2711        assert!(!caps.logging);
2712
2713        let caps = caps
2714            .with_tools()
2715            .with_resources(false)
2716            .with_prompts()
2717            .with_logging();
2718        assert!(caps.tools);
2719        assert!(caps.resources);
2720        assert!(!caps.resources_subscribe);
2721        assert!(caps.prompts);
2722        assert!(caps.logging);
2723    }
2724}