From ea1439a009c2fc6cc9619836accd8d6476bcd085 Mon Sep 17 00:00:00 2001 From: William Easton Date: Tue, 14 Apr 2026 10:23:40 -0500 Subject: [PATCH] Use _query_scalar_to_str for multipart booleans, add tuple passthrough test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reuse existing boolean serialization (true/false not True/False) for multipart form fields. Add test for file-like tuple passthrough. 🤖 Generated with Claude Code Co-Authored-By: Claude Opus 4.6 (1M context) --- src/fastmcp/utilities/openapi/director.py | 5 ++-- tests/utilities/openapi/test_director.py | 30 ++++++++++++++++++++++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/fastmcp/utilities/openapi/director.py b/src/fastmcp/utilities/openapi/director.py index 82ca17dd9..c3225af92 100644 --- a/src/fastmcp/utilities/openapi/director.py +++ b/src/fastmcp/utilities/openapi/director.py @@ -94,11 +94,12 @@ class RequestDirector: if declared_content_type == "multipart/form-data" and isinstance( body, dict ): - # Wrap plain values as (None, str(value)) tuples so httpx + # Wrap plain values as (None, stringified) tuples so httpx # treats them as form fields. Scalars must be stringified # because httpx rejects non-string/bytes values in files=. + # Use _query_scalar_to_str for booleans (true/false, not True/False). files = { - k: v if isinstance(v, tuple) else (None, str(v)) + k: v if isinstance(v, tuple) else (None, _query_scalar_to_str(v)) for k, v in body.items() } elif ( diff --git a/tests/utilities/openapi/test_director.py b/tests/utilities/openapi/test_director.py index d2828e76a..82035a79c 100644 --- a/tests/utilities/openapi/test_director.py +++ b/tests/utilities/openapi/test_director.py @@ -1292,7 +1292,35 @@ class TestContentTypeDispatch: request.read() body = request.content.decode("utf-8") assert "42" in body - assert "True" in body + assert "true" in body + + def test_multipart_preserves_tuple_values(self, director): + """Tuple values (file-like) are passed through unchanged to httpx files=.""" + route = HTTPRoute( + path="/upload", + method="POST", + operation_id="upload", + request_body=RequestBodyInfo( + required=True, + content_schema={ + "multipart/form-data": { + "type": "object", + "properties": {"file": {"type": "string"}}, + } + }, + ), + parameter_map={ + "file": {"location": "body", "openapi_name": "file"}, + }, + ) + # Tuple values represent file-like objects for httpx + request = director.build(route, {"file": ("report.csv", b"a,b,c")}) + content_type = request.headers.get("content-type", "") + assert "multipart/form-data" in content_type + request.read() + body = request.content.decode("utf-8", errors="replace") + assert "report.csv" in body + assert "a,b,c" in body def test_json_body_still_works(self, director, json_route): """application/json bodies should still be sent as JSON (regression)."""