Skip to content

Commit 3d7b311

Browse files
paikendclaudemaxisbey
authored
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>
1 parent 437d15a commit 3d7b311

File tree

4 files changed

+55
-49
lines changed

4 files changed

+55
-49
lines changed

README.v2.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -681,11 +681,11 @@ The Context object provides the following capabilities:
681681
- `ctx.mcp_server` - Access to the MCPServer server instance (see [MCPServer Properties](#mcpserver-properties))
682682
- `ctx.session` - Access to the underlying session for advanced communication (see [Session Properties and Methods](#session-properties-and-methods))
683683
- `ctx.request_context` - Access to request-specific data and lifespan resources (see [Request Context Properties](#request-context-properties))
684-
- `await ctx.debug(message)` - Send debug log message
685-
- `await ctx.info(message)` - Send info log message
686-
- `await ctx.warning(message)` - Send warning log message
687-
- `await ctx.error(message)` - Send error log message
688-
- `await ctx.log(level, message, logger_name=None)` - Send log with custom level
684+
- `await ctx.debug(data)` - Send debug log message
685+
- `await ctx.info(data)` - Send info log message
686+
- `await ctx.warning(data)` - Send warning log message
687+
- `await ctx.error(data)` - Send error log message
688+
- `await ctx.log(level, data, logger_name=None)` - Send log with custom level
689689
- `await ctx.report_progress(progress, total=None, message=None)` - Report operation progress
690690
- `await ctx.read_resource(uri)` - Read a resource by URI
691691
- `await ctx.elicit(message, schema)` - Request additional information from user with validation

docs/migration.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,26 @@ mcp._lowlevel_server._add_request_handler("resources/subscribe", handle_subscrib
467467

468468
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.
469469

470+
### `MCPServer`'s `Context` logging: `message` renamed to `data`, `extra` removed
471+
472+
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`.
473+
474+
The lowlevel `ServerSession.send_log_message(data: Any)` already accepted arbitrary data and is unchanged.
475+
476+
`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.
477+
478+
```python
479+
# Before
480+
await ctx.info("Connection failed", extra={"host": "localhost", "port": 5432})
481+
await ctx.log(level="info", message="hello")
482+
483+
# After
484+
await ctx.info({"message": "Connection failed", "host": "localhost", "port": 5432})
485+
await ctx.log(level="info", data="hello")
486+
```
487+
488+
Positional calls (`await ctx.info("hello")`) are unaffected.
489+
470490
### Replace `RootModel` by union types with `TypeAdapter` validation
471491

472492
The following union types are no longer `RootModel` subclasses:

src/mcp/server/mcpserver/context.py

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from collections.abc import Iterable
4-
from typing import TYPE_CHECKING, Any, Generic, Literal
4+
from typing import TYPE_CHECKING, Any, Generic
55

66
from pydantic import AnyUrl, BaseModel
77

@@ -14,6 +14,7 @@
1414
elicit_with_validation,
1515
)
1616
from mcp.server.lowlevel.helper_types import ReadResourceContents
17+
from mcp.types import LoggingLevel
1718

1819
if TYPE_CHECKING:
1920
from mcp.server.mcpserver.server import MCPServer
@@ -186,29 +187,23 @@ async def elicit_url(
186187

187188
async def log(
188189
self,
189-
level: Literal["debug", "info", "warning", "error"],
190-
message: str,
190+
level: LoggingLevel,
191+
data: Any,
191192
*,
192193
logger_name: str | None = None,
193-
extra: dict[str, Any] | None = None,
194194
) -> None:
195195
"""Send a log message to the client.
196196
197197
Args:
198-
level: Log level (debug, info, warning, error)
199-
message: Log message
198+
level: Log level (debug, info, notice, warning, error, critical,
199+
alert, emergency)
200+
data: The data to be logged. Any JSON serializable type is allowed
201+
(string, dict, list, number, bool, etc.) per the MCP specification.
200202
logger_name: Optional logger name
201-
extra: Optional dictionary with additional structured data to include
202203
"""
203-
204-
if extra:
205-
log_data = {"message": message, **extra}
206-
else:
207-
log_data = message
208-
209204
await self.request_context.session.send_log_message(
210205
level=level,
211-
data=log_data,
206+
data=data,
212207
logger=logger_name,
213208
related_request_id=self.request_id,
214209
)
@@ -261,20 +256,18 @@ async def close_standalone_sse_stream(self) -> None:
261256
await self._request_context.close_standalone_sse_stream()
262257

263258
# Convenience methods for common log levels
264-
async def debug(self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None) -> None:
259+
async def debug(self, data: Any, *, logger_name: str | None = None) -> None:
265260
"""Send a debug log message."""
266-
await self.log("debug", message, logger_name=logger_name, extra=extra)
261+
await self.log("debug", data, logger_name=logger_name)
267262

268-
async def info(self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None) -> None:
263+
async def info(self, data: Any, *, logger_name: str | None = None) -> None:
269264
"""Send an info log message."""
270-
await self.log("info", message, logger_name=logger_name, extra=extra)
265+
await self.log("info", data, logger_name=logger_name)
271266

272-
async def warning(
273-
self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None
274-
) -> None:
267+
async def warning(self, data: Any, *, logger_name: str | None = None) -> None:
275268
"""Send a warning log message."""
276-
await self.log("warning", message, logger_name=logger_name, extra=extra)
269+
await self.log("warning", data, logger_name=logger_name)
277270

278-
async def error(self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None) -> None:
271+
async def error(self, data: Any, *, logger_name: str | None = None) -> None:
279272
"""Send an error log message."""
280-
await self.log("error", message, logger_name=logger_name, extra=extra)
273+
await self.log("error", data, logger_name=logger_name)

tests/client/test_logging_callback.py

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Literal
1+
from typing import Literal
22

33
import pytest
44

@@ -36,24 +36,20 @@ async def test_tool_with_log(
3636
message: str, level: Literal["debug", "info", "warning", "error"], logger: str, ctx: Context
3737
) -> bool:
3838
"""Send a log notification to the client."""
39-
await ctx.log(level=level, message=message, logger_name=logger)
39+
await ctx.log(level=level, data=message, logger_name=logger)
4040
return True
4141

42-
@server.tool("test_tool_with_log_extra")
43-
async def test_tool_with_log_extra(
44-
message: str,
42+
@server.tool("test_tool_with_log_dict")
43+
async def test_tool_with_log_dict(
4544
level: Literal["debug", "info", "warning", "error"],
4645
logger: str,
47-
extra_string: str,
48-
extra_dict: dict[str, Any],
4946
ctx: Context,
5047
) -> bool:
51-
"""Send a log notification to the client with extra fields."""
48+
"""Send a log notification with a dict payload."""
5249
await ctx.log(
5350
level=level,
54-
message=message,
51+
data={"message": "Test log message", "extra_string": "example", "extra_dict": {"a": 1, "b": 2, "c": 3}},
5552
logger_name=logger,
56-
extra={"extra_string": extra_string, "extra_dict": extra_dict},
5753
)
5854
return True
5955

@@ -84,29 +80,26 @@ async def message_handler(
8480
"logger": "test_logger",
8581
},
8682
)
87-
log_result_with_extra = await client.call_tool(
88-
"test_tool_with_log_extra",
83+
log_result_with_dict = await client.call_tool(
84+
"test_tool_with_log_dict",
8985
{
90-
"message": "Test log message",
9186
"level": "info",
9287
"logger": "test_logger",
93-
"extra_string": "example",
94-
"extra_dict": {"a": 1, "b": 2, "c": 3},
9588
},
9689
)
9790
assert log_result.is_error is False
98-
assert log_result_with_extra.is_error is False
91+
assert log_result_with_dict.is_error is False
9992
assert len(logging_collector.log_messages) == 2
10093
# Create meta object with related_request_id added dynamically
10194
log = logging_collector.log_messages[0]
10295
assert log.level == "info"
10396
assert log.logger == "test_logger"
10497
assert log.data == "Test log message"
10598

106-
log_with_extra = logging_collector.log_messages[1]
107-
assert log_with_extra.level == "info"
108-
assert log_with_extra.logger == "test_logger"
109-
assert log_with_extra.data == {
99+
log_with_dict = logging_collector.log_messages[1]
100+
assert log_with_dict.level == "info"
101+
assert log_with_dict.logger == "test_logger"
102+
assert log_with_dict.data == {
110103
"message": "Test log message",
111104
"extra_string": "example",
112105
"extra_dict": {"a": 1, "b": 2, "c": 3},

0 commit comments

Comments
 (0)