mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-24 06:24:18 +02:00
fix: ResponseLimitingMiddleware no longer breaks outputSchema tools (#3756)
This commit is contained in:
parent
34e632db7f
commit
4bbc4eec3b
2 changed files with 41 additions and 3 deletions
|
|
@ -3,6 +3,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import mcp.types as mt
|
||||
import pydantic_core
|
||||
|
|
@ -67,7 +68,11 @@ class ResponseLimitingMiddleware(Middleware):
|
|||
self.truncation_suffix = truncation_suffix
|
||||
self.tools = set(tools) if tools is not None else None
|
||||
|
||||
def _truncate_to_result(self, text: str) -> ToolResult:
|
||||
def _truncate_to_result(
|
||||
self,
|
||||
text: str,
|
||||
meta: dict[str, Any] | None = None,
|
||||
) -> ToolResult:
|
||||
"""Truncate text to fit within max_size and wrap in ToolResult."""
|
||||
suffix_bytes = len(self.truncation_suffix.encode("utf-8"))
|
||||
# Account for JSON wrapper overhead: {"content":[{"type":"text","text":"..."}]}
|
||||
|
|
@ -88,7 +93,14 @@ class ResponseLimitingMiddleware(Middleware):
|
|||
+ self.truncation_suffix
|
||||
)
|
||||
|
||||
return ToolResult(content=[TextContent(type="text", text=truncated)])
|
||||
# Preserve original meta, falling back to {} when absent. Having
|
||||
# meta set ensures to_mcp_result() returns a CallToolResult, which
|
||||
# bypasses MCP SDK outputSchema validation — a truncated response
|
||||
# is no longer valid structured output.
|
||||
return ToolResult(
|
||||
content=[TextContent(type="text", text=truncated)],
|
||||
meta=meta if meta is not None else {},
|
||||
)
|
||||
|
||||
async def on_call_tool(
|
||||
self,
|
||||
|
|
@ -122,4 +134,4 @@ class ResponseLimitingMiddleware(Middleware):
|
|||
else serialized.decode("utf-8", errors="replace")
|
||||
)
|
||||
|
||||
return self._truncate_to_result(text)
|
||||
return self._truncate_to_result(text, meta=result.meta)
|
||||
|
|
|
|||
|
|
@ -2,12 +2,18 @@
|
|||
|
||||
import pytest
|
||||
from mcp.types import ImageContent, TextContent
|
||||
from pydantic import BaseModel
|
||||
|
||||
from fastmcp import Client, FastMCP
|
||||
from fastmcp.server.middleware.response_limiting import ResponseLimitingMiddleware
|
||||
from fastmcp.tools.base import ToolResult
|
||||
|
||||
|
||||
# Regression test model for #3717
|
||||
class Answer(BaseModel):
|
||||
text: str
|
||||
|
||||
|
||||
class TestResponseLimitingMiddleware:
|
||||
"""Tests for ResponseLimitingMiddleware."""
|
||||
|
||||
|
|
@ -143,6 +149,26 @@ class TestResponseLimitingMiddleware:
|
|||
with pytest.raises(ValueError, match="max_size must be positive"):
|
||||
ResponseLimitingMiddleware(max_size=-100)
|
||||
|
||||
async def test_truncation_does_not_break_output_schema_tools(
|
||||
self, mcp_server: FastMCP
|
||||
):
|
||||
"""Truncating a tool with outputSchema must not cause validation errors.
|
||||
|
||||
Regression test for #3717: the MCP SDK rejects truncated results
|
||||
from tools with outputSchema because structured_content is dropped.
|
||||
We verify the server returns a successful (non-error) truncated result.
|
||||
"""
|
||||
mcp_server.add_middleware(ResponseLimitingMiddleware(max_size=1_000))
|
||||
|
||||
@mcp_server.tool()
|
||||
def big_answer() -> Answer:
|
||||
return Answer(text="x" * 2_000)
|
||||
|
||||
result = await mcp_server.call_tool("big_answer", {})
|
||||
first = result.content[0]
|
||||
assert isinstance(first, TextContent)
|
||||
assert "[Response truncated" in first.text
|
||||
|
||||
def test_utf8_truncation_preserves_characters(self):
|
||||
"""Test that UTF-8 truncation doesn't break multi-byte characters."""
|
||||
middleware = ResponseLimitingMiddleware(max_size=100)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue