modelcontextprotocol / modelcontextprotocol/python-sdk
A tool returning a non-finite float fails on the client with an error naming the tool
Chưa có ai nhận issue này.
- Ngôn ngữ chính
- Python
- Star
- 24.3k
- Fork
- 4k
- Merge trung bình
- 1 ngày 1 giờ
- Pull request đã merge (30 ngày)
- 31
Mô tả
Initial Checks
- I confirm that I'm using the newest release of my line (verified on 2.0.0)
- I confirm that I searched for my issue before opening this issue
Release line
2.x (current stable). Also reproduces on 1.29.0.
Bug description
A tool returning a non-finite float fails on the client with an error that names
the tool. float("inf"), float("-inf") and float("nan") all reproduce it. A
finite float is fine.
The handler returned an ordinary Python float. The value that fails validation is
produced by the SDK's own outbound serialization.
Steps to reproduce
import anyio
from mcp.client import Client
from mcp.server.mcpserver import MCPServer
server = MCPServer("probe")
@server.tool()
def ratio(a: float, b: float) -> float:
"""Divide a by b."""
return a / b if b else float("inf")
async def main() -> None:
async with Client(server) as client:
print(await client.call_tool("ratio", {"a": 1.0, "b": 2.0}))
print(await client.call_tool("ratio", {"a": 1.0, "b": 0.0}))
anyio.run(main)
Actual behaviour
The first call returns structured_content={'result': 0.5}. The second raises:
RuntimeError: Invalid structured content returned by tool ratio: None is not of type 'number'
Failed validating 'type' in schema['properties']['result']:
{'title': 'Result', 'type': 'number'}
On instance['result']:
None
Uncaught, this arrives as two nested ExceptionGroups out of Client.__aexit__,
84 lines in total, with the line naming the tool last. Catching the RuntimeError
leaves the session usable and later calls succeed.
The value cannot be recovered. validate_tool_result runs unconditionally for any
non-error result (client/session.py:1109, client/client.py:828) with no opt-out,
so the caller never reaches the CallToolResult to read either
structured_content or the text block.
Expected behaviour
Either the value survives the round trip, or the failure names the real cause. At
present the message directs the reader to the handler, which is the one component
behaving correctly.
The SDK already rejects non-finite floats explicitly elsewhere, at
server/request_state.py:128, so refusing them at the point of return with a clear
message would match existing practice.
Root cause
func_metadata.convert_resultbuildsstructured_content = {'result': inf}.
Correct so far._dump_result(server/runner.py:118) serializes with
model_dump(by_alias=True, mode="json", exclude_none=True). Pydantic's JSON mode
writes non-finite floats asnull, since JSON has noInfinityorNaNand
ser_json_inf_nandefaults to"null". The SDK does not set that setting
anywhere.- The client validates
{"result": null}against the generated output schema
{"result": {"type": "number"}}, which rejects it. client/session.py:1155raises, interpolating the tool name.
This is server-side and independent of transport, so it is not specific to the
in-memory client used above.
The emitted JSON stays valid, so this is not an interoperability problem. The
problem is that the value is dropped silently and the diagnostic points at the
wrong component.
Non-finite floats arise from ordinary arithmetic, including division by zero,
overflow, statistics on degenerate input, and numpy interop, so a handler can
produce one without ever writing float("inf").
Why the tests do not catch it
No test returns a non-finite float from a tool. math.inf appears in the suite
only as an unbounded stream buffer size (tests/client/test_stdio.py:149,
tests/interaction/transports/_bridge.py:120) and as a rejected TTL input
(tests/server/test_request_state.py:355). The one -> float division helper,
tests/server/mcpserver/test_tool_manager.py:838, is marked # pragma: no cover.
Not a duplicate of #3100 or PR #3118
#3100 reports the same end state, an output schema that rejects the SDK's own
structured result, but its cause is validation and serialization shapes differing
on aliases and computed fields. This reproduces with a plain -> float return and
no Pydantic model.
PR #3118 moves output schema generation into serialization mode. That does not
change this case: a float serializes as number either way, so null still
fails the schema.
#3224 is also distinct. It concerns nulls injected for absent NotRequired
TypedDict keys, not values destroyed by JSON serialization.
Python & MCP Python SDK
Python 3.13.11
mcp 2.0.0 (also reproduced on main @ 0d921927 and on mcp 1.29.0)
pydantic 2.12.5
Linux x86_64
AI assistance was used to investigate and draft this report.
Hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Hướng nghiên cứu
Bắt đầu với _dump_result trong server/runner.py và theo dõi cách func_metadata.convert_result đi tới client/session.py:1109 và 1155. Đọc phần từ chối số dấu phẩy động không hữu hạn hiện có trong server/request_state.py:128, sau đó thêm một kiểm thử hồi quy cho inf, -inf và nan bên cạnh các kiểm thử liên quan của server hoặc client. Hoàn tất khi giá trị được giữ nguyên hoặc bị từ chối bằng một thông báo xác định kết quả không hữu hạn thay vì đổ lỗi một cách gây hiểu lầm cho công cụ.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Đánh giá
- Công nghệ
- python
- Lĩnh vực
- api, backend
- Loại issue
- Lỗi
- Độ khó
- 3/5
- Thời gian dự kiến
- 1-2 ngày
- Mức độ hoạt động
- Ít trao đổi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức phù hợp với người mới
- 72/100