This is an automated email from the ASF dual-hosted git repository.
eschutho pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 90040fc1f6a fix(mcp_service): downgrade client-disconnect transport
noise to WARNING (SC-115264) (#42441)
90040fc1f6a is described below
commit 90040fc1f6a9b81e65fa8434daa750e6069f7c79
Author: Elizabeth Thompson <[email protected]>
AuthorDate: Sun Jul 26 15:02:15 2026 -0700
fix(mcp_service): downgrade client-disconnect transport noise to WARNING
(SC-115264) (#42441)
Co-authored-by: Claude <[email protected]>
Co-authored-by: Amin Ghadersohi <[email protected]>
---
superset/mcp_service/server.py | 65 +++++++-
.../unit_tests/mcp_service/test_logging_filters.py | 168 +++++++++++++++++++++
2 files changed, 231 insertions(+), 2 deletions(-)
diff --git a/superset/mcp_service/server.py b/superset/mcp_service/server.py
index 9d6b7a5d487..37101cc0011 100644
--- a/superset/mcp_service/server.py
+++ b/superset/mcp_service/server.py
@@ -30,6 +30,7 @@ from typing import Annotated, Any, Callable
import uvicorn
from fastmcp.exceptions import ToolError
from fastmcp.server.middleware import Middleware
+from starlette.requests import ClientDisconnect
from superset.mcp_service.app import create_mcp_app, init_fastmcp_server
from superset.mcp_service.jwt_verifier import BrowserHelloMiddleware
@@ -84,6 +85,12 @@ def _suppress_third_party_warnings() -> None:
)
+def _downgrade_to_warning(record: logging.LogRecord) -> None:
+ """Mutate *record* in place to WARNING level."""
+ record.levelno = logging.WARNING
+ record.levelname = "WARNING"
+
+
class FastMCPValidationFilter(logging.Filter):
"""Downgrade FastMCP's user-error logs from ERROR to WARNING.
@@ -107,8 +114,51 @@ class FastMCPValidationFilter(logging.Filter):
if record.levelno != logging.ERROR:
return True
if "Error validating tool" in record.getMessage():
- record.levelno = logging.WARNING
- record.levelname = "WARNING"
+ _downgrade_to_warning(record)
+ return True
+
+
+class MCPTransportDisconnectFilter(logging.Filter):
+ """Downgrade MCP SDK client-disconnect transport logs from ERROR to
WARNING.
+
+ When an MCP client disconnects mid-request (a cancelled or timed-out tool
+ call — normal client behavior, not a Superset bug), the ``mcp`` SDK's own
+ transport code logs it at ERROR with a full traceback, and separately
+ re-raises it into the session's message loop, which logs a second ERROR.
+ Both are expected under normal client behavior and should not page or
+ open incidents; downgrading to WARNING keeps them visible in log
+ aggregation without polluting ERROR-level alerting.
+
+ Two different loggers require two different matching strategies:
+
+ - ``mcp.server.streamable_http`` (``_handle_post_request``) catches the
+ disconnect via a broad ``except Exception`` and logs it with
+ ``logger.exception(...)``, so ``record.exc_info`` carries the actual
+ exception object — we can check its type directly.
+ - ``mcp.server.lowlevel.server`` (``_handle_message``) receives the same
+ exception secondhand, already wrapped as a bare
+ ``Exception(ClientDisconnect())`` pushed onto the read stream. The
+ original type is lost by the time it's logged, so exception-type
+ checks are impossible here. ``ClientDisconnect`` is always raised with
+ zero args, so ``str(ClientDisconnect())`` is always ``""`` — making the
+ rendered message a fixed, matchable string instead.
+ """
+
+ def filter(self, record: logging.LogRecord) -> bool:
+ if record.levelno != logging.ERROR:
+ return True
+ if record.name == "mcp.server.streamable_http":
+ if record.exc_info and isinstance(record.exc_info[1],
ClientDisconnect):
+ _downgrade_to_warning(record)
+ elif record.name == "mcp.server.lowlevel.server":
+ # NOTE: This matches the literal log message from the mcp SDK's
+ # lowlevel/server.py (``_handle_message``, line ~689 in the
+ # ``mcp==1.24.0`` pinned in requirements/development.txt):
+ # ``logger.error(f"Received exception from stream: {message}")``.
+ # If the SDK changes this f-string's wording, this filter will
+ # stop working silently.
+ if record.getMessage() == "Received exception from stream: ":
+ _downgrade_to_warning(record)
return True
@@ -147,6 +197,17 @@ def configure_logging(debug: bool = False) -> None:
fastmcp_server_logger = logging.getLogger("fastmcp.server.server")
fastmcp_server_logger.addFilter(FastMCPValidationFilter())
+ # MCP client disconnects (cancelled/timed-out tool calls) are logged at
+ # ERROR by the mcp SDK's transport and lowlevel server loggers. These are
+ # expected client behavior, not Superset bugs — downgrade to WARNING.
+ transport_disconnect_filter = MCPTransportDisconnectFilter()
+ logging.getLogger("mcp.server.streamable_http").addFilter(
+ transport_disconnect_filter
+ )
+ logging.getLogger("mcp.server.lowlevel.server").addFilter(
+ transport_disconnect_filter
+ )
+
def create_event_store(config: dict[str, Any] | None = None) -> Any | None:
"""
diff --git a/tests/unit_tests/mcp_service/test_logging_filters.py
b/tests/unit_tests/mcp_service/test_logging_filters.py
new file mode 100644
index 00000000000..aaa7bd34774
--- /dev/null
+++ b/tests/unit_tests/mcp_service/test_logging_filters.py
@@ -0,0 +1,168 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+Unit tests for MCPTransportDisconnectFilter.
+
+Tests verify that:
+- streamable_http ERROR records carrying a ClientDisconnect exc_info are
+ downgraded to WARNING
+- streamable_http ERROR records carrying an unrelated exception stay at ERROR
+- lowlevel.server ERROR records with the exact "Received exception from
+ stream: " message are downgraded to WARNING
+- lowlevel.server ERROR records with a different message stay at ERROR
+- records below ERROR level pass through unchanged regardless of logger name
+"""
+
+import logging
+from types import TracebackType
+
+from starlette.requests import ClientDisconnect
+
+from superset.mcp_service.server import MCPTransportDisconnectFilter
+
+_SysExcInfo = (
+ tuple[type[BaseException], BaseException, TracebackType | None]
+ | tuple[None, None, None]
+)
+
+
+def _make_record(
+ name: str,
+ level: int,
+ msg: str,
+ exc_info: _SysExcInfo | None = None,
+) -> logging.LogRecord:
+ return logging.getLogger(name).makeRecord(
+ name, level, "test_file.py", 1, msg, (), exc_info
+ )
+
+
+def _get_client_disconnect_exc_info() -> _SysExcInfo:
+ try:
+ raise ClientDisconnect()
+ except ClientDisconnect:
+ import sys
+
+ return sys.exc_info()
+
+
+def _get_value_error_exc_info() -> _SysExcInfo:
+ try:
+ raise ValueError("boom")
+ except ValueError:
+ import sys
+
+ return sys.exc_info()
+
+
+class TestMCPTransportDisconnectFilterStreamableHttp:
+ """Tests for the mcp.server.streamable_http branch of the filter."""
+
+ def test_client_disconnect_downgraded_to_warning(self) -> None:
+ record = _make_record(
+ "mcp.server.streamable_http",
+ logging.ERROR,
+ "Error handling POST request",
+ exc_info=_get_client_disconnect_exc_info(),
+ )
+
+ assert MCPTransportDisconnectFilter().filter(record) is True
+ assert record.levelno == logging.WARNING
+ assert record.levelname == "WARNING"
+
+ def test_other_exception_stays_at_error(self) -> None:
+ record = _make_record(
+ "mcp.server.streamable_http",
+ logging.ERROR,
+ "Error handling POST request",
+ exc_info=_get_value_error_exc_info(),
+ )
+
+ assert MCPTransportDisconnectFilter().filter(record) is True
+ assert record.levelno == logging.ERROR
+ assert record.levelname == "ERROR"
+
+ def test_no_exc_info_stays_at_error(self) -> None:
+ record = _make_record(
+ "mcp.server.streamable_http",
+ logging.ERROR,
+ "Error handling POST request",
+ )
+
+ assert MCPTransportDisconnectFilter().filter(record) is True
+ assert record.levelno == logging.ERROR
+
+
+class TestMCPTransportDisconnectFilterLowlevelServer:
+ """Tests for the mcp.server.lowlevel.server branch of the filter."""
+
+ def test_empty_exception_message_downgraded_to_warning(self) -> None:
+ record = _make_record(
+ "mcp.server.lowlevel.server",
+ logging.ERROR,
+ "Received exception from stream: ",
+ )
+
+ assert MCPTransportDisconnectFilter().filter(record) is True
+ assert record.levelno == logging.WARNING
+ assert record.levelname == "WARNING"
+
+ def test_different_message_stays_at_error(self) -> None:
+ record = _make_record(
+ "mcp.server.lowlevel.server",
+ logging.ERROR,
+ "Received exception from stream: something else",
+ )
+
+ assert MCPTransportDisconnectFilter().filter(record) is True
+ assert record.levelno == logging.ERROR
+
+ def test_unrelated_message_stays_at_error(self) -> None:
+ record = _make_record(
+ "mcp.server.lowlevel.server",
+ logging.ERROR,
+ "Some unrelated error",
+ )
+
+ assert MCPTransportDisconnectFilter().filter(record) is True
+ assert record.levelno == logging.ERROR
+
+
+class TestMCPTransportDisconnectFilterPassthrough:
+ """Records below ERROR level are always left unchanged."""
+
+ def test_warning_level_passes_through_unchanged(self) -> None:
+ record = _make_record(
+ "mcp.server.lowlevel.server",
+ logging.WARNING,
+ "Received exception from stream: ",
+ )
+
+ assert MCPTransportDisconnectFilter().filter(record) is True
+ assert record.levelno == logging.WARNING
+
+ def test_info_level_passes_through_unchanged(self) -> None:
+ record = _make_record(
+ "mcp.server.streamable_http",
+ logging.INFO,
+ "Error handling POST request",
+ exc_info=_get_client_disconnect_exc_info(),
+ )
+
+ assert MCPTransportDisconnectFilter().filter(record) is True
+ assert record.levelno == logging.INFO