"""Tests for MCP server log notification handling (port of anomalyco/opencode#34529). MCP servers can emit ``notifications/message`` logging notifications (RFC 5424 syslog levels). The MCP SDK's default ``logging_callback`` silently discards them; Hermes now passes ``_make_logging_callback()`` to ``ClientSession`` so server-side diagnostics land in agent.log, tagged with the server name. """ import logging from types import SimpleNamespace import pytest from tools.mcp_tool import ( _MCP_LOG_LEVEL_MAP, _MCP_LOGGING_CALLBACK_SUPPORTED, MCPServerTask, ) def _params(level="info", data="hello", logger_name=None): return SimpleNamespace(level=level, data=data, logger=logger_name) class TestLogLevelMap: def test_all_mcp_levels_mapped(self): # MCP spec (RFC 5424) defines these eight levels. for lvl in ("debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"): assert lvl in _MCP_LOG_LEVEL_MAP def test_severity_ordering(self): assert _MCP_LOG_LEVEL_MAP["debug"] == logging.DEBUG assert _MCP_LOG_LEVEL_MAP["notice"] == logging.INFO assert _MCP_LOG_LEVEL_MAP["warning"] == logging.WARNING assert _MCP_LOG_LEVEL_MAP["emergency"] == logging.ERROR class TestLoggingCallback: @pytest.mark.asyncio async def test_routes_to_hermes_logger_with_server_tag(self, caplog): server = MCPServerTask("log_srv") callback = server._make_logging_callback() with caplog.at_level(logging.INFO, logger="tools.mcp_tool"): await callback(_params(level="info", data="server started")) assert any( "MCP server log [log_srv]: server started" in rec.getMessage() for rec in caplog.records ) @pytest.mark.asyncio async def test_includes_sub_logger_name(self, caplog): server = MCPServerTask("log_srv") callback = server._make_logging_callback() with caplog.at_level(logging.WARNING, logger="tools.mcp_tool"): await callback(_params(level="warning", data="rate limited", logger_name="http")) assert any( "MCP server log [log_srv/http]: rate limited" in rec.getMessage() and rec.levelno == logging.WARNING for rec in caplog.records ) @pytest.mark.asyncio async def test_handler_never_raises(self): server = MCPServerTask("log_srv") callback = server._make_logging_callback() # A params object missing every attribute must not blow up the # SDK's notification dispatch loop. await callback(object()) class TestSDKSupportGate: def test_current_sdk_supports_logging_callback(self): # The pinned MCP SDK in this repo supports logging_callback; if this # starts failing after an SDK downgrade the feature silently degrades # (by design), but we want to know. import inspect from mcp import ClientSession expected = "logging_callback" in inspect.signature(ClientSession).parameters assert _MCP_LOGGING_CALLBACK_SUPPORTED == expected