Skip to content

fix(server): register SSE GET as raw ASGI so BaseHTTPMiddleware does not see a second response start - #3381

Closed
jwilson411 wants to merge 1 commit into
modelcontextprotocol:mainfrom
jwilson411:oss/UP-12/20260824222122
Closed

fix(server): register SSE GET as raw ASGI so BaseHTTPMiddleware does not see a second response start#3381
jwilson411 wants to merge 1 commit into
modelcontextprotocol:mainfrom
jwilson411:oss/UP-12/20260824222122

Conversation

@jwilson411

Copy link
Copy Markdown

Fixes #883.

What I hit

I reproduced this on current main (56af447) by mounting MCPServer.sse_app(host="0.0.0.0") under a Starlette app that uses a no-op BaseHTTPMiddleware, opening /sse, reading the first endpoint event, then closing the client. Same AssertionError:

AssertionError: Unexpected message: {'type': 'http.response.start', 'status': 200, 'headers': [(b'content-length', b'0')]}

Stashing only src/mcp/server/mcpserver/server.py after this change makes test_sse_app_under_base_http_middleware_survives_client_disconnect_without_second_response fail with that same error.

Why

sse_app() registered the unauth SSE GET as a Starlette function endpoint. Starlette wraps those in request_response. EventSourceResponse already finalized the stream; the wrapper then sent a second http.response.start (empty Response()). BaseHTTPMiddleware asserts it only sees http.response.body after the first start. Kludex already called this on the issue: the SDK must not send two starts.

The auth path already passed a class instance (RequireAuthMiddleware) to Route, which Starlette treats as raw ASGI. The unauth path did not.

Fix

  • Register the SSE GET as a raw ASGI app (_RawASGIEndpoint) instead of a function endpoint.
  • Stop returning Response() after connect_sse has already sent the complete response.
  • Update the SseServerTransport module example so it no longer tells users to return a second Response().

No new public API. Route shape is unchanged (one Route at /sse, one Mount at /messages).

Tests

  • test_sse_app_under_base_http_middleware_survives_client_disconnect_without_second_response — in-process StreamingASGITransport, no-op BaseHTTPMiddleware, asserts exactly one http.response.start after client disconnect.

uv run --frozen pytest tests/shared/test_sse.py tests/server/test_sse_security.py tests/interaction/transports/test_sse.py tests/server/mcpserver/test_server.py → 227 passed.
uv run --frozen pyright on the three changed files → 0 errors.

Notes

#2016 / #2029 tried a similar ASGI-callable approach and were closed unmerged. #1222 is still an open draft. This is a fresh patch against current main, not a rebase of those.

AI-assisted implementation (Claude Code). I reproduced the failure on this tree, reviewed the diff, and reran the SSE / MCPServer tests before opening this.

I have not asked to be assigned. If the auto-close bot parks this until a maintainer wants an outside PR, that is understood.

…not see a second response start

Starlette wraps function endpoints in request_response. After EventSourceResponse
has already finalized the SSE stream, that wrapper sent a second
http.response.start (empty Response), which BaseHTTPMiddleware asserts against
on client disconnect (modelcontextprotocol#883).
@github-actions github-actions Bot added the missing-issue-link Auto-closed: PR needs a linked issue assigned to its author (see CONTRIBUTING.md) label Aug 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for the contribution. This repository only keeps pull requests open when they're linked to an issue that a maintainer has assigned to the author — CONTRIBUTING.md explains why and how we work. This PR has been closed for now because you aren't currently assigned to #883.

If a maintainer would like this change as a PR from you, they'll assign you to #883 and this PR will reopen automatically — there's nothing more you need to do. (If you opened the issue, this PR already shows up on its timeline.)

There's no need to open a new PR — this one will be reopened. While it's closed, please push any updates as new commits rather than force-pushing, since GitHub can't reopen a PR whose branch has been rewritten.

Maintainers: reopening this PR, removing the missing-issue-link label, or adding bypass-issue-check bypasses the check.

@github-actions github-actions Bot closed this Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

missing-issue-link Auto-closed: PR needs a linked issue assigned to its author (see CONTRIBUTING.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AssertionError: Unexpected message while using middlewares

1 participant