From bea636c0075b503c31a241191c499128cc92f3f0 Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Tue, 11 Aug 2026 03:21:33 +0900 Subject: [PATCH] Refuse server-to-client requests in the modern lifecycle per SEP-2575 ## Motivation and Context The modern lifecycle removes server-initiated JSON-RPC requests: a stateless server has no connection to send them on, and SEP-2322 multi round-trip `input_required` results replace what sampling and elicitation covered before. `StdioTransport#send_request` already says so, refusing outright when its session is modern. `StreamableHTTPTransport#send_request` says nothing, so a modern handler that reaches for elicitation or sampling falls through to the session lookup and is told `Session not found: ` against a session id it never chose. Failing is right, the explanation is not: `handle_modern` mints a session per request without ever registering it in `@sessions`, so that lookup cannot succeed for a modern request no matter what the handler does. `send_request` now refuses a modern-era session up front with the message `StdioTransport` uses, which turns an incidental registration detail into a stated rule and leaves the rest of the method a legacy-only path by construction. Callers see the same `RuntimeError` class as before, only with a message naming the actual constraint. ## How Has This Been Tested? A new test in `test/mcp/server/transports/streamable_http_transport_test.rb` sends `elicitation/create` from a session constructed with `era: :modern` and asserts the SEP-2575 message. It sits with the existing guard tests for stateless mode, a missing `session_id`, and an unknown session, which still pass unchanged. `bundle exec rake` is green, and both conformance legs pass their baseline check. ## Breaking Changes None. The call already failed; only the message changes. --- .../server/transports/streamable_http_transport.rb | 10 ++++++++++ .../transports/streamable_http_transport_test.rb | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/lib/mcp/server/transports/streamable_http_transport.rb b/lib/mcp/server/transports/streamable_http_transport.rb index e0ac8678..48e8de2a 100644 --- a/lib/mcp/server/transports/streamable_http_transport.rb +++ b/lib/mcp/server/transports/streamable_http_transport.rb @@ -479,6 +479,16 @@ def close_streams(streams) # so a client that never answers cannot park the calling thread for good. On expiry the peer is # sent `notifications/cancelled` and `MCP::Server::RequestTimeoutError` is raised. def send_request(method, params = nil, session_id: nil, related_request_id: nil, parent_cancellation: nil, server_session: nil, timeout: nil) + # The modern lifecycle (SEP-2575) forbids server-initiated JSON-RPC requests; + # multi round-trip `input_required` results (SEP-2322) replace them. A modern session has never reached + # the rest of this method anyway, since `handle_modern` mints its session without registering it in `@sessions`, + # but that is incidental: without the rule stated here a modern handler that reaches for elicitation + # or sampling is told "Session not found: ", which points at everything except the actual reason. + # `StdioTransport#send_request` refuses the same way. + if server_session&.era == :modern + raise "Server-initiated requests are not available in the modern lifecycle (SEP-2575)." + end + if @stateless raise "Stateless mode does not support server-to-client requests." end diff --git a/test/mcp/server/transports/streamable_http_transport_test.rb b/test/mcp/server/transports/streamable_http_transport_test.rb index 9d177970..30853563 100644 --- a/test/mcp/server/transports/streamable_http_transport_test.rb +++ b/test/mcp/server/transports/streamable_http_transport_test.rb @@ -2939,6 +2939,18 @@ def string assert_equal("Stateless mode does not support server-to-client requests.", error.message) end + test "send_request refuses to run at all in the modern lifecycle" do + # SEP-2575 removes server-initiated requests, so a modern handler reaching for elicitation or sampling + # is told why rather than being sent down a path that fails on an unrelated session lookup. + modern_session = ServerSession.new(server: @server, transport: @transport, session_id: "modern-1", era: :modern) + + error = assert_raises(RuntimeError) do + @transport.send_request("elicitation/create", { message: "hi" }, session_id: "modern-1", server_session: modern_session) + end + + assert_equal("Server-initiated requests are not available in the modern lifecycle (SEP-2575).", error.message) + end + test "send_request raises error when session_id is not provided" do error = assert_raises(RuntimeError) do @transport.send_request("sampling/createMessage", { "messages" => [] })