Skip to content

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

Merged
DaleSeo merged 3 commits intomodelcontextprotocol: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 intomodelcontextprotocol:mainfrom
thiagomendes:feat/streamable-http-server-json-response

Conversation

@thiagomendes
Copy link
Contributor

@thiagomendes thiagomendes commented Feb 24, 2026

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 {
    return server
}, &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
RPS 770 1139 +48%
Tool avg latency (Redis + HTTP) 41ms 0.76ms -98%
Tool avg latency (HTTP + pipeline) 41ms 0.55ms -99%
Errors 0 0 none

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 owner February 24, 2026 19:01
@github-actions github-actions bot 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
Copy link
Member

@DaleSeo DaleSeo left a comment

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.

@thiagomendes
Copy link
Contributor Author

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.

@thiagomendes thiagomendes requested a review from DaleSeo February 25, 2026 23:55
@DaleSeo
Copy link
Member

DaleSeo commented Feb 26, 2026

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 thiagomendes force-pushed the feat/streamable-http-server-json-response branch 2 times, most recently from 8bfd1e2 to 93634fc Compare February 26, 2026 18:34
@thiagomendes thiagomendes force-pushed the feat/streamable-http-server-json-response branch from 5f171cf to b6be330 Compare February 26, 2026 19:15
@thiagomendes
Copy link
Contributor Author

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:main Feb 27, 2026
16 checks passed
@github-actions github-actions bot mentioned this pull request Feb 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-config Configuration file changes T-core Core library changes T-dependencies Dependencies related changes T-test Testing related changes T-transport Transport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants