* 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>
ToolRouter::list_all() and PromptRouter::list_all() iterate over a
HashMap, which returns items in non-deterministic order. Since list_all()
backs the tools/list and prompts/list MCP protocol responses, this causes
MCP clients to receive differently-ordered results across calls and
process restarts, leading to intermittent tool discovery failures.
Sort the output alphabetically by name to guarantee stable ordering.
* feat: add support for custom HTTP headers in StreamableHttpClient
* feat: implement reserved header checks for custom HTTP headers in StreamableHttpClient
* chore(deps): update rand requirement from 0.9 to 0.10
Updates the requirements on [rand](https://github.com/rust-random/rand) to permit the latest version.
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/rand_core-0.9.1...0.10.0)
---
updated-dependencies:
- dependency-name: rand
dependency-version: 0.10.0
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
* fix: update rand import from Rng to RngExt for rand 0.10 compatibility
In rand 0.10, the Rng trait was renamed to RngExt. This updates the
imports in the example servers to use the new trait name.
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Alex Hancock <alexhancock@block.xyz>
* feat(auth): add token_endpoint_auth_method to OAuthClientConfig
Some OAuth providers (e.g. HubSpot) require client credentials to be
sent as POST body parameters (client_secret_post) instead of via HTTP
Basic Auth header. The oauth2 crate defaults to BasicAuth, and rmcp
had no way to override this, causing TokenExchangeFailed errors.
Add an optional `token_endpoint_auth_method` field to OAuthClientConfig
that accepts "client_secret_post" (RequestBody) and "client_secret_basic"
(BasicAuth). Unknown values are silently ignored, preserving the default.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(auth): derive token_endpoint_auth_method from server metadata
Move auth method selection from per-client config to server's
AuthorizationMetadata, which is the correct OAuth 2.0 approach.
Servers like HubSpot advertise token_endpoint_auth_methods_supported
in their metadata; reading it from there avoids manual configuration
and prevents TokenExchangeFailed errors with non-BasicAuth providers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(auth): read token_endpoint_auth_methods_supported from additional_fields
Move token_endpoint_auth_methods_supported out of AuthorizationMetadata
as an explicit field and read it from the serde(flatten) additional_fields
HashMap instead. This avoids serializing `null` when the field is absent,
which broke Zod validation in downstream consumers like MCP Inspector.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(auth): prefer basic auth when both methods supported and improve test assertions
When token_endpoint_auth_methods_supported contains both client_secret_post
and client_secret_basic, default to basic auth per RFC 6749 §2.3.1.
Update configure_client tests to assert actual AuthType instead of is_some().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style(auth): apply cargo fmt formatting
* style(auth): apply nightly cargo fmt import grouping
* revert: undo .gitignore change
---------
Co-authored-by: Anar Azadaliyev <anar.azadaliye@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: remove unused axum dependency from server-side-http feature
The `server-side-http` feature included `dep:axum` but axum was never
actually used in the rmcp library source code (0 references found).
The `StreamableHttpService` is a tower service that works with any
HTTP server framework. Users can choose to use:
- axum (via `Router::nest_service()` or `fallback_service()`)
- hyper directly (via `hyper_util::service::TowerToHyperService`)
- any other tower-compatible HTTP server
This change removes the unnecessary transitive dependency, giving users
more flexibility in their choice of HTTP server framework.
Examples that use axum already have their own explicit axum dependency
in their Cargo.toml, so they continue to work unchanged.
* refactor: move axum to dev-dependencies with minimal features
- Remove axum from library dependencies (not used in library source)
- Add axum to dev-dependencies for tests with minimal features:
default-features = false, features = ["http1", "tokio"]
- Examples have their own axum dependency and are unaffected
This addresses review feedback from @ofek to use minimal features,
while ensuring axum is only bundled for running rmcp's own tests,
not for downstream users.
Add reqwest-native-tls feature flag to allow users to choose between
rustls (default) and native-tls for HTTP transports.
native-tls uses platform-native TLS implementations:
- OpenSSL on Linux
- Secure Transport on macOS
- SChannel on Windows
This is particularly useful for Linux distribution packagers who need
to link against system TLS libraries (e.g., OpenSSL) rather than
bundling a separate TLS implementation. Linking against system libs
ensures security updates are applied system-wide and satisfies
distribution packaging policies.
Updated documentation to explain the available TLS backend options.
Add support for MCP extension capabilities in both ClientCapabilities
and ServerCapabilities structs, as specified in SEP-1724.
Changes:
- Add ExtensionCapabilities type alias (BTreeMap<String, JsonObject>)
- Add 'extensions' field to ClientCapabilities struct
- Add 'extensions' field to ServerCapabilities struct
- Update builder macros and impl blocks for both structs
- Add comprehensive tests for extension capabilities
- Update JSON schema test fixtures
This enables clients to advertise extension support during initialize,
such as:
{
"capabilities": {
"extensions": {
"io.modelcontextprotocol/ui": {
"mimeTypes": ["text/html;profile=mcp-app"]
}
}
}
}
Closes#530
Move CustomRequest and CustomResult to end of their respective untagged
enums to ensure specific task variants match before catch-all custom types.
Add deny_unknown_fields to GetTaskInfoResult to prevent matching arbitrary
JSON objects.
Fixes issue where tasks/get, tasks/list, tasks/result, and tasks/cancel
incorrectly deserialized as CustomRequest instead of their typed variants.
Use `#![doc = include_str!("../README.md")]` to display README as crate
documentation on docs.rs for both `rmcp` and `rmcp-macros`.
Changes to support this:
- Fix code examples to compile as doc tests (`rust,no_run`)
- Fix broken rustdoc links with explicit `crate::` paths
- Add "Structured Output" section and examples link to rmcp README
- Simplify rmcp-macros README to a summary table with doc links
- Fix grammar throughout
- Add CSS to hide GitHub badges when rendered as rustdoc
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat: implement SEP-1319 Decouple Request Payload from RPC Methods
* test: update tests
* fix: update handler trait methods to use new types
* fix: update examples
* fix: correct deprecation version
* fix: update wrapper macros to use new *Params type names
* fix(docs): Add -p mcp-client-examples to cargo run commands in clients/README.md
* fix(docs): Add -p mcp-server-examples to cargo run commands in examples/servers/README.md
* fix(docs): Add -p parameter to cargo run commands in other documentation