Skip to content

Commit a89047a

Browse files
authored
Merge pull request #328 from koic/support_json_response_mode
Support JSON response mode for `StreamableHTTPTransport`
2 parents a2706cc + 017b0bb commit a89047a

3 files changed

Lines changed: 448 additions & 4 deletions

File tree

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1425,6 +1425,22 @@ Set `stateless: true` in `MCP::Server::Transports::StreamableHTTPTransport.new`
14251425
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, stateless: true)
14261426
```
14271427

1428+
You can enable JSON response mode, where the server returns `application/json` instead of `text/event-stream`.
1429+
Set `enable_json_response: true` in `MCP::Server::Transports::StreamableHTTPTransport.new`:
1430+
1431+
```ruby
1432+
# JSON response mode
1433+
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, enable_json_response: true)
1434+
```
1435+
1436+
In JSON response mode, the POST response is a single JSON object, so server-to-client messages
1437+
that need to arrive during request processing are not supported:
1438+
request-scoped notifications (`progress`, `log`) are silently dropped, and all server-to-client requests
1439+
(`sampling/createMessage`, `roots/list`, `elicitation/create`) raise an error.
1440+
Session-scoped standalone notifications (`resources/updated`, `elicitation/complete`) and
1441+
broadcast notifications (`tools/list_changed`, etc.) still flow to clients connected to the GET SSE stream.
1442+
This mode is suitable for simple tool servers that do not need server-initiated requests.
1443+
14281444
By default, sessions do not expire. To mitigate session hijacking risks, you can set a `session_idle_timeout` (in seconds).
14291445
When configured, sessions that receive no HTTP requests for this duration are automatically expired and cleaned up:
14301446

lib/mcp/server/transports/streamable_http_transport.rb

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,14 @@ class StreamableHTTPTransport < Transport
2222
"Connection" => "keep-alive",
2323
}.freeze
2424

25-
def initialize(server, stateless: false, session_idle_timeout: nil)
25+
def initialize(server, stateless: false, enable_json_response: false, session_idle_timeout: nil)
2626
super(server)
2727
# Maps `session_id` to `{ get_sse_stream: stream_object, server_session: ServerSession, last_active_at: float_from_monotonic_clock }`.
2828
@sessions = {}
2929
@mutex = Mutex.new
3030

3131
@stateless = stateless
32+
@enable_json_response = enable_json_response
3233
@session_idle_timeout = session_idle_timeout
3334
@pending_responses = {}
3435

@@ -43,7 +44,8 @@ def initialize(server, stateless: false, session_idle_timeout: nil)
4344
start_reaper_thread if @session_idle_timeout
4445
end
4546

46-
REQUIRED_POST_ACCEPT_TYPES = ["application/json", "text/event-stream"].freeze
47+
REQUIRED_POST_ACCEPT_TYPES_SSE = ["application/json", "text/event-stream"].freeze
48+
REQUIRED_POST_ACCEPT_TYPES_JSON = ["application/json"].freeze
4749
REQUIRED_GET_ACCEPT_TYPES = ["text/event-stream"].freeze
4850
STREAM_WRITE_ERRORS = [IOError, Errno::EPIPE, Errno::ECONNRESET].freeze
4951
SESSION_REAP_INTERVAL = 60
@@ -94,6 +96,12 @@ def send_notification(method, params = nil, session_id: nil, related_request_id:
9496

9597
result = @mutex.synchronize do
9698
if session_id
99+
# JSON response mode returns a single JSON object as the POST response,
100+
# so request-scoped notifications (e.g. progress, log) cannot be delivered
101+
# alongside it. Session-scoped standalone notifications
102+
# (e.g. `resources/updated`, `elicitation/complete`) still flow via GET SSE.
103+
next false if @enable_json_response && related_request_id
104+
97105
# Send to specific session
98106
if (session = @sessions[session_id])
99107
stream = active_stream(session, related_request_id: related_request_id)
@@ -172,6 +180,10 @@ def send_request(method, params = nil, session_id: nil, related_request_id: nil)
172180
raise "Stateless mode does not support server-to-client requests."
173181
end
174182

183+
if @enable_json_response
184+
raise "JSON response mode does not support server-to-client requests."
185+
end
186+
175187
unless session_id
176188
raise "session_id is required for server-to-client requests."
177189
end
@@ -278,7 +290,8 @@ def send_ping_to_stream(stream)
278290
end
279291

280292
def handle_post(request)
281-
accept_error = validate_accept_header(request, REQUIRED_POST_ACCEPT_TYPES)
293+
required_types = @enable_json_response ? REQUIRED_POST_ACCEPT_TYPES_JSON : REQUIRED_POST_ACCEPT_TYPES_SSE
294+
accept_error = validate_accept_header(request, required_types)
282295
return accept_error if accept_error
283296

284297
content_type_error = validate_content_type(request)
@@ -519,7 +532,7 @@ def handle_regular_request(body_string, session_id, related_request_id: nil)
519532
end
520533
end
521534

522-
if session_id && !@stateless
535+
if session_id && !@stateless && !@enable_json_response
523536
handle_request_with_sse_response(body_string, session_id, server_session, related_request_id: related_request_id)
524537
else
525538
response = dispatch_handle_json(body_string, server_session)

0 commit comments

Comments
 (0)