Skip to content

Rationalize feature flags for v3.0 #1067

Description

@DaleSeo

Labels: T-enhancement, T-config, T-documentation

Problem

rmcp exposes 26 feature flags (including implicit ones). They have grown organically — roughly one flag per new optional dependency — rather than from a user-facing design. Concretely:

  • Names mix four unrelated concerns. Role (client, server), protocol capability (elicitation, request-state, auth), transport (transport-io, transport-streamable-http-*), and raw dependency crate names (base64, uuid, schemars, tower, reqwest) all sit in one flat list with no way to tell them apart.
  • Internal building blocks look like public flags. client-side-sse, server-side-http, transport-streamable-http-server-session, base64, uuid all appear on docs.rs but should never be enabled directly. Meanwhile __reqwest is correctly hidden — so the convention exists but is applied to exactly one flag.
  • Common use cases need 3+ flags, and one of them is a silent trap. A Streamable HTTP client needs client + transport-streamable-http-client-reqwest + reqwest. Omit the last one and it still compiles — reqwest is pulled without any TLS backend, so every https:// request fails at runtime. Our own conformance/Cargo.toml does exactly this; it survives only because it targets http://localhost.
  • Protocol capabilities are gated inconsistently. elicitation is opt-in (only to gate the url crate), while sampling, tasks (SEP-2663), and response caching (SEP-2549) are always on. There is no principle a user can infer.
  • local violates Cargo's additivity contract. It removes Send bounds crate-wide for WASM/single-threaded use. Because Cargo features are additive, any crate anywhere in the dependency graph enabling local silently changes trait bounds for everyone. The name gives no hint of this, and it is undocumented.

Use cases today

What a user must put in Cargo.toml right now, per scenario:

Use case Required features today Pain
Stdio server (most common server) default (server, macros, base64) + transport-io transport-io doesn't say "stdio"; spec's term is stdio transport
Stdio client (spawns a server) client, transport-child-process, default-features = false default drags in server + schemars + pastey the client never uses
Streamable HTTP server server, macros, transport-streamable-http-server OK; tower integration arrives via an internal flag
Streamable HTTP client client, transport-streamable-http-client-reqwest, reqwest Trap: omit reqwest → compiles, but all TLS fails at runtime
HTTP client, non-default TLS above but reqwest-native-tls / reqwest-tls-no-provider reqwest actually means "reqwest + rustls"; segment order inconsistent (reqwest-native-tls vs reqwest-tls-no-provider)
HTTP client, custom HTTP stack client, transport-streamable-http-client fine
HTTP client over Unix socket client, transport-streamable-http-client-unix-socket six-segment name; undocumented in README
OAuth client + auth (+ auth-client-credentials-jwt) auth pulls reqwest via __reqwest — same silent no-TLS trap
Server using elicitation + elicitation the only protocol capability behind a flag; exists only to gate url
Sealed request state (SEP-2322) + request-state fine (real crypto deps), but undocumented in README
WASM / single-threaded local (+ transport-worker) non-additive; opaque name; undocumented

Dead remnant: transport-ws exists only as a comment in Cargo.toml, yet src/transport/ws.rs and a commented #[cfg] in transport.rs remain.

Proposal

A naming taxonomy — one rule per axis

Axis Convention Examples
Role bare word client, server
Protocol capability spec terminology, kebab-case request-state, auth
Transport transport- prefix, spec terminology transport-stdio, transport-streamable-http-server
Ecosystem integration crate name (established Rust convention) tower
TLS backend tls- prefix tls-rustls, tls-native, tls-no-provider
Use-case bundle <role>-<transport> client-http, server-stdio
Internal building block __ prefix, hidden from docs __server-side-http
1. Renames (v3.0, with one-release aliases)
Today Proposed Rationale
transport-io transport-stdio match spec vocabulary; users search "stdio"
reqwest tls-rustls the flag's real meaning is the TLS choice, not the HTTP crate
reqwest-native-tls tls-native consistent tls- prefix + order
reqwest-tls-no-provider tls-no-provider same
local unsync (or replace with cfg(target_family = "wasm")) name should say what it does; ideally not a feature at all, since it is non-additive
which-command fold into transport-child-process (or command-path-lookup) named after the which crate, not the behavior

