Skip to content

feat(streamable-http): add json_response option for stateless server mode - #683

Merged
DaleSeo merged 3 commits into
modelcontextprotocol:mainfrom
thiagomendes:feat/streamable-http-server-json-response
Feb 27, 2026
Merged

feat(streamable-http): add json_response option for stateless server mode#683
DaleSeo merged 3 commits into
modelcontextprotocol:mainfrom
thiagomendes:feat/streamable-http-server-json-response

Conversation

@thiagomendes

@thiagomendesthiagomendes commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

What this changes

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.

Why

While working on a multi-language MCP server benchmark comparing implementations across Rust, Go, Java and other runtimes under equivalent load, we noticed the Rust server (using this SDK) showed significantly lower throughput than the other runtimes, even when the business logic and external I/O were equivalent.

After profiling the request lifecycle, the bottleneck was not in the server logic itself but in the transport layer. In stateless mode, the SDK was wrapping every single response in a Server-Sent Events stream, even for straightforward request-response interactions that complete in a single round trip.

Looking at how other MCP SDKs handle this, the Go SDK already exposes this as a first-class option via StreamableHTTPOptions:

mcp.NewStreamableHTTPHandler(func(r*http.Request) *mcp.Server {
returnserver
}, &mcp.StreamableHTTPOptions{
Stateless: true,
JSONResponse: true,
})

We looked for an equivalent in this SDK and found none. This PR adds it.

The MCP Streamable HTTP spec (2025-06-18) explicitly allows JSON responses as an alternative to SSE:

"The server MUST reply with Content-Type: text/event-stream OR Content-Type: application/json"

In stateless mode, each request is handled via OneshotTransport with exactly one message in and one message out. Wrapping this in an SSE stream adds unnecessary overhead:

  • Approximately 41ms fixed overhead per request, measured under load
  • Keep-alive pings holding connections open unnecessarily
  • Extra framing and parsing work on both client and server with no benefit

This change allows stateless servers to opt into the simpler and more efficient JSON response format that the spec already permits.

Benchmark evidence

Measurement setup: 50 concurrent virtual users, 5 minutes sustained, 2 CPUs, 2 GB RAM limit, I/O-bound workload with external HTTP calls and Redis operations, representative of typical MCP tool implementations.

Before (SSE)After (JSON)Delta
RPS7701139+48%
Tool avg latency (Redis + HTTP)41ms0.76ms-98%
Tool avg latency (HTTP + pipeline)41ms0.55ms-99%
Errors00none

The overhead was traced to SSE framing on responses from tools that are fully stateless and return a single JSON result, exactly the case this flag addresses.

Backward compatibility

  • json_response: false (default) keeps SSE behaviour unchanged
  • stateful_mode: true path is not touched at all
  • All existing tests pass

Relation to existing work

Types of changes

  • New feature (non-breaking change which adds functionality)

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

@thiagomendes
thiagomendes requested a review from a team as a code ownerFebruary 24, 2026 19:01
@github-actionsgithub-actionsBot added T-dependencies Dependencies related changes T-test Testing related changes T-config Configuration file changes T-core Core library changes T-transport Transport layer changes labels Feb 24, 2026

@DaleSeoDaleSeo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your contribution, @thiagomendes! I really appreciate the detailed PR description. The implementation looks clean, and the test coverage addresses the important cases. I left a couple of comments to make sure the JSON path works consistently with the existing SSE path.

Comment threadcrates/rmcp/src/transport/streamable_http_server/tower.rs Outdated
Comment threadcrates/rmcp/src/transport/streamable_http_server/tower.rs
@thiagomendes

Copy link
Copy Markdown
ContributorAuthor

Thanks for the review @DaleSeo ! I have pushed a new commit addressing both points (cancellation awareness and observability parity). The local tests are passing and everything looks good.

@DaleSeo

Copy link
Copy Markdown
Member

Thanks for addressing my feedback, @thiagomendes. The code looks good, but there's one linting error. We should be good to merge the PR once this is fixed.

error[E0063]: missing field `json_response`in initializer of `rmcp::transport::StreamableHttpServerConfig`
--> crates/rmcp/tests/test_sse_concurrent_streams.rs:82:9
|
82 | StreamableHttpServerConfig {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `json_response`
For more information about this error, try `rustc --explain E0063`.

…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 modelcontextprotocol#540) and contributes to the stateless server goals of SEP-1442 (modelcontextprotocol#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
@thiagomendes
thiagomendesforce-pushed the feat/streamable-http-server-json-response branch 2 times, most recently from 8bfd1e2 to 93634fcCompareFebruary 26, 2026 18:34
@thiagomendes
thiagomendesforce-pushed the feat/streamable-http-server-json-response branch from 5f171cf to b6be330CompareFebruary 26, 2026 19:15
@thiagomendes

Copy link
Copy Markdown
ContributorAuthor

Thanks for addressing my feedback, @thiagomendes. The code looks good, but there's one linting error. We should be good to merge the PR once this is fixed.

error[E0063]: missing field `json_response`in initializer of `rmcp::transport::StreamableHttpServerConfig`
--> crates/rmcp/tests/test_sse_concurrent_streams.rs:82:9
|
82 | StreamableHttpServerConfig {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `json_response`
For more information about this error, try `rustc --explain E0063`.

@DaleSeo Fixed, thanks for catching it! Pushed the correction. CI should be green now.

@DaleSeo
DaleSeo merged commit d6703da into modelcontextprotocol:mainFeb 27, 2026
16 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Feb 26, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-configConfiguration file changesT-coreCore library changesT-dependenciesDependencies related changesT-testTesting related changesT-transportTransport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@thiagomendes@DaleSeo