"""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, 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. # # Read the flag off the module AFTER _ensure_mcp_sdk() — the SDK # import (and therefore this flag) is lazy since the startup-latency # work, so a by-value module-level import would freeze the pre-bind # False and never observe the real support state. import inspect from mcp import ClientSession from tools import mcp_tool mcp_tool._ensure_mcp_sdk() expected = "logging_callback" in inspect.signature(ClientSession).parameters assert mcp_tool._MCP_LOGGING_CALLBACK_SUPPORTED == expected