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}