rmcp validates a per-request protocol revision inside the blanket
impl<H: ServerHandler> Service<RoleServer> for H (handler/server.rs:50,
check at :64-72 in 3.1.4; unsupported_protocol_version → -32022,
model.rs:601) before dispatch reaches call_tool — so a request whose
_meta declares a revision this build does not serve is refused by the SDK
and bugwarden writes no audit record for it at all.
The example this issue originally named is overtaken: 2026-07-28 is in
SUPPORTED_PROTOCOL_VERSIONS (server.rs:49-55) and served since #34
stage 2. The live class is any _meta revision outside the served five —
rmcp's ProtocolVersion deserializer accepts arbitrary strings
(model.rs:204-221), so "9999-01-01" reproduces it on both transports. A
sibling shape is equally pre-dispatch and equally unrecorded: _meta
declaring the served 2026-07-28 but missing clientCapabilities → handler
-32602 (missing_required_keys, handler/server.rs:90-92).
Subclass structure: 3.1.4's KNOWN_VERSIONS (model.rs:181-187) equals the
served five exactly, so today the class holds only fabricated strings. The
"SDK-known but bugwarden-unserved" subclass is empty now and is reborn at the
first rmcp bump that widens KNOWN_VERSIONS before
SUPPORTED_PROTOCOL_VERSIONS follows — the moment real clients start being
refused unrecorded, and the operationally interesting one.
bugwarden's own recorded refusal covers only what reaches the handler:
lifecycle_of (server.rs:523) routes any out-of-contract _meta
declaration to Lifecycle::OutOfContract, which call_tool records before
refusing (server.rs:4123, mixed_lifecycle_refused at :547).
Not silent — logged, not streamed
The original claim that an operator cannot distinguish "nobody tried" from
"tried an unserved revision" does not hold. Both transports funnel through
rmcp's serve loop (stdio directly; HTTP via serve_directly_with_ct and
serve_server, tower.rs:1218/:1309), and a handler Err emits
tracing::warn!(%id, ?error, "response error") (service.rs:1585) whose
Debug-printed ErrorData carries data: {requested, supported: [...]}. The
distinction exists — in the log, not the stream. Source-read, not observed:
bugwarden's default filter is info (main.rs:49), which warn passes, but
whether the line survives the shipped subscriber in operation is unconfirmed.
That removes the strongest argument for recording. What remains is the crux:
There is no hook
The check lives in the blanket impl, so bugwarden cannot interpose on it. A
hand-written Service<RoleServer> wrapper works for stdio, but
StreamableHttpService::new requires S: ServerHandler
(tower.rs:1083-1088), which re-acquires the blanket impl — HTTP has no
interposition point, and a stdio-only wrapper would make audit coverage
differ by transport: worse unevenness than today's. Recording this class
is not implementable from bugwarden alone; it needs upstream rmcp
pre-dispatch refusal observability first.
Considered and rejected:
Service wrapper — stdio-only, above.
- Widening
supported_protocol_versions() so these reach call_tool — the
same list drives initialize negotiation, the stateless path and
server/discover (server.rs:4113), so widening makes the server agree
to revisions it cannot serve.
- Sniffing response bodies in the axum layer — HTTP-only (stdio has no axum
layer), the same by-transport unevenness; and it reconstructs a policy
event from wire bytes instead of from the decision that produced it.
Why the boundary is safe to accept
- Degradation is graceful: if a future rmcp stops refusing first, these fail
lifecycle_of's contains guard (server.rs:528) into
Lifecycle::OutOfContract and become recorded refusals. The class
cannot silently become served.
- I2 does not bear: audit records are operator-side, and the served-revision
set is public twice over — rmcp's -32022 quotes it back, and
server/discover advertises it.
Recommended settlement (not yet actioned)
Do not record. Document the stream's lower boundary in DESIGN.md beside the
existing "Refusals are logged, not audited" bullet (DESIGN.md:934, which
settled #32's bearer class on the same line), and pin it with a test
asserting the -32022 request produces no record — an rmcp bump that moves
the check then shows up in CI. No such test exists today.
Open question
File the upstream rmcp ask for pre-dispatch refusal observability, or only
note it in the rmcp-upstream-watch memory? It is the sole path to ever
recording this class.
Found while planning #180; out of its scope. Planned 2026-09-01; set aside
before implementation.
rmcp validates a per-request protocol revision inside the blanket
impl<H: ServerHandler> Service<RoleServer> for H(handler/server.rs:50,check at
:64-72in 3.1.4;unsupported_protocol_version→-32022,model.rs:601) before dispatch reachescall_tool— so a request whose_metadeclares a revision this build does not serve is refused by the SDKand bugwarden writes no audit record for it at all.
The example this issue originally named is overtaken:
2026-07-28is inSUPPORTED_PROTOCOL_VERSIONS(server.rs:49-55) and served since #34stage 2. The live class is any
_metarevision outside the served five —rmcp's
ProtocolVersiondeserializer accepts arbitrary strings(
model.rs:204-221), so"9999-01-01"reproduces it on both transports. Asibling shape is equally pre-dispatch and equally unrecorded:
_metadeclaring the served
2026-07-28but missingclientCapabilities→ handler-32602(missing_required_keys,handler/server.rs:90-92).Subclass structure: 3.1.4's
KNOWN_VERSIONS(model.rs:181-187) equals theserved five exactly, so today the class holds only fabricated strings. The
"SDK-known but bugwarden-unserved" subclass is empty now and is reborn at the
first rmcp bump that widens
KNOWN_VERSIONSbeforeSUPPORTED_PROTOCOL_VERSIONSfollows — the moment real clients start beingrefused unrecorded, and the operationally interesting one.
bugwarden's own recorded refusal covers only what reaches the handler:
lifecycle_of(server.rs:523) routes any out-of-contract_metadeclaration to
Lifecycle::OutOfContract, whichcall_toolrecords beforerefusing (
server.rs:4123,mixed_lifecycle_refusedat:547).Not silent — logged, not streamed
The original claim that an operator cannot distinguish "nobody tried" from
"tried an unserved revision" does not hold. Both transports funnel through
rmcp's serve loop (stdio directly; HTTP via
serve_directly_with_ctandserve_server,tower.rs:1218/:1309), and a handlerErremitstracing::warn!(%id, ?error, "response error")(service.rs:1585) whoseDebug-printed
ErrorDatacarriesdata: {requested, supported: [...]}. Thedistinction exists — in the log, not the stream. Source-read, not observed:
bugwarden's default filter is
info(main.rs:49), whichwarnpasses, butwhether the line survives the shipped subscriber in operation is unconfirmed.
That removes the strongest argument for recording. What remains is the crux:
There is no hook
The check lives in the blanket impl, so bugwarden cannot interpose on it. A
hand-written
Service<RoleServer>wrapper works for stdio, butStreamableHttpService::newrequiresS: ServerHandler(
tower.rs:1083-1088), which re-acquires the blanket impl — HTTP has nointerposition point, and a stdio-only wrapper would make audit coverage
differ by transport: worse unevenness than today's. Recording this class
is not implementable from bugwarden alone; it needs upstream rmcp
pre-dispatch refusal observability first.
Considered and rejected:
Servicewrapper — stdio-only, above.supported_protocol_versions()so these reachcall_tool— thesame list drives
initializenegotiation, the stateless path andserver/discover(server.rs:4113), so widening makes the server agreeto revisions it cannot serve.
layer), the same by-transport unevenness; and it reconstructs a policy
event from wire bytes instead of from the decision that produced it.
Why the boundary is safe to accept
lifecycle_of'scontainsguard (server.rs:528) intoLifecycle::OutOfContractand become recorded refusals. The classcannot silently become served.
set is public twice over — rmcp's
-32022quotes it back, andserver/discoveradvertises it.Recommended settlement (not yet actioned)
Do not record. Document the stream's lower boundary in DESIGN.md beside the
existing "Refusals are logged, not audited" bullet (
DESIGN.md:934, whichsettled #32's bearer class on the same line), and pin it with a test
asserting the
-32022request produces no record — an rmcp bump that movesthe check then shows up in CI. No such test exists today.
Open question
File the upstream rmcp ask for pre-dispatch refusal observability, or only
note it in the rmcp-upstream-watch memory? It is the sole path to ever
recording this class.
Found while planning #180; out of its scope. Planned 2026-09-01; set aside
before implementation.