use crate::domain::ProtocolVersion;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Side {
Client,
Server,
Both,
}
impl Side {
#[must_use]
pub const fn includes_client(self) -> bool {
matches!(self, Self::Client | Self::Both)
}
#[must_use]
pub const fn includes_server(self) -> bool {
matches!(self, Self::Server | Self::Both)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Capability {
Experimental,
Extensions,
Roots,
RootsListChanged,
Sampling,
SamplingContext,
SamplingTools,
Elicitation,
ElicitationForm,
ElicitationUrl,
Logging,
Prompts,
PromptsListChanged,
Resources,
ResourcesListChanged,
ResourcesSubscribe,
Tools,
ToolsListChanged,
Completions,
Tasks,
TasksCancel,
TasksList,
TasksRequests,
TasksRequestsElicitation,
TasksRequestsElicitationCreate,
TasksRequestsSampling,
TasksRequestsSamplingCreateMessage,
TasksRequestsTools,
TasksRequestsToolsCall,
}
impl Capability {
pub const ALL: [Self; 29] = [
Self::Experimental,
Self::Extensions,
Self::Roots,
Self::RootsListChanged,
Self::Sampling,
Self::SamplingContext,
Self::SamplingTools,
Self::Elicitation,
Self::ElicitationForm,
Self::ElicitationUrl,
Self::Logging,
Self::Prompts,
Self::PromptsListChanged,
Self::Resources,
Self::ResourcesListChanged,
Self::ResourcesSubscribe,
Self::Tools,
Self::ToolsListChanged,
Self::Completions,
Self::Tasks,
Self::TasksCancel,
Self::TasksList,
Self::TasksRequests,
Self::TasksRequestsElicitation,
Self::TasksRequestsElicitationCreate,
Self::TasksRequestsSampling,
Self::TasksRequestsSamplingCreateMessage,
Self::TasksRequestsTools,
Self::TasksRequestsToolsCall,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Experimental => "experimental",
Self::Extensions => "extensions",
Self::Roots => "roots",
Self::RootsListChanged => "roots.listChanged",
Self::Sampling => "sampling",
Self::SamplingContext => "sampling.context",
Self::SamplingTools => "sampling.tools",
Self::Elicitation => "elicitation",
Self::ElicitationForm => "elicitation.form",
Self::ElicitationUrl => "elicitation.url",
Self::Logging => "logging",
Self::Prompts => "prompts",
Self::PromptsListChanged => "prompts.listChanged",
Self::Resources => "resources",
Self::ResourcesListChanged => "resources.listChanged",
Self::ResourcesSubscribe => "resources.subscribe",
Self::Tools => "tools",
Self::ToolsListChanged => "tools.listChanged",
Self::Completions => "completions",
Self::Tasks => "tasks",
Self::TasksCancel => "tasks.cancel",
Self::TasksList => "tasks.list",
Self::TasksRequests => "tasks.requests",
Self::TasksRequestsElicitation => "tasks.requests.elicitation",
Self::TasksRequestsElicitationCreate => "tasks.requests.elicitation.create",
Self::TasksRequestsSampling => "tasks.requests.sampling",
Self::TasksRequestsSamplingCreateMessage => "tasks.requests.sampling.createMessage",
Self::TasksRequestsTools => "tasks.requests.tools",
Self::TasksRequestsToolsCall => "tasks.requests.tools.call",
}
}
#[must_use]
pub const fn side(self) -> Side {
match self {
Self::Experimental
| Self::Extensions
| Self::Tasks
| Self::TasksCancel
| Self::TasksList
| Self::TasksRequests => Side::Both,
Self::Roots
| Self::RootsListChanged
| Self::Sampling
| Self::SamplingContext
| Self::SamplingTools
| Self::Elicitation
| Self::ElicitationForm
| Self::ElicitationUrl
| Self::TasksRequestsElicitation
| Self::TasksRequestsElicitationCreate
| Self::TasksRequestsSampling
| Self::TasksRequestsSamplingCreateMessage => Side::Client,
Self::Logging
| Self::Prompts
| Self::PromptsListChanged
| Self::Resources
| Self::ResourcesListChanged
| Self::ResourcesSubscribe
| Self::Tools
| Self::ToolsListChanged
| Self::Completions
| Self::TasksRequestsTools
| Self::TasksRequestsToolsCall => Side::Server,
}
}
#[must_use]
pub fn bit(self) -> u128 {
let name = self.as_str();
let mut i = 0;
while let Some(candidate) = Self::ALL.get(i) {
if str_eq(candidate.as_str(), name) {
return 1u128 << i;
}
i += 1;
}
0
}
#[must_use]
pub fn defined_in(self, version: ProtocolVersion) -> bool {
defined_in(self, version)
}
}
pub const CAPABILITY_TABLE: &[(Capability, &[ProtocolVersion])] = &[
(
Capability::Experimental,
&[
ProtocolVersion::V2024_11_05,
ProtocolVersion::V2025_03_26,
ProtocolVersion::V2025_06_18,
ProtocolVersion::V2025_11_25,
ProtocolVersion::V2026_07_28,
],
),
(
Capability::Extensions,
&[ProtocolVersion::V2026_07_28],
),
(
Capability::Roots,
&[
ProtocolVersion::V2024_11_05,
ProtocolVersion::V2025_03_26,
ProtocolVersion::V2025_06_18,
ProtocolVersion::V2025_11_25,
ProtocolVersion::V2026_07_28,
],
),
(
Capability::RootsListChanged,
&[
ProtocolVersion::V2024_11_05,
ProtocolVersion::V2025_03_26,
ProtocolVersion::V2025_06_18,
ProtocolVersion::V2025_11_25,
],
),
(
Capability::Sampling,
&[
ProtocolVersion::V2024_11_05,
ProtocolVersion::V2025_03_26,
ProtocolVersion::V2025_06_18,
ProtocolVersion::V2025_11_25,
ProtocolVersion::V2026_07_28,
],
),
(
Capability::SamplingContext,
&[ProtocolVersion::V2025_11_25, ProtocolVersion::V2026_07_28],
),
(
Capability::SamplingTools,
&[ProtocolVersion::V2025_11_25, ProtocolVersion::V2026_07_28],
),
(
Capability::Elicitation,
&[
ProtocolVersion::V2025_06_18,
ProtocolVersion::V2025_11_25,
ProtocolVersion::V2026_07_28,
],
),
(
Capability::ElicitationForm,
&[ProtocolVersion::V2025_11_25, ProtocolVersion::V2026_07_28],
),
(
Capability::ElicitationUrl,
&[ProtocolVersion::V2025_11_25, ProtocolVersion::V2026_07_28],
),
(
Capability::Logging,
&[
ProtocolVersion::V2024_11_05,
ProtocolVersion::V2025_03_26,
ProtocolVersion::V2025_06_18,
ProtocolVersion::V2025_11_25,
ProtocolVersion::V2026_07_28,
],
),
(
Capability::Prompts,
&[
ProtocolVersion::V2024_11_05,
ProtocolVersion::V2025_03_26,
ProtocolVersion::V2025_06_18,
ProtocolVersion::V2025_11_25,
ProtocolVersion::V2026_07_28,
],
),
(
Capability::PromptsListChanged,
&[
ProtocolVersion::V2024_11_05,
ProtocolVersion::V2025_03_26,
ProtocolVersion::V2025_06_18,
ProtocolVersion::V2025_11_25,
ProtocolVersion::V2026_07_28,
],
),
(
Capability::Resources,
&[
ProtocolVersion::V2024_11_05,
ProtocolVersion::V2025_03_26,
ProtocolVersion::V2025_06_18,
ProtocolVersion::V2025_11_25,
ProtocolVersion::V2026_07_28,
],
),
(
Capability::ResourcesListChanged,
&[
ProtocolVersion::V2024_11_05,
ProtocolVersion::V2025_03_26,
ProtocolVersion::V2025_06_18,
ProtocolVersion::V2025_11_25,
ProtocolVersion::V2026_07_28,
],
),
(
Capability::ResourcesSubscribe,
&[
ProtocolVersion::V2024_11_05,
ProtocolVersion::V2025_03_26,
ProtocolVersion::V2025_06_18,
ProtocolVersion::V2025_11_25,
ProtocolVersion::V2026_07_28,
],
),
(
Capability::Tools,
&[
ProtocolVersion::V2024_11_05,
ProtocolVersion::V2025_03_26,
ProtocolVersion::V2025_06_18,
ProtocolVersion::V2025_11_25,
ProtocolVersion::V2026_07_28,
],
),
(
Capability::ToolsListChanged,
&[
ProtocolVersion::V2024_11_05,
ProtocolVersion::V2025_03_26,
ProtocolVersion::V2025_06_18,
ProtocolVersion::V2025_11_25,
ProtocolVersion::V2026_07_28,
],
),
(
Capability::Completions,
&[
ProtocolVersion::V2025_03_26,
ProtocolVersion::V2025_06_18,
ProtocolVersion::V2025_11_25,
ProtocolVersion::V2026_07_28,
],
),
(
Capability::Tasks,
&[ProtocolVersion::V2025_11_25],
),
(Capability::TasksCancel, &[ProtocolVersion::V2025_11_25]),
(Capability::TasksList, &[ProtocolVersion::V2025_11_25]),
(Capability::TasksRequests, &[ProtocolVersion::V2025_11_25]),
(
Capability::TasksRequestsElicitation,
&[ProtocolVersion::V2025_11_25],
),
(
Capability::TasksRequestsElicitationCreate,
&[ProtocolVersion::V2025_11_25],
),
(
Capability::TasksRequestsSampling,
&[ProtocolVersion::V2025_11_25],
),
(
Capability::TasksRequestsSamplingCreateMessage,
&[ProtocolVersion::V2025_11_25],
),
(
Capability::TasksRequestsTools,
&[ProtocolVersion::V2025_11_25],
),
(
Capability::TasksRequestsToolsCall,
&[ProtocolVersion::V2025_11_25],
),
];
#[must_use]
pub fn defined_in(capability: Capability, version: ProtocolVersion) -> bool {
if matches!(version, ProtocolVersion::Unknown(_)) {
return false;
}
CAPABILITY_TABLE
.iter()
.find(|(candidate, _)| *candidate == capability)
.is_some_and(|(_, versions)| versions.contains(&version))
}
impl core::fmt::Display for Capability {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub struct CapabilitySet {
bits: u128,
}
impl CapabilitySet {
#[must_use]
pub const fn empty() -> Self {
Self { bits: 0 }
}
#[must_use]
pub fn all() -> Self {
let mut set = Self::empty();
for capability in Capability::ALL {
set.insert(capability);
}
set
}
#[must_use]
pub fn all_for(version: ProtocolVersion) -> Self {
let mut set = Self::empty();
for (capability, versions) in CAPABILITY_TABLE {
if versions.contains(&version) {
set.insert(*capability);
}
}
set
}
#[must_use]
pub fn with(self, capability: Capability) -> Self {
Self {
bits: self.bits | capability.bit(),
}
}
pub fn insert(&mut self, capability: Capability) {
self.bits |= capability.bit();
}
#[must_use]
pub fn contains(self, capability: Capability) -> bool {
self.bits & capability.bit() != 0
}
#[must_use]
pub const fn is_empty(self) -> bool {
self.bits == 0
}
#[must_use]
pub const fn len(self) -> u32 {
self.bits.count_ones()
}
pub fn iter(self) -> impl Iterator<Item = Capability> {
Capability::ALL
.into_iter()
.filter(move |c| self.contains(*c))
}
#[must_use]
pub fn restricted_to(self, version: ProtocolVersion) -> Self {
let mut out = Self::empty();
for capability in self.iter() {
if capability.defined_in(version) {
out.insert(capability);
}
}
out
}
}
fn str_eq(left: &str, right: &str) -> bool {
let (left, right) = (left.as_bytes(), right.as_bytes());
if left.len() != right.len() {
return false;
}
let mut i = 0;
while i < left.len() {
match (left.get(i), right.get(i)) {
(Some(l), Some(r)) if l == r => {}
_ => return false,
}
i += 1;
}
true
}
pub const SERVER_CAPABILITY_WIRE: &[(Capability, &[&str])] = &[
(Capability::Experimental, &["experimental"]),
(Capability::Tools, &["tools"]),
(Capability::ToolsListChanged, &["tools", "listChanged"]),
(Capability::Resources, &["resources"]),
(Capability::ResourcesSubscribe, &["resources", "subscribe"]),
(
Capability::ResourcesListChanged,
&["resources", "listChanged"],
),
(Capability::Prompts, &["prompts"]),
(Capability::PromptsListChanged, &["prompts", "listChanged"]),
(Capability::Completions, &["completions"]),
(Capability::Logging, &["logging"]),
(Capability::Tasks, &["tasks"]),
];
pub const CLIENT_CAPABILITY_WIRE: &[(Capability, &[&str])] = &[
(Capability::Experimental, &["experimental"]),
(Capability::Extensions, &["extensions"]),
(Capability::Roots, &["roots"]),
(Capability::RootsListChanged, &["roots", "listChanged"]),
(Capability::Sampling, &["sampling"]),
(Capability::SamplingContext, &["sampling", "context"]),
(Capability::SamplingTools, &["sampling", "tools"]),
(Capability::Elicitation, &["elicitation"]),
(Capability::ElicitationForm, &["elicitation", "form"]),
(Capability::ElicitationUrl, &["elicitation", "url"]),
(Capability::Tasks, &["tasks"]),
(Capability::TasksCancel, &["tasks", "cancel"]),
(Capability::TasksList, &["tasks", "list"]),
(Capability::TasksRequests, &["tasks", "requests"]),
(
Capability::TasksRequestsElicitation,
&["tasks", "requests", "elicitation"],
),
(
Capability::TasksRequestsElicitationCreate,
&["tasks", "requests", "elicitation", "create"],
),
(
Capability::TasksRequestsSampling,
&["tasks", "requests", "sampling"],
),
(
Capability::TasksRequestsSamplingCreateMessage,
&["tasks", "requests", "sampling", "createMessage"],
),
];
#[cfg(test)]
mod tests {
#![expect(
clippy::panic,
clippy::unwrap_used,
reason = "tests assert on fallible calls and panic with the failing precondition; propagating errors would replace a clear failure message with noise"
)]
use super::{CAPABILITY_TABLE, Capability, CapabilitySet, Side, defined_in};
use crate::domain::ProtocolVersion;
use std::collections::BTreeSet;
#[test]
fn every_capability_fits_in_the_bitset() {
let mut seen = BTreeSet::new();
for capability in Capability::ALL {
let bit = capability.bit();
assert!(
bit != 0 && bit.is_power_of_two(),
"{} has a bad bit {bit:#x}",
capability.as_str()
);
assert!(bit < 1u128 << 100, "{} overflows", capability.as_str());
assert!(seen.insert(bit), "{} duplicates a bit", capability.as_str());
}
}
#[test]
fn names_are_unique_and_dotted_paths_are_well_formed() {
let mut names = BTreeSet::new();
for capability in Capability::ALL {
assert!(
names.insert(capability.as_str()),
"duplicate name {}",
capability.as_str()
);
assert!(!capability.as_str().is_empty());
assert!(!capability.as_str().contains(' '));
}
for capability in Capability::ALL {
if let Some((container, _)) = capability.as_str().split_once('.') {
let Some(found) = Capability::ALL.iter().find(|c| c.as_str() == container) else {
panic!("{container} has no variant");
};
let (container_side, child_side) = (found.side(), capability.side());
let admitted = match child_side {
Side::Client => container_side.includes_client(),
Side::Server => container_side.includes_server(),
Side::Both => container_side == Side::Both,
};
assert!(
admitted,
"{} is {child_side:?} but its container {container} is {container_side:?}",
capability.as_str()
);
}
}
}
#[test]
fn the_table_covers_every_capability_exactly_once() {
for capability in Capability::ALL {
let count = CAPABILITY_TABLE
.iter()
.filter(|(candidate, _)| candidate.as_str() == capability.as_str())
.count();
assert_eq!(count, 1, "{} appears {count} times", capability.as_str());
}
assert_eq!(CAPABILITY_TABLE.len(), Capability::ALL.len());
}
#[test]
fn every_table_row_names_a_known_revision() {
for (capability, versions) in CAPABILITY_TABLE {
assert!(
!versions.is_empty(),
"{} has no versions",
capability.as_str()
);
for version in *versions {
assert!(
version.is_known(),
"{} cites {version}",
capability.as_str()
);
}
assert!(
versions.is_sorted(),
"{} versions are unsorted",
capability.as_str()
);
}
}
#[test]
fn the_table_matches_every_vendored_schema() {
let spec_root =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../protocol-droid-schema/spec");
let mut mismatches: Vec<String> = Vec::new();
let mut revisions_checked = 0usize;
for version in ProtocolVersion::KNOWN {
let path = spec_root.join(version.to_string()).join("schema.json");
let Ok(bytes) = std::fs::read(&path) else {
continue;
};
let Ok(document) = serde_json::from_slice::<serde_json::Value>(&bytes) else {
continue;
};
revisions_checked += 1;
let defs = document
.get("$defs")
.or_else(|| document.get("definitions"))
.and_then(serde_json::Value::as_object);
for (root, side) in [
("ClientCapabilities", Side::Client),
("ServerCapabilities", Side::Server),
] {
let found = leaf_names(defs, root);
for name in &found {
let modelled = Capability::ALL
.iter()
.find(|c| c.as_str() == name.as_str())
.copied();
match modelled {
None => mismatches.push(format!(
"{version} {root}.{name}: in the schema, not modelled"
)),
Some(capability) => {
if !capability.side().includes_client() && side == Side::Client
|| !capability.side().includes_server() && side == Side::Server
{
mismatches.push(format!(
"{version} {root}.{name}: declared on the wrong side"
));
}
if !defined_in(capability, version) {
mismatches.push(format!(
"{version} {root}.{name}: defined in the schema, \
absent from CAPABILITY_TABLE"
));
}
}
}
}
for (capability, versions) in CAPABILITY_TABLE {
if !versions.contains(&version) {
continue;
}
let declared_here = capability.side().includes_client() && side == Side::Client
|| capability.side().includes_server() && side == Side::Server;
if declared_here && !found.contains(capability.as_str()) {
mismatches.push(format!(
"{version} {root}.{}: in CAPABILITY_TABLE, not in the schema",
capability.as_str()
));
}
}
}
}
assert!(
revisions_checked >= 5,
"only checked {revisions_checked} schemas"
);
assert!(
mismatches.is_empty(),
"CAPABILITY_TABLE disagrees with the vendored schemas:\n {}",
mismatches.join("\n ")
);
}
fn leaf_names(
defs: Option<&serde_json::Map<String, serde_json::Value>>,
root: &str,
) -> BTreeSet<String> {
fn walk(node: &serde_json::Value, prefix: &str, out: &mut BTreeSet<String>) {
for (name, value) in node
.get("properties")
.and_then(serde_json::Value::as_object)
.into_iter()
.flatten()
{
let path = if prefix.is_empty() {
name.clone()
} else {
format!("{prefix}.{name}")
};
out.insert(path.clone());
walk(value, &path, out);
}
}
let mut out = BTreeSet::new();
let Some(node) = defs.and_then(|d| d.get(root)) else {
return out;
};
walk(node, "", &mut out);
out
}
#[test]
fn sets_behave_like_sets() {
let empty = CapabilitySet::empty();
assert!(empty.is_empty());
assert_eq!(empty.len(), 0);
assert!(!empty.contains(Capability::Tools));
let one = empty.with(Capability::Tools);
assert!(one.contains(Capability::Tools));
assert!(!one.is_empty());
assert_eq!(one.len(), 1);
assert_eq!(one.with(Capability::Tools), one);
assert_eq!(one.with(Capability::Roots).len(), 2);
}
#[test]
fn all_for_is_scoped_to_one_revision() {
let oldest = CapabilitySet::all_for(ProtocolVersion::V2024_11_05);
assert!(oldest.contains(Capability::Roots));
assert!(oldest.contains(Capability::Tools));
assert!(oldest.contains(Capability::Logging));
assert!(!oldest.contains(Capability::Elicitation));
assert!(!oldest.contains(Capability::Completions));
assert!(!oldest.contains(Capability::Tasks));
assert!(!oldest.contains(Capability::Extensions));
let newest = CapabilitySet::all_for(ProtocolVersion::V2026_07_28);
assert!(newest.contains(Capability::Elicitation));
assert!(newest.contains(Capability::Extensions));
assert!(!newest.contains(Capability::Tasks));
assert!(!newest.contains(Capability::RootsListChanged));
assert!(newest.contains(Capability::ResourcesSubscribe));
}
#[test]
fn restricting_drops_what_a_revision_cannot_express() {
let newer = CapabilitySet::all_for(ProtocolVersion::V2025_11_25);
assert!(newer.contains(Capability::Tasks));
let downgraded = newer.restricted_to(ProtocolVersion::V2024_11_05);
assert!(!downgraded.contains(Capability::Tasks));
assert!(!downgraded.contains(Capability::Elicitation));
assert!(downgraded.contains(Capability::Tools));
assert!(downgraded.contains(Capability::RootsListChanged));
}
#[test]
fn an_unknown_revision_defines_nothing() {
let future = ProtocolVersion::Unknown("2027-01-01".parse().unwrap());
for capability in Capability::ALL {
assert!(!defined_in(capability, future), "{}", capability.as_str());
}
let set = CapabilitySet::all();
assert!(set.restricted_to(future).is_empty());
}
#[test]
fn iteration_is_declaration_ordered_and_complete() {
let set = CapabilitySet::all().with(Capability::Experimental);
let collected: Vec<Capability> = set.iter().collect();
assert_eq!(collected, Capability::ALL.to_vec());
let doubled = set.with(Capability::Tools).with(Capability::Tools);
assert_eq!(doubled.len() as usize, Capability::ALL.len());
}
#[test]
fn tasks_is_defined_in_exactly_one_revision() {
for version in ProtocolVersion::KNOWN {
let expected = version == ProtocolVersion::V2025_11_25;
assert_eq!(
defined_in(Capability::Tasks, version),
expected,
"tasks in {version}"
);
for nested in [
Capability::TasksCancel,
Capability::TasksList,
Capability::TasksRequests,
Capability::TasksRequestsTools,
Capability::TasksRequestsToolsCall,
Capability::TasksRequestsSampling,
Capability::TasksRequestsSamplingCreateMessage,
Capability::TasksRequestsElicitation,
Capability::TasksRequestsElicitationCreate,
] {
assert_eq!(
defined_in(nested, version),
expected,
"{nested:?} in {version}"
);
}
}
}
#[test]
fn sides_are_as_the_schemas_declare_them() {
assert_eq!(Capability::Roots.side(), Side::Client);
assert_eq!(Capability::SamplingTools.side(), Side::Client);
assert_eq!(Capability::Tools.side(), Side::Server);
assert_eq!(Capability::TasksRequestsTools.side(), Side::Server);
assert_eq!(Capability::Experimental.side(), Side::Both);
assert_eq!(Capability::Extensions.side(), Side::Both);
assert_eq!(Capability::Tasks.side(), Side::Both);
}
}