fix: align Context logging methods with MCP spec data type (#2366)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
This commit is contained in:
parent
437d15aa71
commit
3d7b311de0
4 changed files with 55 additions and 49 deletions
10
README.v2.md
10
README.v2.md
|
|
@ -681,11 +681,11 @@ The Context object provides the following capabilities:
|
|||
- `ctx.mcp_server` - Access to the MCPServer server instance (see [MCPServer Properties](#mcpserver-properties))
|
||||
- `ctx.session` - Access to the underlying session for advanced communication (see [Session Properties and Methods](#session-properties-and-methods))
|
||||
- `ctx.request_context` - Access to request-specific data and lifespan resources (see [Request Context Properties](#request-context-properties))
|
||||
- `await ctx.debug(message)` - Send debug log message
|
||||
- `await ctx.info(message)` - Send info log message
|
||||
- `await ctx.warning(message)` - Send warning log message
|
||||
- `await ctx.error(message)` - Send error log message
|
||||
- `await ctx.log(level, message, logger_name=None)` - Send log with custom level
|
||||
- `await ctx.debug(data)` - Send debug log message
|
||||
- `await ctx.info(data)` - Send info log message
|
||||
- `await ctx.warning(data)` - Send warning log message
|
||||
- `await ctx.error(data)` - Send error log message
|
||||
- `await ctx.log(level, data, logger_name=None)` - Send log with custom level
|
||||
- `await ctx.report_progress(progress, total=None, message=None)` - Report operation progress
|
||||
- `await ctx.read_resource(uri)` - Read a resource by URI
|
||||
- `await ctx.elicit(message, schema)` - Request additional information from user with validation
|
||||
|
|
|
|||
|
|
@ -467,6 +467,26 @@ mcp._lowlevel_server._add_request_handler("resources/subscribe", handle_subscrib
|
|||
|
||||
This is a private API and may change. A public way to register these handlers on `MCPServer` is planned; until then, use this workaround or use the lowlevel `Server` directly.
|
||||
|
||||
### `MCPServer`'s `Context` logging: `message` renamed to `data`, `extra` removed
|
||||
|
||||
On the high-level `Context` object (`mcp.server.mcpserver.Context`), `log()`, `.debug()`, `.info()`, `.warning()`, and `.error()` now take `data: Any` instead of `message: str`, matching the MCP spec's `LoggingMessageNotificationParams.data` field which allows any JSON-serializable value. The `extra` parameter has been removed — pass structured data directly as `data`.
|
||||
|
||||
The lowlevel `ServerSession.send_log_message(data: Any)` already accepted arbitrary data and is unchanged.
|
||||
|
||||
`Context.log()` also now accepts all eight RFC-5424 log levels (`debug`, `info`, `notice`, `warning`, `error`, `critical`, `alert`, `emergency`) via the `LoggingLevel` type, not just the four it previously allowed.
|
||||
|
||||
```python
|
||||
# Before
|
||||
await ctx.info("Connection failed", extra={"host": "localhost", "port": 5432})
|
||||
await ctx.log(level="info", message="hello")
|
||||
|
||||
# After
|
||||
await ctx.info({"message": "Connection failed", "host": "localhost", "port": 5432})
|
||||
await ctx.log(level="info", data="hello")
|
||||
```
|
||||
|
||||
Positional calls (`await ctx.info("hello")`) are unaffected.
|
||||
|
||||
### Replace `RootModel` by union types with `TypeAdapter` validation
|
||||
|
||||
The following union types are no longer `RootModel` subclasses:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING, Any, Generic, Literal
|
||||
from typing import TYPE_CHECKING, Any, Generic
|
||||
|
||||
from pydantic import AnyUrl, BaseModel
|
||||
|
||||
|
|
@ -14,6 +14,7 @@ from mcp.server.elicitation import (
|
|||
elicit_with_validation,
|
||||
)
|
||||
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
||||
from mcp.types import LoggingLevel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mcp.server.mcpserver.server import MCPServer
|
||||
|
|
@ -186,29 +187,23 @@ class Context(BaseModel, Generic[LifespanContextT, RequestT]):
|
|||
|
||||
async def log(
|
||||
self,
|
||||
level: Literal["debug", "info", "warning", "error"],
|
||||
message: str,
|
||||
level: LoggingLevel,
|
||||
data: Any,
|
||||
*,
|
||||
logger_name: str | None = None,
|
||||
extra: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Send a log message to the client.
|
||||
|
||||
Args:
|
||||
level: Log level (debug, info, warning, error)
|
||||
message: Log message
|
||||
level: Log level (debug, info, notice, warning, error, critical,
|
||||
alert, emergency)
|
||||
data: The data to be logged. Any JSON serializable type is allowed
|
||||
(string, dict, list, number, bool, etc.) per the MCP specification.
|
||||
logger_name: Optional logger name
|
||||
extra: Optional dictionary with additional structured data to include
|
||||
"""
|
||||
|
||||
if extra:
|
||||
log_data = {"message": message, **extra}
|
||||
else:
|
||||
log_data = message
|
||||
|
||||
await self.request_context.session.send_log_message(
|
||||
level=level,
|
||||
data=log_data,
|
||||
data=data,
|
||||
logger=logger_name,
|
||||
related_request_id=self.request_id,
|
||||
)
|
||||
|
|
@ -261,20 +256,18 @@ class Context(BaseModel, Generic[LifespanContextT, RequestT]):
|
|||
await self._request_context.close_standalone_sse_stream()
|
||||
|
||||
# Convenience methods for common log levels
|
||||
async def debug(self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None) -> None:
|
||||
async def debug(self, data: Any, *, logger_name: str | None = None) -> None:
|
||||
"""Send a debug log message."""
|
||||
await self.log("debug", message, logger_name=logger_name, extra=extra)
|
||||
await self.log("debug", data, logger_name=logger_name)
|
||||
|
||||
async def info(self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None) -> None:
|
||||
async def info(self, data: Any, *, logger_name: str | None = None) -> None:
|
||||
"""Send an info log message."""
|
||||
await self.log("info", message, logger_name=logger_name, extra=extra)
|
||||
await self.log("info", data, logger_name=logger_name)
|
||||
|
||||
async def warning(
|
||||
self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None
|
||||
) -> None:
|
||||
async def warning(self, data: Any, *, logger_name: str | None = None) -> None:
|
||||
"""Send a warning log message."""
|
||||
await self.log("warning", message, logger_name=logger_name, extra=extra)
|
||||
await self.log("warning", data, logger_name=logger_name)
|
||||
|
||||
async def error(self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None) -> None:
|
||||
async def error(self, data: Any, *, logger_name: str | None = None) -> None:
|
||||
"""Send an error log message."""
|
||||
await self.log("error", message, logger_name=logger_name, extra=extra)
|
||||
await self.log("error", data, logger_name=logger_name)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Any, Literal
|
||||
from typing import Literal
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -36,24 +36,20 @@ async def test_logging_callback():
|
|||
message: str, level: Literal["debug", "info", "warning", "error"], logger: str, ctx: Context
|
||||
) -> bool:
|
||||
"""Send a log notification to the client."""
|
||||
await ctx.log(level=level, message=message, logger_name=logger)
|
||||
await ctx.log(level=level, data=message, logger_name=logger)
|
||||
return True
|
||||
|
||||
@server.tool("test_tool_with_log_extra")
|
||||
async def test_tool_with_log_extra(
|
||||
message: str,
|
||||
@server.tool("test_tool_with_log_dict")
|
||||
async def test_tool_with_log_dict(
|
||||
level: Literal["debug", "info", "warning", "error"],
|
||||
logger: str,
|
||||
extra_string: str,
|
||||
extra_dict: dict[str, Any],
|
||||
ctx: Context,
|
||||
) -> bool:
|
||||
"""Send a log notification to the client with extra fields."""
|
||||
"""Send a log notification with a dict payload."""
|
||||
await ctx.log(
|
||||
level=level,
|
||||
message=message,
|
||||
data={"message": "Test log message", "extra_string": "example", "extra_dict": {"a": 1, "b": 2, "c": 3}},
|
||||
logger_name=logger,
|
||||
extra={"extra_string": extra_string, "extra_dict": extra_dict},
|
||||
)
|
||||
return True
|
||||
|
||||
|
|
@ -84,18 +80,15 @@ async def test_logging_callback():
|
|||
"logger": "test_logger",
|
||||
},
|
||||
)
|
||||
log_result_with_extra = await client.call_tool(
|
||||
"test_tool_with_log_extra",
|
||||
log_result_with_dict = await client.call_tool(
|
||||
"test_tool_with_log_dict",
|
||||
{
|
||||
"message": "Test log message",
|
||||
"level": "info",
|
||||
"logger": "test_logger",
|
||||
"extra_string": "example",
|
||||
"extra_dict": {"a": 1, "b": 2, "c": 3},
|
||||
},
|
||||
)
|
||||
assert log_result.is_error is False
|
||||
assert log_result_with_extra.is_error is False
|
||||
assert log_result_with_dict.is_error is False
|
||||
assert len(logging_collector.log_messages) == 2
|
||||
# Create meta object with related_request_id added dynamically
|
||||
log = logging_collector.log_messages[0]
|
||||
|
|
@ -103,10 +96,10 @@ async def test_logging_callback():
|
|||
assert log.logger == "test_logger"
|
||||
assert log.data == "Test log message"
|
||||
|
||||
log_with_extra = logging_collector.log_messages[1]
|
||||
assert log_with_extra.level == "info"
|
||||
assert log_with_extra.logger == "test_logger"
|
||||
assert log_with_extra.data == {
|
||||
log_with_dict = logging_collector.log_messages[1]
|
||||
assert log_with_dict.level == "info"
|
||||
assert log_with_dict.logger == "test_logger"
|
||||
assert log_with_dict.data == {
|
||||
"message": "Test log message",
|
||||
"extra_string": "example",
|
||||
"extra_dict": {"a": 1, "b": 2, "c": 3},
|
||||
|
|
|
|||
Loading…
Reference in a new issue