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.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.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))
|
- `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.debug(data)` - Send debug log message
|
||||||
- `await ctx.info(message)` - Send info log message
|
- `await ctx.info(data)` - Send info log message
|
||||||
- `await ctx.warning(message)` - Send warning log message
|
- `await ctx.warning(data)` - Send warning log message
|
||||||
- `await ctx.error(message)` - Send error log message
|
- `await ctx.error(data)` - Send error log message
|
||||||
- `await ctx.log(level, message, logger_name=None)` - Send log with custom level
|
- `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.report_progress(progress, total=None, message=None)` - Report operation progress
|
||||||
- `await ctx.read_resource(uri)` - Read a resource by URI
|
- `await ctx.read_resource(uri)` - Read a resource by URI
|
||||||
- `await ctx.elicit(message, schema)` - Request additional information from user with validation
|
- `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.
|
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
|
### Replace `RootModel` by union types with `TypeAdapter` validation
|
||||||
|
|
||||||
The following union types are no longer `RootModel` subclasses:
|
The following union types are no longer `RootModel` subclasses:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Iterable
|
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
|
from pydantic import AnyUrl, BaseModel
|
||||||
|
|
||||||
|
|
@ -14,6 +14,7 @@ from mcp.server.elicitation import (
|
||||||
elicit_with_validation,
|
elicit_with_validation,
|
||||||
)
|
)
|
||||||
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
||||||
|
from mcp.types import LoggingLevel
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mcp.server.mcpserver.server import MCPServer
|
from mcp.server.mcpserver.server import MCPServer
|
||||||
|
|
@ -186,29 +187,23 @@ class Context(BaseModel, Generic[LifespanContextT, RequestT]):
|
||||||
|
|
||||||
async def log(
|
async def log(
|
||||||
self,
|
self,
|
||||||
level: Literal["debug", "info", "warning", "error"],
|
level: LoggingLevel,
|
||||||
message: str,
|
data: Any,
|
||||||
*,
|
*,
|
||||||
logger_name: str | None = None,
|
logger_name: str | None = None,
|
||||||
extra: dict[str, Any] | None = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Send a log message to the client.
|
"""Send a log message to the client.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
level: Log level (debug, info, warning, error)
|
level: Log level (debug, info, notice, warning, error, critical,
|
||||||
message: Log message
|
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
|
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(
|
await self.request_context.session.send_log_message(
|
||||||
level=level,
|
level=level,
|
||||||
data=log_data,
|
data=data,
|
||||||
logger=logger_name,
|
logger=logger_name,
|
||||||
related_request_id=self.request_id,
|
related_request_id=self.request_id,
|
||||||
)
|
)
|
||||||
|
|
@ -261,20 +256,18 @@ class Context(BaseModel, Generic[LifespanContextT, RequestT]):
|
||||||
await self._request_context.close_standalone_sse_stream()
|
await self._request_context.close_standalone_sse_stream()
|
||||||
|
|
||||||
# Convenience methods for common log levels
|
# 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."""
|
"""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."""
|
"""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(
|
async def warning(self, data: Any, *, logger_name: str | None = None) -> None:
|
||||||
self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None
|
|
||||||
) -> None:
|
|
||||||
"""Send a warning log message."""
|
"""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."""
|
"""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
|
import pytest
|
||||||
|
|
||||||
|
|
@ -36,24 +36,20 @@ async def test_logging_callback():
|
||||||
message: str, level: Literal["debug", "info", "warning", "error"], logger: str, ctx: Context
|
message: str, level: Literal["debug", "info", "warning", "error"], logger: str, ctx: Context
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Send a log notification to the client."""
|
"""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
|
return True
|
||||||
|
|
||||||
@server.tool("test_tool_with_log_extra")
|
@server.tool("test_tool_with_log_dict")
|
||||||
async def test_tool_with_log_extra(
|
async def test_tool_with_log_dict(
|
||||||
message: str,
|
|
||||||
level: Literal["debug", "info", "warning", "error"],
|
level: Literal["debug", "info", "warning", "error"],
|
||||||
logger: str,
|
logger: str,
|
||||||
extra_string: str,
|
|
||||||
extra_dict: dict[str, Any],
|
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Send a log notification to the client with extra fields."""
|
"""Send a log notification with a dict payload."""
|
||||||
await ctx.log(
|
await ctx.log(
|
||||||
level=level,
|
level=level,
|
||||||
message=message,
|
data={"message": "Test log message", "extra_string": "example", "extra_dict": {"a": 1, "b": 2, "c": 3}},
|
||||||
logger_name=logger,
|
logger_name=logger,
|
||||||
extra={"extra_string": extra_string, "extra_dict": extra_dict},
|
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -84,18 +80,15 @@ async def test_logging_callback():
|
||||||
"logger": "test_logger",
|
"logger": "test_logger",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
log_result_with_extra = await client.call_tool(
|
log_result_with_dict = await client.call_tool(
|
||||||
"test_tool_with_log_extra",
|
"test_tool_with_log_dict",
|
||||||
{
|
{
|
||||||
"message": "Test log message",
|
|
||||||
"level": "info",
|
"level": "info",
|
||||||
"logger": "test_logger",
|
"logger": "test_logger",
|
||||||
"extra_string": "example",
|
|
||||||
"extra_dict": {"a": 1, "b": 2, "c": 3},
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert log_result.is_error is False
|
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
|
assert len(logging_collector.log_messages) == 2
|
||||||
# Create meta object with related_request_id added dynamically
|
# Create meta object with related_request_id added dynamically
|
||||||
log = logging_collector.log_messages[0]
|
log = logging_collector.log_messages[0]
|
||||||
|
|
@ -103,10 +96,10 @@ async def test_logging_callback():
|
||||||
assert log.logger == "test_logger"
|
assert log.logger == "test_logger"
|
||||||
assert log.data == "Test log message"
|
assert log.data == "Test log message"
|
||||||
|
|
||||||
log_with_extra = logging_collector.log_messages[1]
|
log_with_dict = logging_collector.log_messages[1]
|
||||||
assert log_with_extra.level == "info"
|
assert log_with_dict.level == "info"
|
||||||
assert log_with_extra.logger == "test_logger"
|
assert log_with_dict.logger == "test_logger"
|
||||||
assert log_with_extra.data == {
|
assert log_with_dict.data == {
|
||||||
"message": "Test log message",
|
"message": "Test log message",
|
||||||
"extra_string": "example",
|
"extra_string": "example",
|
||||||
"extra_dict": {"a": 1, "b": 2, "c": 3},
|
"extra_dict": {"a": 1, "b": 2, "c": 3},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue