Skip to content
Open
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
32 changes: 22 additions & 10 deletions src/mcp/server/fastmcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1206,18 +1206,28 @@ async def log(
message: str,
*,
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
logger_name: Optional logger name
**extra: Additional structured data to include
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=message,
data=log_data,
logger=logger_name,
related_request_id=self.request_id,
)
Expand All @@ -1238,18 +1248,20 @@ def session(self):
return self.request_context.session

# Convenience methods for common log levels
async def debug(self, message: str, **extra: Any) -> None:
async def debug(self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None) -> None:
"""Send a debug log message."""
await self.log("debug", message, **extra)
await self.log("debug", message, logger_name=logger_name, extra=extra)

async def info(self, message: str, **extra: Any) -> None:
async def info(self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None) -> None:
"""Send an info log message."""
await self.log("info", message, **extra)
await self.log("info", message, logger_name=logger_name, extra=extra)

async def warning(self, message: str, **extra: Any) -> None:
async def warning(
self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None
) -> None:
"""Send a warning log message."""
await self.log("warning", message, **extra)
await self.log("warning", message, logger_name=logger_name, extra=extra)

async def error(self, message: str, **extra: Any) -> None:
async def error(self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None) -> None:
"""Send an error log message."""
await self.log("error", message, **extra)
await self.log("error", message, logger_name=logger_name, extra=extra)
41 changes: 39 additions & 2 deletions tests/client/test_logging_callback.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Literal
from typing import Any, Literal

import pytest

Expand Down Expand Up @@ -47,6 +47,23 @@ async def test_tool_with_log(
)
return True

@server.tool("test_tool_with_log_extra")
async def test_tool_with_log_extra(
message: str,
level: Literal["debug", "info", "warning", "error"],
logger: str,
extra_string: str,
extra_dict: dict[str, Any],
) -> bool:
"""Send a log notification to the client with extra fields."""
await server.get_context().log(
level=level,
message=message,
logger_name=logger,
extra={"extra_string": extra_string, "extra_dict": extra_dict},
)
return True

# Create a message handler to catch exceptions
async def message_handler(
message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
Expand Down Expand Up @@ -74,10 +91,30 @@ async def message_handler(
"logger": "test_logger",
},
)
log_result_with_extra = await client_session.call_tool(
"test_tool_with_log_extra",
{
"message": "Test log message",
"level": "info",
"logger": "test_logger",
"extra_string": "example",
"extra_dict": {"a": 1, "b": 2, "c": 3},
},
)
assert log_result.isError is False
assert len(logging_collector.log_messages) == 1
assert log_result_with_extra.isError 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 == {
"message": "Test log message",
"extra_string": "example",
"extra_dict": {"a": 1, "b": 2, "c": 3},
}