modelcontextprotocol / modelcontextprotocol/python-sdk

A tool returning a non-finite float fails on the client with an error naming the tool

未關閉
#3,341 2 則留言 0 個 reaction 已指派 0 人 在 GitHub 檢視

還沒有人認領這個 Issue。

v1 v2
主要語言
Python
星號
24.3k
分支
4k
平均合併
1 天 1 小時
30 天內合併 PR
31

描述

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

  1. func_metadata.convert_result builds structured_content = {'result': inf}.
    Correct so far.
  2. _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 as null, since JSON has no Infinity or NaN and
    ser_json_inf_nan defaults to "null". The SDK does not set that setting
    anywhere.
  3. The client validates {"result": null} against the generated output schema
    {"result": {"type": "number"}}, which rejects it.
  4. client/session.py:1155 raises, 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.

貢獻指南

開啟貢獻指南

從這裡開始

  1. 先讀完整個 Issue,再讀專案的貢獻指南。
  2. 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
  3. Fork 儲存庫,在一個分支上完成修改。
  4. 送出 Pull Request,並在描述裡引用這個 Issue 編號。

研究方向

從 server/runner.py 中的 _dump_result 開始,追蹤 func_metadata.convert_result 如何到達 client/session.py:1109 和 1155。閱讀 server/request_state.py:128 中現有的非有限浮點數拒絕邏輯,然後在相關的 server 或 client 測試旁新增針對 inf、-inf 和 nan 的回歸測試。完成的標準是:該值被保留,或以一則能指出非有限結果的訊息拒絕,而不是誤導性地將問題歸咎於工具。

由索引模型根據 Issue 內容生成。

評估

技術堆疊
python
領域
api, backend
Issue 類型
缺陷
難度
3/5
預估耗時
1-2 天
活躍度
冷清
描述清晰度
描述清楚
新手友好度
72/100

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。