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):
v3.0 (breaking):
v4.0:
Open questions
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.
- Should defaults shrink? Shrinking away from
server/macros is a bigger break for the majority (server authors). This proposal leaves defaults unchanged.
tls-* vs keeping reqwest-tls-*? If a second HTTP backend ever lands, tls-* generalizes better; reqwest-tls-* is more honest today. Proposal prefers tls-*.
Labels:
T-enhancement,T-config,T-documentationProblem
rmcpexposes 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: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.client-side-sse,server-side-http,transport-streamable-http-server-session,base64,uuidall appear on docs.rs but should never be enabled directly. Meanwhile__reqwestis correctly hidden — so the convention exists but is applied to exactly one flag.client+transport-streamable-http-client-reqwest+reqwest. Omit the last one and it still compiles — reqwest is pulled without any TLS backend, so everyhttps://request fails at runtime. Our ownconformance/Cargo.tomldoes exactly this; it survives only because it targetshttp://localhost.elicitationis opt-in (only to gate theurlcrate), while sampling, tasks (SEP-2663), and response caching (SEP-2549) are always on. There is no principle a user can infer.localviolates Cargo's additivity contract. It removesSendbounds crate-wide for WASM/single-threaded use. Because Cargo features are additive, any crate anywhere in the dependency graph enablinglocalsilently 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.tomlright now, per scenario:server,macros,base64) +transport-iotransport-iodoesn't say "stdio"; spec's term is stdio transportclient,transport-child-process,default-features = falseserver,macros,transport-streamable-http-servertowerintegration arrives via an internal flagclient,transport-streamable-http-client-reqwest,reqwestreqwest→ compiles, but all TLS fails at runtimereqwest-native-tls/reqwest-tls-no-providerreqwestactually means "reqwest + rustls"; segment order inconsistent (reqwest-native-tlsvsreqwest-tls-no-provider)client,transport-streamable-http-clientclient,transport-streamable-http-client-unix-socketauth(+auth-client-credentials-jwt)authpulls reqwest via__reqwest— same silent no-TLS trapelicitationurlrequest-statelocal(+transport-worker)Dead remnant:
transport-wsexists only as a comment inCargo.toml, yetsrc/transport/ws.rsand a commented#[cfg]intransport.rsremain.Proposal
A naming taxonomy — one rule per axis
client,serverrequest-state,authtransport-prefix, spec terminologytransport-stdio,transport-streamable-http-servertowertls-prefixtls-rustls,tls-native,tls-no-provider<role>-<transport>client-http,server-stdio__prefix, hidden from docs__server-side-http1. Renames (v3.0, with one-release aliases)
transport-iotransport-stdioreqwesttls-rustlsreqwest-native-tlstls-nativetls-prefix + orderreqwest-tls-no-providertls-no-providerlocalunsync(or replace withcfg(target_family = "wasm"))which-commandtransport-child-process(orcommand-path-lookup)whichcrate, not the behaviorOld 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)
elicitationurlis not a trivial dep — making it mandatory adds 29 crates to a minimal build viaidna→ ICU4X (37 → 66). Un-gating therefore also requires the elicitation API to take URLs as plain strings, sourlstays behindauth. New rule: gate a capability only when it carries a heavy dep —request-state(hmac/sha2) andauth(oauth2) stay opt-inbase64uuiddep:uuid3. Hide internal building blocks (v3.0)
client-side-sse,server-side-http,transport-streamable-http-server-sessionget the__prefix (matching__reqwest) and leave the docs.rs feature list. Users compose transports; they don't assemble the parts.transport-workerwas originally listed here too, but it stays public:crates/rmcp/README.mdand thetransportmodule docs both present implementingWorkeras 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)
Getting-started docs become one flag per scenario.
client-httpbakes 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)
request-state,auth-client-credentials-jwt,which-command,tower,local, unix-socket transport).src/transport/ws.rsand the commentedtransport-wsremnants (recoverable from git if WebSocket ever lands).Use cases after this proposal
server-stdio(or defaults +transport-stdio)client-stdio,default-features = falseserver-httpclient-httpclient,transport-streamable-http-client-reqwest,tls-nativeclient,transport-streamable-http-clientclient-http,authrequest-stateunsync(or automatic via target cfg)Rollout
Ship now (minor, non-breaking):
ws.rs/transport-wsremnants (§5)v3.0 (breaking):
elicitation/base64, hideuuid(§2)__-prefix internal flags (§3)v4.0:
Open questions
local→ renamed feature vs.cfg(target_family = "wasm")? Does anyone uselocalon 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.server/macrosis a bigger break for the majority (server authors). This proposal leaves defaults unchanged.tls-*vs keepingreqwest-tls-*? If a second HTTP backend ever lands,tls-*generalizes better;reqwest-tls-*is more honest today. Proposal preferstls-*.