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)."""