Skip to content

chore: filter upstream MCP-SDK race-condition log noise on session teardown#6

Merged
romainvaltier-work merged 2 commits into
masterfrom
chore/filter-streamable-http-race-noise
May 4, 2026
Merged

chore: filter upstream MCP-SDK race-condition log noise on session teardown#6
romainvaltier-work merged 2 commits into
masterfrom
chore/filter-streamable-http-race-noise

Conversation

@romainvaltier-work
Copy link
Copy Markdown
Contributor

Summary

The `mcp.server.streamable_http` logger emits an ERROR with a full `anyio.ClosedResourceError` traceback on every session teardown (open SSE via `GET /mcp` → terminate via `DELETE /mcp` while the standalone SSE writer is mid-`checkpoint`). The `DELETE` itself returns 200, the session ends cleanly, the response reaches the client — the trace is purely log noise, but it bloats logs and dashboards.

Upstream PR modelcontextprotocol/python-sdk#1384 (merged 2025-12-04) added explicit `ClosedResourceError` handling to the sibling `message_router`, but the same pattern hasn't been ported to `standalone_sse_writer` yet. Until it is, drop just those records via a tightly-scoped `logging.Filter`.

Approach

`StandaloneSseWriterRaceFilter` matches exactly:

  • logger `mcp.server.streamable_http`
  • message string `Error in standalone SSE writer`
  • `exc_info` is `anyio.ClosedResourceError` (or subclass)

Every other ERROR from that logger — including legitimate streamable-HTTP failures and even `Error in standalone SSE writer` records with non-`ClosedResourceError` exceptions — passes through unchanged.

The filter is installed only when the wrapper runs under `streamable-http` or `sse` transport; stdio is unaffected.

Test plan

  • `uv run pytest tests/ -q` — 63 passed (9 new cases in `tests/test_logging_filters.py`)
  • `uv run ruff check src/ tests/` — clean
  • `uv run ruff format --check src/ tests/` — clean
  • New tests cover: unrelated message passes, target message without exc_info passes, target message with unrelated exception passes, target message with `ClosedResourceError` is dropped, subclass of `ClosedResourceError` is dropped, malformed `exc_info` is handled gracefully, exact-match-only of the upstream message string

Notes

  • Intentionally uses an exact message-string match rather than a regex. If upstream rewords the log message (or fixes the underlying race in line with #1384), the filter stops matching and the noise either disappears or starts surfacing again — both acceptable failure modes, neither silent.
  • No `feat:` / `fix:` prefix → no semantic-release bump. This is pure log hygiene; `chore:` is correct.

…ardown

The mcp.server.streamable_http logger emits an ERROR with a full
ClosedResourceError traceback on every session teardown — when a client
opens an SSE stream via GET /mcp and immediately calls DELETE /mcp,
the standalone_sse_writer task is mid-checkpoint while the GET stream
gets closed by cleanup. The DELETE itself returns 200, the session
ends cleanly, the response reaches the client; the trace is purely
log noise.

Upstream PR #1384 (merged 2025-12-04) added explicit
ClosedResourceError handling to the sibling message_router but did
not port the same pattern to standalone_sse_writer. Until upstream
catches up, drop just those records via a logging.Filter scoped to
the upstream logger. The filter matches the exact upstream message
string + ClosedResourceError exc_info — every other ERROR from that
logger, including legitimate streamable-HTTP failures, passes through.

Filter is installed only when the wrapper runs under streamable-http
or sse transport; stdio is unaffected.
…tests

Round-1 review fixes for PR #6:

- Use module-level `import logging` instead of locally aliasing inside
  `_run` (`logging` is already imported at the top of server.py).
- Make race-filter attachment idempotent — re-entry into `_run` no
  longer stacks duplicate filters on the upstream logger.
- Drop the `# noqa: A003` annotation; rule A is not in this project's
  ruff selection so the comment was inert.
- New `TestRaceFilterInstallation` class in test_run_wiring.py with a
  cleanup fixture and three cases: filter attached on streamable-http,
  not attached on stdio, attachment is idempotent across two `_run`
  invocations. Closes the wiring loop the same way the bearer
  middleware attachment is verified.
@romainvaltier-work romainvaltier-work merged commit 8e9da73 into master May 4, 2026
4 checks passed
@romainvaltier-work romainvaltier-work deleted the chore/filter-streamable-http-race-noise branch May 4, 2026 19:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant