FeatureProfile and gating
The FeatureProfile is a bitset of capabilities a peer actually
negotiated, derived from four inputs:
- the protocol version,
- the client’s capabilities,
- the server’s capabilities,
- the transport.
A pure domain function in protocol_droid::domain::feature maps
those four to the FeatureProfile. Every feature-specific code path
consults the profile before it runs. The profile is the source of
truth; the capabilities the peer advertised are inputs to it.
Why this is not a runtime check
A peer that does not advertise tools.list_changed cannot receive
a notifications/tools/list_changed. A peer that does not advertise
resources.subscribe cannot receive a resources/updated. The
peer’s capabilities are the contract; the wire code that would
emit a notification a peer cannot receive is a wire code that
should not run.
The library enforces this by consulting the FeatureProfile at the
boundary where a feature-specific path would otherwise fire. The
two outcomes are:
- Refuse with a typed error. The peer did not advertise
the feature; the request is rejected with a
ProtocolErrorthat names the missing capability. This is the default. - Degrade under an explicit
DegradePolicy. The caller has opted in to a documented, lossy-or-flagged downgrade. ADegradePolicy::Skipon alist_changednotification drops the notification; aDegradePolicy::Annotateon a tool result adds an_metaflag the caller can inspect.
The library does not silently drop a notification or swallow an
error. A DegradePolicy is the explicit opt-in to do either, and
the policy is part of the call site, not part of the codec.
How to read the matrix
The matrix is in crates/protocol-droid/src/domain/feature.rs. The
shape is:
- Rows are features (
Feature::Tools,Feature::StreamableHttp, …). - Columns are protocol versions.
- Cells are gated or not, depending on the version.
The matrix is also rendered as a snapshot in
crates/protocol-droid/tests/feature_matrix_golden.rs. The
snapshot is the human-readable form, and any change to the matrix
moves the snapshot, which is what surfaces a regression in code
review rather than at runtime.
An example
use protocol_droid::domain::{CapabilitySet, Feature, FeatureProfile, ProtocolVersion};
let profile = CapabilitySet::all_for(ProtocolVersion::V2025_06_18) .with_client(&client_capabilities) .with_server(&server_capabilities) .resolve(ProtocolVersion::V2025_06_18, TransportKind::StreamableHttp);
if profile.is_enabled(Feature::ToolStructuredOutput) { // The peer can carry `structuredContent`; lift the result // into the typed form.} else { // The peer cannot; the result is `JsonObject`-shaped, and // any attempt to read it as typed will fail with a typed // `LowerError::UnknownField` rather than a serde error.}is_enabled is const fn-shaped in the hot path. The compiler
will fold the check at the call site, and the if branch is a
single branch on a bitset bit.