fix(sse-client): consume control frames; refresh message endpoint #447
+193
−17
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Gracefully handle SSE control frames; refresh endpoint on reconnect; add hooks; reduce noisy logs
event:endpoint
)sessionId
)Resolves repeated warnings like:
and reconnection issues with servers that emit
endpoint
control frames (e.g., git-mcp). Improves compatibility with mixed/extended SSE implementations while preserving strict MCP parsing for actual messages.Motivation and Context
Certain MCP servers resend control information over SSE when a stream reconnects. Typical examples include
event: endpoint
frames whose data carries the message POST endpoint (often with a newsessionId
). Previously, the client-side SSE path attempted to parse every SSEdata
as a JSON-RPC message, which produced repeated warnings and interfered with reconnection and message submission.In particular, GitMCP (idosal/git-mcp) emits
event:endpoint
with a refreshed endpoint that can include asessionId
. This triggered repeated decode warnings and unstable reconnection in downstream projects (e.g., MCPMate) and motivated this compatibility fix.This change clearly separates JSON message frames from control frames: control frames are consumed by a reconnect hook and never fed into the JSON parser; valid JSON message frames continue to be parsed strictly per MCP.
How Has This Been Tested?
event:endpoint
updates the shared POST endpoint URI on reconnectdebug
withlast_event_id
contextBreaking Changes
None. Changes are internal. Hooks are
pub(crate)
and do not alter public APIs. Default behavior for compliant servers remains identical.Types of changes
Checklist
Additional context
Implementation Details
crates/rmcp/src/transport/common/client_side_sse.rs:214–223
— non-""|"message"
SSE events are treated as control frames and passed tohandle_control_event(&Sse)
; only""|"message"
are parsed as JSON.crates/rmcp/src/transport/common/client_side_sse.rs:228–239
— JSON decode failures downgraded todebug
and includelast_event_id
for troubleshooting.crates/rmcp/src/transport/common/client_side_sse.rs:101–124
—SseStreamReconnect
provideshandle_control_event
(no-op by default) andhandle_stream_error
.handle_stream_error
now accepts&(dyn std::error::Error + 'static)
so call sites can forward underlyingsse_stream::Error
directly, avoiding generic constraints and fixing the E0308 type mismatch observed in integration builds.crates/rmcp/src/transport/sse_client.rs:63–105
—SseClientReconnect<C>
implements the hooks: consumesevent:endpoint
, resolves the new message endpoint, updates a sharedArc<RwLock<Uri)>
, and logs stream errors withuri
andlast_event_id
context. The override ofhandle_stream_error
matches the new signature (&(dyn Error)
), see:98–104
.http(s)://
data → used as-is.?query
only → keep base path, append query.path_and_query
in base.message_endpoint(base, endpoint)
incrates/rmcp/src/transport/sse_client.rs:300–308
.sse.id
viaif let Some(ref event_id)
before cloning tolast_event_id
(crates/rmcp/src/transport/common/client_side_sse.rs:211–213
).&(dyn Error)
and forwarding the underlying SSE error at the call site (:251
).//!
module docs.Design Decisions
event == "" | "message"
go through JSON parsing; others are consumed by a hook.handle_control_event
andhandle_stream_error
enable future extensions (e.g., ping, other control signals) without API changes; default implementations remain backward compatible.