Skip to main content

protocol_droid/domain/
capability.rs

1//! `Capability` — the flags a peer advertises, and which revisions define them.
2//!
3//! # Why this table is not written from memory
4//!
5//! Capabilities are the input to [`resolve`](crate::domain::resolve) and they
6//! change shape silently between revisions: `tasks` appears on both sides in
7//! `2025-11-25` and is gone from both in `2026-07-28`, `roots.listChanged`
8//! disappears when `notifications/roots/list_changed` is removed, and
9//! `elicitation` grows `form` and `url` sub-flags. A table written from
10//! recollection would be wrong in at least one row and the error would surface
11//! as a feature that silently fails to enable.
12//!
13//! So the table is hardcoded — it has to be, it is the domain's own model — but
14//! it is **checked against the vendored `schema.json` at test time**. See
15//! `the_table_matches_every_vendored_schema`, which walks the real
16//! capability tree of all six revisions and fails if the two disagree in either
17//! direction. That makes the table self-verifying: a future revision that adds
18//! or drops a capability fails the build rather than quietly getting a
19//! nonsensical feature profile.
20//!
21//! # Capability is not method
22//!
23//! The two come apart, and the clearest case is `resources.subscribe`:
24//! `2026-07-28` replaces the `resources/subscribe` and `resources/unsubscribe`
25//! *methods* with `subscriptions/listen`, and still defines
26//! `resources.subscribe` as a server *capability*. A capability records that a
27//! peer opted into a kind of behaviour; the methods that implement it are
28//! T08's concern. Conflating them would remove the capability in
29//! `2026-07-28` and lose the information that the peer is still subscribed.
30
31use crate::domain::ProtocolVersion;
32
33/// Which side of the connection a capability is declared on.
34///
35/// A capability that is not declared by the side that would provide it cannot
36/// be enabled, and keeping the two apart is what makes that checkable.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
38pub enum Side {
39    /// Declared by the client, describing what the client can do.
40    Client,
41    /// Declared by the server, describing what the server can do.
42    Server,
43    /// Declared by both, with the same meaning.
44    Both,
45}
46
47impl Side {
48    /// Whether a client may declare this side.
49    #[must_use]
50    pub const fn includes_client(self) -> bool {
51        matches!(self, Self::Client | Self::Both)
52    }
53
54    /// Whether a server may declare this side.
55    #[must_use]
56    pub const fn includes_server(self) -> bool {
57        matches!(self, Self::Server | Self::Both)
58    }
59}
60
61/// A capability name as the specification spells it.
62///
63/// Fieldless, and one variant per leaf name in the capability trees. The
64/// container names (`roots`, `tasks`, …) are variants too: a peer that
65/// advertises `roots` has advertised that it has roots, and a feature gate on
66/// "the peer has roots at all" is a real question that
67/// [`FeatureProfile`](crate::domain::FeatureProfile) has to answer.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
69pub enum Capability {
70    /// Client and server `experimental` bags. Present in every revision.
71    Experimental,
72    /// `extensions`, new in `2026-07-28` on both sides.
73    Extensions,
74    /// Client `roots`. Deprecated in `2026-07-28` but still defined.
75    Roots,
76    /// Client `roots.listChanged`, removed in `2026-07-28` along with the
77    /// `notifications/roots/list_changed` method.
78    RootsListChanged,
79    /// Client `sampling`. Deprecated in `2026-07-28` but still defined.
80    Sampling,
81    /// Client `sampling.context`, added `2025-11-25`.
82    SamplingContext,
83    /// Client `sampling.tools`, added `2025-11-25` (SEP-1577).
84    SamplingTools,
85    /// Client `elicitation`, added `2025-06-18`.
86    Elicitation,
87    /// Client `elicitation.form`, added `2025-11-25` (SEP-1330).
88    ElicitationForm,
89    /// Client `elicitation.url`, added `2025-11-25` (SEP-1036).
90    ElicitationUrl,
91    /// Server `logging`. Deprecated in `2026-07-28`; `logging/setLevel` is
92    /// removed, log level moves to `_meta`.
93    Logging,
94    /// Server `prompts`, every revision.
95    Prompts,
96    /// Server `prompts.listChanged`, every revision.
97    PromptsListChanged,
98    /// Server `resources`, every revision.
99    Resources,
100    /// Server `resources.listChanged`, every revision.
101    ResourcesListChanged,
102    /// Server `resources.subscribe`. The *methods* behind it are replaced in
103    /// `2026-07-28`; the capability is not.
104    ResourcesSubscribe,
105    /// Server `tools`, every revision.
106    Tools,
107    /// Server `tools.listChanged`, every revision.
108    ToolsListChanged,
109    /// Server `completions`, added `2025-03-26`.
110    Completions,
111    /// `tasks`, added `2025-11-25` (SEP-1686) on both sides and **removed**
112    /// from both in `2026-07-28`, where it moves to the
113    /// `io.modelcontextprotocol/tasks` extension (SEP-2663).
114    Tasks,
115    /// Client and server `tasks.cancel`, `2025-11-25` only.
116    TasksCancel,
117    /// Client and server `tasks.list`, `2025-11-25` only. The method was
118    /// removed in `2026-07-28` along with the capability.
119    TasksList,
120    /// Client and server `tasks.requests`, `2025-11-25` only.
121    TasksRequests,
122    /// Client `tasks.requests.elicitation`, `2025-11-25` only.
123    TasksRequestsElicitation,
124    /// Client `tasks.requests.elicitation.create`, `2025-11-25` only.
125    TasksRequestsElicitationCreate,
126    /// Client `tasks.requests.sampling`, `2025-11-25` only.
127    TasksRequestsSampling,
128    /// Client `tasks.requests.sampling.createMessage`, `2025-11-25` only.
129    TasksRequestsSamplingCreateMessage,
130    /// Server `tasks.requests.tools`, `2025-11-25` only.
131    TasksRequestsTools,
132    /// Server `tasks.requests.tools.call`, `2025-11-25` only.
133    TasksRequestsToolsCall,
134}
135
136impl Capability {
137    /// Every capability, in declaration order.
138    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    /// The name the specification uses, dot-separated.
171    #[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    /// Which side declares it.
207    #[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    /// The bit this capability occupies in a [`CapabilitySet`].
243    ///
244    /// The index is the position in [`Capability::ALL`], found by name rather
245    /// than written by hand, so adding a variant cannot leave a stale
246    /// hand-assigned bit behind. Asserted to be a distinct in-range power of
247    /// two by `every_capability_fits_in_the_bitset`.
248    ///
249    /// Not `const`: the `indexing_slicing` lint is denied workspace-wide, and
250    /// `slice::get` — the non-panicking form it demands — is not a
251    /// const-callable function on 1.98. The two cannot both be satisfied, and
252    /// an unchecked index in a `const` fn is not worth suppressing the lint
253    /// for: nothing here is evaluated at compile time.
254    #[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    /// Whether the given revision defines this capability at all.
268    ///
269    /// Not the same question as whether a particular peer advertises it: a
270    /// revision either has the name in its schema or it does not, and a peer on
271    /// that revision either sends it or does not.
272    #[must_use]
273    pub fn defined_in(self, version: ProtocolVersion) -> bool {
274        defined_in(self, version)
275    }
276}
277
278/// A capability and the revisions whose schemas define it.
279///
280/// The table is the domain's model; the test
281/// `the_table_matches_every_vendored_schema` is what keeps it honest
282/// against the vendored bytes. Citations are to the changelog of the revision
283/// that introduced the capability, which is the only place the fact is written
284/// down.
285pub 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        // SEP-2575, `2026-07-28`: `extensions` replaces per-revision ad-hoc
298        // extension flags with a declared bag.
299        Capability::Extensions,
300        &[ProtocolVersion::V2026_07_28],
301    ),
302    (
303        // Present from the first revision. Deprecated by SEP-2577 in
304        // `2026-07-28`, but still in the schema.
305        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        // `roots.listChanged` goes with `notifications/roots/list_changed`,
316        // removed by SEP-2575 in `2026-07-28`.
317        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        // Present from the first revision; deprecated by SEP-2577 in
327        // `2026-07-28`.
328        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        // SEP-1577, `2025-11-25`.
339        Capability::SamplingContext,
340        &[ProtocolVersion::V2025_11_25, ProtocolVersion::V2026_07_28],
341    ),
342    (
343        // SEP-1577, `2025-11-25`: tool calling inside sampling.
344        Capability::SamplingTools,
345        &[ProtocolVersion::V2025_11_25, ProtocolVersion::V2026_07_28],
346    ),
347    (
348        // `2025-06-18` major change 6, PR #382.
349        Capability::Elicitation,
350        &[
351            ProtocolVersion::V2025_06_18,
352            ProtocolVersion::V2025_11_25,
353            ProtocolVersion::V2026_07_28,
354        ],
355    ),
356    (
357        // SEP-1330, `2025-11-25`: titled/untitled, single- and multi-select.
358        Capability::ElicitationForm,
359        &[ProtocolVersion::V2025_11_25, ProtocolVersion::V2026_07_28],
360    ),
361    (
362        // SEP-1036, `2025-11-25`. The `notifications/elicitation/complete`
363        // notification and the `elicitationId` field behind it are removed in
364        // `2026-07-28`, but the capability itself survives.
365        Capability::ElicitationUrl,
366        &[ProtocolVersion::V2025_11_25, ProtocolVersion::V2026_07_28],
367    ),
368    (
369        // Present from the first revision; deprecated by SEP-2577 in
370        // `2026-07-28`, which also removes `logging/setLevel`.
371        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        // Defined in every revision including `2026-07-28`, where the
422        // `resources/subscribe` and `resources/unsubscribe` methods are
423        // replaced by `subscriptions/listen` (SEP-2575). The capability
424        // records that the peer is subscribed; the method is T08's problem.
425        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        // `2025-03-26`: "Added `completions` capability to explicitly indicate
456        // support for argument autocompletion suggestions".
457        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        // SEP-1686 adds it in `2025-11-25`; SEP-2663 moves it out of the core
467        // protocol into the `io.modelcontextprotocol/tasks` extension in
468        // `2026-07-28`, so it is defined in exactly one revision.
469        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/// Whether `version`'s schema defines `capability`.
502///
503/// An unknown revision defines nothing, for the same reason
504/// [`transport_allowed`](crate::domain::transport_allowed) denies: this build
505/// has no wire types for it, so no flag in it can be trusted.
506///
507/// Not `const`: `PartialEq` on an enum is not a const-callable operator on
508/// 1.98, so the table walk cannot be one. The cost is a scan of at most
509/// [`CAPABILITY_TABLE`] rows, and this is called from
510/// [`resolve`](crate::domain::resolve) and the tests rather than per message;
511/// [`CapabilitySet::restricted_to`] is the form that runs in bulk.
512#[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/// A set of advertised capabilities.
530///
531/// A `u128` bitset, so [`contains`](Self::contains) is a mask and a test rather
532/// than a search. This matters because [`resolve`](crate::domain::resolve) asks
533/// roughly thirty questions of two sets, and a session asks
534/// [`FeatureProfile::allows`](crate::domain::FeatureProfile::allows) on every
535/// message.
536#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
537pub struct CapabilitySet {
538    bits: u128,
539}
540
541impl CapabilitySet {
542    /// The empty set, which is what a peer that advertises nothing gets.
543    #[must_use]
544    pub const fn empty() -> Self {
545        Self { bits: 0 }
546    }
547
548    /// Every capability, on both sides. A superset of anything real, and
549    /// mostly useful in tests and in the "what could this peer possibly do"
550    /// query.
551    #[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    /// Every capability `version` defines, on both sides.
561    ///
562    /// The "all capabilities on" case for a golden test, scoped to one revision
563    /// so the result is something a peer could actually have sent.
564    #[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    /// Adds a capability, returning the new set.
576    ///
577    /// By value rather than `&mut self` so that building a set in an
578    /// expression does not need a mutable binding, which is what every caller
579    /// ends up writing.
580    #[must_use]
581    pub fn with(self, capability: Capability) -> Self {
582        Self {
583            bits: self.bits | capability.bit(),
584        }
585    }
586
587    /// Adds a capability in place.
588    pub fn insert(&mut self, capability: Capability) {
589        self.bits |= capability.bit();
590    }
591
592    /// Whether the capability is in the set.
593    #[must_use]
594    pub fn contains(self, capability: Capability) -> bool {
595        self.bits & capability.bit() != 0
596    }
597
598    /// Whether the set holds nothing.
599    #[must_use]
600    pub const fn is_empty(self) -> bool {
601        self.bits == 0
602    }
603
604    /// How many capabilities the set holds.
605    #[must_use]
606    pub const fn len(self) -> u32 {
607        self.bits.count_ones()
608    }
609
610    /// The capabilities in the set, in declaration order.
611    pub fn iter(self) -> impl Iterator<Item = Capability> {
612        Capability::ALL
613            .into_iter()
614            .filter(move |c| self.contains(*c))
615    }
616
617    /// Drops every capability `version` does not define.
618    ///
619    /// A peer cannot advertise a flag its own revision's schema has no field
620    /// for, and a flag that survives the round trip from a newer peer into an
621    /// older profile is exactly the "confident wrong" the crate exists to
622    /// avoid. An unknown revision therefore reduces the set to empty.
623    #[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
635/// `str` equality by hand, because `==` on slices is not what this needs and
636/// a name makes the two call sites in `bit` and `bit_of` read the same.
637fn 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
653/// How one server capability appears in a `ServerCapabilities` object.
654///
655/// A *key path* rather than a key and an optional child, because the nesting
656/// is not two deep everywhere: `tasks.requests.tools.call` is three, and a
657/// `(key, child)` row would have to lie about it or leave it unrepresentable.
658///
659/// Rows are only ever walked from the outside in, so a parent with no child of
660/// its own still emits its own object — `tools` is present exactly when the
661/// server has tools, and that presence is the whole signal.
662///
663/// Data rather than a `match` for the same reason [`FEATURES`] is data: a
664/// capability added to the domain without a row here would be dropped
665/// silently, and `capability_wire_is_total` is what keeps the two in step.
666///
667/// [`FEATURES`]: crate::domain::FEATURES
668pub 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
685/// How one client capability appears in a `ClientCapabilities` object.
686///
687/// The mirror of [`SERVER_CAPABILITY_WIRE`], and in the same place for the same
688/// reason: a client that advertises a capability the reader cannot recognise
689/// loses it silently, which is a feature that appears to work right up until
690/// the peer uses it.
691///
692/// Deliberately a *separate* table rather than one table with a side column.
693/// The two sides do not have the same shapes — `roots` is client-only,
694/// `tools` is server-only, and `tasks` nests differently under each — and one
695/// table with a side column would carry a discriminator through both the
696/// renderer and the reader to express a difference that the data already has.
697pub 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    // Test code unwraps deliberately: the strict profile exists to keep
733    // production paths fallible, and a test that cannot unwrap cannot assert.
734    // The messages name the failing precondition, which is the useful part.
735    #![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        // Every dotted name has a container variant that is also a capability,
773        // and the container's side admits the child's: `tasks.requests` is
774        // declared on both sides while `tasks.requests.tools` is server-only.
775        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                    // A `Both` child needs a `Both` container: neither side on
785                    // its own declares a capability the other side also has.
786                    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            // Sorted, so the list reads as a history rather than a set.
825            assert!(
826                versions.is_sorted(),
827                "{} versions are unsorted",
828                capability.as_str()
829            );
830        }
831    }
832
833    /// The load-bearing test. It reads the real vendored schemas and checks
834    /// this module's table against them, in both directions, for all six
835    /// revisions — so a table row that is wrong, or a capability that upstream
836    /// added, fails here rather than producing a nonsensical feature profile
837    /// later.
838    #[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                // The other direction: a table row that the schema no longer has.
893                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    /// Collects every dotted name under `root`, e.g. `tasks.requests.tools.call`.
921    ///
922    /// Recurses all the way down rather than a fixed depth: `2025-11-25` nests
923    /// four levels under `tasks`, and a walker that stopped at one would report
924    /// six table rows as missing from the schema.
925    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        // Idempotent.
967        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        // Nothing that arrived later.
978        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        // The two that came and went.
987        assert!(!newest.contains(Capability::Tasks));
988        assert!(!newest.contains(Capability::RootsListChanged));
989        // Still there despite the methods behind it changing.
990        assert!(newest.contains(Capability::ResourcesSubscribe));
991    }
992
993    #[test]
994    fn restricting_drops_what_a_revision_cannot_express() {
995        // A newer peer advertises tasks to an older peer.
996        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        // So restricting a real set to it leaves nothing at all.
1012        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        // Duplicates do not double up.
1022        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}