Gate log notifications on the per-request log-level opt-in at 2026-07-28 (#3198)
This commit is contained in:
parent
b61ce388dd
commit
89c5e700f2
1 changed files with 406 additions and 31 deletions
|
|
@ -8,8 +8,16 @@ streamable HTTP, so plain-list collection is deterministic on every transport le
|
|||
import mcp_types as types
|
||||
import pytest
|
||||
from inline_snapshot import snapshot
|
||||
from mcp_types import CallToolResult, EmptyResult, LoggingMessageNotificationParams, TextContent
|
||||
from mcp_types import (
|
||||
INVALID_PARAMS,
|
||||
LOG_LEVEL_META_KEY,
|
||||
CallToolResult,
|
||||
EmptyResult,
|
||||
LoggingMessageNotificationParams,
|
||||
TextContent,
|
||||
)
|
||||
|
||||
from mcp import MCPError
|
||||
from mcp.server import Server, ServerRequestContext
|
||||
from tests._stamp import Unstamp
|
||||
from tests.interaction._connect import Connect
|
||||
|
|
@ -81,7 +89,7 @@ async def test_log_messages_reach_logging_callback_in_order(connect: Connect, un
|
|||
"logger", on_list_tools=list_tools, on_call_tool=call_tool, on_set_logging_level=set_logging_level
|
||||
)
|
||||
|
||||
async with connect(server, logging_callback=collect) as client:
|
||||
async with connect(server, logging_callback=collect, log_level="debug") as client:
|
||||
result = await client.call_tool("chatty", {})
|
||||
|
||||
assert unstamped(result) == snapshot(CallToolResult(content=[TextContent(text="done")]))
|
||||
|
|
@ -122,7 +130,63 @@ async def test_log_messages_at_every_severity_level(connect: Connect) -> None:
|
|||
"logger", on_list_tools=list_tools, on_call_tool=call_tool, on_set_logging_level=set_logging_level
|
||||
)
|
||||
|
||||
async with connect(server, logging_callback=collect) as client:
|
||||
async with connect(server, logging_callback=collect, log_level="debug") as client:
|
||||
await client.call_tool("siren", {})
|
||||
|
||||
assert [params.level for params in received] == list(ALL_LEVELS)
|
||||
|
||||
|
||||
def _siren_server() -> Server:
|
||||
"""A server whose `siren` tool logs one message at each of the eight severity levels.
|
||||
|
||||
The messages are sent without `related_request_id`: on 2026-07-28+ log delivery is
|
||||
request-scoped by construction, so they still ride the requesting stream on every leg.
|
||||
"""
|
||||
|
||||
async def list_tools(
|
||||
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
|
||||
) -> types.ListToolsResult:
|
||||
return types.ListToolsResult(tools=[types.Tool(name="siren", input_schema={"type": "object"})])
|
||||
|
||||
async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> CallToolResult:
|
||||
assert params.name == "siren"
|
||||
for level in ALL_LEVELS:
|
||||
await ctx.session.send_log_message(level=level, data=f"a {level} message") # pyright: ignore[reportDeprecated]
|
||||
return CallToolResult(content=[TextContent(text="logged")])
|
||||
|
||||
return Server("logger", on_list_tools=list_tools, on_call_tool=call_tool)
|
||||
|
||||
|
||||
@requirement("logging:per-request:opt-in")
|
||||
@requirement("logging:per-request:threshold")
|
||||
async def test_log_delivery_follows_the_per_request_log_level(connect: Connect) -> None:
|
||||
"""Without io.modelcontextprotocol/logLevel in _meta a request gets no log messages;
|
||||
with it, only entries at or above the requested level are delivered, in order.
|
||||
|
||||
The handler emits at every severity in both phases: the un-opted request receives
|
||||
nothing (the log calls are dropped, not delivered on some other stream), and the request
|
||||
opting in at `warning` receives warning and above.
|
||||
"""
|
||||
received: list[types.LoggingLevel] = []
|
||||
|
||||
async def collect(params: LoggingMessageNotificationParams) -> None:
|
||||
received.append(params.level)
|
||||
|
||||
async with connect(_siren_server(), logging_callback=collect) as client:
|
||||
result = await client.call_tool("siren", {})
|
||||
assert isinstance(result.content[0], TextContent) and result.content[0].text == "logged"
|
||||
assert received == []
|
||||
|
||||
async with connect(_siren_server(), logging_callback=collect, log_level="warning") as client:
|
||||
await client.call_tool("siren", {})
|
||||
assert received == ["warning", "error", "critical", "alert", "emergency"]
|
||||
|
||||
|
||||
@requirement("logging:per-request:invalid-level")
|
||||
async def test_a_request_with_an_unrecognized_log_level_is_rejected(connect: Connect) -> None:
|
||||
"""A request whose _meta names an unrecognized log level is rejected with -32602 before the handler runs."""
|
||||
async with connect(_siren_server()) as client:
|
||||
with pytest.raises(MCPError) as exc_info:
|
||||
await client.call_tool("siren", {}, meta={LOG_LEVEL_META_KEY: "verbose"})
|
||||
|
||||
assert exc_info.value.error.code == INVALID_PARAMS
|
||||
|
|
|
|||
Loading…
Reference in a new issue