From fca339084b2e6b79173bad1a6fe2fd8b3c774e2f Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 26 Jul 2026 14:39:31 -0400 Subject: [PATCH] Era-gate client.set_logging_level on modern connections --- fastmcp_slim/fastmcp/client/client.py | 17 ++++++++++++++++- tests/server/test_protocol_eras.py | 22 ++++++++-------------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/fastmcp_slim/fastmcp/client/client.py b/fastmcp_slim/fastmcp/client/client.py index 0bf9b9b54..42192830a 100644 --- a/fastmcp_slim/fastmcp/client/client.py +++ b/fastmcp_slim/fastmcp/client/client.py @@ -1351,7 +1351,22 @@ class Client( ) async def set_logging_level(self, level: mcp_types.LoggingLevel) -> None: - """Send a logging/setLevel request.""" + """Send a logging/setLevel request. + + Handshake-era servers only. `logging/setLevel` asks the server to + remember a level for the rest of the session, and the 2026-07-28 + protocol has no session to remember it in — the method is absent from + that era's registry. Log *notifications* are unaffected: they ride the + request's own stream, so a server's `ctx.info()` still reaches you. + Filter by level on the receiving side instead, in your `log_handler`. + """ + if self.protocol_version in MODERN_PROTOCOL_VERSIONS: + raise RuntimeError( + "logging/setLevel is not available on MCP 2026-07-28 " + "connections; the method requires per-session server state that " + "the modern protocol does not have. Filter incoming log " + "messages by level in your log_handler instead." + ) # Deprecated upstream in SDK v2 but deliberately kept per compat directive; # removed with the multi-round-trip follow-up. await self._await_with_session_monitoring( diff --git a/tests/server/test_protocol_eras.py b/tests/server/test_protocol_eras.py index 47bb0ce75..90cae9bb4 100644 --- a/tests/server/test_protocol_eras.py +++ b/tests/server/test_protocol_eras.py @@ -353,23 +353,17 @@ async def test_session_id_access_does_not_crash_on_modern(sessionless_server, mo @pytest.mark.parametrize("mode", MODERN_MODES) -async def test_set_logging_level_does_not_crash_on_modern(sessionless_server, mode): - """logging/setLevel is a session-id-keyed, deprecated-at-2026 operation. - On a sessionless modern in-memory connection it must degrade cleanly (either - succeed as a no-op or raise a surfaced MCPError) rather than crash the - connection. Characterization: capture whichever the current contract is. +async def test_set_logging_level_is_era_gated_on_modern(sessionless_server, mode): + """`logging/setLevel` asks a server to remember a level for the session, and + the modern era has no session — the method is absent from its registry. The + FastMCP client says so plainly instead of no-opping or surfacing the SDK's + opaque "Method not found", and the connection stays usable afterward. """ - async with SDKClient(_server(sessionless_server), mode=mode) as client: - outcome: str - try: - await client.set_logging_level("debug") # ty: ignore[deprecated] - outcome = "ok" - except MCPError: - outcome = "mcperror" - # Either way the connection is still usable afterward. + async with FastMCPClient(sessionless_server, mode=mode) as client: + with pytest.raises(RuntimeError, match="2026-07-28"): + await client.set_logging_level("debug") result = await client.call_tool("read_session_id", {}) assert result.is_error is False - assert outcome in {"ok", "mcperror"} # ---------------------------------------------------------------------------