Add log tests

This commit is contained in:
Jeremiah Lowin 2025-04-30 11:02:49 -04:00
commit 5443cf59aa
3 changed files with 74 additions and 49 deletions

View file

@ -1,7 +1,8 @@
from __future__ import annotations as _annotations
from typing import Any, Generic, Literal
from typing import Any, Generic
from mcp import LoggingLevel
from mcp.server.lowlevel.helper_types import ReadResourceContents
from mcp.server.session import ServerSessionT
from mcp.shared.context import LifespanContextT, RequestContext
@ -122,19 +123,20 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
async def log(
self,
level: Literal["debug", "info", "warning", "error"],
message: str,
*,
level: LoggingLevel | None = None,
logger_name: str | None = None,
) -> None:
"""Send a log message to the client.
Args:
level: Log level (debug, info, warning, error)
message: Log message
level: Optional log level. One of "debug", "info", "notice", "warning", "error", "critical",
"alert", or "emergency". Default is "info".
logger_name: Optional logger name
**extra: Additional structured data to include
"""
if level is None:
level = "info"
await self.request_context.session.send_log_message(
level=level, data=message, logger=logger_name
)
@ -161,19 +163,19 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
# Convenience methods for common log levels
async def debug(self, message: str, **extra: Any) -> None:
"""Send a debug log message."""
await self.log("debug", message, **extra)
await self.log(level="debug", message=message, **extra)
async def info(self, message: str, **extra: Any) -> None:
"""Send an info log message."""
await self.log("info", message, **extra)
await self.log(level="info", message=message, **extra)
async def warning(self, message: str, **extra: Any) -> None:
"""Send a warning log message."""
await self.log("warning", message, **extra)
await self.log(level="warning", message=message, **extra)
async def error(self, message: str, **extra: Any) -> None:
"""Send an error log message."""
await self.log("error", message, **extra)
await self.log(level="error", message=message, **extra)
async def list_roots(self) -> list[Root]:
"""List the roots available to the server, as indicated by the client."""

60
tests/client/test_logs.py Normal file
View file

@ -0,0 +1,60 @@
import pytest
from mcp import LoggingLevel
from fastmcp import Client, Context, FastMCP
from fastmcp.client.logging import LogMessage
class LogHandler:
def __init__(self):
self.logs: list[LogMessage] = []
async def handle_log(self, params: LogMessage) -> None:
self.logs.append(params)
@pytest.fixture
def fastmcp_server():
mcp = FastMCP()
@mcp.tool()
async def log(context: Context) -> None:
await context.log(message="hello?")
@mcp.tool()
async def echo_log(
message: str,
context: Context,
level: LoggingLevel | None = None,
logger: str | None = None,
) -> None:
await context.log(message=message, level=level)
return mcp
class TestClientLogs:
async def test_log(self, fastmcp_server: FastMCP):
log_handler = LogHandler()
async with Client(fastmcp_server, log_handler=log_handler.handle_log) as client:
await client.call_tool("log", {})
assert len(log_handler.logs) == 1
assert log_handler.logs[0].data == "hello?"
assert log_handler.logs[0].level == "info"
async def test_echo_log(self, fastmcp_server: FastMCP):
log_handler = LogHandler()
async with Client(fastmcp_server, log_handler=log_handler.handle_log) as client:
await client.call_tool("echo_log", {"message": "this is a log"})
assert len(log_handler.logs) == 1
await client.call_tool(
"echo_log", {"message": "this is a warning log", "level": "warning"}
)
assert len(log_handler.logs) == 2
assert log_handler.logs[0].data == "this is a log"
assert log_handler.logs[0].level == "info"
assert log_handler.logs[1].data == "this is a warning log"
assert log_handler.logs[1].level == "warning"

View file

@ -689,10 +689,10 @@ class TestToolContextInjection:
"""Test that context parameters are properly detected."""
mcp = FastMCP()
@mcp.tool()
def tool_with_context(x: int, ctx: Context) -> str:
return f"Request {ctx.request_id}: {x}"
mcp.add_tool(tool_with_context)
async with Client(mcp) as client:
tools = await client.list_tools()
assert len(tools) == 1
@ -719,11 +719,11 @@ class TestToolContextInjection:
"""Test that context works in async functions."""
mcp = FastMCP()
@mcp.tool()
async def async_tool(x: int, ctx: Context) -> str:
assert ctx.request_id is not None
return f"Async request {ctx.request_id}: {x}"
mcp.add_tool(async_tool)
async with Client(mcp) as client:
result = await client.call_tool("async_tool", {"x": 42})
assert len(result) == 1
@ -732,51 +732,14 @@ class TestToolContextInjection:
assert "Async request" in content.text
assert "42" in content.text
async def test_context_logging(self):
from unittest.mock import patch
import mcp.server.session
"""Test that context logging methods work."""
mcp = FastMCP()
async def logging_tool(msg: str, ctx: Context) -> str:
await ctx.debug("Debug message")
await ctx.info("Info message")
await ctx.warning("Warning message")
await ctx.error("Error message")
return f"Logged messages for {msg}"
mcp.add_tool(logging_tool)
with patch("mcp.server.session.ServerSession.send_log_message") as mock_log:
async with Client(mcp) as client:
result = await client.call_tool("logging_tool", {"msg": "test"})
assert len(result) == 1
content = result[0]
assert isinstance(content, TextContent)
assert "Logged messages for test" in content.text
assert mock_log.call_count == 4
mock_log.assert_any_call(
level="debug", data="Debug message", logger=None
)
mock_log.assert_any_call(level="info", data="Info message", logger=None)
mock_log.assert_any_call(
level="warning", data="Warning message", logger=None
)
mock_log.assert_any_call(
level="error", data="Error message", logger=None
)
async def test_optional_context(self):
"""Test that context is optional."""
mcp = FastMCP()
@mcp.tool()
def no_context(x: int) -> int:
return x * 2
mcp.add_tool(no_context)
async with Client(mcp) as client:
result = await client.call_tool("no_context", {"x": 21})
assert len(result) == 1