fix: prevent infinite warning-logging loop in _handle_message

RA
3mo ago
+89-2

Summary

Fixes #3122

Server._handle_message (mcp/server/lowlevel/server.py) logs the warnings recorded during message handling while still inside the warnings.catch_warnings(record=True) block. Because record=True installs an "always" filter, any warning emitted by a logging handler while those logger.info calls run is appended to the very list being iterated, so the for warning in w loop never terminates. The loop is synchronous with no await points, so task cancellation cannot interrupt it; the handling task wedges until an external timeout kills the process. This shows up in practice when a formatter on the logger chain warns per record (for example a datetime.utcnow()-based JSON timestamp formatter on Python 3.12+), and it is easy to trigger under pytest, which runs with an "always" warning filter.

Fix

Snapshot the recorded warnings and emit the log lines after leaving the catch_warnings block. Once the block exits, the recording showwarning is restored, so a warning raised by a logging handler goes to the normal warnings machinery instead of being appended to the snapshot, and the loop iterates a fixed list. Behaviour for the normal case (recorded warnings are still logged once each) is unchanged.

Tests

Added tests/issues/test_3122_warning_loop.py. It drives _handle_message with a request that records one warning and attaches a logging handler that itself warns on every record, then asserts the handler is invoked exactly once. The handler stops re-warning after a cap so that a regressed (looping) build terminates and fails the assertion rather than hanging the session. The test fails on v1.x before this change (handler invoked many times) and passes after.

This targets v1.x; main already dropped this block in the v2 receive-path rewrite, so it is not affected.