From 74f4c75a8fc47d4817ba5248216b3a374af29962 Mon Sep 17 00:00:00 2001 From: opspawn Date: Mon, 16 Feb 2026 05:24:39 +0000 Subject: [PATCH 1/5] fix: downgrade MCP "Unexpected content type" log spam from ERROR to DEBUG The observability-agent (and any agent using the MCP streamable-HTTP client) logs "Unexpected content type:" at ERROR level every 30 seconds when connected to the kagent-tools MCP server. This happens because the Go mcp-go library returns 200 OK without a Content-Type header when it handles heartbeat ping-responses, and the Python MCP SDK treats any non-JSON/SSE Content-Type as an error. These messages are benign -- actual MCP tool calls work correctly -- but they spam the logs and obscure real problems. This commit installs a logging filter on the `mcp.client.streamable_http` logger that downgrades matching ERROR records to DEBUG. The messages are still emitted when DEBUG logging is enabled, but they no longer pollute the default log output. Fixes #1200 Signed-off-by: opspawn --- .../kagent-adk/src/kagent/adk/__init__.py | 10 ++ .../kagent-adk/src/kagent/adk/_logging.py | 60 +++++++ .../tests/unittests/test_logging_filter.py | 151 ++++++++++++++++++ 3 files changed, 221 insertions(+) create mode 100644 python/packages/kagent-adk/src/kagent/adk/_logging.py create mode 100644 python/packages/kagent-adk/tests/unittests/test_logging_filter.py diff --git a/python/packages/kagent-adk/src/kagent/adk/__init__.py b/python/packages/kagent-adk/src/kagent/adk/__init__.py index 28c5caa58..f324dc50e 100644 --- a/python/packages/kagent-adk/src/kagent/adk/__init__.py +++ b/python/packages/kagent-adk/src/kagent/adk/__init__.py @@ -1,8 +1,18 @@ import importlib.metadata +import logging from ._a2a import KAgentApp +from ._logging import install_mcp_content_type_filter from .types import AgentConfig __version__ = importlib.metadata.version("kagent_adk") __all__ = ["KAgentApp", "AgentConfig"] + +# Suppress noisy "Unexpected content type" ERROR messages from the MCP +# streamable-HTTP client. These are emitted every heartbeat interval +# (typically 30 s) when the MCP Go server responds to ping-responses +# without a Content-Type header. The errors are harmless -- actual MCP +# tool calls are unaffected -- but they obscure real problems in the logs. +# See https://github.com/kagent-dev/kagent/issues/1200 +install_mcp_content_type_filter() diff --git a/python/packages/kagent-adk/src/kagent/adk/_logging.py b/python/packages/kagent-adk/src/kagent/adk/_logging.py new file mode 100644 index 000000000..4ae563bd1 --- /dev/null +++ b/python/packages/kagent-adk/src/kagent/adk/_logging.py @@ -0,0 +1,60 @@ +"""Logging filters for suppressing noisy MCP client messages. + +The MCP Python SDK's streamable-HTTP client logs an ERROR every time it +receives a response whose ``Content-Type`` header is empty or unexpected. +In kagent deployments the kagent-tools MCP server (built on ``mcp-go``) +returns ``200 OK`` **without** a ``Content-Type`` header when it handles +heartbeat ping-responses. Because the heartbeat fires every 30 seconds, +the resulting log spam makes it hard to spot real errors. + +This module installs a lightweight ``logging.Filter`` on the +``mcp.client.streamable_http`` logger that downgrades those specific +messages from ERROR to DEBUG, keeping the logs clean while still +preserving the information for anyone who enables DEBUG-level logging. + +Reference: https://github.com/kagent-dev/kagent/issues/1200 +""" + +from __future__ import annotations + +import logging + +_MCP_STREAMABLE_HTTP_LOGGER = "mcp.client.streamable_http" +_UNEXPECTED_CT_PREFIX = "Unexpected content type" + + +class _UnexpectedContentTypeFilter(logging.Filter): + """Downgrade 'Unexpected content type' ERROR messages to DEBUG. + + The filter intercepts log records from the ``mcp.client.streamable_http`` + logger. If the record is at ERROR level and its message starts with + ``"Unexpected content type"``, the level is lowered to DEBUG so that the + message is still emitted when debug logging is active but no longer + pollutes the default (INFO / WARNING / ERROR) output. + + All other records pass through unchanged. + """ + + def filter(self, record: logging.LogRecord) -> bool: + if ( + record.levelno == logging.ERROR + and isinstance(record.msg, str) + and record.msg.startswith(_UNEXPECTED_CT_PREFIX) + ): + record.levelno = logging.DEBUG + record.levelname = logging.getLevelName(logging.DEBUG) + return True + + +def install_mcp_content_type_filter() -> None: + """Install the filter on the ``mcp.client.streamable_http`` logger. + + This function is idempotent -- calling it multiple times will not add + duplicate filters. + """ + mcp_logger = logging.getLogger(_MCP_STREAMABLE_HTTP_LOGGER) + # Guard against duplicate installation. + for existing in mcp_logger.filters: + if isinstance(existing, _UnexpectedContentTypeFilter): + return + mcp_logger.addFilter(_UnexpectedContentTypeFilter()) diff --git a/python/packages/kagent-adk/tests/unittests/test_logging_filter.py b/python/packages/kagent-adk/tests/unittests/test_logging_filter.py new file mode 100644 index 000000000..7753ccd54 --- /dev/null +++ b/python/packages/kagent-adk/tests/unittests/test_logging_filter.py @@ -0,0 +1,151 @@ +"""Tests for the MCP content-type logging filter.""" + +import logging + +import pytest + +from kagent.adk._logging import ( + _MCP_STREAMABLE_HTTP_LOGGER, + _UnexpectedContentTypeFilter, + install_mcp_content_type_filter, +) + + +@pytest.fixture(autouse=True) +def _cleanup_filters(): + """Remove test filters after each test to avoid cross-test contamination.""" + mcp_logger = logging.getLogger(_MCP_STREAMABLE_HTTP_LOGGER) + original_filters = list(mcp_logger.filters) + yield + mcp_logger.filters = original_filters + + +class TestUnexpectedContentTypeFilter: + """Tests for _UnexpectedContentTypeFilter.""" + + def test_downgrades_unexpected_content_type_error_to_debug(self): + filt = _UnexpectedContentTypeFilter() + record = logging.LogRecord( + name=_MCP_STREAMABLE_HTTP_LOGGER, + level=logging.ERROR, + pathname="", + lineno=0, + msg="Unexpected content type: ", + args=(), + exc_info=None, + ) + assert filt.filter(record) is True + assert record.levelno == logging.DEBUG + assert record.levelname == "DEBUG" + + def test_downgrades_unexpected_content_type_with_value(self): + filt = _UnexpectedContentTypeFilter() + record = logging.LogRecord( + name=_MCP_STREAMABLE_HTTP_LOGGER, + level=logging.ERROR, + pathname="", + lineno=0, + msg="Unexpected content type: text/html", + args=(), + exc_info=None, + ) + assert filt.filter(record) is True + assert record.levelno == logging.DEBUG + + def test_does_not_affect_other_error_messages(self): + filt = _UnexpectedContentTypeFilter() + record = logging.LogRecord( + name=_MCP_STREAMABLE_HTTP_LOGGER, + level=logging.ERROR, + pathname="", + lineno=0, + msg="Some other error", + args=(), + exc_info=None, + ) + assert filt.filter(record) is True + assert record.levelno == logging.ERROR + assert record.levelname == "ERROR" + + def test_does_not_affect_non_error_levels(self): + filt = _UnexpectedContentTypeFilter() + record = logging.LogRecord( + name=_MCP_STREAMABLE_HTTP_LOGGER, + level=logging.WARNING, + pathname="", + lineno=0, + msg="Unexpected content type: ", + args=(), + exc_info=None, + ) + assert filt.filter(record) is True + assert record.levelno == logging.WARNING + + def test_always_returns_true(self): + """The filter should never suppress records, only downgrade the level.""" + filt = _UnexpectedContentTypeFilter() + + for level in (logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL): + record = logging.LogRecord( + name=_MCP_STREAMABLE_HTTP_LOGGER, + level=level, + pathname="", + lineno=0, + msg="Unexpected content type: ", + args=(), + exc_info=None, + ) + assert filt.filter(record) is True + + +class TestInstallMcpContentTypeFilter: + """Tests for install_mcp_content_type_filter.""" + + def test_installs_filter_on_mcp_logger(self): + mcp_logger = logging.getLogger(_MCP_STREAMABLE_HTTP_LOGGER) + # Remove any pre-existing filters (e.g., from kagent.adk.__init__) + mcp_logger.filters = [ + f for f in mcp_logger.filters if not isinstance(f, _UnexpectedContentTypeFilter) + ] + assert len( + [f for f in mcp_logger.filters if isinstance(f, _UnexpectedContentTypeFilter)] + ) == 0 + + install_mcp_content_type_filter() + + count = len( + [f for f in mcp_logger.filters if isinstance(f, _UnexpectedContentTypeFilter)] + ) + assert count == 1 + + def test_idempotent(self): + """Calling install_mcp_content_type_filter multiple times should not + add duplicate filters.""" + install_mcp_content_type_filter() + install_mcp_content_type_filter() + install_mcp_content_type_filter() + + mcp_logger = logging.getLogger(_MCP_STREAMABLE_HTTP_LOGGER) + count = len( + [f for f in mcp_logger.filters if isinstance(f, _UnexpectedContentTypeFilter)] + ) + assert count == 1 + + def test_integration_error_becomes_debug(self, caplog): + """End-to-end: an ERROR log from the MCP logger with the matching + message should appear at DEBUG level.""" + install_mcp_content_type_filter() + mcp_logger = logging.getLogger(_MCP_STREAMABLE_HTTP_LOGGER) + + with caplog.at_level(logging.DEBUG, logger=_MCP_STREAMABLE_HTTP_LOGGER): + mcp_logger.error("Unexpected content type: ") + + # The record should be present but at DEBUG level. + matching = [ + r + for r in caplog.records + if r.name == _MCP_STREAMABLE_HTTP_LOGGER + and "Unexpected content type" in r.message + ] + assert len(matching) == 1 + assert matching[0].levelno == logging.DEBUG From f12b4821d57e997280d3334141cc7b91c1bbfede Mon Sep 17 00:00:00 2001 From: opspawn Date: Mon, 16 Feb 2026 05:40:08 +0000 Subject: [PATCH 2/5] style: fix ruff format check in logging filter tests Signed-off-by: opspawn --- .../tests/unittests/test_logging_filter.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/python/packages/kagent-adk/tests/unittests/test_logging_filter.py b/python/packages/kagent-adk/tests/unittests/test_logging_filter.py index 7753ccd54..a0e9dd666 100644 --- a/python/packages/kagent-adk/tests/unittests/test_logging_filter.py +++ b/python/packages/kagent-adk/tests/unittests/test_logging_filter.py @@ -104,18 +104,12 @@ class TestInstallMcpContentTypeFilter: def test_installs_filter_on_mcp_logger(self): mcp_logger = logging.getLogger(_MCP_STREAMABLE_HTTP_LOGGER) # Remove any pre-existing filters (e.g., from kagent.adk.__init__) - mcp_logger.filters = [ - f for f in mcp_logger.filters if not isinstance(f, _UnexpectedContentTypeFilter) - ] - assert len( - [f for f in mcp_logger.filters if isinstance(f, _UnexpectedContentTypeFilter)] - ) == 0 + mcp_logger.filters = [f for f in mcp_logger.filters if not isinstance(f, _UnexpectedContentTypeFilter)] + assert len([f for f in mcp_logger.filters if isinstance(f, _UnexpectedContentTypeFilter)]) == 0 install_mcp_content_type_filter() - count = len( - [f for f in mcp_logger.filters if isinstance(f, _UnexpectedContentTypeFilter)] - ) + count = len([f for f in mcp_logger.filters if isinstance(f, _UnexpectedContentTypeFilter)]) assert count == 1 def test_idempotent(self): @@ -126,9 +120,7 @@ def test_idempotent(self): install_mcp_content_type_filter() mcp_logger = logging.getLogger(_MCP_STREAMABLE_HTTP_LOGGER) - count = len( - [f for f in mcp_logger.filters if isinstance(f, _UnexpectedContentTypeFilter)] - ) + count = len([f for f in mcp_logger.filters if isinstance(f, _UnexpectedContentTypeFilter)]) assert count == 1 def test_integration_error_becomes_debug(self, caplog): @@ -144,8 +136,7 @@ def test_integration_error_becomes_debug(self, caplog): matching = [ r for r in caplog.records - if r.name == _MCP_STREAMABLE_HTTP_LOGGER - and "Unexpected content type" in r.message + if r.name == _MCP_STREAMABLE_HTTP_LOGGER and "Unexpected content type" in r.message ] assert len(matching) == 1 assert matching[0].levelno == logging.DEBUG From 3bf413c17d0a9be98a6e26fb96687ac80dc7f647 Mon Sep 17 00:00:00 2001 From: opspawn Date: Mon, 16 Feb 2026 07:21:03 +0000 Subject: [PATCH 3/5] fix: remove unused import logging from __init__.py Signed-off-by: opspawn --- python/packages/kagent-adk/src/kagent/adk/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/packages/kagent-adk/src/kagent/adk/__init__.py b/python/packages/kagent-adk/src/kagent/adk/__init__.py index f324dc50e..9d25edf78 100644 --- a/python/packages/kagent-adk/src/kagent/adk/__init__.py +++ b/python/packages/kagent-adk/src/kagent/adk/__init__.py @@ -1,5 +1,4 @@ import importlib.metadata -import logging from ._a2a import KAgentApp from ._logging import install_mcp_content_type_filter From a982c24ffe5cee03f7307453df06cbec5162c359 Mon Sep 17 00:00:00 2001 From: opspawn Date: Mon, 16 Feb 2026 12:41:50 +0000 Subject: [PATCH 4/5] ci: retrigger CI workflow Signed-off-by: opspawn From 8baec6b8193b6be9f9a77055d3791a3279af7ccd Mon Sep 17 00:00:00 2001 From: opspawn Date: Mon, 16 Feb 2026 14:40:21 +0000 Subject: [PATCH 5/5] =?UTF-8?q?ci:=20retrigger=20e2e=20(flaky=20TestE2EInv?= =?UTF-8?q?okeInlineAgentWithStreaming=20=E2=80=94=20agent=20CRD=20race=20?= =?UTF-8?q?condition,=20unrelated=20to=20Python=20logging=20filter)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: opspawn