Commit graph rust-sdk/crates/rmcp/src/transport/streamable_http_server
Author SHA1 Message Date
Dale Seo
ef74147113
fix(http): fall back to :authority for HTTP/2 (#827) 2026-05-01 09:17:06 -04:00
Dale Seo
4cf78736e7
feat(http): log Host/Origin rejections (#826) 2026-05-01 14:42:47 +02:00
Dale Seo
9753d61510
feat(http): add Origin header validation (#823) 2026-04-23 15:41:06 -04:00
Guy Lichtman
8f696e6788
feat: optional session store (resumabillity support) (#775)
* feat: optional session store

* fix: docs

* fix: pr review comments

* fix: add non_exhaustive

* fix: support for non_exhaustive StreamableHttpServerConfig

* fix: add SessionState::new
2026-04-21 17:06:18 -04:00
Dale Seo
3e56d52764
fix: include http_request_id in request-wise priming event IDs (#799)
* fix: include http_request_id in request-wise priming event IDs

* refactor: use Option::into_iter and usize::from for priming

* fix: retain event cache for completed request-wise channels

* fix: track completed_at for cache eviction and resume

* fix: log resume failures at warn level

* test: add completed_cache_ttl eviction test

* fix: return empty stream on failed resume

* test: add resume after completion test
2026-04-16 12:02:35 -04:00
Dale Seo
c99903a67a
fix(http): drain SSE stream for connection reuse (#790)
* fix(http): reduce latency on subsequent StreamableHttp calls

* refactor: rely on stream drain for connection reuse

* refactor: clean up comments and naming

* fix: restore pool_max_idle_per_host(0) for Linux
2026-04-13 16:33:30 -04:00
Eren Atas
45a4cc5316
feat: add Default and constructors to ServerSseMessage (#794)
* feat: add Default and constructors to ServerSseMessage

* fix: add tests, missing feature gates, small test issues
2026-04-08 16:39:16 -04:00
Dale Seo
929441e443
fix: default session keep_alive to 5 minutes (#780) 2026-04-08 10:36:19 -04:00
jokemanfire
8e22aa2de2
fix(http): add host check (#764)
Signed-off-by: jokemanfire <hu.dingyang@zte.com.cn>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-01 19:28:56 -04:00
Dale Seo
6a3b32d3ab
chore: add #[non_exhaustive] to remaining public structs (#768)
* chore: add #[non_exhaustive] to remaining public structs

* chore: add #[non_exhaustive] to remaining public types

* chore: enable exhaustive_structs/enums clippy lints

* test: add untagged ServerResult deserialization regression tests
2026-03-26 10:23:58 -04:00
Dale Seo
e709d0d084
fix: remove default type param from StreamableHttpService (#758) 2026-03-18 11:37:51 -04:00
Dale Seo
1a4a52a173
feat: add local feature for !Send tool handler support (#740)
* feat: add local feature for !Send tool handler support

* fix: gate streamable HTTP transport on not(local) feature
2026-03-11 17:22:56 -04:00
Jack Amadeo
f63718d202
chore: add #[non_exhaustive] and mutation methods to improve compatibility (#715)
* chore: add #[non_exhaustive] to reduce backwards-incompatible changes going forward

* fix: remove ProtocolVersion import

* fix: add a few more with_ mutator methods

---------

Co-authored-by: Alex Hancock <alexhancock@block.xyz>
2026-03-03 10:38:01 -05:00
Kristof Mattei
876da50271
fix: downgrade logging of message to TRACE to avoid spamming logs (#699) 2026-02-27 18:11:22 -05:00
Thiago Mendes
d6703dad75
feat(streamable-http): add json_response option for stateless server mode (#683)
* feat(streamable-http): add json_response option for stateless server mode

Adds `json_response: bool` field to `StreamableHttpServerConfig`.
When true and `stateful_mode` is false, the server returns
`Content-Type: application/json` directly instead of `text/event-stream`,
eliminating SSE framing overhead for simple request-response patterns.

This completes server-side JSON response support (client-side was added
in #540) and contributes to the stateless server goals of SEP-1442 (#526).

Backwards-compatible: `json_response: false` (default) preserves all
existing SSE behaviour unchanged, and `stateful_mode: true` is unaffected.

Benchmark evidence (50 VUs, 5min, 2 CPUs):
- RPS: 770 → 1139 (+48%)
- get_user_cart latency: 41ms → 0.76ms (-98%)
- checkout latency: 41ms → 0.55ms (-99%)
- Zero regressions, zero errors

* fix(tower): add cancellation awareness and logging to JSON response path

* fix(test): add missing Default to StreamableHttpServerConfig in concurrent streams test

Made-with: Cursor
2026-02-26 22:23:05 -05:00
Alex Hancock
332fcbfb91
Fix/sse channel replacement conflict (#682)
* fix(streamable-http): return 409 Conflict when standalone SSE stream already active

LocalSessionWorker::resume() unconditionally replaced self.common.tx on
every GET request, orphaning the receiver the first SSE stream was
reading from. All subsequent server-to-client notifications were sent to
the new sender while the original client was still listening on the old,
now-dead receiver. notify_tool_list_changed().await returned Ok(())
silently.

This is triggered by VS Code's MCP extension which reconnects SSE every
~5 minutes with the same session ID.

Fix: Check tx.is_closed() before replacing the common channel sender.
If an active stream exists, return SessionError::Conflict which is
propagated as HTTP 409 Conflict. This matches the TypeScript SDK
behavior (streamableHttp.ts:423).

Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>

* fix(streamable-http): handle resume with completed request-wise channel

When a client sends GET with Last-Event-ID from a completed POST SSE
response, the request-wise channel no longer exists in tx_router.
Previously this returned ChannelClosed -> 500, causing clients like
Cursor to enter an infinite re-initialization loop.

Now falls back to the common channel when the request-wise channel is
completed, per MCP spec: "Resumption applies regardless of how the
original stream was initiated (POST or GET)."

* fix: allow SSE channel replacement instead of 409 Conflict

Per MCP spec §Streamable HTTP, "The client MAY remain connected to
multiple SSE streams simultaneously." Returning 409 Conflict when a
second GET arrives causes Cursor to enter an infinite re-initialization
loop (~3s cycle).

Instead of rejecting, replace the old common channel sender. Dropping
the old sender closes the old receiver, cleanly terminating the
previous SSE stream so the client can reconnect on the new stream.

This fixes both code paths:
- GET with Last-Event-ID from a completed POST SSE response
- GET without Last-Event-ID (standalone stream reconnection)

* fix: skip cache replay when replacing active SSE stream

When a client opens a new GET SSE stream while a previous one is
still active, the old sender is dropped (terminating the old stream)
and a new channel is created.  Previously, sync() replayed all cached
events to the new stream, but the client already received those events
on the old stream.  This caused an infinite notification loop:

1. Client receives notifications (e.g. ResourceListChanged)
2. Old SSE stream dies (sender replaced)
3. Client reconnects after sse_retry (3s)
4. sync() replays cached notifications the client already handled
5. Client processes them again → goto 2

Fix: check tx.is_closed() BEFORE replacing the sender.  If the old
stream was still alive, skip replay entirely — the client already has
those events.  Only replay when the old stream was genuinely dead
(network failure, timeout) so the client catches up on missed events.

* fix: use shadow channels to prevent SSE reconnect loops

When POST SSE responses include a `retry` field, the browser's
EventSource automatically reconnects via GET after the stream ends.
This creates multiple competing EventSource connections that each
replace the common channel sender, killing the other stream's receiver.
Both reconnect every sse_retry seconds, creating an infinite loop.

Instead of always replacing the common channel, check if the primary
is still active. If so, create a "shadow" stream — an idle SSE
connection kept alive by keep-alive pings that doesn't receive
notifications or interfere with the primary channel.

Also removes cache replay (sync) on common channel resume, as
replaying server-initiated list_changed notifications causes clients
to re-process old signals.

Signed-off-by: Myko Ash <myko@mcpmux.com>
Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>

* test: comprehensive shadow channel tests (15 cases)

Rewrite test suite for SSE channel replacement fix:
- Shadow creation: standalone GET returns 200, multiple GETs coexist
- Dead primary: replacement, notification delivery, repeated cycles
- Notification routing: primary receives, shadow does not
- Resume paths: completed request-wise, common alive/dead
- Real scenarios: Cursor leapfrog, VS Code reconnect
- Edge cases: invalid session, missing header, shadow cleanup

Fix Accept header bug (was missing text/event-stream for
notifications/initialized POST, causing 406 rejection).

* fix: use correct HTTP status codes for session errors per MCP spec

MCP spec (2025-11-25) section "Session Management" requires:
- Missing session ID header → 400 Bad Request (not 401)
- Unknown/terminated session → 404 Not Found (not 401)

Using 401 Unauthorized caused MCP clients (e.g. VS Code) to
trigger full OAuth re-authentication on server restart, instead
of simply re-initializing the session.

Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>

* fix: address review feedback — remove dead Conflict variant, restore sync on resume, rename test

- Remove unused SessionError::Conflict and dead string-matching in tower.rs
  (leftover from abandoned 409 approach)
- Restore sync() replay when replacing a dead primary common channel so
  server-initiated requests and cached notifications are not lost on reconnect
- Rename test from test_sse_channel_replacement_bug to test_sse_concurrent_streams
  per reviewer suggestion (describe what tests verify, not what triggered them)
- Add test for cache replay on dead primary replacement
- Use generic "MCP clients" in comments instead of specific client names

Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>

* fix: use minimal buffer for shadow streams and cap at 32

- Shadow streams only receive SSE keep-alive pings, so use capacity 1
  instead of full channel_capacity
- Cap shadow_txs at 32 to prevent unbounded growth from misbehaving
  clients, dropping the oldest shadow when the limit is reached
- Add test verifying primary works after exceeding shadow limit

Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>

* fix: remove redundant single-component `use reqwest` import

Fixes clippy::single_component_path_imports lint error in
test_sse_concurrent_streams.rs.

---------

Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
Signed-off-by: Myko Ash <myko@mcpmux.com>
Co-authored-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
2026-02-24 17:16:42 -05:00
Dale Seo
66c7000626
docs: document session management for streamable HTTP transport (#674) 2026-02-24 11:58:55 -05:00
Dale Seo
5fa012d163
feat: send and validate MCP-Protocol-Version header (#675) 2026-02-24 11:08:49 -05:00
Dale Seo
2286564f65
refactor: re-export ServerSseMessage from session module (#612) 2026-01-12 14:22:29 -05:00
Dale Seo
971c64c31f
Implement SEP-1699: Support SSE Polling via Server-Side Disconnect (#604)
* feat: implement SEP-1699 SSE polling via server-side disconnect

* test: add tests for priming behavior on stream start and close
2026-01-09 13:22:57 -05:00
Jonson Petard
3dee024325
fix(streamable-http): gracefully shutdown while client connected (#494)
* fix(streamable-http): gracefully shutdown while client connected

* fix: adviced comments

* fix: windows test build
2025-12-02 09:22:52 +08:00
Jack Amadeo
57fc428c57
chore: bump to rust 1.90.0 (#453) 2025-09-24 12:58:54 -04:00
4t145
f381c2b899
fix: remove batched json rpc support (#408)
* fix: remove batched json rpc  support

* fix(test): fix schema
2025-09-03 14:13:55 +08:00
4t145
0a2f77de41
feat: keep internal error in worker's quit reason (#372)
* feat: support downcast WorkerQuitReason::Fatal

* feat(transport): expose internal worker error in fatal

* Update crates/rmcp/src/transport/streamable_http_client.rs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update crates/rmcp/src/transport/streamable_http_client.rs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-18 11:13:10 +08:00
4t145
28f4781332
doc(streamable): add document for extracting http info (#373)
* docs(streamable): add document for extracting http info

* docs: fix doc test
2025-08-15 11:12:48 +08:00
Nate Usher
d251800d19
fix: return a 405 for GET and DELETE if stateful_mode=false (#331) 2025-07-29 02:41:17 +08:00
4t145
9ca20c69b2
feat: mark boxed http body as sync (#291) 2025-07-01 11:27:15 +08:00
4t145
6d3190504c
fix: inject part into extension when handing init req (#275) 2025-06-24 00:52:38 +08:00
4t145
8a577659f0
fix: streamable http server close request channel on response(#266) (#270)
* fix: improve resource unregistration logic in LocalSessionWorker

* fix: fix typo

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-19 01:09:36 +08:00
John Howard
73188eff6f
feat: allow failable service creation in streamable HTTP tower service (#244)
* feat: allow failable service creation in streamable HTTP tower service

In our setup we need to lookup some info that may fail before we serve
the request.

* Fix examples build
2025-06-16 01:13:35 +08:00
4t145
969fad26cc
feat: provide more context information (#236)
* feat: provide more context information

1. provide context for notification
2. allow extract more info from tool call
3. inject http request part for streamable http server

* docs: add document for getting peer from context
2025-05-29 11:52:54 +08:00
4t145
209be7bdb8
refactor: provide http server as tower service (#228)
* refactor: streamable http server as tower service
2025-05-28 19:21:40 +08:00
4t145
817e62d895
feat: throw initialize error detail (#192) 2025-05-19 09:41:56 +08:00
4t145
9a771fb157
refactor: Transport trait and worker transport, and streamable http client with those new features. (#167)
* refactor: a transport trait and streamable http client

* test: test with js streamable http server

* refactor: separate sse stream connection

* fix(test): streamable http client test with js

* fix(test): wait longer time for js server startup
2025-05-17 23:53:38 +08:00
4t145
787cc015b7
feat(extension): extract http request part into rmcp extension (#163)
* feat(extension): extract http request part into rmcp extension

* perf(extension): insert extension from one entrypoint
2025-05-09 10:36:52 +08:00
4t145
57e14c56e8
feat(transport): support streamable http server (#152)
* feat(transport): support streamable http server

* refactor(transport): add common module for shared transport utilities

* perf(tracing): add more log for streamable http session

* fix(test): fix port conflict in test with js

* fix(transport): fix id generating and session management

1. cancel when deleting
2. use `wrapping` and `saturating` correctly

* chore(naming): rename sse server to streamable http server
2025-05-08 13:57:51 +08:00