Tool returning `nan` or `inf` silently comes back as `null`
Ran into this on a tool that averages a list of numbers. Empty list, so the mean is
nan, and what the client gets back is None with is_error: false and no warning
anywhere. Spent a while looking for the bug in my own code before realizing the value was
being rewritten on the way out.
import asyncio
from fastmcp import Client, FastMCP
mcp = FastMCP("mre")
@mcp.tool
def average(values: list[float]) -> dict:
n = len(values)
return {"count": n, "mean": sum(values) / n if n else float("nan")}
@mcp.tool
def scalar() -> float:
return float("nan")
async def main():
async with Client(mcp) as c:
for name, args in [("average", {"values": []}), ("scalar", {})]:
try:
r = await c.call_tool(name, args)
print(f"{name}: OK data={r.data!r} text={[b.text for b in r.content]}")
except Exception as e:
print(f"{name}: {type(e).__name__}: {str(e).splitlines()[0]}")
asyncio.run(main())On 4.0.5 with pydantic 2.13.5:
average: OK data={'count': 0, 'mean': None} text=['{"count":0,"mean":null}']
scalar: RuntimeError: Invalid structured content returned by tool scalar: None is not of type 'number'I'd expect either the value to survive in some form, or the call to fail with an error
that actually mentions nan. What happens instead is that the dict case succeeds with a
wrong number in it, and the scalar case dies in the client complaining about None,
which isn't what the tool returned.
You don't need to write float("nan") yourself to land here. An empty-list average does
it, so does 0.0 / 0.0, and so does most of what comes out of numpy or pandas. The input
side is happy to take them, too: a v: float parameter accepts the string "nan" and the
tool body sees float('nan').
From poking at it: structured content goes through _serialize_to_jsonable in
fastmcp/tools/base.py (dump_python(..., mode="json")), text content through
default_serializer (dump_json). Pydantic's ser_json_inf_nan defaults to 'null', so
both paths drop the value. In the scalar case that leaves the server sending
{"result": null} while the output schema it published for that same tool says
{"result": {"type": "number"}}, and that mismatch is what the client error is really
reporting.
Not new in 4.x, for what it's worth. 3.4.7 loses the value as well, except there the text
content is a bare NaN literal, which isn't valid JSON for any client that isn't Python.
Source: PrefectHQ/fastmcp