1use crate::domain::ProtocolVersion;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
38pub enum Side {
39 Client,
41 Server,
43 Both,
45}
46
47impl Side {
48 #[must_use]
50 pub const fn includes_client(self) -> bool {
51 matches!(self, Self::Client | Self::Both)
52 }
53
54 #[must_use]
56 pub const fn includes_server(self) -> bool {
57 matches!(self, Self::Server | Self::Both)
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
69pub enum Capability {
70 Experimental,
72 Extensions,
74 Roots,
76 RootsListChanged,
79 Sampling,
81 SamplingContext,
83 SamplingTools,
85 Elicitation,
87 ElicitationForm,
89 ElicitationUrl,
91 Logging,
94 Prompts,
96 PromptsListChanged,
98 Resources,
100 ResourcesListChanged,
102 ResourcesSubscribe,
105 Tools,
107 ToolsListChanged,
109 Completions,
111 Tasks,
115 TasksCancel,
117 TasksList,
120 TasksRequests,
122 TasksRequestsElicitation,
124 TasksRequestsElicitationCreate,
126 TasksRequestsSampling,
128 TasksRequestsSamplingCreateMessage,
130 TasksRequestsTools,
132 TasksRequestsToolsCall,
134}
135
136impl Capability {
137 pub const ALL: [Self; 29] = [
139 Self::Experimental,
140 Self::Extensions,
141 Self::Roots,
142 Self::RootsListChanged,
143 Self::Sampling,
144 Self::SamplingContext,
145 Self::SamplingTools,
146 Self::Elicitation,
147 Self::ElicitationForm,
148 Self::ElicitationUrl,
149 Self::Logging,
150 Self::Prompts,
151 Self::PromptsListChanged,
152 Self::Resources,
153 Self::ResourcesListChanged,
154 Self::ResourcesSubscribe,
155 Self::Tools,
156 Self::ToolsListChanged,
157 Self::Completions,
158 Self::Tasks,
159 Self::TasksCancel,
160 Self::TasksList,
161 Self::TasksRequests,
162 Self::TasksRequestsElicitation,
163 Self::TasksRequestsElicitationCreate,
164 Self::TasksRequestsSampling,
165 Self::TasksRequestsSamplingCreateMessage,
166 Self::TasksRequestsTools,
167 Self::TasksRequestsToolsCall,
168 ];
169
170 #[must_use]
172 pub const fn as_str(self) -> &'static str {
173 match self {
174 Self::Experimental => "experimental",
175 Self::Extensions => "extensions",
176 Self::Roots => "roots",
177 Self::RootsListChanged => "roots.listChanged",
178 Self::Sampling => "sampling",
179 Self::SamplingContext => "sampling.context",
180 Self::SamplingTools => "sampling.tools",
181 Self::Elicitation => "elicitation",
182 Self::ElicitationForm => "elicitation.form",
183 Self::ElicitationUrl => "elicitation.url",
184 Self::Logging => "logging",
185 Self::Prompts => "prompts",
186 Self::PromptsListChanged => "prompts.listChanged",
187 Self::Resources => "resources",
188 Self::ResourcesListChanged => "resources.listChanged",
189 Self::ResourcesSubscribe => "resources.subscribe",
190 Self::Tools => "tools",
191 Self::ToolsListChanged => "tools.listChanged",
192 Self::Completions => "completions",
193 Self::Tasks => "tasks",
194 Self::TasksCancel => "tasks.cancel",
195 Self::TasksList => "tasks.list",
196 Self::TasksRequests => "tasks.requests",
197 Self::TasksRequestsElicitation => "tasks.requests.elicitation",
198 Self::TasksRequestsElicitationCreate => "tasks.requests.elicitation.create",
199 Self::TasksRequestsSampling => "tasks.requests.sampling",
200 Self::TasksRequestsSamplingCreateMessage => "tasks.requests.sampling.createMessage",
201 Self::TasksRequestsTools => "tasks.requests.tools",
202 Self::TasksRequestsToolsCall => "tasks.requests.tools.call",
203 }
204 }
205
206 #[must_use]
208 pub const fn side(self) -> Side {
209 match self {
210 Self::Experimental
211 | Self::Extensions
212 | Self::Tasks
213 | Self::TasksCancel
214 | Self::TasksList
215 | Self::TasksRequests => Side::Both,
216 Self::Roots
217 | Self::RootsListChanged
218 | Self::Sampling
219 | Self::SamplingContext
220 | Self::SamplingTools
221 | Self::Elicitation
222 | Self::ElicitationForm
223 | Self::ElicitationUrl
224 | Self::TasksRequestsElicitation
225 | Self::TasksRequestsElicitationCreate
226 | Self::TasksRequestsSampling
227 | Self::TasksRequestsSamplingCreateMessage => Side::Client,
228 Self::Logging
229 | Self::Prompts
230 | Self::PromptsListChanged
231 | Self::Resources
232 | Self::ResourcesListChanged
233 | Self::ResourcesSubscribe
234 | Self::Tools
235 | Self::ToolsListChanged
236 | Self::Completions
237 | Self::TasksRequestsTools
238 | Self::TasksRequestsToolsCall => Side::Server,
239 }
240 }
241
242 #[must_use]
255 pub fn bit(self) -> u128 {
256 let name = self.as_str();
257 let mut i = 0;
258 while let Some(candidate) = Self::ALL.get(i) {
259 if str_eq(candidate.as_str(), name) {
260 return 1u128 << i;
261 }
262 i += 1;
263 }
264 0
265 }
266
267 #[must_use]
273 pub fn defined_in(self, version: ProtocolVersion) -> bool {
274 defined_in(self, version)
275 }
276}
277
278pub const CAPABILITY_TABLE: &[(Capability, &[ProtocolVersion])] = &[
286 (
287 Capability::Experimental,
288 &[
289 ProtocolVersion::V2024_11_05,
290 ProtocolVersion::V2025_03_26,
291 ProtocolVersion::V2025_06_18,
292 ProtocolVersion::V2025_11_25,
293 ProtocolVersion::V2026_07_28,
294 ],
295 ),
296 (
297 Capability::Extensions,
300 &[ProtocolVersion::V2026_07_28],
301 ),
302 (
303 Capability::Roots,
306 &[
307 ProtocolVersion::V2024_11_05,
308 ProtocolVersion::V2025_03_26,
309 ProtocolVersion::V2025_06_18,
310 ProtocolVersion::V2025_11_25,
311 ProtocolVersion::V2026_07_28,
312 ],
313 ),
314 (
315 Capability::RootsListChanged,
318 &[
319 ProtocolVersion::V2024_11_05,
320 ProtocolVersion::V2025_03_26,
321 ProtocolVersion::V2025_06_18,
322 ProtocolVersion::V2025_11_25,
323 ],
324 ),
325 (
326 Capability::Sampling,
329 &[
330 ProtocolVersion::V2024_11_05,
331 ProtocolVersion::V2025_03_26,
332 ProtocolVersion::V2025_06_18,
333 ProtocolVersion::V2025_11_25,
334 ProtocolVersion::V2026_07_28,
335 ],
336 ),
337 (
338 Capability::SamplingContext,
340 &[ProtocolVersion::V2025_11_25, ProtocolVersion::V2026_07_28],
341 ),
342 (
343 Capability::SamplingTools,
345 &[ProtocolVersion::V2025_11_25, ProtocolVersion::V2026_07_28],
346 ),
347 (
348 Capability::Elicitation,
350 &[
351 ProtocolVersion::V2025_06_18,
352 ProtocolVersion::V2025_11_25,
353 ProtocolVersion::V2026_07_28,
354 ],
355 ),
356 (
357 Capability::ElicitationForm,
359 &[ProtocolVersion::V2025_11_25, ProtocolVersion::V2026_07_28],
360 ),
361 (
362 Capability::ElicitationUrl,
366 &[ProtocolVersion::V2025_11_25, ProtocolVersion::V2026_07_28],
367 ),
368 (
369 Capability::Logging,
372 &[
373 ProtocolVersion::V2024_11_05,
374 ProtocolVersion::V2025_03_26,
375 ProtocolVersion::V2025_06_18,
376 ProtocolVersion::V2025_11_25,
377 ProtocolVersion::V2026_07_28,
378 ],
379 ),
380 (
381 Capability::Prompts,
382 &[
383 ProtocolVersion::V2024_11_05,
384 ProtocolVersion::V2025_03_26,
385 ProtocolVersion::V2025_06_18,
386 ProtocolVersion::V2025_11_25,
387 ProtocolVersion::V2026_07_28,
388 ],
389 ),
390 (
391 Capability::PromptsListChanged,
392 &[
393 ProtocolVersion::V2024_11_05,
394 ProtocolVersion::V2025_03_26,
395 ProtocolVersion::V2025_06_18,
396 ProtocolVersion::V2025_11_25,
397 ProtocolVersion::V2026_07_28,
398 ],
399 ),
400 (
401 Capability::Resources,
402 &[
403 ProtocolVersion::V2024_11_05,
404 ProtocolVersion::V2025_03_26,
405 ProtocolVersion::V2025_06_18,
406 ProtocolVersion::V2025_11_25,
407 ProtocolVersion::V2026_07_28,
408 ],
409 ),
410 (
411 Capability::ResourcesListChanged,
412 &[
413 ProtocolVersion::V2024_11_05,
414 ProtocolVersion::V2025_03_26,
415 ProtocolVersion::V2025_06_18,
416 ProtocolVersion::V2025_11_25,
417 ProtocolVersion::V2026_07_28,
418 ],
419 ),
420 (
421 Capability::ResourcesSubscribe,
426 &[
427 ProtocolVersion::V2024_11_05,
428 ProtocolVersion::V2025_03_26,
429 ProtocolVersion::V2025_06_18,
430 ProtocolVersion::V2025_11_25,
431 ProtocolVersion::V2026_07_28,
432 ],
433 ),
434 (
435 Capability::Tools,
436 &[
437 ProtocolVersion::V2024_11_05,
438 ProtocolVersion::V2025_03_26,
439 ProtocolVersion::V2025_06_18,
440 ProtocolVersion::V2025_11_25,
441 ProtocolVersion::V2026_07_28,
442 ],
443 ),
444 (
445 Capability::ToolsListChanged,
446 &[
447 ProtocolVersion::V2024_11_05,
448 ProtocolVersion::V2025_03_26,
449 ProtocolVersion::V2025_06_18,
450 ProtocolVersion::V2025_11_25,
451 ProtocolVersion::V2026_07_28,
452 ],
453 ),
454 (
455 Capability::Completions,
458 &[
459 ProtocolVersion::V2025_03_26,
460 ProtocolVersion::V2025_06_18,
461 ProtocolVersion::V2025_11_25,
462 ProtocolVersion::V2026_07_28,
463 ],
464 ),
465 (
466 Capability::Tasks,
470 &[ProtocolVersion::V2025_11_25],
471 ),
472 (Capability::TasksCancel, &[ProtocolVersion::V2025_11_25]),
473 (Capability::TasksList, &[ProtocolVersion::V2025_11_25]),
474 (Capability::TasksRequests, &[ProtocolVersion::V2025_11_25]),
475 (
476 Capability::TasksRequestsElicitation,
477 &[ProtocolVersion::V2025_11_25],
478 ),
479 (
480 Capability::TasksRequestsElicitationCreate,
481 &[ProtocolVersion::V2025_11_25],
482 ),
483 (
484 Capability::TasksRequestsSampling,
485 &[ProtocolVersion::V2025_11_25],
486 ),
487 (
488 Capability::TasksRequestsSamplingCreateMessage,
489 &[ProtocolVersion::V2025_11_25],
490 ),
491 (
492 Capability::TasksRequestsTools,
493 &[ProtocolVersion::V2025_11_25],
494 ),
495 (
496 Capability::TasksRequestsToolsCall,
497 &[ProtocolVersion::V2025_11_25],
498 ),
499];
500
501#[must_use]
513pub fn defined_in(capability: Capability, version: ProtocolVersion) -> bool {
514 if matches!(version, ProtocolVersion::Unknown(_)) {
515 return false;
516 }
517 CAPABILITY_TABLE
518 .iter()
519 .find(|(candidate, _)| *candidate == capability)
520 .is_some_and(|(_, versions)| versions.contains(&version))
521}
522
523impl core::fmt::Display for Capability {
524 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
525 formatter.write_str(self.as_str())
526 }
527}
528
529#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
537pub struct CapabilitySet {
538 bits: u128,
539}
540
541impl CapabilitySet {
542 #[must_use]
544 pub const fn empty() -> Self {
545 Self { bits: 0 }
546 }
547
548 #[must_use]
552 pub fn all() -> Self {
553 let mut set = Self::empty();
554 for capability in Capability::ALL {
555 set.insert(capability);
556 }
557 set
558 }
559
560 #[must_use]
565 pub fn all_for(version: ProtocolVersion) -> Self {
566 let mut set = Self::empty();
567 for (capability, versions) in CAPABILITY_TABLE {
568 if versions.contains(&version) {
569 set.insert(*capability);
570 }
571 }
572 set
573 }
574
575 #[must_use]
581 pub fn with(self, capability: Capability) -> Self {
582 Self {
583 bits: self.bits | capability.bit(),
584 }
585 }
586
587 pub fn insert(&mut self, capability: Capability) {
589 self.bits |= capability.bit();
590 }
591
592 #[must_use]
594 pub fn contains(self, capability: Capability) -> bool {
595 self.bits & capability.bit() != 0
596 }
597
598 #[must_use]
600 pub const fn is_empty(self) -> bool {
601 self.bits == 0
602 }
603
604 #[must_use]
606 pub const fn len(self) -> u32 {
607 self.bits.count_ones()
608 }
609
610 pub fn iter(self) -> impl Iterator<Item = Capability> {
612 Capability::ALL
613 .into_iter()
614 .filter(move |c| self.contains(*c))
615 }
616
617 #[must_use]
624 pub fn restricted_to(self, version: ProtocolVersion) -> Self {
625 let mut out = Self::empty();
626 for capability in self.iter() {
627 if capability.defined_in(version) {
628 out.insert(capability);
629 }
630 }
631 out
632 }
633}
634
635fn str_eq(left: &str, right: &str) -> bool {
638 let (left, right) = (left.as_bytes(), right.as_bytes());
639 if left.len() != right.len() {
640 return false;
641 }
642 let mut i = 0;
643 while i < left.len() {
644 match (left.get(i), right.get(i)) {
645 (Some(l), Some(r)) if l == r => {}
646 _ => return false,
647 }
648 i += 1;
649 }
650 true
651}
652
653pub const SERVER_CAPABILITY_WIRE: &[(Capability, &[&str])] = &[
669 (Capability::Experimental, &["experimental"]),
670 (Capability::Tools, &["tools"]),
671 (Capability::ToolsListChanged, &["tools", "listChanged"]),
672 (Capability::Resources, &["resources"]),
673 (Capability::ResourcesSubscribe, &["resources", "subscribe"]),
674 (
675 Capability::ResourcesListChanged,
676 &["resources", "listChanged"],
677 ),
678 (Capability::Prompts, &["prompts"]),
679 (Capability::PromptsListChanged, &["prompts", "listChanged"]),
680 (Capability::Completions, &["completions"]),
681 (Capability::Logging, &["logging"]),
682 (Capability::Tasks, &["tasks"]),
683];
684
685pub const CLIENT_CAPABILITY_WIRE: &[(Capability, &[&str])] = &[
698 (Capability::Experimental, &["experimental"]),
699 (Capability::Extensions, &["extensions"]),
700 (Capability::Roots, &["roots"]),
701 (Capability::RootsListChanged, &["roots", "listChanged"]),
702 (Capability::Sampling, &["sampling"]),
703 (Capability::SamplingContext, &["sampling", "context"]),
704 (Capability::SamplingTools, &["sampling", "tools"]),
705 (Capability::Elicitation, &["elicitation"]),
706 (Capability::ElicitationForm, &["elicitation", "form"]),
707 (Capability::ElicitationUrl, &["elicitation", "url"]),
708 (Capability::Tasks, &["tasks"]),
709 (Capability::TasksCancel, &["tasks", "cancel"]),
710 (Capability::TasksList, &["tasks", "list"]),
711 (Capability::TasksRequests, &["tasks", "requests"]),
712 (
713 Capability::TasksRequestsElicitation,
714 &["tasks", "requests", "elicitation"],
715 ),
716 (
717 Capability::TasksRequestsElicitationCreate,
718 &["tasks", "requests", "elicitation", "create"],
719 ),
720 (
721 Capability::TasksRequestsSampling,
722 &["tasks", "requests", "sampling"],
723 ),
724 (
725 Capability::TasksRequestsSamplingCreateMessage,
726 &["tasks", "requests", "sampling", "createMessage"],
727 ),
728];
729
730#[cfg(test)]
731mod tests {
732 #![expect(
736 clippy::panic,
737 clippy::unwrap_used,
738 reason = "tests assert on fallible calls and panic with the failing precondition; propagating errors would replace a clear failure message with noise"
739 )]
740
741 use super::{CAPABILITY_TABLE, Capability, CapabilitySet, Side, defined_in};
742 use crate::domain::ProtocolVersion;
743 use std::collections::BTreeSet;
744
745 #[test]
746 fn every_capability_fits_in_the_bitset() {
747 let mut seen = BTreeSet::new();
748 for capability in Capability::ALL {
749 let bit = capability.bit();
750 assert!(
751 bit != 0 && bit.is_power_of_two(),
752 "{} has a bad bit {bit:#x}",
753 capability.as_str()
754 );
755 assert!(bit < 1u128 << 100, "{} overflows", capability.as_str());
756 assert!(seen.insert(bit), "{} duplicates a bit", capability.as_str());
757 }
758 }
759
760 #[test]
761 fn names_are_unique_and_dotted_paths_are_well_formed() {
762 let mut names = BTreeSet::new();
763 for capability in Capability::ALL {
764 assert!(
765 names.insert(capability.as_str()),
766 "duplicate name {}",
767 capability.as_str()
768 );
769 assert!(!capability.as_str().is_empty());
770 assert!(!capability.as_str().contains(' '));
771 }
772 for capability in Capability::ALL {
776 if let Some((container, _)) = capability.as_str().split_once('.') {
777 let Some(found) = Capability::ALL.iter().find(|c| c.as_str() == container) else {
778 panic!("{container} has no variant");
779 };
780 let (container_side, child_side) = (found.side(), capability.side());
781 let admitted = match child_side {
782 Side::Client => container_side.includes_client(),
783 Side::Server => container_side.includes_server(),
784 Side::Both => container_side == Side::Both,
787 };
788 assert!(
789 admitted,
790 "{} is {child_side:?} but its container {container} is {container_side:?}",
791 capability.as_str()
792 );
793 }
794 }
795 }
796
797 #[test]
798 fn the_table_covers_every_capability_exactly_once() {
799 for capability in Capability::ALL {
800 let count = CAPABILITY_TABLE
801 .iter()
802 .filter(|(candidate, _)| candidate.as_str() == capability.as_str())
803 .count();
804 assert_eq!(count, 1, "{} appears {count} times", capability.as_str());
805 }
806 assert_eq!(CAPABILITY_TABLE.len(), Capability::ALL.len());
807 }
808
809 #[test]
810 fn every_table_row_names_a_known_revision() {
811 for (capability, versions) in CAPABILITY_TABLE {
812 assert!(
813 !versions.is_empty(),
814 "{} has no versions",
815 capability.as_str()
816 );
817 for version in *versions {
818 assert!(
819 version.is_known(),
820 "{} cites {version}",
821 capability.as_str()
822 );
823 }
824 assert!(
826 versions.is_sorted(),
827 "{} versions are unsorted",
828 capability.as_str()
829 );
830 }
831 }
832
833 #[test]
839 fn the_table_matches_every_vendored_schema() {
840 let spec_root =
841 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../protocol-droid-schema/spec");
842
843 let mut mismatches: Vec<String> = Vec::new();
844 let mut revisions_checked = 0usize;
845
846 for version in ProtocolVersion::KNOWN {
847 let path = spec_root.join(version.to_string()).join("schema.json");
848 let Ok(bytes) = std::fs::read(&path) else {
849 continue;
850 };
851 let Ok(document) = serde_json::from_slice::<serde_json::Value>(&bytes) else {
852 continue;
853 };
854 revisions_checked += 1;
855
856 let defs = document
857 .get("$defs")
858 .or_else(|| document.get("definitions"))
859 .and_then(serde_json::Value::as_object);
860
861 for (root, side) in [
862 ("ClientCapabilities", Side::Client),
863 ("ServerCapabilities", Side::Server),
864 ] {
865 let found = leaf_names(defs, root);
866 for name in &found {
867 let modelled = Capability::ALL
868 .iter()
869 .find(|c| c.as_str() == name.as_str())
870 .copied();
871 match modelled {
872 None => mismatches.push(format!(
873 "{version} {root}.{name}: in the schema, not modelled"
874 )),
875 Some(capability) => {
876 if !capability.side().includes_client() && side == Side::Client
877 || !capability.side().includes_server() && side == Side::Server
878 {
879 mismatches.push(format!(
880 "{version} {root}.{name}: declared on the wrong side"
881 ));
882 }
883 if !defined_in(capability, version) {
884 mismatches.push(format!(
885 "{version} {root}.{name}: defined in the schema, \
886 absent from CAPABILITY_TABLE"
887 ));
888 }
889 }
890 }
891 }
892 for (capability, versions) in CAPABILITY_TABLE {
894 if !versions.contains(&version) {
895 continue;
896 }
897 let declared_here = capability.side().includes_client() && side == Side::Client
898 || capability.side().includes_server() && side == Side::Server;
899 if declared_here && !found.contains(capability.as_str()) {
900 mismatches.push(format!(
901 "{version} {root}.{}: in CAPABILITY_TABLE, not in the schema",
902 capability.as_str()
903 ));
904 }
905 }
906 }
907 }
908
909 assert!(
910 revisions_checked >= 5,
911 "only checked {revisions_checked} schemas"
912 );
913 assert!(
914 mismatches.is_empty(),
915 "CAPABILITY_TABLE disagrees with the vendored schemas:\n {}",
916 mismatches.join("\n ")
917 );
918 }
919
920 fn leaf_names(
926 defs: Option<&serde_json::Map<String, serde_json::Value>>,
927 root: &str,
928 ) -> BTreeSet<String> {
929 fn walk(node: &serde_json::Value, prefix: &str, out: &mut BTreeSet<String>) {
930 for (name, value) in node
931 .get("properties")
932 .and_then(serde_json::Value::as_object)
933 .into_iter()
934 .flatten()
935 {
936 let path = if prefix.is_empty() {
937 name.clone()
938 } else {
939 format!("{prefix}.{name}")
940 };
941 out.insert(path.clone());
942 walk(value, &path, out);
943 }
944 }
945
946 let mut out = BTreeSet::new();
947 let Some(node) = defs.and_then(|d| d.get(root)) else {
948 return out;
949 };
950 walk(node, "", &mut out);
951 out
952 }
953
954 #[test]
955 fn sets_behave_like_sets() {
956 let empty = CapabilitySet::empty();
957 assert!(empty.is_empty());
958 assert_eq!(empty.len(), 0);
959 assert!(!empty.contains(Capability::Tools));
960
961 let one = empty.with(Capability::Tools);
962 assert!(one.contains(Capability::Tools));
963 assert!(!one.is_empty());
964 assert_eq!(one.len(), 1);
965
966 assert_eq!(one.with(Capability::Tools), one);
968 assert_eq!(one.with(Capability::Roots).len(), 2);
969 }
970
971 #[test]
972 fn all_for_is_scoped_to_one_revision() {
973 let oldest = CapabilitySet::all_for(ProtocolVersion::V2024_11_05);
974 assert!(oldest.contains(Capability::Roots));
975 assert!(oldest.contains(Capability::Tools));
976 assert!(oldest.contains(Capability::Logging));
977 assert!(!oldest.contains(Capability::Elicitation));
979 assert!(!oldest.contains(Capability::Completions));
980 assert!(!oldest.contains(Capability::Tasks));
981 assert!(!oldest.contains(Capability::Extensions));
982
983 let newest = CapabilitySet::all_for(ProtocolVersion::V2026_07_28);
984 assert!(newest.contains(Capability::Elicitation));
985 assert!(newest.contains(Capability::Extensions));
986 assert!(!newest.contains(Capability::Tasks));
988 assert!(!newest.contains(Capability::RootsListChanged));
989 assert!(newest.contains(Capability::ResourcesSubscribe));
991 }
992
993 #[test]
994 fn restricting_drops_what_a_revision_cannot_express() {
995 let newer = CapabilitySet::all_for(ProtocolVersion::V2025_11_25);
997 assert!(newer.contains(Capability::Tasks));
998 let downgraded = newer.restricted_to(ProtocolVersion::V2024_11_05);
999 assert!(!downgraded.contains(Capability::Tasks));
1000 assert!(!downgraded.contains(Capability::Elicitation));
1001 assert!(downgraded.contains(Capability::Tools));
1002 assert!(downgraded.contains(Capability::RootsListChanged));
1003 }
1004
1005 #[test]
1006 fn an_unknown_revision_defines_nothing() {
1007 let future = ProtocolVersion::Unknown("2027-01-01".parse().unwrap());
1008 for capability in Capability::ALL {
1009 assert!(!defined_in(capability, future), "{}", capability.as_str());
1010 }
1011 let set = CapabilitySet::all();
1013 assert!(set.restricted_to(future).is_empty());
1014 }
1015
1016 #[test]
1017 fn iteration_is_declaration_ordered_and_complete() {
1018 let set = CapabilitySet::all().with(Capability::Experimental);
1019 let collected: Vec<Capability> = set.iter().collect();
1020 assert_eq!(collected, Capability::ALL.to_vec());
1021 let doubled = set.with(Capability::Tools).with(Capability::Tools);
1023 assert_eq!(doubled.len() as usize, Capability::ALL.len());
1024 }
1025
1026 #[test]
1027 fn tasks_is_defined_in_exactly_one_revision() {
1028 for version in ProtocolVersion::KNOWN {
1029 let expected = version == ProtocolVersion::V2025_11_25;
1030 assert_eq!(
1031 defined_in(Capability::Tasks, version),
1032 expected,
1033 "tasks in {version}"
1034 );
1035 for nested in [
1036 Capability::TasksCancel,
1037 Capability::TasksList,
1038 Capability::TasksRequests,
1039 Capability::TasksRequestsTools,
1040 Capability::TasksRequestsToolsCall,
1041 Capability::TasksRequestsSampling,
1042 Capability::TasksRequestsSamplingCreateMessage,
1043 Capability::TasksRequestsElicitation,
1044 Capability::TasksRequestsElicitationCreate,
1045 ] {
1046 assert_eq!(
1047 defined_in(nested, version),
1048 expected,
1049 "{nested:?} in {version}"
1050 );
1051 }
1052 }
1053 }
1054
1055 #[test]
1056 fn sides_are_as_the_schemas_declare_them() {
1057 assert_eq!(Capability::Roots.side(), Side::Client);
1058 assert_eq!(Capability::SamplingTools.side(), Side::Client);
1059 assert_eq!(Capability::Tools.side(), Side::Server);
1060 assert_eq!(Capability::TasksRequestsTools.side(), Side::Server);
1061 assert_eq!(Capability::Experimental.side(), Side::Both);
1062 assert_eq!(Capability::Extensions.side(), Side::Both);
1063 assert_eq!(Capability::Tasks.side(), Side::Both);
1064 }
1065}