Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
41 changes: 17 additions & 24 deletions src/mcp/server/mcpserver/context.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -14,6 +14,7 @@
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
Expand Down Expand Up @@ -186,29 +187,23 @@ async def elicit_url(

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,
)
Expand Down Expand Up @@ -261,20 +256,18 @@ async def close_standalone_sse_stream(self) -> None:
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)
33 changes: 13 additions & 20 deletions tests/client/test_logging_callback.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Literal
from typing import Literal

import pytest

Expand Down Expand Up @@ -36,24 +36,20 @@ async def test_tool_with_log(
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

Expand Down Expand Up @@ -84,29 +80,26 @@ async def message_handler(
"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]
assert log.level == "info"
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},
Expand Down
Loading