Skip to content

v3.2.0

Choose a tag to compare

@nicholasjpaterno nicholasjpaterno released this 27 Aug 16:31
· 336 commits to main since this release

Streamable HTTP correctness — a client response hang, CRLF SSE parsing,
POST-stream primer events, a reconnect storm — plus transport hot-path
performance and an interoperability fix for clients that recycle request ids.

Most of this release was contributed by @ForrestThump (#14, #15, #16, #18,
#19, #20, #21, #24) — thank you!

This is a minor, not a patch, release. Two changes below alter the public
API in ways a strict semver check calls breaking; both are called out under
Changed with the one-line fix. Nothing else in the 3.x surface moved.

Fixed

  • A client that reuses JSON-RPC request ids is no longer locked out — every
    transport kept a per-session set of every id it had ever seen and answered a
    repeat with -32600 "Request ID already used in this session". The rule is
    real but it binds the other party: the spec's "MUST NOT have been previously
    used" constrains the requestor, whose purpose is correlating a response with
    its request, while a receiver's only obligation is to echo the id back. So the
    check bought the server nothing and made interop depend on a client's
    id-allocation strategy; SEP-2567 re-scopes the constraint to ids that are
    concurrently outstanding, and neither rmcp nor TurboMCP v4 enforces it. The
    set was also unbounded, growing for the life of the session. Concurrent reuse
    is still logged as a warning — two responses carrying one id are unmatchable —
    but never refused. (#25)

  • The SSE reconnect loop no longer hammers a server that closes idle
    streams
    — backoff was keyed on whether the GET was accepted, so a server
    that accepted and then closed the stream was retried with zero delay
    indefinitely (measured: 7,657 connections in 2s). Backoff is now keyed on how
    long the stream actually stayed up. Separately, a routine reconnect was logged
    at error/warn; a server closing an idle stream is behaving correctly and
    the client reconnecting is it doing its job, so that is now debug. Measured
    on the reporter's deployment: 61 SSE error/warn lines in a 90-second window
    before, 0 after. (#24)

  • Streamable HTTP client no longer hangs waiting for POST-response SSE
    streams to close
    — send() used to block until the server ended the
    request's SSE stream, stalling clients against servers that keep the
    stream open after answering. The client now returns as soon as the
    correlated response (matching request ID with a result or error)
    arrives; remaining stream messages are still consumed and queued. (#21)

  • SSE event parsing accepts CRLF and lone-CR line endings — the client
    HTTP transport now normalizes \r\n and \r to \n per the SSE
    specification before splitting events, fixing interop with servers (and
    proxies) that emit CRLF-framed streams. (#21)

  • Spec-required primer event restored on POST-response SSE streams —
    POST-initiated streams once again open with an empty-data event carrying a
    globally unique, stream-encoding event ID ({session}-{stream}-0), giving
    clients a Last-Event-ID for resumption as the 2025-11-25 Streamable HTTP
    transport spec recommends. (#16, #17)

  • McpHandler public-method types are re-exported so downstream code can
    name them without depending on internal crates. (#15)

  • just test works on Windows. (#14)

Changed

  • StreamableHttpClientConfig gained sse_healthy_stream_threshold
    (default 10s), which decides whether a closed SSE stream counts as a failure
    worth backing off from. Keep it comfortably under your server's idle timeout.
    Breaking for exhaustive struct literals — if you construct this config by
    naming every field, add ..Default::default(). (#24)
  • turbomcp_server::transport::http::SessionManager is no longer public
    unless the non-public internal-bench feature is on. It was internal
    plumbing that had been exposed unintentionally; it is now pub(crate) except
    where the benchmarks need it. Breaking if you named this type — there is no
    supported replacement, as it was never meant to be part of the API. (#20)
  • Transport per-message overhead reduced (#20, criterion-verified):
    HTTP client transport metrics moved from RwLock to the same lock-free
    AtomicMetrics the stdio transport uses; server SSE broadcast payloads are
    shared as Arc<str> instead of cloned Strings per subscriber; SSE frames
    are encoded with a single exact-size allocation; the stdio transport slices
    trimmed lines zero-copy from the read buffer. Benchmarks ship behind the
    new non-public internal-bench feature.
  • Duplicate dependency versions consolidated across the workspace. (#19)
  • Documentation ASCII diagrams realigned. (#18)