mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 15:19:10 +02:00
Compare commits
2 commits
main
...
fix/input-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
212425a13d | ||
|
|
4cfeb63b95 |
6 changed files with 98 additions and 15 deletions
|
|
@ -25,6 +25,8 @@ Usage::
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import types
|
||||
import typing
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
|
|
@ -54,6 +56,16 @@ import pydantic
|
|||
from fastmcp.apps.app import FastMCPApp
|
||||
|
||||
|
||||
def _is_bool_type(annotation: Any) -> bool:
|
||||
"""Check if annotation is bool or Optional[bool]."""
|
||||
if annotation is bool:
|
||||
return True
|
||||
origin = typing.get_origin(annotation)
|
||||
if origin is typing.Union or isinstance(annotation, types.UnionType):
|
||||
return bool in typing.get_args(annotation)
|
||||
return False
|
||||
|
||||
|
||||
def _backfill_boolean_defaults(
|
||||
model: type[pydantic.BaseModel],
|
||||
data: dict[str, Any],
|
||||
|
|
@ -67,7 +79,7 @@ def _backfill_boolean_defaults(
|
|||
for name, field_info in model.model_fields.items():
|
||||
if name in data:
|
||||
continue
|
||||
if field_info.annotation is bool:
|
||||
if _is_bool_type(field_info.annotation):
|
||||
if field_info.default is not pydantic.fields.PydanticUndefined:
|
||||
data[name] = field_info.default
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class ErrorHandlingMiddleware(Middleware):
|
|||
self,
|
||||
logger: logging.Logger | None = None,
|
||||
include_traceback: bool = False,
|
||||
error_callback: Callable[[Exception, MiddlewareContext], None] | None = None,
|
||||
error_callback: Callable[[Exception, MiddlewareContext], Any] | None = None,
|
||||
transform_errors: bool = True,
|
||||
):
|
||||
"""Initialize error handling middleware.
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ class Settings(BaseSettings):
|
|||
|
||||
# HTTP settings
|
||||
host: str = "127.0.0.1"
|
||||
port: int = 8000
|
||||
port: int = Field(default=8000, ge=1, le=65535)
|
||||
sse_path: str = "/sse"
|
||||
message_path: str = "/messages/"
|
||||
streamable_http_path: str = "/mcp"
|
||||
|
|
|
|||
|
|
@ -168,3 +168,24 @@ class TestBackfillBooleanDefaults:
|
|||
data = {"title": "Note"}
|
||||
result = _backfill_boolean_defaults(NoteForm, data)
|
||||
assert "content" not in result
|
||||
|
||||
def test_optional_bool_backfilled(self):
|
||||
class ModelWithOptionalBool(pydantic.BaseModel):
|
||||
flag: bool = False
|
||||
opt: bool | None = False
|
||||
|
||||
data: dict = {}
|
||||
_backfill_boolean_defaults(ModelWithOptionalBool, data)
|
||||
assert "flag" in data
|
||||
assert "opt" in data
|
||||
assert data["flag"] is False
|
||||
assert data["opt"] is False
|
||||
|
||||
def test_optional_bool_without_default(self):
|
||||
class ModelWithRequiredOptionalBool(pydantic.BaseModel):
|
||||
opt: bool | None
|
||||
|
||||
data: dict = {}
|
||||
_backfill_boolean_defaults(ModelWithRequiredOptionalBool, data)
|
||||
assert "opt" in data
|
||||
assert data["opt"] is False
|
||||
|
|
|
|||
|
|
@ -58,48 +58,48 @@ class TestErrorHandlingMiddleware:
|
|||
assert middleware.error_callback is callback
|
||||
assert middleware.transform_errors is False
|
||||
|
||||
def test_log_error_basic(self, mock_context, caplog):
|
||||
async def test_log_error_basic(self, mock_context, caplog):
|
||||
"""Test basic error logging."""
|
||||
middleware = ErrorHandlingMiddleware()
|
||||
error = ValueError("test error")
|
||||
|
||||
with caplog.at_level(logging.ERROR):
|
||||
middleware._log_error(error, mock_context)
|
||||
await middleware._log_error(error, mock_context)
|
||||
|
||||
assert "Error in test_method: ValueError: test error" in caplog.text
|
||||
assert "ValueError:test_method" in middleware.error_counts
|
||||
assert middleware.error_counts["ValueError:test_method"] == 1
|
||||
|
||||
def test_log_error_with_traceback(self, mock_context, caplog):
|
||||
async def test_log_error_with_traceback(self, mock_context, caplog):
|
||||
"""Test error logging with traceback."""
|
||||
middleware = ErrorHandlingMiddleware(include_traceback=True)
|
||||
error = ValueError("test error")
|
||||
|
||||
with caplog.at_level(logging.ERROR):
|
||||
middleware._log_error(error, mock_context)
|
||||
await middleware._log_error(error, mock_context)
|
||||
|
||||
assert "Error in test_method: ValueError: test error" in caplog.text
|
||||
# The traceback is added to the log message
|
||||
assert "Error in test_method: ValueError: test error" in caplog.text
|
||||
|
||||
def test_log_error_with_callback(self, mock_context):
|
||||
async def test_log_error_with_callback(self, mock_context):
|
||||
"""Test error logging with callback."""
|
||||
callback = MagicMock()
|
||||
middleware = ErrorHandlingMiddleware(error_callback=callback)
|
||||
error = ValueError("test error")
|
||||
|
||||
middleware._log_error(error, mock_context)
|
||||
await middleware._log_error(error, mock_context)
|
||||
|
||||
callback.assert_called_once_with(error, mock_context)
|
||||
|
||||
def test_log_error_callback_exception(self, mock_context, caplog):
|
||||
async def test_log_error_callback_exception(self, mock_context, caplog):
|
||||
"""Test error logging when callback raises exception."""
|
||||
callback = MagicMock(side_effect=RuntimeError("callback error"))
|
||||
middleware = ErrorHandlingMiddleware(error_callback=callback)
|
||||
error = ValueError("test error")
|
||||
|
||||
with caplog.at_level(logging.ERROR):
|
||||
middleware._log_error(error, mock_context)
|
||||
await middleware._log_error(error, mock_context)
|
||||
|
||||
assert "Error in error callback: callback error" in caplog.text
|
||||
|
||||
|
|
@ -236,16 +236,16 @@ class TestErrorHandlingMiddleware:
|
|||
assert "Invalid params: test error" in exc_info.value.error.message
|
||||
assert "Error in test_method: ToolError: test error" in caplog.text
|
||||
|
||||
def test_get_error_stats(self, mock_context):
|
||||
async def test_get_error_stats(self, mock_context):
|
||||
"""Test getting error statistics."""
|
||||
middleware = ErrorHandlingMiddleware()
|
||||
error1 = ValueError("error1")
|
||||
error2 = ValueError("error2")
|
||||
error3 = RuntimeError("error3")
|
||||
|
||||
middleware._log_error(error1, mock_context)
|
||||
middleware._log_error(error2, mock_context)
|
||||
middleware._log_error(error3, mock_context)
|
||||
await middleware._log_error(error1, mock_context)
|
||||
await middleware._log_error(error2, mock_context)
|
||||
await middleware._log_error(error3, mock_context)
|
||||
|
||||
stats = middleware.get_error_stats()
|
||||
assert stats["ValueError:test_method"] == 2
|
||||
|
|
@ -566,6 +566,27 @@ class TestErrorHandlingMiddlewareIntegration:
|
|||
# Error should still exist (may be wrapped by FastMCP)
|
||||
assert exc_info.value is not None
|
||||
|
||||
async def test_async_error_callback(self):
|
||||
"""Test that async error callbacks are properly awaited."""
|
||||
called = False
|
||||
|
||||
async def callback(error, context):
|
||||
nonlocal called
|
||||
called = True
|
||||
|
||||
mcp = FastMCP("test")
|
||||
mcp.add_middleware(ErrorHandlingMiddleware(error_callback=callback))
|
||||
|
||||
@mcp.tool
|
||||
def bad_tool() -> str:
|
||||
raise ValueError("test")
|
||||
|
||||
async with Client(mcp) as client:
|
||||
with pytest.raises(Exception):
|
||||
await client.call_tool("bad_tool")
|
||||
|
||||
assert called, "Async callback was not awaited"
|
||||
|
||||
|
||||
class TestRetryMiddlewareIntegration:
|
||||
"""Integration tests for retry middleware with real FastMCP server."""
|
||||
|
|
|
|||
29
tests/test_settings.py
Normal file
29
tests/test_settings.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"""Tests for FastMCP settings validation."""
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from fastmcp.settings import Settings
|
||||
|
||||
|
||||
class TestPortValidation:
|
||||
def test_invalid_port_negative(self):
|
||||
with pytest.raises(ValidationError):
|
||||
Settings(port=-1)
|
||||
|
||||
def test_invalid_port_zero(self):
|
||||
with pytest.raises(ValidationError):
|
||||
Settings(port=0)
|
||||
|
||||
def test_invalid_port_too_high(self):
|
||||
with pytest.raises(ValidationError):
|
||||
Settings(port=99999)
|
||||
|
||||
def test_valid_port_accepted(self):
|
||||
assert Settings(port=8080).port == 8080
|
||||
|
||||
def test_valid_port_min(self):
|
||||
assert Settings(port=1).port == 1
|
||||
|
||||
def test_valid_port_max(self):
|
||||
assert Settings(port=65535).port == 65535
|
||||
Loading…
Add table
Add a link
Reference in a new issue