fix(server): return 405 on GET/DELETE in stateless HTTP mode#2509
Draft
faridun-ag2 wants to merge 1 commit intomodelcontextprotocol:mainfrom
Draft
fix(server): return 405 on GET/DELETE in stateless HTTP mode#2509faridun-ag2 wants to merge 1 commit intomodelcontextprotocol:mainfrom
faridun-ag2 wants to merge 1 commit intomodelcontextprotocol:mainfrom
Conversation
In stateless mode the manager was creating a transport for every GET, opening an SSE stream that could never receive server-initiated messages and idling until timeout — wasteful on serverless platforms. DELETE had the same shape (no session to terminate). Reject GET and DELETE with 405 (Allow: POST) before any transport is spawned. Stateful mode is unchanged. Closes modelcontextprotocol#2474
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
In stateless HTTP mode (
stateless_http=True), GET requests to the MCP endpoint were creating a transport and opening an SSE stream that could never receive server-initiated messages, idling until timeout. DELETE had the same shape (no session to terminate). This wastes connections, especially on serverless platforms (Cloud Run, Lambda).This PR makes
StreamableHTTPSessionManager._handle_stateless_requestshort-circuit GET and DELETE with HTTP 405 before any transport is spawned. POST is unchanged. Stateful mode is unchanged.The MCP spec permits this: "Servers MAY return HTTP 405 Method Not Allowed if an SSE stream is not offered at the endpoint." The TypeScript SDK already implements this behavior.
Closes #2474.
What changed
src/mcp/server/streamable_http_manager.py— early-return guard at the top of_handle_stateless_request. Ifrequest.methodisGETorDELETE, return a JSON-RPC formatted 405 withAllow: POSTand bail out before transport creation.tests/server/test_streamable_http_manager.py— three new tests:test_stateless_get_returns_405— status,Allowheader, JSON-RPC error bodytest_stateless_delete_returns_405— same checks for DELETEtest_stateless_get_does_not_create_transport— asserts noStreamableHTTPServerTransportis instantiated for a stateless GETDesign notes
StreamableHTTPServerTransportstays mode-agnostic.JSONRPCError(jsonrpc="2.0", id=None, error=ErrorData(code=INVALID_REQUEST, ...))serialized viamodel_dump_json(by_alias=True, exclude_unset=True)), with an addedAllow: POSTheader.Allowheader. ReturnsAllow: POST(notGET, POST, DELETElike the stateful transport's_handle_unsupported_request). The difference is intentional — stateless mode genuinely only supports POST._handle_unsupported_requestand still return 405. TheAllowheader from that path advertises GET/DELETE that stateless doesn't actually support — minor inconsistency, happy to extend the guard if reviewers prefer.Request,Response,HTTPStatus,INVALID_REQUEST,ErrorData,JSONRPCErrorwere all already imported in this module.Test plan
uv run --frozen pytest tests/server/test_streamable_http_manager.py— 14/14 passuv run --frozen pytest tests/ -k stateless— 13/13 passuv run --frozen pytest tests/server/— 477/477 pass (no regressions)uv run --frozen ruff format+ruff check— cleanuv run --frozen pyright— 0 errorsstreamable_http_manager.py— 100% (branch-covered)strict-no-cover— cleanpre-commit runon changed files — all applicable hooks pass