use crate::domain::ir::FeatureSet;
use crate::domain::{
Capability, CapabilitySet, ProtocolVersion, Side, TransportKind, transport_allowed,
};
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
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Feature {
Tools,
ToolsListChanged,
ToolAnnotations,
ToolStructuredOutput,
ToolResourceLinks,
ToolAnnotationsLastModified,
ToolResourceLinkIcons,
ToolAudioContent,
ToolResultType,
Resources,
ResourcesListChanged,
ResourcesSubscribe,
Prompts,
PromptsListChanged,
Completions,
Roots,
RootsListChanged,
Sampling,
SamplingContext,
SamplingTools,
Elicitation,
ElicitationForm,
ElicitationUrl,
Logging,
JsonRpcBatching,
StreamableHttp,
LegacySse,
McpProtocolVersionHeader,
MetaExtensions,
StatelessLifecycle,
ServerDiscover,
SubscriptionsListen,
StandardRoutingHeaders,
ResponseCaching,
MultiRoundTripRequests,
Ping,
Tasks,
Extensions,
Experimental,
}
impl Feature {
pub const ALL: [Self; 39] = [
Self::Tools,
Self::ToolsListChanged,
Self::ToolAnnotations,
Self::ToolStructuredOutput,
Self::ToolResourceLinks,
Self::ToolAnnotationsLastModified,
Self::ToolResourceLinkIcons,
Self::ToolAudioContent,
Self::ToolResultType,
Self::Resources,
Self::ResourcesListChanged,
Self::ResourcesSubscribe,
Self::Prompts,
Self::PromptsListChanged,
Self::Completions,
Self::Roots,
Self::RootsListChanged,
Self::Sampling,
Self::SamplingContext,
Self::SamplingTools,
Self::Elicitation,
Self::ElicitationForm,
Self::ElicitationUrl,
Self::Logging,
Self::JsonRpcBatching,
Self::StreamableHttp,
Self::LegacySse,
Self::McpProtocolVersionHeader,
Self::MetaExtensions,
Self::StatelessLifecycle,
Self::ServerDiscover,
Self::SubscriptionsListen,
Self::StandardRoutingHeaders,
Self::ResponseCaching,
Self::MultiRoundTripRequests,
Self::Ping,
Self::Tasks,
Self::Extensions,
Self::Experimental,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Tools => "tools",
Self::ToolsListChanged => "tools.listChanged",
Self::ToolAnnotations => "tool.annotations",
Self::ToolStructuredOutput => "tool.structuredOutput",
Self::ToolResourceLinks => "tool.resourceLinks",
Self::ToolAnnotationsLastModified => "tool.annotations.lastModified",
Self::ToolResourceLinkIcons => "tool.resourceLinks.icons",
Self::ToolAudioContent => "tool.audioContent",
Self::ToolResultType => "tool.resultType",
Self::Resources => "resources",
Self::ResourcesListChanged => "resources.listChanged",
Self::ResourcesSubscribe => "resources.subscribe",
Self::Prompts => "prompts",
Self::PromptsListChanged => "prompts.listChanged",
Self::Completions => "completions",
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::JsonRpcBatching => "jsonRpcBatching",
Self::StreamableHttp => "streamableHttp",
Self::LegacySse => "legacySse",
Self::McpProtocolVersionHeader => "mcpProtocolVersionHeader",
Self::MetaExtensions => "metaExtensions",
Self::StatelessLifecycle => "statelessLifecycle",
Self::ServerDiscover => "server.discover",
Self::SubscriptionsListen => "subscriptions.listen",
Self::StandardRoutingHeaders => "standardRoutingHeaders",
Self::ResponseCaching => "responseCaching",
Self::MultiRoundTripRequests => "multiRoundTripRequests",
Self::Ping => "ping",
Self::Tasks => "tasks",
Self::Extensions => "extensions",
Self::Experimental => "experimental",
}
}
}
impl core::fmt::Display for Feature {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FeatureSpec {
pub feature: Feature,
pub side: Side,
pub client_cap: Option<Capability>,
pub server_cap: Option<Capability>,
pub transport: Option<TransportKind>,
pub first: ProtocolVersion,
pub last: Option<ProtocolVersion>,
}
impl FeatureSpec {
#[must_use]
pub fn defined_in(&self, version: ProtocolVersion) -> bool {
if !version.is_known() || version < self.first {
return false;
}
self.last.is_none_or(|last| version < last)
}
}
use Capability::{
Completions as CapCompletions, Elicitation as CapElicitation,
ElicitationForm as CapElicitationForm, ElicitationUrl as CapElicitationUrl,
Experimental as CapExperimental, Extensions as CapExtensions, Logging as CapLogging,
Prompts as CapPrompts, PromptsListChanged as CapPromptsListChanged, Resources as CapResources,
ResourcesListChanged as CapResourcesListChanged, ResourcesSubscribe as CapResourcesSubscribe,
Roots as CapRoots, RootsListChanged as CapRootsListChanged, Sampling as CapSampling,
SamplingContext as CapSamplingContext, SamplingTools as CapSamplingTools, Tasks as CapTasks,
Tools as CapTools, ToolsListChanged as CapToolsListChanged,
};
use ProtocolVersion::{V2024_11_05, V2025_03_26, V2025_06_18, V2025_11_25, V2026_07_28};
pub const FEATURES: &[FeatureSpec] = &[
FeatureSpec {
feature: Feature::Tools,
side: Side::Server,
client_cap: None,
server_cap: Some(CapTools),
transport: None,
first: V2024_11_05,
last: None,
},
FeatureSpec {
feature: Feature::ToolsListChanged,
side: Side::Server,
client_cap: None,
server_cap: Some(CapToolsListChanged),
transport: None,
first: V2024_11_05,
last: None,
},
FeatureSpec {
feature: Feature::ToolAnnotations,
side: Side::Server,
client_cap: None,
server_cap: Some(CapTools),
transport: None,
first: V2025_03_26,
last: None,
},
FeatureSpec {
feature: Feature::ToolAnnotationsLastModified,
side: Side::Server,
client_cap: None,
server_cap: Some(CapTools),
transport: None,
first: V2025_06_18,
last: None,
},
FeatureSpec {
feature: Feature::ToolResourceLinkIcons,
side: Side::Server,
client_cap: None,
server_cap: Some(CapTools),
transport: None,
first: V2025_11_25,
last: None,
},
FeatureSpec {
feature: Feature::ToolStructuredOutput,
side: Side::Server,
client_cap: None,
server_cap: Some(CapTools),
transport: None,
first: V2025_06_18,
last: None,
},
FeatureSpec {
feature: Feature::ToolResourceLinks,
side: Side::Server,
client_cap: None,
server_cap: Some(CapTools),
transport: None,
first: V2025_06_18,
last: None,
},
FeatureSpec {
feature: Feature::ToolAudioContent,
side: Side::Server,
client_cap: None,
server_cap: Some(CapTools),
transport: None,
first: V2025_03_26,
last: None,
},
FeatureSpec {
feature: Feature::ToolResultType,
side: Side::Server,
client_cap: None,
server_cap: Some(CapTools),
transport: None,
first: V2026_07_28,
last: None,
},
FeatureSpec {
feature: Feature::Resources,
side: Side::Server,
client_cap: None,
server_cap: Some(CapResources),
transport: None,
first: V2024_11_05,
last: None,
},
FeatureSpec {
feature: Feature::ResourcesListChanged,
side: Side::Server,
client_cap: None,
server_cap: Some(CapResourcesListChanged),
transport: None,
first: V2024_11_05,
last: None,
},
FeatureSpec {
feature: Feature::ResourcesSubscribe,
side: Side::Server,
client_cap: None,
server_cap: Some(CapResourcesSubscribe),
transport: None,
first: V2024_11_05,
last: None,
},
FeatureSpec {
feature: Feature::Prompts,
side: Side::Server,
client_cap: None,
server_cap: Some(CapPrompts),
transport: None,
first: V2024_11_05,
last: None,
},
FeatureSpec {
feature: Feature::PromptsListChanged,
side: Side::Server,
client_cap: None,
server_cap: Some(CapPromptsListChanged),
transport: None,
first: V2024_11_05,
last: None,
},
FeatureSpec {
feature: Feature::Completions,
side: Side::Server,
client_cap: None,
server_cap: Some(CapCompletions),
transport: None,
first: V2025_03_26,
last: None,
},
FeatureSpec {
feature: Feature::Roots,
side: Side::Client,
client_cap: Some(CapRoots),
server_cap: None,
transport: None,
first: V2024_11_05,
last: None,
},
FeatureSpec {
feature: Feature::RootsListChanged,
side: Side::Client,
client_cap: Some(CapRootsListChanged),
server_cap: None,
transport: None,
first: V2024_11_05,
last: Some(V2026_07_28),
},
FeatureSpec {
feature: Feature::Sampling,
side: Side::Client,
client_cap: Some(CapSampling),
server_cap: None,
transport: None,
first: V2024_11_05,
last: None,
},
FeatureSpec {
feature: Feature::SamplingContext,
side: Side::Client,
client_cap: Some(CapSamplingContext),
server_cap: None,
transport: None,
first: V2025_11_25,
last: None,
},
FeatureSpec {
feature: Feature::SamplingTools,
side: Side::Client,
client_cap: Some(CapSamplingTools),
server_cap: None,
transport: None,
first: V2025_11_25,
last: None,
},
FeatureSpec {
feature: Feature::Elicitation,
side: Side::Client,
client_cap: Some(CapElicitation),
server_cap: None,
transport: None,
first: V2025_06_18,
last: None,
},
FeatureSpec {
feature: Feature::ElicitationForm,
side: Side::Client,
client_cap: Some(CapElicitationForm),
server_cap: None,
transport: None,
first: V2025_11_25,
last: None,
},
FeatureSpec {
feature: Feature::ElicitationUrl,
side: Side::Client,
client_cap: Some(CapElicitationUrl),
server_cap: None,
transport: None,
first: V2025_11_25,
last: None,
},
FeatureSpec {
feature: Feature::Logging,
side: Side::Server,
client_cap: None,
server_cap: Some(CapLogging),
transport: None,
first: V2024_11_05,
last: None,
},
FeatureSpec {
feature: Feature::JsonRpcBatching,
side: Side::Server,
client_cap: None,
server_cap: None,
transport: None,
first: V2025_03_26,
last: Some(V2025_06_18),
},
FeatureSpec {
feature: Feature::StreamableHttp,
side: Side::Server,
client_cap: None,
server_cap: None,
transport: Some(TransportKind::StreamableHttp),
first: V2025_03_26,
last: None,
},
FeatureSpec {
feature: Feature::LegacySse,
side: Side::Server,
client_cap: None,
server_cap: None,
transport: Some(TransportKind::LegacySse),
first: V2024_11_05,
last: Some(V2025_03_26),
},
FeatureSpec {
feature: Feature::McpProtocolVersionHeader,
side: Side::Server,
client_cap: None,
server_cap: None,
transport: None,
first: V2025_06_18,
last: None,
},
FeatureSpec {
feature: Feature::MetaExtensions,
side: Side::Server,
client_cap: None,
server_cap: None,
transport: None,
first: V2025_06_18,
last: None,
},
FeatureSpec {
feature: Feature::StatelessLifecycle,
side: Side::Server,
client_cap: None,
server_cap: None,
transport: None,
first: V2026_07_28,
last: None,
},
FeatureSpec {
feature: Feature::ServerDiscover,
side: Side::Server,
client_cap: None,
server_cap: None,
transport: None,
first: V2026_07_28,
last: None,
},
FeatureSpec {
feature: Feature::SubscriptionsListen,
side: Side::Server,
client_cap: None,
server_cap: None,
transport: None,
first: V2026_07_28,
last: None,
},
FeatureSpec {
feature: Feature::StandardRoutingHeaders,
side: Side::Server,
client_cap: None,
server_cap: None,
transport: None,
first: V2026_07_28,
last: None,
},
FeatureSpec {
feature: Feature::ResponseCaching,
side: Side::Server,
client_cap: None,
server_cap: None,
transport: None,
first: V2026_07_28,
last: None,
},
FeatureSpec {
feature: Feature::MultiRoundTripRequests,
side: Side::Server,
client_cap: None,
server_cap: None,
transport: None,
first: V2026_07_28,
last: None,
},
FeatureSpec {
feature: Feature::Ping,
side: Side::Server,
client_cap: None,
server_cap: None,
transport: None,
first: V2024_11_05,
last: Some(V2026_07_28),
},
FeatureSpec {
feature: Feature::Tasks,
side: Side::Both,
client_cap: Some(CapTasks),
server_cap: Some(CapTasks),
transport: None,
first: V2025_11_25,
last: Some(V2026_07_28),
},
FeatureSpec {
feature: Feature::Extensions,
side: Side::Both,
client_cap: Some(CapExtensions),
server_cap: Some(CapExtensions),
transport: None,
first: V2026_07_28,
last: None,
},
FeatureSpec {
feature: Feature::Experimental,
side: Side::Both,
client_cap: Some(CapExperimental),
server_cap: Some(CapExperimental),
transport: None,
first: V2024_11_05,
last: None,
},
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub struct FeatureProfile {
bits: u128,
}
impl FeatureProfile {
#[must_use]
pub const fn empty() -> Self {
Self { bits: 0 }
}
#[must_use]
pub fn all() -> Self {
let mut profile = Self::empty();
for feature in Feature::ALL {
profile.insert(feature);
}
profile
}
#[must_use]
pub fn with(self, feature: Feature) -> Self {
Self {
bits: self.bits | Self::bit_of(feature),
}
}
pub fn insert(&mut self, feature: Feature) {
self.bits |= Self::bit_of(feature);
}
pub fn remove(&mut self, feature: Feature) {
self.bits &= !Self::bit_of(feature);
}
#[must_use]
pub fn allows(self, feature: Feature) -> bool {
self.bits & Self::bit_of(feature) != 0
}
#[must_use]
pub const fn len(self) -> u32 {
self.bits.count_ones()
}
#[must_use]
pub const fn is_empty(self) -> bool {
self.bits == 0
}
pub fn iter(self) -> impl Iterator<Item = Feature> {
Feature::ALL.into_iter().filter(move |f| self.allows(*f))
}
pub fn missing(self) -> impl Iterator<Item = Feature> {
Feature::ALL.into_iter().filter(move |f| !self.allows(*f))
}
#[must_use]
pub const fn difference(self, other: Self) -> Self {
Self {
bits: self.bits & !other.bits,
}
}
#[must_use]
pub const fn intersection(self, other: Self) -> Self {
Self {
bits: self.bits & other.bits,
}
}
#[must_use]
pub const fn as_set(self) -> FeatureSet {
FeatureSet::from_bits(self.bits)
}
#[must_use]
pub fn advertised_by_server(self) -> CapabilitySet {
let mut advertised = CapabilitySet::empty();
for spec in FEATURES {
if !spec.side.includes_server() || !self.allows(spec.feature) {
continue;
}
if let Some(capability) = spec.server_cap {
advertised.insert(capability);
}
}
advertised
}
fn bit_of(feature: Feature) -> u128 {
let name = feature.as_str();
let mut i = 0;
while let Some(candidate) = Feature::ALL.get(i) {
if str_eq(candidate.as_str(), name) {
return 1u128 << i;
}
i += 1;
}
0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Negotiation {
pub version: ProtocolVersion,
pub client_caps: CapabilitySet,
pub server_caps: CapabilitySet,
pub transport: TransportKind,
}
impl Negotiation {
#[must_use]
pub const fn bare(version: ProtocolVersion, transport: TransportKind) -> Self {
Self {
version,
client_caps: CapabilitySet::empty(),
server_caps: CapabilitySet::empty(),
transport,
}
}
}
#[must_use]
pub fn resolve(negotiation: Negotiation) -> FeatureProfile {
let Negotiation {
version,
client_caps,
server_caps,
transport,
} = negotiation;
if !version.is_known() {
return FeatureProfile::empty();
}
let mut profile = FeatureProfile::empty();
for spec in FEATURES {
if !spec.defined_in(version) {
continue;
}
if let Some(required) = spec.transport
&& required != transport
{
continue;
}
if spec.transport.is_some() && !transport_allowed(version, transport) {
continue;
}
if !supported_by(spec.side, spec.client_cap, client_caps, version) {
continue;
}
if !supported_by(spec.side, spec.server_cap, server_caps, version) {
continue;
}
profile.insert(spec.feature);
}
profile
}
fn supported_by(
side: Side,
required: Option<Capability>,
caps: CapabilitySet,
version: ProtocolVersion,
) -> bool {
let Some(required) = required else {
return true;
};
let owner_declares = match side {
Side::Client => required.side().includes_client(),
Side::Server => required.side().includes_server(),
Side::Both => true,
};
owner_declares && required.defined_in(version) && caps.contains(required)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub struct DegradePolicy {
bits: u128,
}
impl DegradePolicy {
#[must_use]
pub const fn strict() -> Self {
Self { bits: 0 }
}
#[must_use]
pub fn permissive() -> Self {
Self::all()
}
#[must_use]
pub fn all() -> Self {
let mut policy = Self::strict();
for feature in Feature::ALL {
policy.insert(feature);
}
policy
}
#[must_use]
pub fn with(self, feature: Feature) -> Self {
Self {
bits: self.bits | FeatureProfile::bit_of(feature),
}
}
pub fn insert(&mut self, feature: Feature) {
self.bits |= FeatureProfile::bit_of(feature);
}
#[must_use]
pub fn allows(self, feature: Feature) -> bool {
self.bits & FeatureProfile::bit_of(feature) != 0
}
#[must_use]
pub fn accepts(self, profile: FeatureProfile) -> bool {
profile.missing().all(|feature| self.allows(feature))
}
}
#[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::{DegradePolicy, FEATURES, Feature, FeatureProfile, Negotiation, resolve};
use crate::domain::{Capability, CapabilitySet, ProtocolVersion, TransportKind};
use std::collections::BTreeSet;
fn everything_on(version: ProtocolVersion, transport: TransportKind) -> FeatureProfile {
let caps = CapabilitySet::all_for(version);
resolve(Negotiation {
version,
client_caps: caps,
server_caps: caps,
transport,
})
}
fn nothing_on(version: ProtocolVersion, transport: TransportKind) -> FeatureProfile {
resolve(Negotiation::bare(version, transport))
}
#[test]
fn the_table_covers_every_feature_exactly_once() {
for feature in Feature::ALL {
assert_eq!(
FEATURES.iter().filter(|s| s.feature == feature).count(),
1,
"{feature} has a duplicate row"
);
}
assert_eq!(FEATURES.len(), Feature::ALL.len());
}
#[test]
fn every_row_is_cited_and_ordered() {
for spec in FEATURES {
assert!(
spec.first.is_known(),
"{} cites {}",
spec.feature,
spec.first
);
if let Some(last) = spec.last {
assert!(last.is_known(), "{} cites {}", spec.feature, last);
assert!(
spec.first < last,
"{} is removed by {last}, which does not follow {}",
spec.feature,
spec.first
);
}
}
}
#[test]
fn no_row_requires_a_capability_its_revisions_lack() {
for spec in FEATURES {
for capability in [spec.client_cap, spec.server_cap].into_iter().flatten() {
for version in ProtocolVersion::KNOWN {
if spec.defined_in(version) {
assert!(
capability.defined_in(version),
"{capability} is required by {} but is not defined in {version}",
spec.feature
);
}
}
}
}
}
#[test]
fn every_feature_fits_in_the_bitset_with_a_distinct_bit() {
let mut seen = BTreeSet::new();
let profile = FeatureProfile::all();
assert_eq!(profile.len() as usize, Feature::ALL.len());
for feature in Feature::ALL {
assert!(profile.allows(feature), "{feature} has no bit");
let alone = FeatureProfile::empty().with(feature);
assert_eq!(alone.len(), 1, "{feature} is not alone in its bit");
assert!(seen.insert(alone.iter().next()), "{feature} shares a bit");
}
}
#[test]
fn the_golden_profile_grows_across_revisions() {
let counts: Vec<(ProtocolVersion, u32)> = ProtocolVersion::KNOWN
.into_iter()
.map(|version| (version, everything_on(version, TransportKind::Stdio).len()))
.collect();
let (Some(first), Some(last)) = (counts.first(), counts.last()) else {
panic!("no revisions were counted");
};
assert!(first.1 < last.1, "{counts:?}");
for pair in counts.windows(2) {
let (Some(earlier), Some(later)) = (pair.first(), pair.last()) else {
continue;
};
assert!(
later.1 >= earlier.1 - 2,
"{earlier:?} to {later:?} lost more than the removals explain"
);
}
}
#[test]
fn a_peer_that_advertises_nothing_gets_no_gated_features() {
for version in ProtocolVersion::KNOWN {
for transport in TransportKind::ALL {
let profile = nothing_on(version, transport);
for spec in FEATURES {
let gated = spec.client_cap.is_some() || spec.server_cap.is_some();
if gated {
assert!(
!profile.allows(spec.feature),
"{version} over {transport}: {} is gated but is on",
spec.feature
);
}
}
}
}
let old = nothing_on(ProtocolVersion::V2024_11_05, TransportKind::Stdio);
assert!(old.allows(Feature::Ping));
assert!(
!old.allows(Feature::MetaExtensions),
"`_meta` starts in 2025-06-18"
);
let mid = nothing_on(ProtocolVersion::V2025_06_18, TransportKind::Stdio);
assert!(mid.allows(Feature::MetaExtensions));
assert!(mid.allows(Feature::McpProtocolVersionHeader));
}
#[test]
fn an_unknown_revision_yields_nothing() {
let future = ProtocolVersion::Unknown("2027-01-01".parse().unwrap());
assert!(everything_on(future, TransportKind::Stdio).is_empty());
}
#[test]
fn batching_exists_for_exactly_one_revision() {
for version in ProtocolVersion::KNOWN {
let on = everything_on(version, TransportKind::Stdio).allows(Feature::JsonRpcBatching);
assert_eq!(
on,
version == ProtocolVersion::V2025_03_26,
"batching in {version}"
);
}
}
#[test]
fn the_2026_removals_are_actually_removed() {
for version in ProtocolVersion::KNOWN {
let profile = everything_on(version, TransportKind::Stdio);
let newest = version == ProtocolVersion::V2026_07_28;
assert_eq!(profile.allows(Feature::Ping), !newest, "ping in {version}");
assert_eq!(
profile.allows(Feature::RootsListChanged),
!newest,
"roots.listChanged in {version}"
);
let expected = version == ProtocolVersion::V2025_11_25;
assert_eq!(
profile.allows(Feature::Tasks),
expected,
"tasks in {version}"
);
}
}
#[test]
fn a_capability_nobody_advertised_keeps_its_feature_off() {
let version = ProtocolVersion::V2026_07_28;
let mut server = CapabilitySet::empty();
server.insert(Capability::Tools);
let profile = resolve(Negotiation {
version,
client_caps: CapabilitySet::all_for(version),
server_caps: server,
transport: TransportKind::Stdio,
});
assert!(profile.allows(Feature::Tools));
assert!(!profile.allows(Feature::Completions));
assert!(!profile.allows(Feature::ToolsListChanged));
assert!(!profile.allows(Feature::Resources));
assert!(!profile.allows(Feature::Prompts));
}
#[test]
fn a_dependent_capability_does_not_imply_its_parent() {
let version = ProtocolVersion::V2025_11_25;
let client = CapabilitySet::empty().with(Capability::ElicitationUrl);
let caps = client;
let profile = resolve(Negotiation {
version,
client_caps: caps,
server_caps: CapabilitySet::all_for(version),
transport: TransportKind::Stdio,
});
assert!(profile.allows(Feature::ElicitationUrl));
assert!(!profile.allows(Feature::Elicitation));
assert!(!profile.allows(Feature::ElicitationForm));
}
#[test]
fn transport_gated_features_follow_the_transport() {
let old = everything_on(ProtocolVersion::V2024_11_05, TransportKind::Stdio);
assert!(!old.allows(Feature::StreamableHttp));
let new = everything_on(ProtocolVersion::V2026_07_28, TransportKind::StreamableHttp);
assert!(new.allows(Feature::StreamableHttp));
let on_stdio = everything_on(ProtocolVersion::V2026_07_28, TransportKind::Stdio);
assert!(!on_stdio.allows(Feature::StreamableHttp));
}
#[test]
fn legacy_sse_survives_only_on_2024_11_05() {
for version in ProtocolVersion::KNOWN {
let profile = everything_on(version, TransportKind::LegacySse);
let expected = version == ProtocolVersion::V2024_11_05;
assert_eq!(
profile.allows(Feature::LegacySse),
expected,
"legacy sse in {version}"
);
}
}
#[test]
fn profile_set_operations_behave() {
let full = FeatureProfile::all();
let empty = FeatureProfile::empty();
assert!(empty.difference(full).is_empty());
assert_eq!(full.difference(empty), full);
assert_eq!(full.intersection(full), full);
assert!(full.intersection(empty).is_empty());
let one = FeatureProfile::empty().with(Feature::Tools);
assert_eq!(one.difference(full).len(), 0);
assert_eq!(full.difference(one).len() as usize, Feature::ALL.len() - 1);
}
#[test]
fn insert_and_remove_work_in_place() {
let mut profile = FeatureProfile::empty();
assert!(!profile.allows(Feature::Tools));
profile.insert(Feature::Tools);
assert!(profile.allows(Feature::Tools));
profile.remove(Feature::Tools);
assert!(!profile.allows(Feature::Tools));
assert!(profile.is_empty());
}
#[test]
fn a_strict_policy_accepts_only_a_complete_profile() {
let strict = DegradePolicy::strict();
let full = everything_on(ProtocolVersion::V2026_07_28, TransportKind::Stdio);
assert!(!strict.accepts(full));
assert!(strict.accepts(FeatureProfile::all()));
let mut lenient = DegradePolicy::strict();
for feature in full.missing() {
lenient.insert(feature);
}
assert!(lenient.accepts(full));
}
#[test]
fn a_permissive_policy_accepts_anything() {
let permissive = DegradePolicy::permissive();
assert!(permissive.accepts(FeatureProfile::empty()));
assert!(permissive.accepts(FeatureProfile::all()));
assert!(permissive.allows(Feature::Ping));
}
#[test]
fn a_policy_tolerating_one_feature_still_rejects_another() {
let policy = DegradePolicy::strict().with(Feature::Ping);
assert!(policy.allows(Feature::Ping));
assert!(!policy.allows(Feature::Tools));
let lossy = FeatureProfile::empty()
.with(Feature::Tools)
.with(Feature::Resources);
assert!(!policy.accepts(lossy));
}
}