Description
In crates/rmcp/src/service.rs, the drain phase that runs after the main message loop exits (QuitReason::Closed / QuitReason::Cancelled, ~line 1722) flushes in-flight handler responses before closing the transport. If transport.send(m).await fails there — typically because the peer/session channel is already closed, i.e. the client already disconnected — the failure is logged at tracing::error! (line 1748: "failed to send pending response during drain").
This is inconsistent with the identical failure mode a few lines earlier in the same file (lines 1694 and 1715: a responder send failing because the receiver is gone), which correctly logs at tracing::warn!.
A "channel closed" failure during drain is the expected outcome of the very state transition (Closed/Cancelled) that triggered the drain in the first place — the peer already disconnected, so of course the response can't be delivered. This path fires on completely ordinary production events: a browser tab closing, a client crash, a network drop, or a client cancelling a request. Logging it at ERROR means every downstream server built on this SDK gets false-alarm ERROR log lines for routine disconnects, which is likely to trigger alerting/paging in any ops setup that treats ERROR-level logs as actionable, training on-call to ignore this SDK's real errors.
Reproduction Steps
Reproduced twice, live, both fully consistent (not a one-off):
-
Conformance suite:
cargo build -p mcp-conformance
PORT=8001 ./target/debug/conformance-server &
npx -y "@modelcontextprotocol/conformance@0.2.0-alpha.10" server \
--url http://127.0.0.1:8001/mcp --suite all --spec-version 2026-07-28
All 115 scenarios pass (this is not a correctness/functional bug), but the server log shows 3 occurrences of the ERROR line, each immediately after a task cancelled / serve finished quit_reason=Cancelled line — triggered by the conformance client legitimately closing/cancelling connections as part of normal test scenarios.
-
Plain example pair:
cargo build --manifest-path examples/servers/Cargo.toml --all-features --example servers_subscriptions_streamhttp
cargo build --manifest-path examples/clients/Cargo.toml --all-features --example clients_subscriptions_streamhttp
./target/debug/examples/servers_subscriptions_streamhttp &
timeout 20 ./target/debug/examples/clients_subscriptions_streamhttp
Killing the client while a subscription stream is open produces the identical ERROR line on the server immediately after disconnect.
Expected Behavior
The drain-path send failure should log at tracing::warn! (or debug!), matching the identical-purpose logging at service.rs:1694 and service.rs:1715, since "receiver gone during an already-closing/cancelled session" is an expected outcome, not an application error.
Actual Behavior
Logs at tracing::error!:
ERROR serve_inner: rmcp::service: failed to send pending response during drain error=channel closed
on every ordinary client disconnect that races an in-flight response.
Environment
- Version: v3.1.2 (commit 02c62ae),
crates/rmcp/src/service.rs (~line 1748)
- Features: default
conformance-server/conformance-client binaries; examples/servers and examples/clients built with --all-features
Upstream
Checked modelcontextprotocol/rust-sdk open+closed issues and merged/closed PRs for "drain", "channel closed", "log level"/"log noise" — found related-but-distinct history: `modelcontextprotocol/rust-sdk#753` (stdio server drops in-flight tool responses when stdin closes, closed) and its fix `modelcontextprotocol/rust-sdk#759` (fix: drain in-flight responses on stdin EOF, merged) — that PR is what introduced this drain mechanism (and its error! call) in the first place. No existing upstream issue specifically about the log-severity inconsistency found.
Logs / Evidence
2026-08-08T15:01:49.222716Z ERROR serve_inner: rmcp::service: failed to send pending response during drain error=channel closed
(preceded by task cancelled / serve finished quit_reason=Cancelled in both reproductions)
Spec
.local/specs/002-drain-error-log-severity/spec.md
Description
In
crates/rmcp/src/service.rs, the drain phase that runs after the main message loop exits (QuitReason::Closed/QuitReason::Cancelled, ~line 1722) flushes in-flight handler responses before closing the transport. Iftransport.send(m).awaitfails there — typically because the peer/session channel is already closed, i.e. the client already disconnected — the failure is logged attracing::error!(line 1748:"failed to send pending response during drain").This is inconsistent with the identical failure mode a few lines earlier in the same file (lines 1694 and 1715: a responder send failing because the receiver is gone), which correctly logs at
tracing::warn!.A "channel closed" failure during drain is the expected outcome of the very state transition (
Closed/Cancelled) that triggered the drain in the first place — the peer already disconnected, so of course the response can't be delivered. This path fires on completely ordinary production events: a browser tab closing, a client crash, a network drop, or a client cancelling a request. Logging it at ERROR means every downstream server built on this SDK gets false-alarm ERROR log lines for routine disconnects, which is likely to trigger alerting/paging in any ops setup that treats ERROR-level logs as actionable, training on-call to ignore this SDK's real errors.Reproduction Steps
Reproduced twice, live, both fully consistent (not a one-off):
Conformance suite:
All 115 scenarios pass (this is not a correctness/functional bug), but the server log shows 3 occurrences of the ERROR line, each immediately after a
task cancelled/serve finished quit_reason=Cancelledline — triggered by the conformance client legitimately closing/cancelling connections as part of normal test scenarios.Plain example pair:
Killing the client while a subscription stream is open produces the identical ERROR line on the server immediately after disconnect.
Expected Behavior
The drain-path send failure should log at
tracing::warn!(ordebug!), matching the identical-purpose logging atservice.rs:1694andservice.rs:1715, since "receiver gone during an already-closing/cancelled session" is an expected outcome, not an application error.Actual Behavior
Logs at
tracing::error!:on every ordinary client disconnect that races an in-flight response.
Environment
crates/rmcp/src/service.rs(~line 1748)conformance-server/conformance-clientbinaries;examples/serversandexamples/clientsbuilt with--all-featuresUpstream
Checked
modelcontextprotocol/rust-sdkopen+closed issues and merged/closed PRs for "drain", "channel closed", "log level"/"log noise" — found related-but-distinct history:`modelcontextprotocol/rust-sdk#753`(stdio server drops in-flight tool responses when stdin closes, closed) and its fix`modelcontextprotocol/rust-sdk#759`(fix: drain in-flight responses on stdin EOF, merged) — that PR is what introduced this drain mechanism (and itserror!call) in the first place. No existing upstream issue specifically about the log-severity inconsistency found.Logs / Evidence
(preceded by
task cancelled/serve finished quit_reason=Cancelledin both reproductions)Spec
.local/specs/002-drain-error-log-severity/spec.md