Old names stay as empty forwarding aliases (transport-io = ["transport-stdio"]) for one major cycle, then are removed.

2. Un-gate capabilities that only exist to hide a trivial dep (v3.0)
Today Proposed Rationale
elicitation always on gates a core capability while nothing else (sampling/tasks/caching) is gated. Correction: url is not a trivial dep — making it mandatory adds 29 crates to a minimal build via idna → ICU4X (37 → 66). Un-gating therefore also requires the elicitation API to take URLs as plain strings, so url stays behind auth. New rule: gate a capability only when it carries a heavy dep — request-state (hmac/sha2) and auth (oauth2) stay opt-in
base64 always on (already in defaults) implicit dep-named flag gating three image-content helpers
uuid hidden via dep:uuid implicit dep-named flag; pure implementation detail
3. Hide internal building blocks (v3.0)

client-side-sse, server-side-http, transport-streamable-http-server-session get the __ prefix (matching __reqwest) and leave the docs.rs feature list. Users compose transports; they don't assemble the parts.

transport-worker was originally listed here too, but it stays public: crates/rmcp/README.md and the transport module docs both present implementing Worker as one of the ways to build a transport, so hiding the flag would make a documented extension point unreachable.

4. Use-case meta-features (non-breaking, can ship now)
server-stdio = ["server", "macros", "transport-stdio"]
server-http  = ["server", "macros", "transport-streamable-http-server"]
client-stdio = ["client", "transport-child-process"]
client-http  = ["client", "transport-streamable-http-client-reqwest", "tls-rustls"]

Getting-started docs become one flag per scenario. client-http bakes in rustls; the raw no-TLS combination stays legal (plain-http:// deployments need it) but becomes an opt-in, not a pitfall.

5. Housekeeping (non-breaking, can ship now)
  • README feature table documents ~13 of 26 flags; document all public ones (missing: request-state, auth-client-credentials-jwt, which-command, tower, local, unix-socket transport).
  • Delete src/transport/ws.rs and the commented transport-ws remnants (recoverable from git if WebSocket ever lands).
  • Document the TLS pitfall until the meta-features land.

Use cases after this proposal

Use case Features after
Stdio server server-stdio (or defaults + transport-stdio)
Stdio client client-stdio, default-features = false
Streamable HTTP server server-http
Streamable HTTP client client-http
HTTP client, native TLS client, transport-streamable-http-client-reqwest, tls-native
HTTP client, custom stack client, transport-streamable-http-client
OAuth client client-http, auth
Server using elicitation nothing extra — always available
Sealed request state + request-state
WASM / single-threaded unsync (or automatic via target cfg)

Rollout

Ship now (minor, non-breaking):

  • Use-case meta-features (§4)
  • README coverage + TLS-pitfall docs (§5)
  • Delete ws.rs / transport-ws remnants (§5)

v3.0 (breaking):

  • Renames with forwarding aliases (§1)
  • Un-gate elicitation / base64, hide uuid (§2)
  • __-prefix internal flags (§3)

v4.0:

  • Drop the forwarding aliases

Open questions

  1. local → renamed feature vs. cfg(target_family = "wasm")? Does anyone use local on native targets (e.g. LocalSet-based single-threaded servers)? If yes, a renamed feature stays; if wasm-only, target cfg is strictly better — it removes the additivity violation entirely.
  2. Should defaults shrink? Shrinking away from server/macros is a bigger break for the majority (server authors). This proposal leaves defaults unchanged.
  3. tls-* vs keeping reqwest-tls-*? If a second HTTP backend ever lands, tls-* generalizes better; reqwest-tls-* is more honest today. Proposal prefers tls-*.

Metadata

Metadata

Assignees

Labels

T-configConfiguration file changesT-documentationDocumentation improvementsT-enhancementNew features and enhancements

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions