mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-19 03:54:18 +02:00
Add log tests
This commit is contained in:
parent
96e8c38c5b
commit
5443cf59aa
3 changed files with 74 additions and 49 deletions
|
|
@ -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
60
tests/client/test_logs.py
Normal 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"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue