1use std::future::Future;
7use std::pin::Pin;
8use std::sync::atomic::{AtomicU8, AtomicU32, Ordering};
9use std::sync::{Arc, Mutex};
10
11use asupersync::sync::Notify;
12use asupersync::types::{CancelReason, MAX_MASK_DEPTH};
13use asupersync::{Budget, Cx, Outcome, RegionId, TaskId, Time};
14
15#[cfg(test)]
16use asupersync::time::wall_now;
17
18use crate::{AuthContext, SessionState};
19
20const REQUEST_LEASE_UNMANAGED: u8 = 0;
21const REQUEST_LEASE_ACTIVE: u8 = 1;
22const REQUEST_LEASE_CLOSED: u8 = 2;
23const REQUEST_CANCELLATION_ACTIVE: u8 = 0;
24const REQUEST_CANCELLATION_CANCELLED: u8 = 1;
25const REQUEST_CANCELLATION_FINALIZING: u8 = 2;
26const REQUEST_AUTH_UNCOMMITTED: u8 = 0;
27const REQUEST_AUTH_ANONYMOUS: u8 = 1;
28const REQUEST_AUTH_AUTHENTICATED: u8 = 2;
29
30#[derive(Debug, Default)]
37struct McpRequestCancellationInner {
38 state: AtomicU8,
39 notify: Notify,
40}
41
42#[derive(Clone, Debug, Default)]
43#[doc(hidden)]
44pub struct McpRequestCancellation {
45 inner: Arc<McpRequestCancellationInner>,
46}
47
48impl McpRequestCancellation {
49 #[must_use]
51 pub fn new() -> Self {
52 Self::default()
53 }
54
55 pub fn cancel(&self) -> bool {
57 let cancelled = self
58 .inner
59 .state
60 .compare_exchange(
61 REQUEST_CANCELLATION_ACTIVE,
62 REQUEST_CANCELLATION_CANCELLED,
63 Ordering::AcqRel,
64 Ordering::Acquire,
65 )
66 .is_ok();
67 if cancelled {
68 self.notify_terminal_waiters();
69 }
70 cancelled
71 }
72
73 fn notify_terminal_waiters(&self) {
74 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
78 self.inner.notify.notify_waiters();
79 }));
80 }
81
82 #[must_use]
84 pub fn is_cancel_requested(&self) -> bool {
85 self.inner.state.load(Ordering::Acquire) == REQUEST_CANCELLATION_CANCELLED
86 }
87
88 #[must_use]
94 pub fn is_terminal(&self) -> bool {
95 self.inner.state.load(Ordering::Acquire) != REQUEST_CANCELLATION_ACTIVE
96 }
97
98 pub async fn cancelled(&self) {
104 self.inner
105 .notify
106 .wait_until(|| self.is_cancel_requested())
107 .await;
108 }
109
110 pub async fn terminated(&self) {
115 self.inner.notify.wait_until(|| self.is_terminal()).await;
116 }
117
118 #[must_use]
124 pub fn begin_finalization(&self) -> bool {
125 loop {
126 match self.inner.state.load(Ordering::Acquire) {
127 REQUEST_CANCELLATION_ACTIVE => {
128 if self
129 .inner
130 .state
131 .compare_exchange(
132 REQUEST_CANCELLATION_ACTIVE,
133 REQUEST_CANCELLATION_FINALIZING,
134 Ordering::AcqRel,
135 Ordering::Acquire,
136 )
137 .is_ok()
138 {
139 self.notify_terminal_waiters();
140 return true;
141 }
142 }
143 REQUEST_CANCELLATION_CANCELLED => return false,
144 REQUEST_CANCELLATION_FINALIZING => return true,
145 _ => return false,
146 }
147 }
148 }
149
150 #[must_use]
152 pub fn is_finalizing(&self) -> bool {
153 self.inner.state.load(Ordering::Acquire) == REQUEST_CANCELLATION_FINALIZING
154 }
155}
156
157pub trait NotificationSender: Send + Sync {
166 fn send_progress(&self, progress: f64, total: Option<f64>, message: Option<&str>);
174
175 fn send_progress_exact(
183 &self,
184 _progress: serde_json::Number,
185 _total: Option<serde_json::Number>,
186 _message: Option<&str>,
187 ) {
188 }
189
190 fn send_log(&self, _level: McpLogLevel, _logger: Option<&str>, _data: serde_json::Value) {}
196
197 fn send_catalog_changed(&self, _kind: McpCatalogKind) {}
203
204 fn send_resource_updated(&self, _uri: &str) {}
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub enum McpCatalogKind {
214 Tools,
215 Resources,
216 Prompts,
217}
218
219pub trait CatalogChangePublisher: Send + Sync {
222 fn publish_catalog_changed(&self, kind: McpCatalogKind) -> bool;
224 fn publish_resource_updated(&self, uri: &str) -> bool;
226}
227
228#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
234pub enum McpLogLevel {
235 Debug,
236 Info,
237 Notice,
238 Warning,
239 Error,
240 Critical,
241 Alert,
242 Emergency,
243}
244
245impl McpLogLevel {
246 #[must_use]
248 pub const fn as_str(self) -> &'static str {
249 match self {
250 Self::Debug => "debug",
251 Self::Info => "info",
252 Self::Notice => "notice",
253 Self::Warning => "warning",
254 Self::Error => "error",
255 Self::Critical => "critical",
256 Self::Alert => "alert",
257 Self::Emergency => "emergency",
258 }
259 }
260
261 #[must_use]
263 pub const fn rank(self) -> u8 {
264 match self {
265 Self::Debug => 1,
266 Self::Info => 2,
267 Self::Notice => 3,
268 Self::Warning => 4,
269 Self::Error => 5,
270 Self::Critical => 6,
271 Self::Alert => 7,
272 Self::Emergency => 8,
273 }
274 }
275}
276
277#[derive(Debug, Clone, PartialEq, Eq)]
287pub struct ClientRoot {
288 pub uri: String,
290 pub name: Option<String>,
292}
293
294impl ClientRoot {
295 #[must_use]
297 pub fn new(uri: impl Into<String>) -> Self {
298 Self {
299 uri: uri.into(),
300 name: None,
301 }
302 }
303
304 #[must_use]
306 pub fn with_name(uri: impl Into<String>, name: impl Into<String>) -> Self {
307 Self {
308 uri: uri.into(),
309 name: Some(name.into()),
310 }
311 }
312}
313
314pub trait RootsProvider: Send + Sync {
316 fn list_roots(
318 &self,
319 ) -> std::pin::Pin<
320 Box<dyn std::future::Future<Output = crate::McpResult<Vec<ClientRoot>>> + Send + '_>,
321 >;
322}
323
324pub trait SamplingSender: Send + Sync {
334 fn create_message(
345 &self,
346 request: SamplingRequest,
347 ) -> std::pin::Pin<
348 Box<dyn std::future::Future<Output = crate::McpResult<SamplingResponse>> + Send + '_>,
349 >;
350}
351
352#[derive(Debug, Clone)]
354pub struct SamplingRequest {
355 pub messages: Vec<SamplingRequestMessage>,
357 pub max_tokens: u32,
359 pub system_prompt: Option<String>,
361 pub temperature: Option<f64>,
363 pub stop_sequences: Vec<String>,
365 pub model_hints: Vec<String>,
367}
368
369impl SamplingRequest {
370 #[must_use]
372 pub fn new(messages: Vec<SamplingRequestMessage>, max_tokens: u32) -> Self {
373 Self {
374 messages,
375 max_tokens,
376 system_prompt: None,
377 temperature: None,
378 stop_sequences: Vec::new(),
379 model_hints: Vec::new(),
380 }
381 }
382
383 #[must_use]
385 pub fn prompt(text: impl Into<String>, max_tokens: u32) -> Self {
386 Self::new(vec![SamplingRequestMessage::user(text)], max_tokens)
387 }
388
389 #[must_use]
391 pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
392 self.system_prompt = Some(prompt.into());
393 self
394 }
395
396 #[must_use]
398 pub fn with_temperature(mut self, temp: f64) -> Self {
399 self.temperature = Some(temp);
400 self
401 }
402
403 #[must_use]
405 pub fn with_stop_sequences(mut self, sequences: Vec<String>) -> Self {
406 self.stop_sequences = sequences;
407 self
408 }
409
410 #[must_use]
412 pub fn with_model_hints(mut self, hints: Vec<String>) -> Self {
413 self.model_hints = hints;
414 self
415 }
416}
417
418#[derive(Debug, Clone)]
420pub struct SamplingRequestMessage {
421 pub role: SamplingRole,
423 pub text: String,
425}
426
427impl SamplingRequestMessage {
428 #[must_use]
430 pub fn user(text: impl Into<String>) -> Self {
431 Self {
432 role: SamplingRole::User,
433 text: text.into(),
434 }
435 }
436
437 #[must_use]
439 pub fn assistant(text: impl Into<String>) -> Self {
440 Self {
441 role: SamplingRole::Assistant,
442 text: text.into(),
443 }
444 }
445}
446
447#[derive(Debug, Clone, Copy, PartialEq, Eq)]
449pub enum SamplingRole {
450 User,
452 Assistant,
454}
455
456#[derive(Debug, Clone)]
458pub struct SamplingResponse {
459 pub text: String,
461 pub model: String,
463 pub stop_reason: SamplingStopReason,
465}
466
467impl SamplingResponse {
468 #[must_use]
470 pub fn new(text: impl Into<String>, model: impl Into<String>) -> Self {
471 Self {
472 text: text.into(),
473 model: model.into(),
474 stop_reason: SamplingStopReason::EndTurn,
475 }
476 }
477}
478
479#[derive(Debug, Clone, PartialEq, Eq, Default)]
481pub enum SamplingStopReason {
482 #[default]
484 EndTurn,
485 StopSequence,
487 MaxTokens,
489 Unspecified,
491 Other(String),
493}
494
495impl SamplingStopReason {
496 #[must_use]
499 pub fn from_wire_value(value: Option<String>) -> Self {
500 match value {
501 Some(value) => match value.as_str() {
502 "endTurn" => Self::EndTurn,
503 "stopSequence" => Self::StopSequence,
504 "maxTokens" => Self::MaxTokens,
505 _ => Self::Other(value),
506 },
507 None => Self::Unspecified,
508 }
509 }
510
511 #[must_use]
514 pub fn as_wire_value(&self) -> Option<&str> {
515 match self {
516 Self::EndTurn => Some("endTurn"),
517 Self::StopSequence => Some("stopSequence"),
518 Self::MaxTokens => Some("maxTokens"),
519 Self::Unspecified => None,
520 Self::Other(value) => Some(value),
521 }
522 }
523}
524
525#[derive(Debug, Clone, Copy, Default)]
529pub struct NoOpSamplingSender;
530
531impl SamplingSender for NoOpSamplingSender {
532 fn create_message(
533 &self,
534 _request: SamplingRequest,
535 ) -> std::pin::Pin<
536 Box<dyn std::future::Future<Output = crate::McpResult<SamplingResponse>> + Send + '_>,
537 > {
538 Box::pin(async {
539 Err(crate::McpError::new(
540 crate::McpErrorCode::InvalidRequest,
541 "Sampling not supported: client does not have sampling capability",
542 ))
543 })
544 }
545}
546
547pub trait ElicitationSender: Send + Sync {
557 fn elicit(
568 &self,
569 request: ElicitationRequest,
570 ) -> std::pin::Pin<
571 Box<dyn std::future::Future<Output = crate::McpResult<ElicitationResponse>> + Send + '_>,
572 >;
573}
574
575#[derive(Debug, Clone)]
577pub struct ElicitationRequest {
578 pub mode: ElicitationMode,
580 pub message: String,
582 pub schema: Option<serde_json::Value>,
584 pub url: Option<String>,
586 pub elicitation_id: Option<String>,
588}
589
590impl ElicitationRequest {
591 #[must_use]
593 pub fn form(message: impl Into<String>, schema: serde_json::Value) -> Self {
594 Self {
595 mode: ElicitationMode::Form,
596 message: message.into(),
597 schema: Some(schema),
598 url: None,
599 elicitation_id: None,
600 }
601 }
602
603 #[must_use]
605 pub fn url(
606 message: impl Into<String>,
607 url: impl Into<String>,
608 elicitation_id: impl Into<String>,
609 ) -> Self {
610 Self {
611 mode: ElicitationMode::Url,
612 message: message.into(),
613 schema: None,
614 url: Some(url.into()),
615 elicitation_id: Some(elicitation_id.into()),
616 }
617 }
618}
619
620#[derive(Debug, Clone, Copy, PartialEq, Eq)]
622pub enum ElicitationMode {
623 Form,
625 Url,
627}
628
629#[derive(Debug, Clone)]
631pub struct ElicitationResponse {
632 pub action: ElicitationAction,
634 pub content: Option<std::collections::HashMap<String, serde_json::Value>>,
636}
637
638impl ElicitationResponse {
639 #[must_use]
641 pub fn accept(content: std::collections::HashMap<String, serde_json::Value>) -> Self {
642 Self {
643 action: ElicitationAction::Accept,
644 content: Some(content),
645 }
646 }
647
648 #[must_use]
650 pub fn accept_url() -> Self {
651 Self {
652 action: ElicitationAction::Accept,
653 content: None,
654 }
655 }
656
657 #[must_use]
659 pub fn decline() -> Self {
660 Self {
661 action: ElicitationAction::Decline,
662 content: None,
663 }
664 }
665
666 #[must_use]
668 pub fn cancel() -> Self {
669 Self {
670 action: ElicitationAction::Cancel,
671 content: None,
672 }
673 }
674
675 #[must_use]
677 pub fn is_accepted(&self) -> bool {
678 matches!(self.action, ElicitationAction::Accept)
679 }
680
681 #[must_use]
683 pub fn is_declined(&self) -> bool {
684 matches!(self.action, ElicitationAction::Decline)
685 }
686
687 #[must_use]
689 pub fn is_cancelled(&self) -> bool {
690 matches!(self.action, ElicitationAction::Cancel)
691 }
692
693 #[must_use]
695 pub fn get_string(&self, key: &str) -> Option<&str> {
696 self.content.as_ref()?.get(key)?.as_str()
697 }
698
699 #[must_use]
701 pub fn get_bool(&self, key: &str) -> Option<bool> {
702 self.content.as_ref()?.get(key)?.as_bool()
703 }
704
705 #[must_use]
707 pub fn get_int(&self, key: &str) -> Option<i64> {
708 self.content.as_ref()?.get(key)?.as_i64()
709 }
710}
711
712#[derive(Debug, Clone, Copy, PartialEq, Eq)]
714pub enum ElicitationAction {
715 Accept,
717 Decline,
719 Cancel,
721}
722
723#[derive(Debug, Clone, Copy, Default)]
727pub struct NoOpElicitationSender;
728
729impl ElicitationSender for NoOpElicitationSender {
730 fn elicit(
731 &self,
732 _request: ElicitationRequest,
733 ) -> std::pin::Pin<
734 Box<dyn std::future::Future<Output = crate::McpResult<ElicitationResponse>> + Send + '_>,
735 > {
736 Box::pin(async {
737 Err(crate::McpError::new(
738 crate::McpErrorCode::InvalidRequest,
739 "Elicitation not supported: client does not have elicitation capability",
740 ))
741 })
742 }
743}
744
745pub const MAX_RESOURCE_READ_DEPTH: u32 = 10;
751
752#[derive(Debug, Clone)]
757pub struct ResourceContentItem {
758 pub uri: String,
760 pub mime_type: Option<String>,
762 pub text: Option<String>,
764 pub blob: Option<String>,
766}
767
768impl ResourceContentItem {
769 #[must_use]
771 pub fn text(uri: impl Into<String>, text: impl Into<String>) -> Self {
772 Self {
773 uri: uri.into(),
774 mime_type: Some("text/plain".to_string()),
775 text: Some(text.into()),
776 blob: None,
777 }
778 }
779
780 #[must_use]
782 pub fn json(uri: impl Into<String>, text: impl Into<String>) -> Self {
783 Self {
784 uri: uri.into(),
785 mime_type: Some("application/json".to_string()),
786 text: Some(text.into()),
787 blob: None,
788 }
789 }
790
791 #[must_use]
793 pub fn blob(
794 uri: impl Into<String>,
795 mime_type: impl Into<String>,
796 blob: impl Into<String>,
797 ) -> Self {
798 Self {
799 uri: uri.into(),
800 mime_type: Some(mime_type.into()),
801 text: None,
802 blob: Some(blob.into()),
803 }
804 }
805
806 #[must_use]
808 pub fn as_text(&self) -> Option<&str> {
809 self.text.as_deref()
810 }
811
812 #[must_use]
814 pub fn as_blob(&self) -> Option<&str> {
815 self.blob.as_deref()
816 }
817
818 #[must_use]
820 pub fn is_text(&self) -> bool {
821 self.text.is_some()
822 }
823
824 #[must_use]
826 pub fn is_blob(&self) -> bool {
827 self.blob.is_some()
828 }
829}
830
831#[derive(Debug, Clone)]
833pub struct ResourceReadResult {
834 pub contents: Vec<ResourceContentItem>,
836}
837
838impl ResourceReadResult {
839 #[must_use]
841 pub fn new(contents: Vec<ResourceContentItem>) -> Self {
842 Self { contents }
843 }
844
845 #[must_use]
847 pub fn text(uri: impl Into<String>, text: impl Into<String>) -> Self {
848 Self {
849 contents: vec![ResourceContentItem::text(uri, text)],
850 }
851 }
852
853 #[must_use]
855 pub fn first_text(&self) -> Option<&str> {
856 self.contents.first().and_then(|c| c.as_text())
857 }
858
859 #[must_use]
861 pub fn first_blob(&self) -> Option<&str> {
862 self.contents.first().and_then(|c| c.as_blob())
863 }
864}
865
866pub trait ResourceReader: Send + Sync {
875 fn read_resource<'a>(
888 &'a self,
889 context: &'a McpContext,
890 uri: &'a str,
891 depth: u32,
892 ) -> Pin<Box<dyn Future<Output = crate::McpResult<ResourceReadResult>> + Send + 'a>>;
893}
894
895pub const MAX_TOOL_CALL_DEPTH: u32 = 10;
901
902#[derive(Debug, Clone)]
907pub enum ToolContentItem {
908 Text {
910 text: String,
912 },
913 Image {
915 data: String,
917 mime_type: String,
919 },
920 Audio {
922 data: String,
924 mime_type: String,
926 },
927 Resource {
929 uri: String,
931 mime_type: Option<String>,
933 text: Option<String>,
935 blob: Option<String>,
937 },
938}
939
940impl ToolContentItem {
941 #[must_use]
943 pub fn text(text: impl Into<String>) -> Self {
944 Self::Text { text: text.into() }
945 }
946
947 #[must_use]
949 pub fn as_text(&self) -> Option<&str> {
950 match self {
951 Self::Text { text } => Some(text),
952 _ => None,
953 }
954 }
955
956 #[must_use]
958 pub fn is_text(&self) -> bool {
959 matches!(self, Self::Text { .. })
960 }
961}
962
963#[derive(Debug, Clone)]
965pub struct ToolCallResult {
966 pub content: Vec<ToolContentItem>,
968 pub is_error: bool,
970}
971
972impl ToolCallResult {
973 #[must_use]
975 pub fn success(content: Vec<ToolContentItem>) -> Self {
976 Self {
977 content,
978 is_error: false,
979 }
980 }
981
982 #[must_use]
984 pub fn text(text: impl Into<String>) -> Self {
985 Self {
986 content: vec![ToolContentItem::text(text)],
987 is_error: false,
988 }
989 }
990
991 #[must_use]
993 pub fn error(message: impl Into<String>) -> Self {
994 Self {
995 content: vec![ToolContentItem::text(message)],
996 is_error: true,
997 }
998 }
999
1000 #[must_use]
1002 pub fn first_text(&self) -> Option<&str> {
1003 self.content.first().and_then(|c| c.as_text())
1004 }
1005}
1006
1007pub trait ToolCaller: Send + Sync {
1016 fn call_tool<'a>(
1030 &'a self,
1031 context: &'a McpContext,
1032 name: &'a str,
1033 args: serde_json::Value,
1034 depth: u32,
1035 ) -> Pin<Box<dyn Future<Output = crate::McpResult<ToolCallResult>> + Send + 'a>>;
1036}
1037
1038pub const MAX_PROMPT_GET_DEPTH: u32 = 10;
1044
1045#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1047pub enum PromptMessageRole {
1048 User,
1050 Assistant,
1052}
1053
1054#[derive(Debug, Clone, PartialEq, Eq)]
1056pub struct PromptMessageItem {
1057 pub role: PromptMessageRole,
1059 pub text: Option<String>,
1061}
1062
1063impl PromptMessageItem {
1064 #[must_use]
1066 pub fn user_text(text: impl Into<String>) -> Self {
1067 Self {
1068 role: PromptMessageRole::User,
1069 text: Some(text.into()),
1070 }
1071 }
1072
1073 #[must_use]
1075 pub fn as_text(&self) -> Option<&str> {
1076 self.text.as_deref()
1077 }
1078}
1079
1080#[derive(Debug, Clone, PartialEq, Eq)]
1082pub struct PromptGetResult {
1083 pub description: Option<String>,
1085 pub messages: Vec<PromptMessageItem>,
1087}
1088
1089impl PromptGetResult {
1090 #[must_use]
1092 pub fn new(messages: Vec<PromptMessageItem>) -> Self {
1093 Self {
1094 description: None,
1095 messages,
1096 }
1097 }
1098
1099 #[must_use]
1101 pub fn first_text(&self) -> Option<&str> {
1102 self.messages.iter().find_map(PromptMessageItem::as_text)
1103 }
1104}
1105
1106pub trait PromptCaller: Send + Sync {
1112 fn get_prompt<'a>(
1114 &'a self,
1115 context: &'a McpContext,
1116 name: &'a str,
1117 arguments: std::collections::HashMap<String, String>,
1118 depth: u32,
1119 ) -> Pin<Box<dyn Future<Output = crate::McpResult<PromptGetResult>> + Send + 'a>>;
1120}
1121
1122#[derive(Debug, Clone, Default)]
1131pub struct ClientCapabilityInfo {
1132 pub sampling: bool,
1134 pub elicitation: bool,
1136 pub elicitation_form: bool,
1138 pub elicitation_url: bool,
1140 pub roots: bool,
1142 pub roots_list_changed: bool,
1144}
1145
1146impl ClientCapabilityInfo {
1147 #[must_use]
1149 pub fn new() -> Self {
1150 Self::default()
1151 }
1152
1153 #[must_use]
1155 pub fn with_sampling(mut self) -> Self {
1156 self.sampling = true;
1157 self
1158 }
1159
1160 #[must_use]
1162 pub fn with_elicitation(mut self, form: bool, url: bool) -> Self {
1163 self.elicitation = form || url;
1164 self.elicitation_form = form;
1165 self.elicitation_url = url;
1166 self
1167 }
1168
1169 #[must_use]
1171 pub fn with_roots(mut self, list_changed: bool) -> Self {
1172 self.roots = true;
1173 self.roots_list_changed = list_changed;
1174 self
1175 }
1176}
1177
1178#[derive(Debug, Clone, PartialEq, Eq)]
1183pub struct ClientImplementationInfo {
1184 pub name: String,
1186 pub version: String,
1188 pub title: Option<String>,
1190 pub description: Option<String>,
1192 pub website_url: Option<String>,
1194 pub icon_sources: Vec<String>,
1196}
1197
1198impl ClientImplementationInfo {
1199 #[must_use]
1201 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
1202 Self {
1203 name: name.into(),
1204 version: version.into(),
1205 title: None,
1206 description: None,
1207 website_url: None,
1208 icon_sources: Vec::new(),
1209 }
1210 }
1211
1212 #[must_use]
1214 pub fn has_extras(&self) -> bool {
1215 self.title.is_some()
1216 || self.description.is_some()
1217 || self.website_url.is_some()
1218 || !self.icon_sources.is_empty()
1219 }
1220}
1221
1222#[derive(Debug, Clone, Default)]
1226pub struct ServerCapabilityInfo {
1227 pub tools: bool,
1229 pub resources: bool,
1231 pub resources_subscribe: bool,
1233 pub prompts: bool,
1235 pub logging: bool,
1237}
1238
1239impl ServerCapabilityInfo {
1240 #[must_use]
1242 pub fn new() -> Self {
1243 Self::default()
1244 }
1245
1246 #[must_use]
1248 pub fn with_tools(mut self) -> Self {
1249 self.tools = true;
1250 self
1251 }
1252
1253 #[must_use]
1255 pub fn with_resources(mut self, subscribe: bool) -> Self {
1256 self.resources = true;
1257 self.resources_subscribe = subscribe;
1258 self
1259 }
1260
1261 #[must_use]
1263 pub fn with_prompts(mut self) -> Self {
1264 self.prompts = true;
1265 self
1266 }
1267
1268 #[must_use]
1270 pub fn with_logging(mut self) -> Self {
1271 self.logging = true;
1272 self
1273 }
1274}
1275
1276#[derive(Debug, Clone, Copy, Default)]
1278pub struct NoOpNotificationSender;
1279
1280impl NotificationSender for NoOpNotificationSender {
1281 fn send_progress(&self, _progress: f64, _total: Option<f64>, _message: Option<&str>) {
1282 }
1284}
1285
1286#[derive(Clone)]
1291pub struct ProgressReporter {
1292 sender: Arc<dyn NotificationSender>,
1293 marker: Option<serde_json::Value>,
1298}
1299
1300impl ProgressReporter {
1301 pub fn new(sender: Arc<dyn NotificationSender>) -> Self {
1303 Self {
1304 sender,
1305 marker: None,
1306 }
1307 }
1308
1309 #[must_use]
1314 pub fn with_marker(marker: serde_json::Value, sender: Arc<dyn NotificationSender>) -> Self {
1315 Self {
1316 sender,
1317 marker: Some(marker),
1318 }
1319 }
1320
1321 #[must_use]
1323 pub fn marker(&self) -> Option<&serde_json::Value> {
1324 self.marker.as_ref()
1325 }
1326
1327 pub fn report(&self, progress: f64, message: Option<&str>) {
1334 self.sender.send_progress(progress, None, message);
1335 }
1336
1337 pub fn report_with_total(&self, progress: f64, total: f64, message: Option<&str>) {
1345 self.sender.send_progress(progress, Some(total), message);
1346 }
1347
1348 pub fn report_exact(
1355 &self,
1356 progress: serde_json::Number,
1357 total: Option<serde_json::Number>,
1358 message: Option<&str>,
1359 ) {
1360 self.sender.send_progress_exact(progress, total, message);
1361 }
1362}
1363
1364impl std::fmt::Debug for ProgressReporter {
1365 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1366 f.debug_struct("ProgressReporter").finish_non_exhaustive()
1367 }
1368}
1369
1370#[derive(Clone)]
1413pub struct McpContext {
1414 cx: Cx,
1416 budget_state: Arc<Mutex<FrameworkBudgetState>>,
1421 framework_mask_depth: Arc<AtomicU32>,
1424 mask_transition: Arc<Mutex<()>>,
1430 operation_deadline: Option<Time>,
1437 request_lease: Arc<AtomicU8>,
1445 request_cancellation: McpRequestCancellation,
1447 request_id: u64,
1449 progress_reporter: Option<ProgressReporter>,
1451 state: Option<SessionState>,
1453 cache_admission_partition: Arc<Mutex<Option<([u8; 32], u64)>>>,
1455 response_cache_hits: Arc<Mutex<Vec<u64>>>,
1457 auth: Arc<Mutex<Option<AuthContext>>>,
1459 auth_state: Arc<AtomicU8>,
1462 sampling_sender: Option<Arc<dyn SamplingSender>>,
1464 elicitation_sender: Option<Arc<dyn ElicitationSender>>,
1466 roots_provider: Option<Arc<dyn RootsProvider>>,
1468 resource_reader: Option<Arc<dyn ResourceReader>>,
1470 resource_read_depth: u32,
1472 tool_caller: Option<Arc<dyn ToolCaller>>,
1474 tool_call_depth: u32,
1476 prompt_caller: Option<Arc<dyn PromptCaller>>,
1478 prompt_get_depth: u32,
1480 client_capabilities: Option<ClientCapabilityInfo>,
1482 client_implementation: Option<ClientImplementationInfo>,
1484 server_capabilities: Option<ServerCapabilityInfo>,
1486 log_sender: Option<Arc<dyn NotificationSender>>,
1488 min_log_level: Option<McpLogLevel>,
1493 resource_subscriptions: Option<Arc<std::collections::HashSet<String>>>,
1495 catalog_publisher: Option<Arc<dyn CatalogChangePublisher>>,
1497}
1498
1499impl std::fmt::Debug for McpContext {
1500 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1501 let budget_state = *self
1502 .budget_state
1503 .lock()
1504 .unwrap_or_else(std::sync::PoisonError::into_inner);
1505 f.debug_struct("McpContext")
1506 .field("cx", &self.cx)
1507 .field("budget_ceiling", &budget_state.ceiling)
1508 .field("ambient_poll_debits", &budget_state.ambient_poll_debits)
1509 .field("ambient_cost_debits", &budget_state.ambient_cost_debits)
1510 .field("deferred_overrun", &budget_state.deferred_overrun)
1511 .field(
1512 "framework_mask_depth",
1513 &self.framework_mask_depth.load(Ordering::Relaxed),
1514 )
1515 .field("operation_deadline", &self.operation_deadline)
1516 .field("request_lease_active", &self.request_scope_is_active())
1517 .field(
1518 "request_cancel_requested",
1519 &self.request_cancellation.is_cancel_requested(),
1520 )
1521 .field("request_id", &self.request_id)
1522 .field("progress_reporter", &self.progress_reporter)
1523 .field("state", &self.state.is_some())
1524 .field(
1525 "cache_admission_partition",
1526 &self
1527 .cache_admission_partition
1528 .lock()
1529 .unwrap_or_else(std::sync::PoisonError::into_inner)
1530 .is_some(),
1531 )
1532 .field(
1533 "response_cache_hit_count",
1534 &self
1535 .response_cache_hits
1536 .lock()
1537 .unwrap_or_else(std::sync::PoisonError::into_inner)
1538 .len(),
1539 )
1540 .field(
1541 "auth",
1542 &self
1543 .auth
1544 .lock()
1545 .unwrap_or_else(std::sync::PoisonError::into_inner)
1546 .is_some(),
1547 )
1548 .field(
1549 "auth_committed",
1550 &(self.auth_state.load(Ordering::Acquire) != REQUEST_AUTH_UNCOMMITTED),
1551 )
1552 .field("sampling_sender", &self.sampling_sender.is_some())
1553 .field("elicitation_sender", &self.elicitation_sender.is_some())
1554 .field("roots_provider", &self.roots_provider.is_some())
1555 .field("resource_reader", &self.resource_reader.is_some())
1556 .field("resource_read_depth", &self.resource_read_depth)
1557 .field("tool_caller", &self.tool_caller.is_some())
1558 .field("tool_call_depth", &self.tool_call_depth)
1559 .field("prompt_caller", &self.prompt_caller.is_some())
1560 .field("prompt_get_depth", &self.prompt_get_depth)
1561 .field("client_capabilities", &self.client_capabilities)
1562 .field("client_implementation", &self.client_implementation)
1563 .field("server_capabilities", &self.server_capabilities)
1564 .field("log_sender", &self.log_sender.is_some())
1565 .field("min_log_level", &self.min_log_level)
1566 .field(
1567 "resource_subscription_count",
1568 &self
1569 .resource_subscriptions
1570 .as_ref()
1571 .map_or(0, |uris| uris.len()),
1572 )
1573 .field("catalog_publisher", &self.catalog_publisher.is_some())
1574 .finish()
1575 }
1576}
1577
1578#[derive(Clone, Copy, Debug, Default)]
1579struct FrameworkBudgetState {
1580 ceiling: Option<Budget>,
1581 ambient_poll_debits: u32,
1584 ambient_cost_debits: u64,
1591 deferred_overrun: bool,
1595}
1596
1597impl FrameworkBudgetState {
1598 fn adjusted_ambient(self, mut ambient: Budget) -> Budget {
1599 if ambient.poll_quota != u32::MAX {
1600 ambient.poll_quota = ambient.poll_quota.saturating_sub(self.ambient_poll_debits);
1601 }
1602 if let Some(remaining) = ambient.cost_quota.as_mut() {
1603 *remaining = remaining.saturating_sub(self.ambient_cost_debits);
1604 }
1605 ambient
1606 }
1607
1608 fn effective(self, ambient: Budget) -> Budget {
1609 let ambient = self.adjusted_ambient(ambient);
1610 self.ceiling
1611 .map_or(ambient, |ceiling| ambient.meet(ceiling))
1612 }
1613}
1614
1615struct FrameworkMaskGuard<'a> {
1616 depth: &'a AtomicU32,
1617}
1618
1619#[doc(hidden)]
1624pub struct McpContextLeaseGuard {
1625 lease: Arc<AtomicU8>,
1626}
1627
1628impl std::fmt::Debug for McpContextLeaseGuard {
1629 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1630 f.debug_struct("McpContextLeaseGuard")
1631 .field(
1632 "active",
1633 &(self.lease.load(Ordering::Acquire) == REQUEST_LEASE_ACTIVE),
1634 )
1635 .finish()
1636 }
1637}
1638
1639impl Drop for McpContextLeaseGuard {
1640 fn drop(&mut self) {
1641 self.lease.store(REQUEST_LEASE_CLOSED, Ordering::Release);
1642 }
1643}
1644
1645impl Drop for FrameworkMaskGuard<'_> {
1646 fn drop(&mut self) {
1647 self.depth.fetch_sub(1, Ordering::SeqCst);
1648 }
1649}
1650
1651impl McpContext {
1652 #[must_use]
1660 pub fn new(cx: Cx, request_id: u64) -> Self {
1661 Self {
1662 cx,
1663 budget_state: Arc::new(Mutex::new(FrameworkBudgetState::default())),
1664 framework_mask_depth: Arc::new(AtomicU32::new(0)),
1665 mask_transition: Arc::new(Mutex::new(())),
1666 operation_deadline: None,
1667 request_lease: Arc::new(AtomicU8::new(REQUEST_LEASE_UNMANAGED)),
1668 request_cancellation: McpRequestCancellation::new(),
1669 request_id,
1670 progress_reporter: None,
1671 state: None,
1672 cache_admission_partition: Arc::new(Mutex::new(None)),
1673 response_cache_hits: Arc::new(Mutex::new(Vec::new())),
1674 auth: Arc::new(Mutex::new(None)),
1675 auth_state: Arc::new(AtomicU8::new(REQUEST_AUTH_UNCOMMITTED)),
1676 sampling_sender: None,
1677 elicitation_sender: None,
1678 roots_provider: None,
1679 resource_reader: None,
1680 resource_read_depth: 0,
1681 tool_caller: None,
1682 tool_call_depth: 0,
1683 prompt_caller: None,
1684 prompt_get_depth: 0,
1685 client_capabilities: None,
1686 client_implementation: None,
1687 server_capabilities: None,
1688 log_sender: None,
1689 min_log_level: None,
1690 resource_subscriptions: None,
1691 catalog_publisher: None,
1692 }
1693 }
1694
1695 #[must_use]
1701 pub fn with_state(cx: Cx, request_id: u64, state: SessionState) -> Self {
1702 Self {
1703 cx,
1704 budget_state: Arc::new(Mutex::new(FrameworkBudgetState::default())),
1705 framework_mask_depth: Arc::new(AtomicU32::new(0)),
1706 mask_transition: Arc::new(Mutex::new(())),
1707 operation_deadline: None,
1708 request_lease: Arc::new(AtomicU8::new(REQUEST_LEASE_UNMANAGED)),
1709 request_cancellation: McpRequestCancellation::new(),
1710 request_id,
1711 progress_reporter: None,
1712 state: Some(state),
1713 cache_admission_partition: Arc::new(Mutex::new(None)),
1714 response_cache_hits: Arc::new(Mutex::new(Vec::new())),
1715 auth: Arc::new(Mutex::new(None)),
1716 auth_state: Arc::new(AtomicU8::new(REQUEST_AUTH_UNCOMMITTED)),
1717 sampling_sender: None,
1718 elicitation_sender: None,
1719 roots_provider: None,
1720 resource_reader: None,
1721 resource_read_depth: 0,
1722 tool_caller: None,
1723 tool_call_depth: 0,
1724 prompt_caller: None,
1725 prompt_get_depth: 0,
1726 client_capabilities: None,
1727 client_implementation: None,
1728 server_capabilities: None,
1729 log_sender: None,
1730 min_log_level: None,
1731 resource_subscriptions: None,
1732 catalog_publisher: None,
1733 }
1734 }
1735
1736 #[must_use]
1743 pub fn with_progress(cx: Cx, request_id: u64, reporter: ProgressReporter) -> Self {
1744 Self {
1745 cx,
1746 budget_state: Arc::new(Mutex::new(FrameworkBudgetState::default())),
1747 framework_mask_depth: Arc::new(AtomicU32::new(0)),
1748 mask_transition: Arc::new(Mutex::new(())),
1749 operation_deadline: None,
1750 request_lease: Arc::new(AtomicU8::new(REQUEST_LEASE_UNMANAGED)),
1751 request_cancellation: McpRequestCancellation::new(),
1752 request_id,
1753 progress_reporter: Some(reporter),
1754 state: None,
1755 cache_admission_partition: Arc::new(Mutex::new(None)),
1756 response_cache_hits: Arc::new(Mutex::new(Vec::new())),
1757 auth: Arc::new(Mutex::new(None)),
1758 auth_state: Arc::new(AtomicU8::new(REQUEST_AUTH_UNCOMMITTED)),
1759 sampling_sender: None,
1760 elicitation_sender: None,
1761 roots_provider: None,
1762 resource_reader: None,
1763 resource_read_depth: 0,
1764 tool_caller: None,
1765 tool_call_depth: 0,
1766 prompt_caller: None,
1767 prompt_get_depth: 0,
1768 client_capabilities: None,
1769 client_implementation: None,
1770 server_capabilities: None,
1771 log_sender: None,
1772 min_log_level: None,
1773 resource_subscriptions: None,
1774 catalog_publisher: None,
1775 }
1776 }
1777
1778 #[must_use]
1784 pub fn with_state_and_progress(
1785 cx: Cx,
1786 request_id: u64,
1787 state: SessionState,
1788 reporter: ProgressReporter,
1789 ) -> Self {
1790 Self {
1791 cx,
1792 budget_state: Arc::new(Mutex::new(FrameworkBudgetState::default())),
1793 framework_mask_depth: Arc::new(AtomicU32::new(0)),
1794 mask_transition: Arc::new(Mutex::new(())),
1795 operation_deadline: None,
1796 request_lease: Arc::new(AtomicU8::new(REQUEST_LEASE_UNMANAGED)),
1797 request_cancellation: McpRequestCancellation::new(),
1798 request_id,
1799 progress_reporter: Some(reporter),
1800 state: Some(state),
1801 cache_admission_partition: Arc::new(Mutex::new(None)),
1802 response_cache_hits: Arc::new(Mutex::new(Vec::new())),
1803 auth: Arc::new(Mutex::new(None)),
1804 auth_state: Arc::new(AtomicU8::new(REQUEST_AUTH_UNCOMMITTED)),
1805 sampling_sender: None,
1806 elicitation_sender: None,
1807 roots_provider: None,
1808 resource_reader: None,
1809 resource_read_depth: 0,
1810 tool_caller: None,
1811 tool_call_depth: 0,
1812 prompt_caller: None,
1813 prompt_get_depth: 0,
1814 client_capabilities: None,
1815 client_implementation: None,
1816 server_capabilities: None,
1817 log_sender: None,
1818 min_log_level: None,
1819 resource_subscriptions: None,
1820 catalog_publisher: None,
1821 }
1822 }
1823
1824 #[must_use]
1831 pub fn with_progress_reporter(mut self, reporter: ProgressReporter) -> Self {
1832 self.progress_reporter = Some(reporter);
1833 self
1834 }
1835
1836 #[must_use]
1838 pub fn with_log_sender(mut self, sender: Arc<dyn NotificationSender>) -> Self {
1839 self.log_sender = Some(sender);
1840 self
1841 }
1842
1843 #[must_use]
1848 pub fn with_min_log_level(mut self, level: Option<McpLogLevel>) -> Self {
1849 self.min_log_level = level;
1850 self
1851 }
1852
1853 #[must_use]
1857 pub fn min_log_level(&self) -> Option<McpLogLevel> {
1858 self.min_log_level
1859 }
1860
1861 #[must_use]
1863 pub fn with_resource_subscriptions(
1864 mut self,
1865 uris: impl IntoIterator<Item = impl Into<String>>,
1866 ) -> Self {
1867 self.resource_subscriptions = Some(Arc::new(uris.into_iter().map(Into::into).collect()));
1868 self
1869 }
1870
1871 #[must_use]
1873 pub fn with_catalog_publisher(mut self, publisher: Arc<dyn CatalogChangePublisher>) -> Self {
1874 self.catalog_publisher = Some(publisher);
1875 self
1876 }
1877
1878 #[must_use]
1883 pub fn with_sampling(mut self, sender: Arc<dyn SamplingSender>) -> Self {
1884 self.sampling_sender = Some(sender);
1885 self
1886 }
1887
1888 #[must_use]
1893 pub fn with_elicitation(mut self, sender: Arc<dyn ElicitationSender>) -> Self {
1894 self.elicitation_sender = Some(sender);
1895 self
1896 }
1897
1898 #[must_use]
1902 pub fn with_roots_provider(mut self, provider: Arc<dyn RootsProvider>) -> Self {
1903 self.roots_provider = Some(provider);
1904 self
1905 }
1906
1907 #[must_use]
1916 pub fn with_budget_ceiling(self, ceiling: Budget) -> Self {
1917 {
1918 let mut current = self
1919 .budget_state
1920 .lock()
1921 .unwrap_or_else(std::sync::PoisonError::into_inner);
1922 current.ceiling = Some(
1923 current
1924 .ceiling
1925 .map_or(ceiling, |budget| budget.meet(ceiling)),
1926 );
1927 }
1928 self
1929 }
1930
1931 #[must_use]
1939 pub fn with_operation_deadline(mut self, deadline: Option<Time>) -> Self {
1940 if let Some(deadline) = deadline {
1941 self.operation_deadline = Some(
1942 self.operation_deadline
1943 .map_or(deadline, |current| current.min(deadline)),
1944 );
1945 }
1946 self
1947 }
1948
1949 #[doc(hidden)]
1954 #[must_use]
1955 pub fn with_request_cancellation(mut self, cancellation: McpRequestCancellation) -> Self {
1956 if self.request_lease.load(Ordering::Acquire) == REQUEST_LEASE_UNMANAGED {
1960 self.request_cancellation = cancellation;
1961 }
1962 self
1963 }
1964
1965 #[doc(hidden)]
1975 #[must_use]
1976 pub fn begin_request_scope(self) -> Option<(Self, McpContextLeaseGuard)> {
1977 if self
1978 .request_lease
1979 .compare_exchange(
1980 REQUEST_LEASE_UNMANAGED,
1981 REQUEST_LEASE_ACTIVE,
1982 Ordering::AcqRel,
1983 Ordering::Acquire,
1984 )
1985 .is_err()
1986 {
1987 return None;
1988 }
1989 let guard = McpContextLeaseGuard {
1990 lease: Arc::clone(&self.request_lease),
1991 };
1992 Some((self, guard))
1993 }
1994
1995 #[must_use]
2000 pub fn with_resource_reader(mut self, reader: Arc<dyn ResourceReader>) -> Self {
2001 self.resource_reader = Some(reader);
2002 self
2003 }
2004
2005 #[must_use]
2010 pub fn with_resource_read_depth(mut self, depth: u32) -> Self {
2011 self.resource_read_depth = self.resource_read_depth.max(depth);
2012 self
2013 }
2014
2015 #[must_use]
2020 pub fn with_tool_caller(mut self, caller: Arc<dyn ToolCaller>) -> Self {
2021 self.tool_caller = Some(caller);
2022 self
2023 }
2024
2025 #[must_use]
2030 pub fn with_tool_call_depth(mut self, depth: u32) -> Self {
2031 self.tool_call_depth = self.tool_call_depth.max(depth);
2032 self
2033 }
2034
2035 #[must_use]
2040 pub fn with_prompt_caller(mut self, caller: Arc<dyn PromptCaller>) -> Self {
2041 self.prompt_caller = Some(caller);
2042 self
2043 }
2044
2045 #[must_use]
2050 pub fn with_prompt_get_depth(mut self, depth: u32) -> Self {
2051 self.prompt_get_depth = self.prompt_get_depth.max(depth);
2052 self
2053 }
2054
2055 #[must_use]
2060 pub fn with_client_capabilities(mut self, capabilities: ClientCapabilityInfo) -> Self {
2061 self.client_capabilities = Some(capabilities);
2062 self
2063 }
2064
2065 #[must_use]
2067 pub fn with_client_implementation(mut self, identity: ClientImplementationInfo) -> Self {
2068 self.client_implementation = Some(identity);
2069 self
2070 }
2071
2072 #[must_use]
2077 pub fn with_server_capabilities(mut self, capabilities: ServerCapabilityInfo) -> Self {
2078 self.server_capabilities = Some(capabilities);
2079 self
2080 }
2081
2082 #[must_use]
2084 pub fn has_progress_reporter(&self) -> bool {
2085 self.ensure_live().is_ok() && self.progress_reporter.is_some()
2086 }
2087
2088 #[must_use]
2093 pub fn progress_marker(&self) -> Option<&serde_json::Value> {
2094 self.ensure_live()
2095 .ok()
2096 .and_then(|()| self.progress_reporter.as_ref()?.marker())
2097 }
2098
2099 pub fn report_progress(&self, progress: f64, message: Option<&str>) {
2122 if self.ensure_live().is_ok()
2123 && let Some(ref reporter) = self.progress_reporter
2124 {
2125 reporter.report(progress, message);
2126 }
2127 }
2128
2129 pub fn report_progress_with_total(&self, progress: f64, total: f64, message: Option<&str>) {
2152 if self.ensure_live().is_ok()
2153 && let Some(ref reporter) = self.progress_reporter
2154 {
2155 reporter.report_with_total(progress, total, message);
2156 }
2157 }
2158
2159 pub fn report_progress_exact(
2165 &self,
2166 progress: serde_json::Number,
2167 total: Option<serde_json::Number>,
2168 message: Option<&str>,
2169 ) {
2170 if self.ensure_live().is_ok()
2171 && let Some(ref reporter) = self.progress_reporter
2172 {
2173 reporter.report_exact(progress, total, message);
2174 }
2175 }
2176
2177 #[must_use]
2182 pub fn request_id(&self) -> u64 {
2183 self.request_id
2184 }
2185
2186 #[must_use]
2193 pub fn region_id(&self) -> RegionId {
2194 self.cx.region_id()
2195 }
2196
2197 #[must_use]
2199 pub fn task_id(&self) -> TaskId {
2200 self.cx.task_id()
2201 }
2202
2203 fn apply_operation_deadline(&self, budget: Budget) -> Budget {
2204 self.operation_deadline.map_or(budget, |deadline| {
2205 budget.meet(Budget::new().with_deadline(deadline))
2206 })
2207 }
2208
2209 fn request_scope_is_active(&self) -> bool {
2210 self.request_lease.load(Ordering::Acquire) != REQUEST_LEASE_CLOSED
2211 }
2212
2213 #[must_use]
2221 pub fn budget(&self) -> Budget {
2222 let ambient = self.cx.budget();
2223 let state = *self
2224 .budget_state
2225 .lock()
2226 .unwrap_or_else(std::sync::PoisonError::into_inner);
2227 self.apply_operation_deadline(state.effective(ambient))
2228 }
2229
2230 #[must_use]
2235 pub fn is_cancelled(&self) -> bool {
2236 self.ensure_live().is_err()
2237 }
2238
2239 #[must_use]
2246 pub fn request_cancellation(&self) -> McpRequestCancellation {
2247 self.request_cancellation.clone()
2248 }
2249
2250 pub fn ensure_live(&self) -> Result<(), CancelledError> {
2265 if !self.request_scope_is_active() {
2266 return Err(CancelledError);
2267 }
2268 let _mask_transition = self
2269 .mask_transition
2270 .lock()
2271 .unwrap_or_else(std::sync::PoisonError::into_inner);
2272 if self.framework_mask_depth.load(Ordering::SeqCst) > 0 {
2273 return Ok(());
2274 }
2275
2276 let ambient = self.cx.budget();
2277 let now = self.cx.now();
2278 let state = *self
2279 .budget_state
2280 .lock()
2281 .unwrap_or_else(std::sync::PoisonError::into_inner);
2282 let effective = self.apply_operation_deadline(state.effective(ambient));
2283 if self.request_cancellation.is_cancel_requested()
2284 || self.cx.is_cancel_requested()
2285 || effective.is_past_deadline(now)
2286 || state.deferred_overrun
2287 {
2288 return Err(CancelledError);
2289 }
2290 Ok(())
2291 }
2292
2293 pub fn checkpoint(&self) -> Result<(), CancelledError> {
2325 if !self.request_scope_is_active() {
2326 return Err(CancelledError);
2327 }
2328 let _mask_transition = self
2329 .mask_transition
2330 .lock()
2331 .unwrap_or_else(std::sync::PoisonError::into_inner);
2332 let masked = self.framework_mask_depth.load(Ordering::SeqCst) > 0;
2333 let ambient = self.cx.budget();
2334 let now = self.cx.now();
2335 let mut state = self
2336 .budget_state
2337 .lock()
2338 .unwrap_or_else(std::sync::PoisonError::into_inner);
2339 let adjusted_ambient = state.adjusted_ambient(ambient);
2340 let effective = self.apply_operation_deadline(
2341 state
2342 .ceiling
2343 .map_or(adjusted_ambient, |ceiling| adjusted_ambient.meet(ceiling)),
2344 );
2345 let poll_unavailable = effective.poll_quota == 0;
2346 let past_deadline = effective.is_past_deadline(now);
2347 let cancelled =
2348 self.request_cancellation.is_cancel_requested() || self.cx.is_cancel_requested();
2349 let deferred_overrun = state.deferred_overrun;
2350
2351 if !masked && (cancelled || poll_unavailable || past_deadline || deferred_overrun) {
2352 return Err(CancelledError);
2353 }
2354
2355 if poll_unavailable {
2356 debug_assert!(masked);
2357 state.deferred_overrun = true;
2358 }
2359
2360 if adjusted_ambient.poll_quota != u32::MAX {
2361 state.ambient_poll_debits = state.ambient_poll_debits.saturating_add(1);
2362 }
2363
2364 if let Some(budget) = state.ceiling.as_mut()
2365 && budget.poll_quota != u32::MAX
2366 {
2367 if budget.consume_poll().is_none() {
2368 debug_assert!(masked);
2369 state.deferred_overrun = true;
2370 }
2371 }
2372
2373 Ok(())
2374 }
2375
2376 pub fn consume_cost(&self, cost: u64) -> Result<(), CancelledError> {
2407 if !self.request_scope_is_active() {
2408 return Err(CancelledError);
2409 }
2410 let _mask_transition = self
2411 .mask_transition
2412 .lock()
2413 .unwrap_or_else(std::sync::PoisonError::into_inner);
2414 let masked = self.framework_mask_depth.load(Ordering::SeqCst) > 0;
2415 let ambient = self.cx.budget();
2416 let now = self.cx.now();
2417 let mut state = self
2418 .budget_state
2419 .lock()
2420 .unwrap_or_else(std::sync::PoisonError::into_inner);
2421 let effective = self.apply_operation_deadline(state.effective(ambient));
2422 let enough_cost = effective
2423 .cost_quota
2424 .is_none_or(|remaining| remaining >= cost);
2425 let past_deadline = effective.is_past_deadline(now);
2426 let cancelled =
2427 self.request_cancellation.is_cancel_requested() || self.cx.is_cancel_requested();
2428
2429 if !masked && (cancelled || past_deadline || state.deferred_overrun || !enough_cost) {
2430 return Err(CancelledError);
2431 }
2432
2433 if !enough_cost {
2434 debug_assert!(masked);
2435 state.deferred_overrun = true;
2436 }
2437 state.ambient_cost_debits = state.ambient_cost_debits.saturating_add(cost);
2438 if let Some(budget) = state.ceiling.as_mut()
2439 && !budget.consume_cost(cost)
2440 {
2441 debug_assert!(masked);
2442 budget.cost_quota = Some(0);
2443 }
2444
2445 Ok(())
2446 }
2447
2448 pub fn masked<F, R>(&self, f: F) -> Result<R, CancelledError>
2476 where
2477 F: FnOnce() -> R,
2478 {
2479 if !self.request_scope_is_active() {
2480 return Err(CancelledError);
2481 }
2482 let entry_transition = self
2483 .mask_transition
2484 .lock()
2485 .unwrap_or_else(std::sync::PoisonError::into_inner);
2486 if self.framework_mask_depth.load(Ordering::SeqCst) >= MAX_MASK_DEPTH {
2487 return Err(CancelledError);
2488 }
2489 if self
2490 .framework_mask_depth
2491 .try_update(Ordering::SeqCst, Ordering::SeqCst, |depth| {
2492 depth.checked_add(1)
2493 })
2494 .is_err()
2495 {
2496 return Err(CancelledError);
2497 }
2498 let framework_mask = FrameworkMaskGuard {
2499 depth: &self.framework_mask_depth,
2500 };
2501 let masked_outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2502 self.cx.masked(|| {
2503 drop(entry_transition);
2504 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
2505 let exit_transition = self
2506 .mask_transition
2507 .lock()
2508 .unwrap_or_else(std::sync::PoisonError::into_inner);
2509 (outcome, exit_transition)
2510 })
2511 }));
2512 let (outcome, exit_transition) = match masked_outcome {
2513 Ok(result) => result,
2514 Err(_runtime_mask_failure) => {
2515 let exit_transition = self
2516 .mask_transition
2517 .lock()
2518 .unwrap_or_else(std::sync::PoisonError::into_inner);
2519 drop(framework_mask);
2520 drop(exit_transition);
2521 return Err(CancelledError);
2522 }
2523 };
2524 drop(framework_mask);
2525 drop(exit_transition);
2526
2527 match outcome {
2528 Ok(result) => Ok(result),
2529 Err(payload) => std::panic::resume_unwind(payload),
2530 }
2531 }
2532
2533 pub fn trace(&self, message: &str) {
2538 if self.ensure_live().is_ok() {
2539 self.cx.trace(message);
2540 }
2541 }
2542
2543 pub fn debug(&self, message: impl AsRef<str>) {
2545 self.log(McpLogLevel::Debug, message);
2546 }
2547
2548 pub fn info(&self, message: impl AsRef<str>) {
2550 self.log(McpLogLevel::Info, message);
2551 }
2552
2553 pub fn notice(&self, message: impl AsRef<str>) {
2555 self.log(McpLogLevel::Notice, message);
2556 }
2557
2558 pub fn warning(&self, message: impl AsRef<str>) {
2560 self.log(McpLogLevel::Warning, message);
2561 }
2562
2563 pub fn error(&self, message: impl AsRef<str>) {
2565 self.log(McpLogLevel::Error, message);
2566 }
2567
2568 pub fn log(&self, level: McpLogLevel, message: impl AsRef<str>) {
2573 self.log_data(
2574 level,
2575 serde_json::Value::String(message.as_ref().to_owned()),
2576 );
2577 }
2578
2579 pub fn log_data(&self, level: McpLogLevel, data: serde_json::Value) {
2581 if self.ensure_live().is_err() {
2582 return;
2583 }
2584 let Some(min_level) = self.min_log_level else {
2585 return;
2586 };
2587 if level.rank() < min_level.rank() {
2588 return;
2589 }
2590 if let Some(sender) = self.log_sender.as_ref() {
2591 sender.send_log(level, Some("fastmcp"), data);
2592 }
2593 }
2594
2595 pub fn notify_resource_updated(&self, uri: impl AsRef<str>) -> bool {
2601 if self.ensure_live().is_err() {
2602 return false;
2603 }
2604 let uri = uri.as_ref();
2605 let mut delivered = false;
2606 if self
2607 .resource_subscriptions
2608 .as_ref()
2609 .is_some_and(|uris| uris.contains(uri))
2610 && let Some(sender) = self.log_sender.as_ref()
2611 {
2612 sender.send_resource_updated(uri);
2613 delivered = true;
2614 }
2615 if let Some(publisher) = self.catalog_publisher.as_ref()
2616 && publisher.publish_resource_updated(uri)
2617 {
2618 delivered = true;
2619 }
2620 delivered
2621 }
2622
2623 #[must_use]
2636 pub fn cx(&self) -> &Cx {
2637 &self.cx
2638 }
2639
2640 #[must_use]
2648 pub fn final_result_outcome<TypedResult, LegacyResult, TerminalReason>(
2649 &self,
2650 result: crate::combinator::FinalRequestResult<TypedResult, LegacyResult, TerminalReason>,
2651 ) -> crate::McpOutcome<
2652 crate::combinator::FinalRequestResult<TypedResult, LegacyResult, TerminalReason>,
2653 > {
2654 if self.ensure_live().is_err() {
2655 return Outcome::Cancelled(self.final_result_cancellation_reason());
2656 }
2657 Outcome::Ok(result)
2658 }
2659
2660 #[must_use]
2665 pub fn adapt_final_request_outcome<TypedResult, LegacyResult, TerminalReason>(
2666 &self,
2667 outcome: crate::McpOutcome<
2668 crate::combinator::FinalRequestResult<TypedResult, LegacyResult, TerminalReason>,
2669 >,
2670 ) -> crate::McpOutcome<
2671 crate::combinator::FinalRequestResult<TypedResult, LegacyResult, TerminalReason>,
2672 > {
2673 match outcome {
2674 Outcome::Ok(result) => self.final_result_outcome(result),
2675 Outcome::Err(error) => Outcome::Err(error),
2676 Outcome::Cancelled(reason) => Outcome::Cancelled(reason),
2677 Outcome::Panicked(payload) => Outcome::Panicked(payload),
2678 }
2679 }
2680
2681 fn final_result_cancellation_reason(&self) -> CancelReason {
2682 self.cx.cancel_reason().unwrap_or_else(|| {
2683 if self.request_cancellation.is_cancel_requested() {
2684 CancelReason::user("FastMCP request-local cancellation")
2685 } else if !self.request_scope_is_active() {
2686 CancelReason::user("FastMCP request lease closed")
2687 } else {
2688 CancelReason::user("FastMCP request liveness rejected final result")
2689 }
2690 })
2691 }
2692
2693 #[must_use]
2716 pub fn get_state<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
2717 if !self.request_scope_is_active() {
2718 return None;
2719 }
2720 self.state.as_ref()?.get(key)
2721 }
2722
2723 #[must_use]
2725 pub fn auth(&self) -> Option<AuthContext> {
2726 if !self.request_scope_is_active() {
2727 return None;
2728 }
2729 self.auth
2730 .lock()
2731 .unwrap_or_else(std::sync::PoisonError::into_inner)
2732 .clone()
2733 }
2734
2735 pub fn set_auth(&self, auth: AuthContext) -> bool {
2744 if self.ensure_live().is_err() {
2745 return false;
2746 }
2747 let mut slot = self
2748 .auth
2749 .lock()
2750 .unwrap_or_else(std::sync::PoisonError::into_inner);
2751 if self.auth_state.load(Ordering::Acquire) != REQUEST_AUTH_UNCOMMITTED {
2752 return false;
2753 }
2754 *slot = Some(auth);
2755 self.auth_state
2756 .store(REQUEST_AUTH_AUTHENTICATED, Ordering::Release);
2757 true
2758 }
2759
2760 #[doc(hidden)]
2768 pub fn commit_anonymous_auth(&self) -> bool {
2769 if self.ensure_live().is_err() {
2770 return false;
2771 }
2772 let slot = self
2773 .auth
2774 .lock()
2775 .unwrap_or_else(std::sync::PoisonError::into_inner);
2776 if self.auth_state.load(Ordering::Acquire) != REQUEST_AUTH_UNCOMMITTED || slot.is_some() {
2777 return false;
2778 }
2779 self.auth_state
2780 .store(REQUEST_AUTH_ANONYMOUS, Ordering::Release);
2781 true
2782 }
2783
2784 #[doc(hidden)]
2790 #[must_use]
2791 pub fn cache_auth_partition(&self) -> Option<Option<AuthContext>> {
2792 if !self.request_scope_is_active() {
2793 return None;
2794 }
2795 let slot = self
2796 .auth
2797 .lock()
2798 .unwrap_or_else(std::sync::PoisonError::into_inner);
2799 match self.auth_state.load(Ordering::Acquire) {
2800 REQUEST_AUTH_ANONYMOUS => Some(None),
2801 REQUEST_AUTH_AUTHENTICATED => slot.clone().map(Some),
2802 _ => None,
2803 }
2804 }
2805
2806 #[must_use]
2808 pub fn with_auth(self, auth: AuthContext) -> Self {
2809 let _ = self.set_auth(auth);
2810 self
2811 }
2812
2813 #[must_use]
2821 pub fn with_isolated_auth(mut self) -> Self {
2822 let already_committed = self.auth_state.load(Ordering::Acquire) != REQUEST_AUTH_UNCOMMITTED;
2823 if already_committed {
2824 return self;
2825 }
2826 self.auth = Arc::new(Mutex::new(None));
2827 self.auth_state = Arc::new(AtomicU8::new(REQUEST_AUTH_UNCOMMITTED));
2828 self.state = None;
2829 self.progress_reporter = None;
2830 self.sampling_sender = None;
2831 self.elicitation_sender = None;
2832 self.roots_provider = None;
2833 self.resource_reader = None;
2834 self.tool_caller = None;
2835 self.prompt_caller = None;
2836 self
2837 }
2838
2839 pub fn set_state<T: serde::Serialize>(&self, key: impl Into<String>, value: T) -> bool {
2856 if self.ensure_live().is_err() {
2857 return false;
2858 }
2859 match &self.state {
2860 Some(state) => state.set(key, value),
2861 None => false,
2862 }
2863 }
2864
2865 pub fn remove_state(&self, key: &str) -> Option<serde_json::Value> {
2871 if self.ensure_live().is_err() {
2872 return None;
2873 }
2874 self.state.as_ref()?.remove(key)
2875 }
2876
2877 #[must_use]
2881 pub fn has_state(&self, key: &str) -> bool {
2882 self.request_scope_is_active() && self.state.as_ref().is_some_and(|s| s.contains(key))
2883 }
2884
2885 #[must_use]
2887 pub fn has_session_state(&self) -> bool {
2888 self.request_scope_is_active() && self.state.is_some()
2889 }
2890
2891 #[doc(hidden)]
2893 #[must_use]
2894 pub fn session_is_ephemeral(&self) -> bool {
2895 self.request_scope_is_active()
2896 && self.state.as_ref().is_some_and(SessionState::is_ephemeral)
2897 }
2898
2899 #[must_use]
2905 pub fn session_state(&self) -> Option<&SessionState> {
2906 self.state.as_ref()
2907 }
2908
2909 #[doc(hidden)]
2917 #[must_use]
2918 pub fn session_cache_partition(&self) -> Option<([u8; 32], u64)> {
2919 if !self.request_scope_is_active() {
2920 return None;
2921 }
2922 self.state.as_ref()?.cache_partition()
2923 }
2924
2925 #[doc(hidden)]
2931 #[must_use]
2932 pub fn begin_session_cache_partition(&self) -> Option<([u8; 32], u64)> {
2933 let current = self.session_cache_partition()?;
2934 let mut admitted = self
2935 .cache_admission_partition
2936 .lock()
2937 .unwrap_or_else(std::sync::PoisonError::into_inner);
2938 match *admitted {
2939 None => {
2940 *admitted = Some(current);
2941 Some(current)
2942 }
2943 Some(existing) if existing == current => Some(existing),
2944 Some(_) => None,
2945 }
2946 }
2947
2948 #[doc(hidden)]
2951 #[must_use]
2952 pub fn complete_session_cache_partition(&self) -> Option<([u8; 32], u64)> {
2953 if !self.request_scope_is_active() {
2954 return None;
2955 }
2956 let admitted = *self
2957 .cache_admission_partition
2958 .lock()
2959 .unwrap_or_else(std::sync::PoisonError::into_inner);
2960 let admitted = admitted?;
2961 (self.state.as_ref()?.cache_partition() == Some(admitted)).then_some(admitted)
2962 }
2963
2964 #[doc(hidden)]
2967 pub fn mark_response_cache_hit(&self, cache_id: u64) -> bool {
2968 const MAX_CACHE_MIDDLEWARE_PER_REQUEST: usize = 64;
2969 if !self.request_scope_is_active() || cache_id == 0 {
2970 return false;
2971 }
2972 let mut hits = self
2973 .response_cache_hits
2974 .lock()
2975 .unwrap_or_else(std::sync::PoisonError::into_inner);
2976 if hits.contains(&cache_id) {
2977 return true;
2978 }
2979 if hits.len() >= MAX_CACHE_MIDDLEWARE_PER_REQUEST || hits.try_reserve(1).is_err() {
2980 return false;
2981 }
2982 hits.push(cache_id);
2983 true
2984 }
2985
2986 #[doc(hidden)]
2989 #[must_use]
2990 pub fn response_was_cache_hit(&self, cache_id: u64) -> bool {
2991 self.request_scope_is_active()
2992 && cache_id != 0
2993 && self
2994 .response_cache_hits
2995 .lock()
2996 .unwrap_or_else(std::sync::PoisonError::into_inner)
2997 .contains(&cache_id)
2998 }
2999
3000 #[doc(hidden)]
3002 #[must_use]
3003 pub fn response_was_served_from_cache(&self) -> bool {
3004 self.request_scope_is_active()
3005 && !self
3006 .response_cache_hits
3007 .lock()
3008 .unwrap_or_else(std::sync::PoisonError::into_inner)
3009 .is_empty()
3010 }
3011
3012 #[must_use]
3021 pub fn client_capabilities(&self) -> Option<&ClientCapabilityInfo> {
3022 self.client_capabilities.as_ref()
3023 }
3024
3025 #[must_use]
3030 pub fn client_implementation(&self) -> Option<&ClientImplementationInfo> {
3031 self.client_implementation.as_ref()
3032 }
3033
3034 #[must_use]
3038 pub fn server_capabilities(&self) -> Option<&ServerCapabilityInfo> {
3039 self.server_capabilities.as_ref()
3040 }
3041
3042 #[must_use]
3047 pub fn client_supports_sampling(&self) -> bool {
3048 self.client_capabilities
3049 .as_ref()
3050 .is_some_and(|c| c.sampling)
3051 }
3052
3053 #[must_use]
3058 pub fn client_supports_elicitation(&self) -> bool {
3059 self.client_capabilities
3060 .as_ref()
3061 .is_some_and(|c| c.elicitation)
3062 }
3063
3064 #[must_use]
3066 pub fn client_supports_elicitation_form(&self) -> bool {
3067 self.client_capabilities
3068 .as_ref()
3069 .is_some_and(|c| c.elicitation_form)
3070 }
3071
3072 #[must_use]
3074 pub fn client_supports_elicitation_url(&self) -> bool {
3075 self.client_capabilities
3076 .as_ref()
3077 .is_some_and(|c| c.elicitation_url)
3078 }
3079
3080 #[must_use]
3085 pub fn client_supports_roots(&self) -> bool {
3086 self.client_capabilities.as_ref().is_some_and(|c| c.roots)
3087 }
3088
3089 const DISABLED_TOOLS_KEY: &'static str = "fastmcp.disabled_tools";
3095 const DISABLED_RESOURCES_KEY: &'static str = "fastmcp.disabled_resources";
3097 const DISABLED_PROMPTS_KEY: &'static str = "fastmcp.disabled_prompts";
3099
3100 pub fn disable_tool(&self, name: impl Into<String>) -> bool {
3118 self.add_to_disabled_set(Self::DISABLED_TOOLS_KEY, name.into(), McpCatalogKind::Tools)
3119 }
3120
3121 pub fn enable_tool(&self, name: &str) -> bool {
3125 self.remove_from_disabled_set(Self::DISABLED_TOOLS_KEY, name, McpCatalogKind::Tools)
3126 }
3127
3128 #[must_use]
3132 pub fn is_tool_enabled(&self, name: &str) -> bool {
3133 self.request_scope_is_active() && !self.is_in_disabled_set(Self::DISABLED_TOOLS_KEY, name)
3134 }
3135
3136 pub fn disable_resource(&self, uri: impl Into<String>) -> bool {
3143 self.add_to_disabled_set(
3144 Self::DISABLED_RESOURCES_KEY,
3145 uri.into(),
3146 McpCatalogKind::Resources,
3147 )
3148 }
3149
3150 pub fn enable_resource(&self, uri: &str) -> bool {
3154 self.remove_from_disabled_set(Self::DISABLED_RESOURCES_KEY, uri, McpCatalogKind::Resources)
3155 }
3156
3157 #[must_use]
3161 pub fn is_resource_enabled(&self, uri: &str) -> bool {
3162 self.request_scope_is_active()
3163 && !self.is_in_disabled_set(Self::DISABLED_RESOURCES_KEY, uri)
3164 }
3165
3166 pub fn disable_prompt(&self, name: impl Into<String>) -> bool {
3173 self.add_to_disabled_set(
3174 Self::DISABLED_PROMPTS_KEY,
3175 name.into(),
3176 McpCatalogKind::Prompts,
3177 )
3178 }
3179
3180 pub fn enable_prompt(&self, name: &str) -> bool {
3184 self.remove_from_disabled_set(Self::DISABLED_PROMPTS_KEY, name, McpCatalogKind::Prompts)
3185 }
3186
3187 #[must_use]
3191 pub fn is_prompt_enabled(&self, name: &str) -> bool {
3192 self.request_scope_is_active() && !self.is_in_disabled_set(Self::DISABLED_PROMPTS_KEY, name)
3193 }
3194
3195 #[must_use]
3197 pub fn disabled_tools(&self) -> std::collections::HashSet<String> {
3198 self.get_disabled_set(Self::DISABLED_TOOLS_KEY)
3199 }
3200
3201 #[must_use]
3203 pub fn disabled_resources(&self) -> std::collections::HashSet<String> {
3204 self.get_disabled_set(Self::DISABLED_RESOURCES_KEY)
3205 }
3206
3207 #[must_use]
3209 pub fn disabled_prompts(&self) -> std::collections::HashSet<String> {
3210 self.get_disabled_set(Self::DISABLED_PROMPTS_KEY)
3211 }
3212
3213 fn add_to_disabled_set(&self, key: &str, name: String, kind: McpCatalogKind) -> bool {
3215 if self.ensure_live().is_err() {
3216 return false;
3217 }
3218 let Some(state) = self.state.as_ref() else {
3219 return false;
3220 };
3221 let mut set: std::collections::HashSet<String> = state.get(key).unwrap_or_default();
3222 let changed = set.insert(name);
3223 let stored = state.set(key, set);
3224 if stored && changed {
3225 self.emit_catalog_changed(kind);
3226 }
3227 stored
3228 }
3229
3230 fn remove_from_disabled_set(&self, key: &str, name: &str, kind: McpCatalogKind) -> bool {
3232 if self.ensure_live().is_err() {
3233 return false;
3234 }
3235 let Some(state) = self.state.as_ref() else {
3236 return false;
3237 };
3238 let mut set: std::collections::HashSet<String> = state.get(key).unwrap_or_default();
3239 let changed = set.remove(name);
3240 let stored = state.set(key, set);
3241 if stored && changed {
3242 self.emit_catalog_changed(kind);
3243 }
3244 stored
3245 }
3246
3247 fn emit_catalog_changed(&self, kind: McpCatalogKind) {
3248 if let Some(sender) = self.log_sender.as_ref() {
3249 sender.send_catalog_changed(kind);
3250 }
3251 if let Some(publisher) = self.catalog_publisher.as_ref() {
3252 let _ = publisher.publish_catalog_changed(kind);
3253 }
3254 }
3255
3256 fn is_in_disabled_set(&self, key: &str, name: &str) -> bool {
3258 if !self.request_scope_is_active() {
3259 return false;
3260 }
3261 let Some(state) = self.state.as_ref() else {
3262 return false;
3263 };
3264 let set: std::collections::HashSet<String> = state.get(key).unwrap_or_default();
3265 set.contains(name)
3266 }
3267
3268 fn get_disabled_set(&self, key: &str) -> std::collections::HashSet<String> {
3270 if !self.request_scope_is_active() {
3271 return std::collections::HashSet::new();
3272 }
3273 self.state
3274 .as_ref()
3275 .and_then(|s| s.get(key))
3276 .unwrap_or_default()
3277 }
3278
3279 #[must_use]
3285 pub fn can_list_roots(&self) -> bool {
3286 self.ensure_live().is_ok() && self.roots_provider.is_some()
3287 }
3288
3289 pub async fn list_roots(&self) -> crate::McpResult<Vec<ClientRoot>> {
3296 self.ensure_live()
3297 .map_err(|_| crate::McpError::request_cancelled())?;
3298 let provider = self.roots_provider.as_ref().ok_or_else(|| {
3299 crate::McpError::new(
3300 crate::McpErrorCode::InvalidRequest,
3301 "Roots not available: client does not support roots capability",
3302 )
3303 })?;
3304
3305 let roots = provider.list_roots().await?;
3306 self.ensure_live()
3307 .map_err(|_| crate::McpError::request_cancelled())?;
3308 Ok(roots)
3309 }
3310
3311 #[must_use]
3320 pub fn can_sample(&self) -> bool {
3321 self.ensure_live().is_ok() && self.sampling_sender.is_some()
3322 }
3323
3324 pub async fn sample(
3349 &self,
3350 prompt: impl Into<String>,
3351 max_tokens: u32,
3352 ) -> crate::McpResult<SamplingResponse> {
3353 let request = SamplingRequest::prompt(prompt, max_tokens);
3354 self.sample_with_request(request).await
3355 }
3356
3357 pub async fn sample_with_request(
3389 &self,
3390 request: SamplingRequest,
3391 ) -> crate::McpResult<SamplingResponse> {
3392 self.ensure_live()
3393 .map_err(|_| crate::McpError::request_cancelled())?;
3394 let sender = self.sampling_sender.as_ref().ok_or_else(|| {
3395 crate::McpError::new(
3396 crate::McpErrorCode::InvalidRequest,
3397 "Sampling not available: client does not support sampling capability",
3398 )
3399 })?;
3400
3401 let response = sender.create_message(request).await?;
3402 self.ensure_live()
3403 .map_err(|_| crate::McpError::request_cancelled())?;
3404 Ok(response)
3405 }
3406
3407 #[must_use]
3416 pub fn can_elicit(&self) -> bool {
3417 self.ensure_live().is_ok() && self.elicitation_sender.is_some()
3418 }
3419
3420 pub async fn elicit_form(
3458 &self,
3459 message: impl Into<String>,
3460 schema: serde_json::Value,
3461 ) -> crate::McpResult<ElicitationResponse> {
3462 let request = ElicitationRequest::form(message, schema);
3463 self.elicit_with_request(request).await
3464 }
3465
3466 pub async fn elicit_url(
3500 &self,
3501 message: impl Into<String>,
3502 url: impl Into<String>,
3503 elicitation_id: impl Into<String>,
3504 ) -> crate::McpResult<ElicitationResponse> {
3505 let request = ElicitationRequest::url(message, url, elicitation_id);
3506 self.elicit_with_request(request).await
3507 }
3508
3509 pub async fn elicit_with_request(
3521 &self,
3522 request: ElicitationRequest,
3523 ) -> crate::McpResult<ElicitationResponse> {
3524 self.ensure_live()
3525 .map_err(|_| crate::McpError::request_cancelled())?;
3526 let sender = self.elicitation_sender.as_ref().ok_or_else(|| {
3527 crate::McpError::new(
3528 crate::McpErrorCode::InvalidRequest,
3529 "Elicitation not available: client does not support elicitation capability",
3530 )
3531 })?;
3532
3533 let response = sender.elicit(request).await?;
3534 self.ensure_live()
3535 .map_err(|_| crate::McpError::request_cancelled())?;
3536 Ok(response)
3537 }
3538
3539 #[must_use]
3548 pub fn can_read_resources(&self) -> bool {
3549 self.ensure_live().is_ok() && self.resource_reader.is_some()
3550 }
3551
3552 #[must_use]
3556 pub fn resource_read_depth(&self) -> u32 {
3557 self.resource_read_depth
3558 }
3559
3560 pub async fn read_resource(&self, uri: &str) -> crate::McpResult<ResourceReadResult> {
3589 self.ensure_live()
3590 .map_err(|_| crate::McpError::request_cancelled())?;
3591 let reader = self.resource_reader.as_ref().ok_or_else(|| {
3593 crate::McpError::new(
3594 crate::McpErrorCode::InternalError,
3595 "Resource reading not available: no router attached to context",
3596 )
3597 })?;
3598
3599 let nested_dispatch_depth = self.nested_dispatch_depth();
3603 if nested_dispatch_depth >= MAX_RESOURCE_READ_DEPTH {
3604 return Err(crate::McpError::new(
3605 crate::McpErrorCode::InternalError,
3606 format!(
3607 "Maximum resource read depth ({}) exceeded; possible infinite recursion",
3608 MAX_RESOURCE_READ_DEPTH
3609 ),
3610 ));
3611 }
3612
3613 let result = reader
3615 .read_resource(self, uri, nested_dispatch_depth + 1)
3616 .await?;
3617 self.ensure_live()
3618 .map_err(|_| crate::McpError::request_cancelled())?;
3619 Ok(result)
3620 }
3621
3622 pub async fn read_resource_text(&self, uri: &str) -> crate::McpResult<String> {
3640 let result = self.read_resource(uri).await?;
3641 result.first_text().map(String::from).ok_or_else(|| {
3642 crate::McpError::new(
3643 crate::McpErrorCode::InternalError,
3644 format!("Resource '{}' has no text content", uri),
3645 )
3646 })
3647 }
3648
3649 pub async fn read_resource_json<T: serde::de::DeserializeOwned>(
3673 &self,
3674 uri: &str,
3675 ) -> crate::McpResult<T> {
3676 let text = self.read_resource_text(uri).await?;
3677 serde_json::from_str(&text).map_err(|e| {
3678 crate::McpError::new(
3679 crate::McpErrorCode::InternalError,
3680 format!("Failed to parse resource '{}' as JSON: {}", uri, e),
3681 )
3682 })
3683 }
3684
3685 #[must_use]
3694 pub fn can_call_tools(&self) -> bool {
3695 self.ensure_live().is_ok() && self.tool_caller.is_some()
3696 }
3697
3698 #[must_use]
3702 pub fn tool_call_depth(&self) -> u32 {
3703 self.tool_call_depth
3704 }
3705
3706 pub async fn call_tool(
3734 &self,
3735 name: &str,
3736 args: serde_json::Value,
3737 ) -> crate::McpResult<ToolCallResult> {
3738 self.ensure_live()
3739 .map_err(|_| crate::McpError::request_cancelled())?;
3740 let caller = self.tool_caller.as_ref().ok_or_else(|| {
3742 crate::McpError::new(
3743 crate::McpErrorCode::InternalError,
3744 "Tool calling not available: no router attached to context",
3745 )
3746 })?;
3747
3748 let nested_dispatch_depth = self.nested_dispatch_depth();
3751 if nested_dispatch_depth >= MAX_TOOL_CALL_DEPTH {
3752 return Err(crate::McpError::new(
3753 crate::McpErrorCode::InternalError,
3754 format!(
3755 "Maximum tool call depth ({}) exceeded calling '{}'; possible infinite recursion",
3756 MAX_TOOL_CALL_DEPTH, name
3757 ),
3758 ));
3759 }
3760
3761 let result = caller
3763 .call_tool(self, name, args, nested_dispatch_depth + 1)
3764 .await?;
3765 self.ensure_live()
3766 .map_err(|_| crate::McpError::request_cancelled())?;
3767 Ok(result)
3768 }
3769
3770 pub async fn call_tool_text(
3789 &self,
3790 name: &str,
3791 args: serde_json::Value,
3792 ) -> crate::McpResult<String> {
3793 let result = self.call_tool(name, args).await?;
3794
3795 if result.is_error {
3797 let error_msg = result.first_text().unwrap_or("Tool returned an error");
3798 return Err(crate::McpError::new(
3799 crate::McpErrorCode::InternalError,
3800 format!("Tool '{}' failed: {}", name, error_msg),
3801 ));
3802 }
3803
3804 result.first_text().map(String::from).ok_or_else(|| {
3805 crate::McpError::new(
3806 crate::McpErrorCode::InternalError,
3807 format!("Tool '{}' returned no text content", name),
3808 )
3809 })
3810 }
3811
3812 pub async fn call_tool_json<T: serde::de::DeserializeOwned>(
3837 &self,
3838 name: &str,
3839 args: serde_json::Value,
3840 ) -> crate::McpResult<T> {
3841 let text = self.call_tool_text(name, args).await?;
3842 serde_json::from_str(&text).map_err(|e| {
3843 crate::McpError::new(
3844 crate::McpErrorCode::InternalError,
3845 format!("Failed to parse tool '{}' result as JSON: {}", name, e),
3846 )
3847 })
3848 }
3849
3850 #[must_use]
3856 pub fn can_get_prompts(&self) -> bool {
3857 self.ensure_live().is_ok() && self.prompt_caller.is_some()
3858 }
3859
3860 #[must_use]
3862 pub fn prompt_get_depth(&self) -> u32 {
3863 self.prompt_get_depth
3864 }
3865
3866 fn nested_dispatch_depth(&self) -> u32 {
3867 self.resource_read_depth
3868 .max(self.tool_call_depth)
3869 .max(self.prompt_get_depth)
3870 }
3871
3872 pub async fn get_prompt(
3877 &self,
3878 name: &str,
3879 arguments: std::collections::HashMap<String, String>,
3880 ) -> crate::McpResult<PromptGetResult> {
3881 self.ensure_live()
3882 .map_err(|_| crate::McpError::request_cancelled())?;
3883 let caller = self.prompt_caller.as_ref().ok_or_else(|| {
3884 crate::McpError::new(
3885 crate::McpErrorCode::InternalError,
3886 "Prompt getting not available: no router attached to context",
3887 )
3888 })?;
3889
3890 let nested_dispatch_depth = self.nested_dispatch_depth();
3891 if nested_dispatch_depth >= MAX_PROMPT_GET_DEPTH {
3892 return Err(crate::McpError::new(
3893 crate::McpErrorCode::InternalError,
3894 format!(
3895 "Maximum prompt get depth ({}) exceeded getting '{}'; possible infinite recursion",
3896 MAX_PROMPT_GET_DEPTH, name
3897 ),
3898 ));
3899 }
3900
3901 let result = caller
3902 .get_prompt(self, name, arguments, nested_dispatch_depth + 1)
3903 .await?;
3904 self.ensure_live()
3905 .map_err(|_| crate::McpError::request_cancelled())?;
3906 Ok(result)
3907 }
3908
3909 pub async fn get_prompt_text(
3911 &self,
3912 name: &str,
3913 arguments: std::collections::HashMap<String, String>,
3914 ) -> crate::McpResult<String> {
3915 let result = self.get_prompt(name, arguments).await?;
3916 result.first_text().map(String::from).ok_or_else(|| {
3917 crate::McpError::new(
3918 crate::McpErrorCode::InternalError,
3919 format!("Prompt '{}' returned no text content", name),
3920 )
3921 })
3922 }
3923
3924 pub async fn join_all<T: Send + 'static>(
3944 &self,
3945 futures: Vec<crate::combinator::BoxFuture<'_, T>>,
3946 ) -> crate::McpResult<Vec<T>> {
3947 self.ensure_live()
3948 .map_err(|_| crate::McpError::request_cancelled())?;
3949 let results = crate::combinator::join_all(&self.cx, futures).await;
3950 self.ensure_live()
3951 .map_err(|_| crate::McpError::request_cancelled())?;
3952 Ok(results)
3953 }
3954
3955 pub async fn race<T: Send + 'static>(
3972 &self,
3973 futures: Vec<crate::combinator::BoxFuture<'_, T>>,
3974 ) -> crate::McpResult<T> {
3975 self.ensure_live()
3976 .map_err(|_| crate::McpError::request_cancelled())?;
3977 let result = crate::combinator::race(&self.cx, futures).await;
3978 self.ensure_live()
3979 .map_err(|_| crate::McpError::request_cancelled())?;
3980 result
3981 }
3982
3983 pub async fn quorum<T: Send + 'static>(
4000 &self,
4001 required: usize,
4002 futures: Vec<crate::combinator::BoxFuture<'_, crate::McpResult<T>>>,
4003 ) -> crate::McpResult<crate::combinator::QuorumResult<T>> {
4004 self.ensure_live()
4005 .map_err(|_| crate::McpError::request_cancelled())?;
4006 let result = crate::combinator::quorum(&self.cx, required, futures).await;
4007 self.ensure_live()
4008 .map_err(|_| crate::McpError::request_cancelled())?;
4009 result
4010 }
4011
4012 pub async fn first_ok<T: Send + 'static>(
4029 &self,
4030 futures: Vec<crate::combinator::BoxFuture<'_, crate::McpResult<T>>>,
4031 ) -> crate::McpResult<T> {
4032 self.ensure_live()
4033 .map_err(|_| crate::McpError::request_cancelled())?;
4034 let result = crate::combinator::first_ok(&self.cx, futures).await;
4035 self.ensure_live()
4036 .map_err(|_| crate::McpError::request_cancelled())?;
4037 result
4038 }
4039}
4040
4041#[derive(Debug, Clone, Copy)]
4047pub struct CancelledError;
4048
4049impl std::fmt::Display for CancelledError {
4050 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4051 write!(f, "request cancelled")
4052 }
4053}
4054
4055impl std::error::Error for CancelledError {}
4056
4057pub trait IntoOutcome<T, E> {
4062 fn into_outcome(self) -> Outcome<T, E>;
4064}
4065
4066impl<T, E> IntoOutcome<T, E> for Result<T, E> {
4067 fn into_outcome(self) -> Outcome<T, E> {
4068 match self {
4069 Ok(v) => Outcome::Ok(v),
4070 Err(e) => Outcome::Err(e),
4071 }
4072 }
4073}
4074
4075impl<T, E> IntoOutcome<T, E> for Result<T, CancelledError>
4076where
4077 E: Default,
4078{
4079 fn into_outcome(self) -> Outcome<T, E> {
4080 match self {
4081 Ok(v) => Outcome::Ok(v),
4082 Err(CancelledError) => Outcome::Cancelled(CancelReason::user("request cancelled")),
4083 }
4084 }
4085}
4086
4087#[cfg(test)]
4088mod tests {
4089 use super::*;
4090
4091 #[test]
4092 fn test_mcp_context_creation() {
4093 let cx = Cx::for_testing();
4094 let ctx = McpContext::new(cx, 42);
4095
4096 assert_eq!(ctx.request_id(), 42);
4097 }
4098
4099 #[test]
4100 fn test_mcp_context_not_cancelled_initially() {
4101 let cx = Cx::for_testing();
4102 let ctx = McpContext::new(cx, 1);
4103
4104 assert!(!ctx.is_cancelled());
4105 }
4106
4107 #[test]
4108 fn test_mcp_context_checkpoint_success() {
4109 let cx = Cx::for_testing();
4110 let ctx = McpContext::new(cx, 1);
4111
4112 assert!(ctx.checkpoint().is_ok());
4114 }
4115
4116 #[test]
4117 fn test_mcp_context_checkpoint_cancelled() {
4118 let cx = Cx::for_testing();
4119 cx.set_cancel_requested(true);
4120 let ctx = McpContext::new(cx, 1);
4121
4122 assert!(ctx.checkpoint().is_err());
4124 }
4125
4126 #[test]
4127 fn request_local_cancellation_does_not_cancel_shared_ambient_context() {
4128 let cx = Cx::for_testing();
4129 let cancellation = McpRequestCancellation::new();
4130 let request =
4131 McpContext::new(cx.clone(), 1).with_request_cancellation(cancellation.clone());
4132 let sibling = McpContext::new(cx.clone(), 2);
4133
4134 cancellation.cancel();
4135
4136 assert!(request.ensure_live().is_err());
4137 assert!(request.checkpoint().is_err());
4138 assert!(sibling.ensure_live().is_ok());
4139 assert!(!cx.is_cancel_requested());
4140 }
4141
4142 #[test]
4143 fn context_exposes_its_request_local_cancellation_handle() {
4144 let cancellation = McpRequestCancellation::new();
4145 let context =
4146 McpContext::new(Cx::for_testing(), 1).with_request_cancellation(cancellation.clone());
4147
4148 let observed = context.request_cancellation();
4149 assert!(observed.cancel());
4150 assert!(cancellation.is_cancel_requested());
4151 assert!(context.is_cancelled());
4152 }
4153
4154 #[test]
4155 fn request_local_cancelled_future_registers_and_is_woken_without_polling() {
4156 use std::sync::atomic::AtomicBool;
4157
4158 struct WakeFlag(AtomicBool);
4159
4160 impl std::task::Wake for WakeFlag {
4161 fn wake(self: Arc<Self>) {
4162 self.0.store(true, Ordering::Release);
4163 }
4164 }
4165
4166 let cancellation = McpRequestCancellation::new();
4167 let wake_flag = Arc::new(WakeFlag(AtomicBool::new(false)));
4168 let waker = std::task::Waker::from(Arc::clone(&wake_flag));
4169 let mut task_cx = std::task::Context::from_waker(&waker);
4170 let mut future = Box::pin(cancellation.cancelled());
4171
4172 assert!(std::future::Future::poll(future.as_mut(), &mut task_cx).is_pending());
4173 assert!(cancellation.cancel());
4174 assert!(wake_flag.0.load(Ordering::Acquire));
4175 assert!(std::future::Future::poll(future.as_mut(), &mut task_cx).is_ready());
4176 }
4177
4178 #[test]
4179 fn request_local_cancelled_future_observes_preexisting_cancellation() {
4180 let cancellation = McpRequestCancellation::new();
4181 assert!(cancellation.cancel());
4182
4183 let mut future = Box::pin(cancellation.cancelled());
4184 let waker = std::task::Waker::noop();
4185 let mut task_cx = std::task::Context::from_waker(waker);
4186
4187 assert!(std::future::Future::poll(future.as_mut(), &mut task_cx).is_ready());
4188 }
4189
4190 #[test]
4191 fn request_terminal_future_is_woken_when_finalization_wins() {
4192 use std::sync::atomic::AtomicBool;
4193
4194 struct WakeFlag(AtomicBool);
4195
4196 impl std::task::Wake for WakeFlag {
4197 fn wake(self: Arc<Self>) {
4198 self.0.store(true, Ordering::Release);
4199 }
4200 }
4201
4202 let cancellation = McpRequestCancellation::new();
4203 let wake_flag = Arc::new(WakeFlag(AtomicBool::new(false)));
4204 let waker = std::task::Waker::from(Arc::clone(&wake_flag));
4205 let mut task_cx = std::task::Context::from_waker(&waker);
4206 let mut future = Box::pin(cancellation.terminated());
4207
4208 assert!(!cancellation.is_terminal());
4209 assert!(std::future::Future::poll(future.as_mut(), &mut task_cx).is_pending());
4210 assert!(cancellation.begin_finalization());
4211 assert!(cancellation.is_terminal());
4212 assert!(wake_flag.0.load(Ordering::Acquire));
4213 assert!(std::future::Future::poll(future.as_mut(), &mut task_cx).is_ready());
4214 }
4215
4216 #[test]
4217 fn request_local_cancellation_is_deferred_inside_framework_mask() {
4218 let cancellation = McpRequestCancellation::new();
4219 let ctx =
4220 McpContext::new(Cx::for_testing(), 1).with_request_cancellation(cancellation.clone());
4221
4222 let checkpoint = ctx
4223 .masked(|| {
4224 cancellation.cancel();
4225 ctx.checkpoint()
4226 })
4227 .expect("framework mask should be admitted");
4228
4229 assert!(checkpoint.is_ok());
4230 assert!(ctx.ensure_live().is_err());
4231 }
4232
4233 #[test]
4234 fn request_local_cancellation_stops_state_and_capability_effects() {
4235 let state = SessionState::new();
4236 assert!(state.set("existing", 1_u32));
4237 let cancellation = McpRequestCancellation::new();
4238 let ctx = McpContext::with_state(Cx::for_testing(), 1, state.clone())
4239 .with_sampling(Arc::new(NoOpSamplingSender))
4240 .with_elicitation(Arc::new(NoOpElicitationSender))
4241 .with_request_cancellation(cancellation.clone());
4242
4243 assert!(ctx.can_sample());
4244 assert!(ctx.can_elicit());
4245 assert!(cancellation.cancel());
4246
4247 assert!(!ctx.set_state("late", 2_u32));
4248 assert!(ctx.remove_state("existing").is_none());
4249 assert!(!ctx.disable_tool("late-tool"));
4250 assert!(!ctx.disable_resource("late://resource"));
4251 assert!(!ctx.disable_prompt("late-prompt"));
4252 assert!(!ctx.can_sample());
4253 assert!(!ctx.can_elicit());
4254 assert_eq!(state.get::<u32>("existing"), Some(1));
4255 assert!(!state.contains("late"));
4256 }
4257
4258 #[test]
4259 fn admitted_mask_allows_critical_state_commit_before_cancellation_surfaces() {
4260 let state = SessionState::new();
4261 let cancellation = McpRequestCancellation::new();
4262 let ctx = McpContext::with_state(Cx::for_testing(), 1, state.clone())
4263 .with_request_cancellation(cancellation.clone());
4264
4265 let committed = ctx
4266 .masked(|| {
4267 assert!(cancellation.cancel());
4268 ctx.set_state("critical-commit", true)
4269 })
4270 .expect("mask should be admitted before cancellation");
4271
4272 assert!(committed);
4273 assert_eq!(state.get::<bool>("critical-commit"), Some(true));
4274 assert!(ctx.ensure_live().is_err());
4275 }
4276
4277 #[test]
4278 fn active_request_clone_cannot_replace_cancellation_authority() {
4279 let original = McpRequestCancellation::new();
4280 let replacement = McpRequestCancellation::new();
4281 let root =
4282 McpContext::new(Cx::for_testing(), 1).with_request_cancellation(original.clone());
4283 let (scoped, _guard) = root
4284 .begin_request_scope()
4285 .expect("new context should activate one request lease");
4286 let attempted_escape = scoped
4287 .clone()
4288 .with_request_cancellation(replacement.clone());
4289
4290 assert!(original.cancel());
4291 assert!(attempted_escape.ensure_live().is_err());
4292 assert!(!replacement.is_cancel_requested());
4293 }
4294
4295 #[test]
4296 fn request_finalization_and_cancellation_have_one_atomic_winner() {
4297 let cancellation_wins = McpRequestCancellation::new();
4298 assert!(cancellation_wins.cancel());
4299 assert!(!cancellation_wins.begin_finalization());
4300 assert!(cancellation_wins.is_cancel_requested());
4301 assert!(cancellation_wins.is_terminal());
4302
4303 let finalization_wins = McpRequestCancellation::new();
4304 assert!(finalization_wins.begin_finalization());
4305 assert!(finalization_wins.is_finalizing());
4306 assert!(finalization_wins.is_terminal());
4307 assert!(!finalization_wins.cancel());
4308 assert!(!finalization_wins.is_cancel_requested());
4309 }
4310
4311 #[test]
4312 fn test_mcp_context_checkpoint_budget_exhausted() {
4313 let cx = Cx::for_testing_with_budget(Budget::ZERO);
4314 let ctx = McpContext::new(cx, 1);
4315
4316 assert!(ctx.checkpoint().is_err());
4318 }
4319
4320 #[test]
4321 fn checkpoint_does_not_treat_zero_cost_as_poll_exhaustion() {
4322 let budget = Budget::new().with_poll_quota(2).with_cost_quota(0);
4323 let cx = Cx::for_testing_with_budget(budget);
4324 let ctx = McpContext::new(cx.clone(), 1);
4325
4326 assert!(ctx.checkpoint().is_ok());
4327 assert!(!cx.is_cancel_requested());
4328 assert_eq!(ctx.budget().cost_quota, Some(0));
4329 }
4330
4331 #[test]
4332 fn closed_request_lease_cannot_be_revived_or_use_framework_capabilities() {
4333 let state = SessionState::new();
4334 let root = McpContext::with_state(Cx::for_testing(), 1, state);
4335 let clone_created_before_scope = root.clone();
4336 let (scoped, guard) = root
4337 .begin_request_scope()
4338 .expect("new context should create one request lease");
4339 let escaped = scoped.clone();
4340 drop(guard);
4341
4342 assert!(escaped.ensure_live().is_err());
4343 assert!(escaped.checkpoint().is_err());
4344 assert!(escaped.consume_cost(0).is_err());
4345 assert!(escaped.masked(|| 42).is_err());
4346 assert!(!escaped.set_auth(AuthContext::with_subject("late")));
4347 assert!(!escaped.set_state("late", true));
4348 assert!(escaped.auth().is_none());
4349 assert!(!escaped.can_call_tools());
4350 assert!(!escaped.can_read_resources());
4351 assert!(clone_created_before_scope.ensure_live().is_err());
4352
4353 assert!(clone_created_before_scope.begin_request_scope().is_none());
4354 }
4355
4356 #[test]
4357 fn test_mcp_context_masked_section() {
4358 let cx = Cx::for_testing();
4359 let ctx = McpContext::new(cx, 1);
4360
4361 let result = ctx.masked(|| 42).expect("mask should be admitted");
4363 assert_eq!(result, 42);
4364 }
4365
4366 #[test]
4367 fn test_mcp_context_budget() {
4368 let cx = Cx::for_testing();
4369 let ctx = McpContext::new(cx, 1);
4370
4371 let budget = ctx.budget();
4373 assert!(!budget.is_exhausted());
4375 }
4376
4377 #[test]
4378 fn budget_ceiling_is_monotone_and_visible_to_checkpoints() {
4379 let ambient_deadline = wall_now().saturating_add_nanos(5_000_000_000);
4380 let tighter_deadline = ambient_deadline.saturating_sub_nanos(1_000_000_000);
4381 let later_deadline = ambient_deadline.saturating_add_nanos(1_000_000_000);
4382 let cx = Cx::for_testing_with_budget(Budget::new().with_deadline(ambient_deadline));
4383 let ctx = McpContext::new(cx, 1)
4384 .with_budget_ceiling(Budget::new().with_deadline(tighter_deadline))
4385 .with_budget_ceiling(Budget::new().with_deadline(later_deadline));
4386
4387 assert_eq!(ctx.budget().deadline, Some(tighter_deadline));
4388 assert!(ctx.checkpoint().is_ok());
4389 }
4390
4391 #[test]
4392 fn operation_deadline_tightens_child_without_leaking_to_parent() {
4393 let parent_deadline = wall_now().saturating_add_nanos(5_000_000_000);
4394 let child_deadline = parent_deadline.saturating_sub_nanos(1_000_000_000);
4395 let parent = McpContext::new(Cx::for_testing(), 1)
4396 .with_budget_ceiling(Budget::new().with_deadline(parent_deadline));
4397 let child = parent.clone().with_operation_deadline(Some(child_deadline));
4398 let grandchild = child.clone().with_operation_deadline(None);
4399
4400 assert_eq!(parent.budget().deadline, Some(parent_deadline));
4401 assert_eq!(child.budget().deadline, Some(child_deadline));
4402 assert_eq!(grandchild.budget().deadline, Some(child_deadline));
4403 }
4404
4405 #[test]
4406 fn framework_poll_ceiling_drains_across_clones_at_n_plus_one() {
4407 const LIMIT: u32 = 3;
4408
4409 let ctx = McpContext::new(Cx::for_testing(), 1)
4410 .with_budget_ceiling(Budget::new().with_poll_quota(LIMIT));
4411 let clone = ctx.clone();
4412
4413 for admitted in 0..LIMIT {
4414 let result = if admitted % 2 == 0 {
4415 ctx.checkpoint()
4416 } else {
4417 clone.checkpoint()
4418 };
4419 assert!(result.is_ok(), "checkpoint {} should fit", admitted + 1);
4420 let expected = LIMIT - admitted - 1;
4421 assert_eq!(ctx.budget().poll_quota, expected);
4422 assert_eq!(clone.budget().poll_quota, expected);
4423 }
4424
4425 assert!(clone.checkpoint().is_err(), "checkpoint N+1 must fail");
4426 assert_eq!(ctx.budget().poll_quota, 0);
4427 assert!(!ctx.cx().is_cancel_requested());
4428 }
4429
4430 #[test]
4431 fn ambient_poll_budget_drains_across_clones_without_mutating_cx() {
4432 const LIMIT: u32 = 3;
4433
4434 let cx = Cx::for_testing_with_budget(Budget::new().with_poll_quota(LIMIT));
4435 let ctx = McpContext::new(cx.clone(), 1);
4436 let clone = ctx.clone();
4437
4438 for admitted in 0..LIMIT {
4439 let result = if admitted % 2 == 0 {
4440 ctx.checkpoint()
4441 } else {
4442 clone.checkpoint()
4443 };
4444 assert!(
4445 result.is_ok(),
4446 "ambient checkpoint {} should fit",
4447 admitted + 1
4448 );
4449 assert_eq!(ctx.budget().poll_quota, LIMIT - admitted - 1);
4450 }
4451
4452 let debits_before_rejection = ctx
4453 .budget_state
4454 .lock()
4455 .unwrap_or_else(std::sync::PoisonError::into_inner)
4456 .ambient_poll_debits;
4457 assert!(
4458 clone.checkpoint().is_err(),
4459 "ambient checkpoint N+1 must fail"
4460 );
4461 assert_eq!(
4462 ctx.budget_state
4463 .lock()
4464 .unwrap_or_else(std::sync::PoisonError::into_inner)
4465 .ambient_poll_debits,
4466 debits_before_rejection,
4467 "a rejected checkpoint must not partially debit the ledger"
4468 );
4469 assert_eq!(ctx.budget().poll_quota, 0);
4470 assert_eq!(cx.budget().poll_quota, LIMIT);
4471 assert!(!cx.is_cancel_requested());
4472 assert!(ctx.ensure_live().is_ok());
4473 }
4474
4475 #[test]
4476 fn tighter_ambient_poll_limit_does_not_debit_looser_ceiling_on_rejection() {
4477 let cx = Cx::for_testing_with_budget(Budget::new().with_poll_quota(2));
4478 let ctx =
4479 McpContext::new(cx.clone(), 1).with_budget_ceiling(Budget::new().with_poll_quota(3));
4480
4481 assert!(ctx.checkpoint().is_ok());
4482 assert!(ctx.checkpoint().is_ok());
4483 assert!(ctx.checkpoint().is_err());
4484
4485 let state = *ctx
4486 .budget_state
4487 .lock()
4488 .unwrap_or_else(std::sync::PoisonError::into_inner);
4489 assert_eq!(state.ambient_poll_debits, 2);
4490 assert_eq!(state.ceiling.map(|budget| budget.poll_quota), Some(1));
4491 assert_eq!(cx.budget().poll_quota, 2);
4492 }
4493
4494 #[test]
4495 fn framework_cost_ceiling_drains_across_clones_at_n_plus_one() {
4496 const LIMIT: u64 = 3;
4497
4498 let ctx = McpContext::new(Cx::for_testing(), 1)
4499 .with_budget_ceiling(Budget::new().with_cost_quota(LIMIT));
4500 let clone = ctx.clone();
4501
4502 for admitted in 0..LIMIT {
4503 let result = if admitted % 2 == 0 {
4504 ctx.consume_cost(1)
4505 } else {
4506 clone.consume_cost(1)
4507 };
4508 assert!(result.is_ok(), "cost debit {} should fit", admitted + 1);
4509 let expected = Some(LIMIT - admitted - 1);
4510 assert_eq!(ctx.budget().cost_quota, expected);
4511 assert_eq!(clone.budget().cost_quota, expected);
4512 }
4513
4514 assert!(clone.consume_cost(1).is_err(), "cost debit N+1 must fail");
4515 assert_eq!(ctx.budget().cost_quota, Some(0));
4516 assert!(!ctx.cx().is_cancel_requested());
4517 assert!(
4518 ctx.ensure_live().is_ok(),
4519 "an exactly admitted final debit is not an overrun"
4520 );
4521 }
4522
4523 #[test]
4524 fn framework_poll_and_cost_debits_are_independent() {
4525 let ctx = McpContext::new(Cx::for_testing(), 1)
4526 .with_budget_ceiling(Budget::new().with_poll_quota(2).with_cost_quota(2));
4527
4528 assert!(ctx.checkpoint().is_ok());
4529 assert_eq!(ctx.budget().poll_quota, 1);
4530 assert_eq!(ctx.budget().cost_quota, Some(2));
4531
4532 assert!(ctx.consume_cost(1).is_ok());
4533 assert_eq!(ctx.budget().poll_quota, 1);
4534 assert_eq!(ctx.budget().cost_quota, Some(1));
4535 }
4536
4537 #[test]
4538 fn exact_poll_depletion_is_live_until_the_next_poll_admission() {
4539 let ctx = McpContext::new(Cx::for_testing(), 1)
4540 .with_budget_ceiling(Budget::new().with_poll_quota(1));
4541
4542 assert!(ctx.checkpoint().is_ok());
4543 assert_eq!(ctx.budget().poll_quota, 0);
4544 assert!(ctx.ensure_live().is_ok());
4545 assert!(ctx.checkpoint().is_err());
4546 }
4547
4548 #[test]
4549 fn zero_framework_quotas_fail_without_cancelling_ambient_context() {
4550 let poll_ctx = McpContext::new(Cx::for_testing(), 1)
4551 .with_budget_ceiling(Budget::new().with_poll_quota(0));
4552 let cost_ctx = McpContext::new(Cx::for_testing(), 2)
4553 .with_budget_ceiling(Budget::new().with_cost_quota(0));
4554
4555 assert!(poll_ctx.checkpoint().is_err());
4556 assert_eq!(poll_ctx.budget().poll_quota, 0);
4557 assert!(!poll_ctx.cx().is_cancel_requested());
4558
4559 assert!(cost_ctx.consume_cost(0).is_ok());
4560 assert!(cost_ctx.consume_cost(1).is_err());
4561 assert_eq!(cost_ctx.budget().cost_quota, Some(0));
4562 assert!(!cost_ctx.cx().is_cancel_requested());
4563 }
4564
4565 #[test]
4566 fn oversized_framework_cost_debit_is_atomic() {
4567 let ctx = McpContext::new(Cx::for_testing(), 1)
4568 .with_budget_ceiling(Budget::new().with_cost_quota(2));
4569
4570 assert!(ctx.consume_cost(3).is_err());
4571 assert_eq!(ctx.budget().cost_quota, Some(2));
4572 assert!(ctx.consume_cost(2).is_ok());
4573 assert_eq!(ctx.budget().cost_quota, Some(0));
4574 assert!(ctx.consume_cost(1).is_err());
4575 }
4576
4577 #[test]
4578 fn zero_ambient_cost_quota_prevents_framework_cost_debit() {
4579 let ambient = Budget::new().with_cost_quota(0);
4580 let ctx = McpContext::new(Cx::for_testing_with_budget(ambient), 1)
4581 .with_budget_ceiling(Budget::new().with_cost_quota(3));
4582
4583 assert!(ctx.consume_cost(1).is_err());
4584 assert_eq!(ctx.budget().cost_quota, Some(0));
4585 }
4586
4587 #[test]
4588 fn positive_ambient_cost_quota_drains_cumulatively_across_clones() {
4589 const LIMIT: u64 = 3;
4590 let ambient = Budget::new().with_cost_quota(LIMIT);
4591 let ctx = McpContext::new(Cx::for_testing_with_budget(ambient), 1);
4592 let clone = ctx.clone();
4593
4594 for admitted in 0..LIMIT {
4595 let result = if admitted % 2 == 0 {
4596 ctx.consume_cost(1)
4597 } else {
4598 clone.consume_cost(1)
4599 };
4600 assert!(result.is_ok(), "ambient debit {} should fit", admitted + 1);
4601 assert_eq!(ctx.budget().cost_quota, Some(LIMIT - admitted - 1));
4602 }
4603
4604 assert!(
4605 clone.consume_cost(1).is_err(),
4606 "ambient debit N+1 must fail"
4607 );
4608 assert_eq!(ctx.budget().cost_quota, Some(0));
4609 assert_eq!(
4610 ctx.cx().budget().cost_quota,
4611 Some(LIMIT),
4612 "request-local accounting must not mutate the caller-owned Cx"
4613 );
4614 }
4615
4616 #[test]
4617 fn rejected_cost_debit_does_not_record_an_ambient_checkpoint() {
4618 let cx = Cx::for_testing_with_budget(Budget::new().with_cost_quota(2));
4619 let ctx = McpContext::new(cx, 1);
4620 let before = ctx.cx().checkpoint_state().checkpoint_count;
4621
4622 assert!(ctx.consume_cost(3).is_err());
4623 assert_eq!(ctx.cx().checkpoint_state().checkpoint_count, before);
4624 assert_eq!(ctx.budget().cost_quota, Some(2));
4625 }
4626
4627 #[test]
4628 fn zero_cost_debit_observes_explicit_cancellation() {
4629 let cx = Cx::for_testing();
4630 cx.set_cancel_requested(true);
4631 let ctx = McpContext::new(cx, 1);
4632
4633 assert!(ctx.consume_cost(0).is_err());
4634 }
4635
4636 #[test]
4637 fn expired_request_ceiling_fails_without_cancelling_ambient_context() {
4638 let cx = Cx::for_testing();
4639 let ctx = McpContext::new(cx, 1)
4640 .with_budget_ceiling(Budget::new().with_deadline(asupersync::Time::ZERO));
4641
4642 assert!(ctx.is_cancelled());
4643 assert!(ctx.checkpoint().is_err());
4644 assert!(!ctx.cx().is_cancel_requested());
4645 }
4646
4647 #[test]
4648 fn framework_budget_ceiling_is_deferred_while_masked() {
4649 let ctx = McpContext::new(Cx::for_testing(), 1)
4650 .with_budget_ceiling(Budget::new().with_deadline(asupersync::Time::ZERO));
4651
4652 assert!(
4653 ctx.masked(|| ctx.checkpoint())
4654 .expect("mask should be admitted")
4655 .is_ok()
4656 );
4657 assert!(ctx.checkpoint().is_err());
4658 }
4659
4660 #[test]
4661 fn framework_poll_debits_continue_while_enforcement_is_masked() {
4662 let ctx = McpContext::new(Cx::for_testing(), 1)
4663 .with_budget_ceiling(Budget::new().with_poll_quota(1));
4664
4665 ctx.masked(|| {
4666 assert!(ctx.checkpoint().is_ok());
4667 assert_eq!(ctx.budget().poll_quota, 0);
4668 assert!(ctx.checkpoint().is_ok());
4669 })
4670 .expect("mask should be admitted");
4671
4672 assert!(ctx.checkpoint().is_err());
4673 }
4674
4675 #[test]
4676 fn masked_cost_overage_saturates_framework_ceiling() {
4677 let ctx = McpContext::new(Cx::for_testing(), 1)
4678 .with_budget_ceiling(Budget::new().with_cost_quota(2));
4679
4680 assert!(
4681 ctx.masked(|| ctx.consume_cost(3))
4682 .expect("mask should be admitted")
4683 .is_ok()
4684 );
4685 assert_eq!(ctx.budget().cost_quota, Some(0));
4686 assert!(ctx.ensure_live().is_err());
4687 assert!(ctx.consume_cost(1).is_err());
4688 }
4689
4690 #[test]
4691 fn masked_exact_cost_depletion_does_not_become_a_deferred_overrun() {
4692 let ctx = McpContext::new(Cx::for_testing(), 1)
4693 .with_budget_ceiling(Budget::new().with_cost_quota(2));
4694
4695 assert!(
4696 ctx.masked(|| ctx.consume_cost(2))
4697 .expect("mask should be admitted")
4698 .is_ok()
4699 );
4700 assert_eq!(ctx.budget().cost_quota, Some(0));
4701 assert!(ctx.ensure_live().is_ok());
4702 assert!(ctx.consume_cost(0).is_ok());
4703 assert!(ctx.consume_cost(1).is_err());
4704 }
4705
4706 #[test]
4707 fn masked_cost_overage_saturates_tighter_ambient_quota() {
4708 let ambient = Budget::new().with_cost_quota(2);
4709 let ctx = McpContext::new(Cx::for_testing_with_budget(ambient), 1)
4710 .with_budget_ceiling(Budget::new().with_cost_quota(10));
4711
4712 assert!(
4713 ctx.masked(|| ctx.consume_cost(3))
4714 .expect("mask should be admitted")
4715 .is_ok()
4716 );
4717 assert_eq!(ctx.budget().cost_quota, Some(0));
4718 assert_eq!(
4719 ctx.budget_state
4720 .lock()
4721 .unwrap_or_else(std::sync::PoisonError::into_inner)
4722 .ceiling
4723 .and_then(|budget| budget.cost_quota),
4724 Some(7),
4725 "the looser framework ceiling is still debited independently"
4726 );
4727 assert!(ctx.consume_cost(1).is_err());
4728 }
4729
4730 #[test]
4731 fn framework_mask_is_shared_with_clones_and_restored_after_exit() {
4732 let ctx = McpContext::new(Cx::for_testing(), 1)
4733 .with_budget_ceiling(Budget::new().with_poll_quota(0));
4734 let clone = ctx.clone();
4735
4736 assert!(
4737 ctx.masked(|| clone.checkpoint())
4738 .expect("mask should be admitted")
4739 .is_ok()
4740 );
4741 assert!(clone.checkpoint().is_err());
4742 }
4743
4744 #[test]
4745 fn framework_mask_depth_is_restored_after_unwind() {
4746 let ctx = McpContext::new(Cx::for_testing(), 1)
4747 .with_budget_ceiling(Budget::new().with_deadline(asupersync::Time::ZERO));
4748
4749 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4750 let _ = ctx.masked(|| panic!("test-only masked-section panic"));
4751 }));
4752
4753 assert!(ctx.checkpoint().is_err());
4754 assert_eq!(ctx.framework_mask_depth.load(Ordering::SeqCst), 0);
4755 }
4756
4757 #[test]
4758 fn test_cancelled_error_display() {
4759 let err = CancelledError;
4760 assert_eq!(err.to_string(), "request cancelled");
4761 }
4762
4763 #[test]
4764 fn handler_log_respects_client_floor_and_missing_floor() {
4765 let captured = Arc::new(Mutex::new(Vec::new()));
4766 struct CaptureSender(Arc<Mutex<Vec<(McpLogLevel, String)>>>);
4767 impl NotificationSender for CaptureSender {
4768 fn send_progress(&self, _progress: f64, _total: Option<f64>, _message: Option<&str>) {}
4769 fn send_log(&self, level: McpLogLevel, _logger: Option<&str>, data: serde_json::Value) {
4770 self.0
4771 .lock()
4772 .unwrap_or_else(std::sync::PoisonError::into_inner)
4773 .push((level, data.as_str().unwrap_or_default().to_owned()));
4774 }
4775 }
4776
4777 let silent = McpContext::new(Cx::for_testing(), 1)
4778 .with_log_sender(Arc::new(CaptureSender(Arc::clone(&captured))));
4779 silent.info("before-floor");
4780 assert!(captured.lock().expect("lock").is_empty());
4781
4782 let ctx = silent.with_min_log_level(Some(McpLogLevel::Info));
4783 assert_eq!(ctx.min_log_level(), Some(McpLogLevel::Info));
4784 ctx.debug("too-low");
4785 ctx.info("admitted");
4786 ctx.warning("also-admitted");
4787 let emitted = captured.lock().expect("lock").clone();
4788 assert_eq!(
4789 emitted,
4790 vec![
4791 (McpLogLevel::Info, "admitted".to_owned()),
4792 (McpLogLevel::Warning, "also-admitted".to_owned()),
4793 ]
4794 );
4795 }
4796
4797 #[test]
4798 fn catalog_change_emits_only_when_the_disabled_set_mutates() {
4799 let captured = Arc::new(Mutex::new(Vec::new()));
4800 struct CaptureSender(Arc<Mutex<Vec<McpCatalogKind>>>);
4801 impl NotificationSender for CaptureSender {
4802 fn send_progress(&self, _progress: f64, _total: Option<f64>, _message: Option<&str>) {}
4803 fn send_catalog_changed(&self, kind: McpCatalogKind) {
4804 self.0
4805 .lock()
4806 .unwrap_or_else(std::sync::PoisonError::into_inner)
4807 .push(kind);
4808 }
4809 }
4810
4811 let ctx = McpContext::with_state(Cx::for_testing(), 1, SessionState::new())
4812 .with_log_sender(Arc::new(CaptureSender(Arc::clone(&captured))));
4813 assert!(ctx.disable_tool("admin"));
4814 assert!(ctx.disable_tool("admin"));
4815 assert!(ctx.enable_tool("admin"));
4816 assert!(ctx.enable_tool("admin"));
4817 assert!(ctx.disable_resource("file://secret"));
4818 assert!(ctx.disable_prompt("hidden"));
4819 assert_eq!(
4820 *captured.lock().expect("lock"),
4821 vec![
4822 McpCatalogKind::Tools,
4823 McpCatalogKind::Tools,
4824 McpCatalogKind::Resources,
4825 McpCatalogKind::Prompts,
4826 ]
4827 );
4828 }
4829
4830 #[test]
4831 fn catalog_publisher_receives_mutations_even_without_a_session_sender() {
4832 let captured = Arc::new(Mutex::new(Vec::new()));
4833 struct CapturePublisher(Arc<Mutex<Vec<McpCatalogKind>>>);
4834 impl CatalogChangePublisher for CapturePublisher {
4835 fn publish_catalog_changed(&self, kind: McpCatalogKind) -> bool {
4836 self.0
4837 .lock()
4838 .unwrap_or_else(std::sync::PoisonError::into_inner)
4839 .push(kind);
4840 true
4841 }
4842 fn publish_resource_updated(&self, _uri: &str) -> bool {
4843 false
4844 }
4845 }
4846
4847 let ctx = McpContext::with_state(Cx::for_testing(), 1, SessionState::new())
4848 .with_catalog_publisher(Arc::new(CapturePublisher(Arc::clone(&captured))));
4849 assert!(ctx.disable_tool("admin"));
4850 assert!(ctx.disable_tool("admin"));
4851 assert_eq!(*captured.lock().expect("lock"), vec![McpCatalogKind::Tools]);
4852 }
4853
4854 #[test]
4855 fn notify_resource_updated_requires_a_live_subscription() {
4856 let captured = Arc::new(Mutex::new(Vec::new()));
4857 struct CaptureSender(Arc<Mutex<Vec<String>>>);
4858 impl NotificationSender for CaptureSender {
4859 fn send_progress(&self, _progress: f64, _total: Option<f64>, _message: Option<&str>) {}
4860 fn send_resource_updated(&self, uri: &str) {
4861 self.0
4862 .lock()
4863 .unwrap_or_else(std::sync::PoisonError::into_inner)
4864 .push(uri.to_owned());
4865 }
4866 }
4867
4868 let ctx = McpContext::new(Cx::for_testing(), 1)
4869 .with_log_sender(Arc::new(CaptureSender(Arc::clone(&captured))))
4870 .with_resource_subscriptions(["file:///watched.txt"]);
4871 assert!(!ctx.notify_resource_updated("file:///other.txt"));
4872 assert!(ctx.notify_resource_updated("file:///watched.txt"));
4873 assert_eq!(
4874 *captured.lock().expect("lock"),
4875 vec!["file:///watched.txt".to_owned()]
4876 );
4877 }
4878
4879 #[test]
4880 fn test_into_outcome_ok() {
4881 let result: Result<i32, CancelledError> = Ok(42);
4882 let outcome: Outcome<i32, CancelledError> = result.into_outcome();
4883 assert!(matches!(outcome, Outcome::Ok(42)));
4884 }
4885
4886 #[test]
4887 fn test_into_outcome_cancelled() {
4888 let result: Result<i32, CancelledError> = Err(CancelledError);
4889 let outcome: Outcome<i32, ()> = result.into_outcome();
4890 assert!(matches!(outcome, Outcome::Cancelled(_)));
4891 }
4892
4893 #[test]
4894 fn test_mcp_context_no_progress_reporter_by_default() {
4895 let cx = Cx::for_testing();
4896 let ctx = McpContext::new(cx, 1);
4897 assert!(!ctx.has_progress_reporter());
4898 }
4899
4900 #[test]
4901 fn test_mcp_context_with_progress_reporter() {
4902 let cx = Cx::for_testing();
4903 let sender = Arc::new(NoOpNotificationSender);
4904 let reporter = ProgressReporter::new(sender);
4905 let ctx = McpContext::with_progress(cx, 1, reporter);
4906 assert!(ctx.has_progress_reporter());
4907 }
4908
4909 #[test]
4910 fn progress_reporter_builder_preserves_request_accounting_domain() {
4911 let ctx = McpContext::new(Cx::for_testing(), 1)
4912 .with_budget_ceiling(Budget::new().with_cost_quota(5));
4913 let reporter = ProgressReporter::new(Arc::new(NoOpNotificationSender));
4914 let derived = ctx.clone().with_progress_reporter(reporter);
4915
4916 assert!(derived.has_progress_reporter());
4917 assert!(!ctx.has_progress_reporter());
4918 assert!(ctx.consume_cost(3).is_ok());
4919 assert_eq!(derived.budget().cost_quota, Some(2));
4920 }
4921
4922 #[test]
4923 fn isolated_auth_stages_identity_without_handler_capabilities() {
4924 let root = McpContext::with_state(Cx::for_testing(), 1, SessionState::new())
4925 .with_budget_ceiling(Budget::new().with_cost_quota(2))
4926 .with_sampling(Arc::new(NoOpSamplingSender))
4927 .with_elicitation(Arc::new(NoOpElicitationSender))
4928 .with_roots_provider(Arc::new(FixedRootsProvider));
4929 let staged = root.clone().with_isolated_auth();
4930
4931 assert!(staged.auth().is_none());
4932 assert!(!staged.has_session_state());
4933 assert!(!staged.can_sample());
4934 assert!(!staged.can_elicit());
4935 assert!(!staged.can_list_roots());
4936 assert!(!staged.can_read_resources());
4937 assert!(!staged.can_call_tools());
4938 assert!(staged.set_auth(AuthContext::with_subject("tentative")));
4939 assert_eq!(
4940 staged.auth().and_then(|auth| auth.subject),
4941 Some("tentative".to_string())
4942 );
4943 assert_eq!(root.auth().and_then(|auth| auth.subject), None);
4944
4945 assert!(root.set_auth(AuthContext::with_subject("committed")));
4946 let attempted_reisolation = root.clone().with_isolated_auth();
4947 assert_eq!(
4948 attempted_reisolation.auth().and_then(|auth| auth.subject),
4949 Some("committed".to_string())
4950 );
4951
4952 assert!(staged.consume_cost(1).is_ok());
4953 assert_eq!(root.budget().cost_quota, Some(1));
4954 }
4955
4956 #[test]
4957 fn committed_anonymous_auth_is_hidden_and_write_once() {
4958 let ctx = McpContext::with_state(Cx::for_testing(), 1, SessionState::new());
4959
4960 assert!(ctx.commit_anonymous_auth());
4961 assert!(ctx.auth().is_none());
4962 assert!(matches!(ctx.cache_auth_partition(), Some(None)));
4963 assert!(!ctx.set_auth(AuthContext::with_subject("forged")));
4964 assert!(!ctx.commit_anonymous_auth());
4965
4966 let clone = ctx.clone();
4967 assert!(clone.auth().is_none());
4968 assert!(matches!(clone.cache_auth_partition(), Some(None)));
4969 }
4970
4971 #[test]
4972 fn authenticated_cache_partition_contains_committed_facts() {
4973 let ctx = McpContext::new(Cx::for_testing(), 1);
4974 assert!(ctx.set_auth(AuthContext::with_subject("alice")));
4975
4976 let Some(Some(auth)) = ctx.cache_auth_partition() else {
4977 panic!("authenticated admission must expose cache partition facts");
4978 };
4979 assert_eq!(auth.subject.as_deref(), Some("alice"));
4980 }
4981
4982 #[test]
4983 fn test_report_progress_without_reporter() {
4984 let cx = Cx::for_testing();
4985 let ctx = McpContext::new(cx, 1);
4986 ctx.report_progress(0.5, Some("test"));
4988 ctx.report_progress_with_total(5.0, 10.0, None);
4989 }
4990
4991 #[test]
4992 fn test_report_progress_with_reporter() {
4993 use std::sync::atomic::{AtomicU32, Ordering};
4994
4995 struct CountingSender {
4996 count: AtomicU32,
4997 }
4998
4999 impl NotificationSender for CountingSender {
5000 fn send_progress(&self, _progress: f64, _total: Option<f64>, _message: Option<&str>) {
5001 self.count.fetch_add(1, Ordering::SeqCst);
5002 }
5003 }
5004
5005 let cx = Cx::for_testing();
5006 let sender = Arc::new(CountingSender {
5007 count: AtomicU32::new(0),
5008 });
5009 let reporter = ProgressReporter::new(sender.clone());
5010 let ctx = McpContext::with_progress(cx, 1, reporter);
5011
5012 ctx.report_progress(0.25, Some("step 1"));
5013 ctx.report_progress(0.5, None);
5014 ctx.report_progress_with_total(3.0, 4.0, Some("step 3"));
5015
5016 assert_eq!(sender.count.load(Ordering::SeqCst), 3);
5017 }
5018
5019 #[test]
5020 fn request_local_cancellation_suppresses_subsequent_progress() {
5021 use std::sync::atomic::{AtomicU32, Ordering};
5022
5023 struct CountingSender {
5024 count: AtomicU32,
5025 }
5026
5027 impl NotificationSender for CountingSender {
5028 fn send_progress(&self, _progress: f64, _total: Option<f64>, _message: Option<&str>) {
5029 self.count.fetch_add(1, Ordering::SeqCst);
5030 }
5031 }
5032
5033 let sender = Arc::new(CountingSender {
5034 count: AtomicU32::new(0),
5035 });
5036 let cancellation = McpRequestCancellation::new();
5037 let ctx =
5038 McpContext::with_progress(Cx::for_testing(), 1, ProgressReporter::new(sender.clone()))
5039 .with_request_cancellation(cancellation.clone());
5040
5041 ctx.report_progress(0.25, Some("before cancellation"));
5042 assert!(cancellation.cancel());
5043 ctx.report_progress(0.5, Some("after cancellation"));
5044
5045 assert_eq!(sender.count.load(Ordering::SeqCst), 1);
5046 assert!(!ctx.has_progress_reporter());
5047 }
5048
5049 #[test]
5050 fn test_progress_reporter_debug() {
5051 let sender = Arc::new(NoOpNotificationSender);
5052 let reporter = ProgressReporter::new(sender);
5053 let debug = format!("{reporter:?}");
5054 assert!(debug.contains("ProgressReporter"));
5055 }
5056
5057 #[test]
5058 fn test_noop_notification_sender() {
5059 let sender = NoOpNotificationSender;
5060 sender.send_progress(0.5, Some(1.0), Some("test"));
5062 }
5063
5064 #[test]
5066 fn test_mcp_context_no_session_state_by_default() {
5067 let cx = Cx::for_testing();
5068 let ctx = McpContext::new(cx, 1);
5069 assert!(!ctx.has_session_state());
5070 }
5071
5072 #[test]
5073 fn test_mcp_context_with_session_state() {
5074 let cx = Cx::for_testing();
5075 let state = SessionState::new();
5076 let ctx = McpContext::with_state(cx, 1, state);
5077 assert!(ctx.has_session_state());
5078 }
5079
5080 #[test]
5081 fn cache_admission_fails_if_session_state_changes_before_completion() {
5082 let state = SessionState::new();
5083 let ctx = McpContext::with_state(Cx::for_testing(), 1, state.clone());
5084 let admitted = ctx
5085 .begin_session_cache_partition()
5086 .expect("test platform must provide cache-partition entropy");
5087 assert_eq!(ctx.complete_session_cache_partition(), Some(admitted));
5088
5089 assert!(state.set("changed", true));
5090 assert!(ctx.complete_session_cache_partition().is_none());
5091 assert!(ctx.begin_session_cache_partition().is_none());
5092 }
5093
5094 #[test]
5095 fn response_cache_hit_markers_are_middleware_specific() {
5096 let ctx = McpContext::new(Cx::for_testing(), 1);
5097 assert!(ctx.mark_response_cache_hit(10));
5098 assert!(ctx.response_was_cache_hit(10));
5099 assert!(!ctx.response_was_cache_hit(11));
5100 assert!(!ctx.mark_response_cache_hit(0));
5101 }
5102
5103 #[test]
5104 fn test_mcp_context_get_set_state() {
5105 let cx = Cx::for_testing();
5106 let state = SessionState::new();
5107 let ctx = McpContext::with_state(cx, 1, state);
5108
5109 assert!(ctx.set_state("counter", 42));
5111
5112 let value: Option<i32> = ctx.get_state("counter");
5114 assert_eq!(value, Some(42));
5115 }
5116
5117 #[test]
5118 fn test_mcp_context_state_not_available() {
5119 let cx = Cx::for_testing();
5120 let ctx = McpContext::new(cx, 1);
5121
5122 assert!(!ctx.set_state("key", "value"));
5124
5125 let value: Option<String> = ctx.get_state("key");
5127 assert!(value.is_none());
5128 }
5129
5130 #[test]
5131 fn test_mcp_context_has_state() {
5132 let cx = Cx::for_testing();
5133 let state = SessionState::new();
5134 let ctx = McpContext::with_state(cx, 1, state);
5135
5136 assert!(!ctx.has_state("missing"));
5137
5138 ctx.set_state("present", true);
5139 assert!(ctx.has_state("present"));
5140 }
5141
5142 #[test]
5143 fn test_mcp_context_remove_state() {
5144 let cx = Cx::for_testing();
5145 let state = SessionState::new();
5146 let ctx = McpContext::with_state(cx, 1, state);
5147
5148 ctx.set_state("key", "value");
5149 assert!(ctx.has_state("key"));
5150
5151 let removed = ctx.remove_state("key");
5152 assert!(removed.is_some());
5153 assert!(!ctx.has_state("key"));
5154 }
5155
5156 #[test]
5157 fn test_mcp_context_with_state_and_progress() {
5158 let cx = Cx::for_testing();
5159 let state = SessionState::new();
5160 let sender = Arc::new(NoOpNotificationSender);
5161 let reporter = ProgressReporter::new(sender);
5162
5163 let ctx = McpContext::with_state_and_progress(cx, 1, state, reporter);
5164
5165 assert!(ctx.has_session_state());
5166 assert!(ctx.has_progress_reporter());
5167 }
5168
5169 #[test]
5170 fn test_mcp_context_auth_is_request_local() {
5171 let cx = Cx::for_testing();
5172 let state = SessionState::new();
5173 let ctx = McpContext::with_state(cx, 1, state.clone());
5174
5175 assert!(ctx.set_auth(AuthContext::with_subject("alice")));
5176
5177 assert_eq!(
5178 ctx.auth().and_then(|auth| auth.subject),
5179 Some("alice".to_string())
5180 );
5181 assert!(
5182 state.is_empty(),
5183 "request auth must not be persisted into session state"
5184 );
5185 }
5186
5187 #[test]
5188 fn test_mcp_context_clones_share_request_auth() {
5189 let cx = Cx::for_testing();
5190 let ctx = McpContext::new(cx, 1);
5191 let cloned = ctx.clone();
5192
5193 assert!(cloned.set_auth(AuthContext::with_subject("bob")));
5194
5195 assert_eq!(
5196 ctx.auth().and_then(|auth| auth.subject),
5197 Some("bob".to_string())
5198 );
5199 }
5200
5201 #[test]
5202 fn committed_request_auth_is_write_once_across_clones() {
5203 let ctx =
5204 McpContext::new(Cx::for_testing(), 1).with_auth(AuthContext::with_subject("verified"));
5205 let clone = ctx.clone();
5206
5207 assert!(!clone.set_auth(AuthContext::with_subject("replacement")));
5208 assert_eq!(
5209 ctx.auth().and_then(|auth| auth.subject),
5210 Some("verified".to_string())
5211 );
5212 }
5213
5214 #[test]
5215 fn test_new_mcp_contexts_do_not_share_request_auth_even_with_same_cx() {
5216 let cx = Cx::for_testing();
5217 let state = SessionState::new();
5218 let first = McpContext::with_state(cx.clone(), 7, state.clone());
5219 let second = McpContext::with_state(cx, 7, state);
5220
5221 assert!(first.set_auth(AuthContext::with_subject("carol")));
5222
5223 assert!(second.auth().is_none());
5224 }
5225
5226 #[test]
5227 fn test_new_mcp_contexts_do_not_share_request_auth_across_requests() {
5228 let state = SessionState::new();
5229 let first = McpContext::with_state(Cx::for_testing(), 7, state.clone());
5230 let second = McpContext::with_state(Cx::for_testing(), 8, state);
5231
5232 assert!(first.set_auth(AuthContext::with_subject("dave")));
5233
5234 assert_eq!(
5235 first.auth().and_then(|auth| auth.subject),
5236 Some("dave".to_string())
5237 );
5238 assert!(second.auth().is_none());
5239 }
5240
5241 #[test]
5242 fn test_mcp_context_drop_does_not_leak_request_auth() {
5243 let cx = Cx::for_testing();
5244
5245 {
5246 let ctx = McpContext::new(cx.clone(), 9);
5247 assert!(ctx.set_auth(AuthContext::with_subject("erin")));
5248 }
5249
5250 assert!(
5251 McpContext::new(cx, 9).auth().is_none(),
5252 "fresh contexts must start without inherited request auth"
5253 );
5254 }
5255
5256 #[test]
5261 fn test_mcp_context_tools_enabled_by_default() {
5262 let cx = Cx::for_testing();
5263 let state = SessionState::new();
5264 let ctx = McpContext::with_state(cx, 1, state);
5265
5266 assert!(ctx.is_tool_enabled("any_tool"));
5267 assert!(ctx.is_tool_enabled("another_tool"));
5268 }
5269
5270 #[test]
5271 fn test_mcp_context_disable_enable_tool() {
5272 let cx = Cx::for_testing();
5273 let state = SessionState::new();
5274 let ctx = McpContext::with_state(cx, 1, state);
5275
5276 assert!(ctx.is_tool_enabled("my_tool"));
5278
5279 assert!(ctx.disable_tool("my_tool"));
5281 assert!(!ctx.is_tool_enabled("my_tool"));
5282 assert!(ctx.is_tool_enabled("other_tool"));
5283
5284 assert!(ctx.enable_tool("my_tool"));
5286 assert!(ctx.is_tool_enabled("my_tool"));
5287 }
5288
5289 #[test]
5290 fn test_mcp_context_disable_enable_resource() {
5291 let cx = Cx::for_testing();
5292 let state = SessionState::new();
5293 let ctx = McpContext::with_state(cx, 1, state);
5294
5295 assert!(ctx.is_resource_enabled("file://secret"));
5297
5298 assert!(ctx.disable_resource("file://secret"));
5300 assert!(!ctx.is_resource_enabled("file://secret"));
5301 assert!(ctx.is_resource_enabled("file://public"));
5302
5303 assert!(ctx.enable_resource("file://secret"));
5305 assert!(ctx.is_resource_enabled("file://secret"));
5306 }
5307
5308 #[test]
5309 fn test_mcp_context_disable_enable_prompt() {
5310 let cx = Cx::for_testing();
5311 let state = SessionState::new();
5312 let ctx = McpContext::with_state(cx, 1, state);
5313
5314 assert!(ctx.is_prompt_enabled("admin_prompt"));
5316
5317 assert!(ctx.disable_prompt("admin_prompt"));
5319 assert!(!ctx.is_prompt_enabled("admin_prompt"));
5320 assert!(ctx.is_prompt_enabled("user_prompt"));
5321
5322 assert!(ctx.enable_prompt("admin_prompt"));
5324 assert!(ctx.is_prompt_enabled("admin_prompt"));
5325 }
5326
5327 #[test]
5328 fn test_mcp_context_disable_multiple_tools() {
5329 let cx = Cx::for_testing();
5330 let state = SessionState::new();
5331 let ctx = McpContext::with_state(cx, 1, state);
5332
5333 ctx.disable_tool("tool1");
5334 ctx.disable_tool("tool2");
5335 ctx.disable_tool("tool3");
5336
5337 assert!(!ctx.is_tool_enabled("tool1"));
5338 assert!(!ctx.is_tool_enabled("tool2"));
5339 assert!(!ctx.is_tool_enabled("tool3"));
5340 assert!(ctx.is_tool_enabled("tool4"));
5341
5342 let disabled = ctx.disabled_tools();
5343 assert_eq!(disabled.len(), 3);
5344 assert!(disabled.contains("tool1"));
5345 assert!(disabled.contains("tool2"));
5346 assert!(disabled.contains("tool3"));
5347 }
5348
5349 #[test]
5350 fn test_mcp_context_disabled_sets_empty_by_default() {
5351 let cx = Cx::for_testing();
5352 let state = SessionState::new();
5353 let ctx = McpContext::with_state(cx, 1, state);
5354
5355 assert!(ctx.disabled_tools().is_empty());
5356 assert!(ctx.disabled_resources().is_empty());
5357 assert!(ctx.disabled_prompts().is_empty());
5358 }
5359
5360 #[test]
5361 fn test_mcp_context_enable_disable_no_state() {
5362 let cx = Cx::for_testing();
5363 let ctx = McpContext::new(cx, 1);
5364
5365 assert!(!ctx.disable_tool("tool"));
5367 assert!(!ctx.enable_tool("tool"));
5368
5369 assert!(ctx.is_tool_enabled("tool"));
5371 }
5372
5373 #[test]
5374 fn test_mcp_context_disabled_state_persists_across_contexts() {
5375 let state = SessionState::new();
5376
5377 {
5379 let cx = Cx::for_testing();
5380 let ctx = McpContext::with_state(cx, 1, state.clone());
5381 ctx.disable_tool("shared_tool");
5382 }
5383
5384 {
5386 let cx = Cx::for_testing();
5387 let ctx = McpContext::with_state(cx, 2, state.clone());
5388 assert!(!ctx.is_tool_enabled("shared_tool"));
5389 }
5390 }
5391
5392 #[test]
5397 fn test_mcp_context_no_capabilities_by_default() {
5398 let cx = Cx::for_testing();
5399 let ctx = McpContext::new(cx, 1);
5400
5401 assert!(ctx.client_capabilities().is_none());
5402 assert!(ctx.server_capabilities().is_none());
5403 assert!(!ctx.client_supports_sampling());
5404 assert!(!ctx.client_supports_elicitation());
5405 assert!(!ctx.client_supports_roots());
5406 }
5407
5408 #[test]
5409 fn test_mcp_context_with_client_capabilities() {
5410 let cx = Cx::for_testing();
5411 let caps = ClientCapabilityInfo::new()
5412 .with_sampling()
5413 .with_elicitation(true, false)
5414 .with_roots(true);
5415
5416 let ctx = McpContext::new(cx, 1).with_client_capabilities(caps);
5417
5418 assert!(ctx.client_capabilities().is_some());
5419 assert!(ctx.client_supports_sampling());
5420 assert!(ctx.client_supports_elicitation());
5421 assert!(ctx.client_supports_elicitation_form());
5422 assert!(!ctx.client_supports_elicitation_url());
5423 assert!(ctx.client_supports_roots());
5424 }
5425
5426 #[test]
5427 fn test_mcp_context_with_client_implementation() {
5428 let cx = Cx::for_testing();
5429 let mut identity = ClientImplementationInfo::new("e2e-client", "1.0.0");
5430 identity.title = Some("Client Title".to_owned());
5431 let ctx = McpContext::new(cx, 1).with_client_implementation(identity);
5432 let observed = ctx
5433 .client_implementation()
5434 .expect("the attached identity must be retained");
5435 assert_eq!(observed.name, "e2e-client");
5436 assert_eq!(observed.title.as_deref(), Some("Client Title"));
5437 assert!(observed.has_extras());
5438 let bare = McpContext::new(Cx::for_testing(), 2);
5439 assert!(bare.client_implementation().is_none());
5440 }
5441
5442 #[test]
5443 fn test_mcp_context_with_server_capabilities() {
5444 let cx = Cx::for_testing();
5445 let caps = ServerCapabilityInfo::new()
5446 .with_tools()
5447 .with_resources(true)
5448 .with_prompts()
5449 .with_logging();
5450
5451 let ctx = McpContext::new(cx, 1).with_server_capabilities(caps);
5452
5453 let server_caps = ctx.server_capabilities().unwrap();
5454 assert!(server_caps.tools);
5455 assert!(server_caps.resources);
5456 assert!(server_caps.resources_subscribe);
5457 assert!(server_caps.prompts);
5458 assert!(server_caps.logging);
5459 }
5460
5461 #[test]
5462 fn test_client_capability_info_builders() {
5463 let caps = ClientCapabilityInfo::new();
5464 assert!(!caps.sampling);
5465 assert!(!caps.elicitation);
5466 assert!(!caps.roots);
5467
5468 let caps = caps.with_sampling();
5469 assert!(caps.sampling);
5470
5471 let caps = ClientCapabilityInfo::new().with_elicitation(true, true);
5472 assert!(caps.elicitation);
5473 assert!(caps.elicitation_form);
5474 assert!(caps.elicitation_url);
5475
5476 let caps = ClientCapabilityInfo::new().with_roots(false);
5477 assert!(caps.roots);
5478 assert!(!caps.roots_list_changed);
5479 }
5480
5481 #[test]
5482 fn test_server_capability_info_builders() {
5483 let caps = ServerCapabilityInfo::new();
5484 assert!(!caps.tools);
5485 assert!(!caps.resources);
5486 assert!(!caps.prompts);
5487 assert!(!caps.logging);
5488
5489 let caps = caps
5490 .with_tools()
5491 .with_resources(false)
5492 .with_prompts()
5493 .with_logging();
5494 assert!(caps.tools);
5495 assert!(caps.resources);
5496 assert!(!caps.resources_subscribe);
5497 assert!(caps.prompts);
5498 assert!(caps.logging);
5499 }
5500
5501 #[test]
5506 fn test_resource_content_item_text() {
5507 let item = ResourceContentItem::text("test://uri", "hello");
5508 assert_eq!(item.uri, "test://uri");
5509 assert_eq!(item.mime_type.as_deref(), Some("text/plain"));
5510 assert_eq!(item.as_text(), Some("hello"));
5511 assert!(item.as_blob().is_none());
5512 assert!(item.is_text());
5513 assert!(!item.is_blob());
5514 }
5515
5516 #[test]
5517 fn test_resource_content_item_json() {
5518 let item = ResourceContentItem::json("data://config", r#"{"key":"val"}"#);
5519 assert_eq!(item.uri, "data://config");
5520 assert_eq!(item.mime_type.as_deref(), Some("application/json"));
5521 assert_eq!(item.as_text(), Some(r#"{"key":"val"}"#));
5522 assert!(item.is_text());
5523 assert!(!item.is_blob());
5524 }
5525
5526 #[test]
5527 fn test_resource_content_item_blob() {
5528 let item = ResourceContentItem::blob("binary://data", "application/octet-stream", "AQID");
5529 assert_eq!(item.uri, "binary://data");
5530 assert_eq!(item.mime_type.as_deref(), Some("application/octet-stream"));
5531 assert!(item.as_text().is_none());
5532 assert_eq!(item.as_blob(), Some("AQID"));
5533 assert!(!item.is_text());
5534 assert!(item.is_blob());
5535 }
5536
5537 #[test]
5542 fn test_resource_read_result_text() {
5543 let result = ResourceReadResult::text("test://doc", "content");
5544 assert_eq!(result.first_text(), Some("content"));
5545 assert!(result.first_blob().is_none());
5546 assert_eq!(result.contents.len(), 1);
5547 }
5548
5549 #[test]
5550 fn test_resource_read_result_new_multiple() {
5551 let result = ResourceReadResult::new(vec![
5552 ResourceContentItem::text("a://1", "first"),
5553 ResourceContentItem::blob("b://2", "image/png", "base64data"),
5554 ]);
5555 assert_eq!(result.contents.len(), 2);
5556 assert_eq!(result.first_text(), Some("first"));
5558 assert!(result.first_blob().is_none());
5560 }
5561
5562 #[test]
5563 fn test_resource_read_result_empty() {
5564 let result = ResourceReadResult::new(vec![]);
5565 assert!(result.first_text().is_none());
5566 assert!(result.first_blob().is_none());
5567 }
5568
5569 #[test]
5570 fn test_resource_read_result_blob_first() {
5571 let result = ResourceReadResult::new(vec![ResourceContentItem::blob(
5572 "b://1",
5573 "image/png",
5574 "data",
5575 )]);
5576 assert!(result.first_text().is_none());
5577 assert_eq!(result.first_blob(), Some("data"));
5578 }
5579
5580 #[test]
5585 fn test_tool_content_item_text() {
5586 let item = ToolContentItem::text("hello");
5587 assert_eq!(item.as_text(), Some("hello"));
5588 assert!(item.is_text());
5589 }
5590
5591 #[test]
5592 fn test_tool_content_item_image() {
5593 let item = ToolContentItem::Image {
5594 data: "base64img".to_string(),
5595 mime_type: "image/png".to_string(),
5596 };
5597 assert!(item.as_text().is_none());
5598 assert!(!item.is_text());
5599 }
5600
5601 #[test]
5602 fn test_tool_content_item_audio() {
5603 let item = ToolContentItem::Audio {
5604 data: "base64audio".to_string(),
5605 mime_type: "audio/wav".to_string(),
5606 };
5607 assert!(item.as_text().is_none());
5608 assert!(!item.is_text());
5609 }
5610
5611 #[test]
5612 fn test_tool_content_item_resource() {
5613 let item = ToolContentItem::Resource {
5614 uri: "file://test".to_string(),
5615 mime_type: Some("text/plain".to_string()),
5616 text: Some("embedded".to_string()),
5617 blob: None,
5618 };
5619 assert!(item.as_text().is_none());
5620 assert!(!item.is_text());
5621 }
5622
5623 #[test]
5628 fn test_tool_call_result_success() {
5629 let result = ToolCallResult::success(vec![
5630 ToolContentItem::text("item1"),
5631 ToolContentItem::text("item2"),
5632 ]);
5633 assert!(!result.is_error);
5634 assert_eq!(result.content.len(), 2);
5635 assert_eq!(result.first_text(), Some("item1"));
5636 }
5637
5638 #[test]
5639 fn test_tool_call_result_text() {
5640 let result = ToolCallResult::text("simple output");
5641 assert!(!result.is_error);
5642 assert_eq!(result.content.len(), 1);
5643 assert_eq!(result.first_text(), Some("simple output"));
5644 }
5645
5646 #[test]
5647 fn test_tool_call_result_error() {
5648 let result = ToolCallResult::error("something failed");
5649 assert!(result.is_error);
5650 assert_eq!(result.first_text(), Some("something failed"));
5651 }
5652
5653 #[test]
5654 fn test_tool_call_result_empty() {
5655 let result = ToolCallResult::success(vec![]);
5656 assert!(!result.is_error);
5657 assert!(result.first_text().is_none());
5658 }
5659
5660 #[test]
5665 fn test_elicitation_response_accept() {
5666 let mut data = std::collections::HashMap::new();
5667 data.insert("name".to_string(), serde_json::json!("Alice"));
5668 data.insert("age".to_string(), serde_json::json!(30));
5669 data.insert("active".to_string(), serde_json::json!(true));
5670
5671 let resp = ElicitationResponse::accept(data);
5672 assert!(resp.is_accepted());
5673 assert!(!resp.is_declined());
5674 assert!(!resp.is_cancelled());
5675 assert_eq!(resp.get_string("name"), Some("Alice"));
5676 assert_eq!(resp.get_int("age"), Some(30));
5677 assert_eq!(resp.get_bool("active"), Some(true));
5678 }
5679
5680 #[test]
5681 fn test_elicitation_response_accept_url() {
5682 let resp = ElicitationResponse::accept_url();
5683 assert!(resp.is_accepted());
5684 assert!(resp.content.is_none());
5685 assert!(resp.get_string("anything").is_none());
5686 }
5687
5688 #[test]
5689 fn test_elicitation_response_decline() {
5690 let resp = ElicitationResponse::decline();
5691 assert!(!resp.is_accepted());
5692 assert!(resp.is_declined());
5693 assert!(!resp.is_cancelled());
5694 assert!(resp.get_string("key").is_none());
5695 }
5696
5697 #[test]
5698 fn test_elicitation_response_cancel() {
5699 let resp = ElicitationResponse::cancel();
5700 assert!(!resp.is_accepted());
5701 assert!(!resp.is_declined());
5702 assert!(resp.is_cancelled());
5703 }
5704
5705 #[test]
5706 fn test_elicitation_response_missing_key() {
5707 let mut data = std::collections::HashMap::new();
5708 data.insert("exists".to_string(), serde_json::json!("value"));
5709 let resp = ElicitationResponse::accept(data);
5710
5711 assert!(resp.get_string("missing").is_none());
5712 assert!(resp.get_bool("missing").is_none());
5713 assert!(resp.get_int("missing").is_none());
5714 }
5715
5716 #[test]
5717 fn test_elicitation_response_type_mismatch() {
5718 let mut data = std::collections::HashMap::new();
5719 data.insert("num".to_string(), serde_json::json!(42));
5720 let resp = ElicitationResponse::accept(data);
5721
5722 assert!(resp.get_string("num").is_none());
5724 assert!(resp.get_bool("num").is_none());
5726 assert_eq!(resp.get_int("num"), Some(42));
5728 }
5729
5730 #[test]
5735 fn test_can_sample_false_by_default() {
5736 let cx = Cx::for_testing();
5737 let ctx = McpContext::new(cx, 1);
5738 assert!(!ctx.can_sample());
5739 }
5740
5741 #[test]
5742 fn test_can_elicit_false_by_default() {
5743 let cx = Cx::for_testing();
5744 let ctx = McpContext::new(cx, 1);
5745 assert!(!ctx.can_elicit());
5746 }
5747
5748 #[test]
5749 fn test_can_read_resources_false_by_default() {
5750 let cx = Cx::for_testing();
5751 let ctx = McpContext::new(cx, 1);
5752 assert!(!ctx.can_read_resources());
5753 }
5754
5755 #[test]
5756 fn test_can_call_tools_false_by_default() {
5757 let cx = Cx::for_testing();
5758 let ctx = McpContext::new(cx, 1);
5759 assert!(!ctx.can_call_tools());
5760 }
5761
5762 #[test]
5763 fn test_resource_read_depth_default() {
5764 let cx = Cx::for_testing();
5765 let ctx = McpContext::new(cx, 1);
5766 assert_eq!(ctx.resource_read_depth(), 0);
5767 }
5768
5769 #[test]
5770 fn test_tool_call_depth_default() {
5771 let cx = Cx::for_testing();
5772 let ctx = McpContext::new(cx, 1);
5773 assert_eq!(ctx.tool_call_depth(), 0);
5774 }
5775
5776 #[test]
5781 fn sampling_request_builder_chain() {
5782 let req = SamplingRequest::prompt("hello", 100)
5783 .with_system_prompt("You are helpful")
5784 .with_temperature(0.7)
5785 .with_stop_sequences(vec!["STOP".into()])
5786 .with_model_hints(vec!["gpt-4".into()]);
5787
5788 assert_eq!(req.messages.len(), 1);
5789 assert_eq!(req.max_tokens, 100);
5790 assert_eq!(req.system_prompt.as_deref(), Some("You are helpful"));
5791 assert_eq!(req.temperature, Some(0.7));
5792 assert_eq!(req.stop_sequences, vec!["STOP"]);
5793 assert_eq!(req.model_hints, vec!["gpt-4"]);
5794 }
5795
5796 #[test]
5797 fn sampling_request_message_roles() {
5798 let user = SamplingRequestMessage::user("hi");
5799 assert_eq!(user.role, SamplingRole::User);
5800 assert_eq!(user.text, "hi");
5801
5802 let asst = SamplingRequestMessage::assistant("hello");
5803 assert_eq!(asst.role, SamplingRole::Assistant);
5804 assert_eq!(asst.text, "hello");
5805 }
5806
5807 #[test]
5808 fn sampling_response_new_default_stop_reason() {
5809 let resp = SamplingResponse::new("output", "model-1");
5810 assert_eq!(resp.text, "output");
5811 assert_eq!(resp.model, "model-1");
5812 assert_eq!(resp.stop_reason, SamplingStopReason::EndTurn);
5813 assert_eq!(SamplingStopReason::default(), SamplingStopReason::EndTurn);
5814 }
5815
5816 #[test]
5817 fn sampling_stop_reason_round_trips_optional_open_wire_values() {
5818 let absent = SamplingStopReason::from_wire_value(None);
5819 assert_eq!(absent, SamplingStopReason::Unspecified);
5820 assert_eq!(absent.as_wire_value(), None);
5821
5822 let provider =
5823 SamplingStopReason::from_wire_value(Some("provider_safety_limit".to_owned()));
5824 assert_eq!(
5825 provider,
5826 SamplingStopReason::Other("provider_safety_limit".to_owned())
5827 );
5828 assert_eq!(provider.as_wire_value(), Some("provider_safety_limit"));
5829 }
5830
5831 #[test]
5832 fn noop_sampling_sender_returns_error() {
5833 let sender = NoOpSamplingSender;
5834 let req = SamplingRequest::prompt("test", 10);
5835 let result = crate::block_on(sender.create_message(req));
5836 assert!(result.is_err());
5837 }
5838
5839 #[test]
5840 fn noop_elicitation_sender_returns_error() {
5841 let sender = NoOpElicitationSender;
5842 let req = ElicitationRequest::form("msg", serde_json::json!({}));
5843 let result = crate::block_on(sender.elicit(req));
5844 assert!(result.is_err());
5845 }
5846
5847 #[test]
5848 fn elicitation_request_form_constructor() {
5849 let req = ElicitationRequest::form("Enter name", serde_json::json!({"type": "string"}));
5850 assert_eq!(req.mode, ElicitationMode::Form);
5851 assert_eq!(req.message, "Enter name");
5852 assert!(req.schema.is_some());
5853 assert!(req.url.is_none());
5854 assert!(req.elicitation_id.is_none());
5855 }
5856
5857 #[test]
5858 fn elicitation_request_url_constructor() {
5859 let req = ElicitationRequest::url("Login", "https://example.com", "id-1");
5860 assert_eq!(req.mode, ElicitationMode::Url);
5861 assert_eq!(req.message, "Login");
5862 assert_eq!(req.url.as_deref(), Some("https://example.com"));
5863 assert_eq!(req.elicitation_id.as_deref(), Some("id-1"));
5864 assert!(req.schema.is_none());
5865 }
5866
5867 #[test]
5868 fn mcp_context_with_sampling_enables_can_sample() {
5869 let cx = Cx::for_testing();
5870 let sender = Arc::new(NoOpSamplingSender);
5871 let ctx = McpContext::new(cx, 1).with_sampling(sender);
5872 assert!(ctx.can_sample());
5873 }
5874
5875 #[test]
5876 fn mcp_context_with_elicitation_enables_can_elicit() {
5877 let cx = Cx::for_testing();
5878 let sender = Arc::new(NoOpElicitationSender);
5879 let ctx = McpContext::new(cx, 1).with_elicitation(sender);
5880 assert!(ctx.can_elicit());
5881 }
5882
5883 struct FixedRootsProvider;
5884
5885 impl RootsProvider for FixedRootsProvider {
5886 fn list_roots(
5887 &self,
5888 ) -> std::pin::Pin<
5889 Box<dyn std::future::Future<Output = crate::McpResult<Vec<ClientRoot>>> + Send + '_>,
5890 > {
5891 Box::pin(async {
5892 Ok(vec![
5893 ClientRoot::with_name("file:///workspace", "workspace"),
5894 ClientRoot::new("file:///tmp"),
5895 ])
5896 })
5897 }
5898 }
5899
5900 #[test]
5901 fn mcp_context_roots_provider_returns_client_roots() {
5902 let ctx =
5903 McpContext::new(Cx::for_testing(), 1).with_roots_provider(Arc::new(FixedRootsProvider));
5904
5905 assert!(ctx.can_list_roots());
5906 let roots = crate::block_on(ctx.list_roots()).expect("configured roots provider succeeds");
5907 assert_eq!(
5908 roots,
5909 vec![
5910 ClientRoot::with_name("file:///workspace", "workspace"),
5911 ClientRoot::new("file:///tmp"),
5912 ]
5913 );
5914 }
5915
5916 #[test]
5917 fn mcp_context_without_roots_provider_rejects_without_authority() {
5918 let ctx = McpContext::new(Cx::for_testing(), 1);
5919
5920 assert!(!ctx.can_list_roots());
5921 let error = crate::block_on(ctx.list_roots())
5922 .expect_err("without only the roots provider, the context must reject the request");
5923 assert_eq!(error.code, crate::McpErrorCode::InvalidRequest);
5924 assert_eq!(
5925 error.message,
5926 "Roots not available: client does not support roots capability"
5927 );
5928 }
5929
5930 #[test]
5931 fn mcp_context_depth_setters() {
5932 let cx = Cx::for_testing();
5933 let ctx = McpContext::new(cx, 1)
5934 .with_resource_read_depth(3)
5935 .with_tool_call_depth(5);
5936 assert_eq!(ctx.resource_read_depth(), 3);
5937 assert_eq!(ctx.tool_call_depth(), 5);
5938
5939 let attempted_reset = ctx.with_resource_read_depth(0).with_tool_call_depth(0);
5940 assert_eq!(attempted_reset.resource_read_depth(), 3);
5941 assert_eq!(attempted_reset.tool_call_depth(), 5);
5942 }
5943
5944 #[test]
5945 fn mcp_context_debug_includes_request_id() {
5946 let cx = Cx::for_testing();
5947 let ctx = McpContext::new(cx, 99);
5948 let debug = format!("{ctx:?}");
5949 assert!(debug.contains("request_id: 99"));
5950 }
5951
5952 #[test]
5953 fn mcp_context_cx_and_trace() {
5954 let cx = Cx::for_testing();
5955 let ctx = McpContext::new(cx, 1);
5956 let _ = ctx.cx();
5958 ctx.trace("test event");
5960 }
5961
5962 #[test]
5963 fn final_result_outcome_preserves_dual_era_and_terminal_reason() {
5964 use crate::combinator::{DualEraFinalResult, FinalRequestResult};
5965
5966 let context = McpContext::new(Cx::for_testing(), 1);
5967 let modern = context.final_result_outcome(
5968 FinalRequestResult::<u64, String, &'static str>::modern("typed-final", 42),
5969 );
5970 let legacy =
5971 context.final_result_outcome(FinalRequestResult::<u64, String, &'static str>::legacy(
5972 "legacy-final",
5973 "legacy wire result".to_owned(),
5974 ));
5975
5976 let Outcome::Ok(modern) = modern else {
5977 panic!("live context admits the modern final result");
5978 };
5979 assert_eq!(modern.terminal_reason(), &"typed-final");
5980 assert_eq!(modern.result(), &DualEraFinalResult::Modern(42));
5981
5982 let Outcome::Ok(legacy) = legacy else {
5983 panic!("live context admits the legacy final result");
5984 };
5985 assert_eq!(legacy.terminal_reason(), &"legacy-final");
5986 assert_eq!(
5987 legacy.result(),
5988 &DualEraFinalResult::Legacy("legacy wire result".to_owned())
5989 );
5990 }
5991
5992 #[test]
5993 fn final_result_outcome_cancellation_negative_preserves_cx_reason() {
5994 use crate::combinator::FinalRequestResult;
5995 use asupersync::types::CancelKind;
5996
5997 let cx = Cx::for_testing();
5998 cx.cancel_with(CancelKind::Timeout, Some("final-result race"));
5999 let expected_reason = cx
6000 .cancel_reason()
6001 .expect("cancel_with records the caller-owned terminal reason");
6002 let context = McpContext::new(cx, 1);
6003
6004 let outcome = context.final_result_outcome(
6005 FinalRequestResult::<u64, String, &'static str>::modern("typed-final", 42),
6006 );
6007
6008 let Outcome::Cancelled(reason) = outcome else {
6009 panic!("changing only caller cancellation rejects the same final result");
6010 };
6011 assert_eq!(reason, expected_reason);
6012 }
6013
6014 #[test]
6015 fn final_result_outcome_panic_negative_preserves_payload() {
6016 use crate::combinator::FinalRequestResult;
6017 use asupersync::types::{CancelKind, PanicPayload};
6018
6019 type Final = FinalRequestResult<u64, String, &'static str>;
6020
6021 let cx = Cx::for_testing();
6022 cx.cancel_with(CancelKind::Timeout, Some("competing terminal state"));
6023 let context = McpContext::new(cx, 1);
6024 let payload = PanicPayload::new("final typed result panicked");
6025 let source: crate::McpOutcome<Final> = Outcome::Panicked(payload.clone());
6026
6027 let outcome = context.adapt_final_request_outcome(source);
6028
6029 let Outcome::Panicked(actual) = outcome else {
6030 panic!("changing only the source terminal state to panic preserves panic");
6031 };
6032 assert_eq!(actual, payload);
6033 }
6034}