Skip to content

fix(server): don't log a client session close as an ERROR in the standalone SSE writer - #3834

Closed
umeGe wants to merge 1 commit into
modelcontextprotocol:v1.xfrom
umeGe:fix/standalone-sse-writer-error-log
Closed

umeGe wants to merge 1 commit into
modelcontextprotocol:v1.xfrom
umeGe:fix/standalone-sse-writer-error-log

Conversation

@umeGe

@umeGe umeGe commented Nov 8, 2026

Copy link
Copy Markdown

Fixes #3833

Problem

On the v1.x line, closing a stateful streamable-HTTP session the normal way (the client's
streamablehttp_client context exits and sends DELETE) logs an ERROR with a full traceback:

ERROR mcp.server.streamable_http: Error in standalone SSE writer
Traceback (most recent call last):
  ... standalone_sse_writer
    async for event_message in standalone_stream_reader:
anyio.ClosedResourceError

StreamableHTTPServerTransport.terminate() closes every request stream, including the standalone
GET stream (GET_STREAM_KEY), while standalone_sse_writer is still iterating it. The resulting
anyio.ClosedResourceError is caught by the writer's generic except Exception: logger.exception(...),
so every successfully-closed session leaves an ERROR traceback in the server log and buries real
errors. Nothing actually fails — the calls succeed and the server keeps serving.

Fix

Catch anyio.ClosedResourceError separately in standalone_sse_writer and log it at DEBUG: a
stream closed under the writer during a client-initiated termination is a normal end of the stream.
All other exceptions keep the existing logger.exception("Error in standalone SSE writer").

Tests

New in-process regression test in tests/shared/test_streamable_http.py:
test_termination_does_not_log_an_error_in_the_standalone_sse_writer

It drives a GET request straight into a StreamableHTTPServerTransport (no server process needed),
waits for the standalone GET stream to register, calls terminate() (exactly what a DELETE does)
and asserts that mcp.server.streamable_http emitted no record at ERROR or above.

On the unpatched v1.x source the test reproduces the issue and fails with the traceback above; with
the fix it passes. Validation on this machine (Windows, Python 3.12):

pytest tests/shared/test_streamable_http.py -q -k "does_not_log_an_error_in_the_standalone" -n0
# 1 passed  (1 failed on unpatched source)

pytest tests/shared/test_streamable_http.py -q -k "streamable_http_transport or termination or idle" -n0
# 13 passed

ruff check src/mcp/server/streamable_http.py tests/shared/test_streamable_http.py   # All checks passed!
ruff format --check src/mcp/server/streamable_http.py tests/shared/test_streamable_http.py  # clean

AI assistance

Tool(s) used: DeepSeek Harness (coding agent). The agent reproduced the exact traceback through the
transport's public ASGI path, added the regression test first (red on unpatched source), split the
ClosedResourceError handling out of the generic branch and ran the focused suite plus ruff. I
reviewed the diff, the log-level choice and the red/green output, and take responsibility for the
change.

…dalone SSE writer

terminate() closes every request stream, including the standalone GET stream,
while standalone_sse_writer is still iterating it. The resulting
anyio.ClosedResourceError fell into the writer's generic `except Exception:
logger.exception(...)`, so every normal client-initiated session close (DELETE)
left an ERROR traceback in the server log.

Catch ClosedResourceError separately and log it at DEBUG: a stream closed under
the writer during termination is a normal end. All other exceptions keep the
existing ERROR report.

Regression test drives a GET request straight into the transport, calls
terminate() and asserts no ERROR record from mcp.server.streamable_http
(reproduces the traceback and fails on unpatched source). Focused transport
subset: 13 passed; ruff check/format clean.
@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 Nov 8, 2026
@github-actions

github-actions Bot commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

This PR has been closed automatically. This repo only keeps pull requests open when they come from a maintainer, or from a contributor a maintainer has assigned to the linked issue, and you aren't currently assigned to #3833.

If a maintainer assigns you to #3833, this PR reopens on its own and there's nothing more you need to do here. Assignment is a maintainer call based on capacity; comments that only ask to be assigned don't factor in. What does help is engaging on the issue itself by confirming the repro, explaining why it matters for your use case, or describing the approach you'd take.

You're welcome to keep pushing commits here (just avoid force-pushing, since GitHub can't reopen a rewritten branch), but that on its own won't get the PR reviewed or the issue assigned, and realistically most auto-closed PRs stay closed. There's no need to open a new PR either way.

CONTRIBUTING.md has the full reasoning, but in short:

  • We're a small team with very little capacity to review community PRs right now.
  • Many recent PRs are AI-generated with little human review, and reviewing one carefully still costs a maintainer as much time as it ever did. A well-described issue is usually more useful to us than the code.

Maintainers: reopen, remove missing-issue-link, or add bypass-issue-check to override.

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.

1 participant