From b853bb85f99cc7145f1383f7f0e3af9b9628500d Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sat, 12 Jul 2025 09:25:05 -0400 Subject: [PATCH] Expand empty parameter filtering and add tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds on PR #1128 by adding comprehensive filtering at parameter selection stage and test coverage for empty arrays/dicts. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/fastmcp/server/openapi.py | 26 ++- .../openapi/test_openapi_path_parameters.py | 148 ++++++++++++++++++ 2 files changed, 166 insertions(+), 8 deletions(-) diff --git a/src/fastmcp/server/openapi.py b/src/fastmcp/server/openapi.py index e94ef179b..a17aeab0e 100644 --- a/src/fastmcp/server/openapi.py +++ b/src/fastmcp/server/openapi.py @@ -344,18 +344,28 @@ class OpenAPITool(Tool): suffixed_name = f"{p.name}__{p.location}" param_value = None + suffixed_value = arguments.get(suffixed_name) if ( suffixed_name in arguments - and arguments.get(suffixed_name) is not None - and arguments.get(suffixed_name) != "" + and suffixed_value is not None + and suffixed_value != "" + and not ( + isinstance(suffixed_value, list | dict) + and len(suffixed_value) == 0 + ) ): param_value = arguments[suffixed_name] - elif ( - p.name in arguments - and arguments.get(p.name) is not None - and arguments.get(p.name) != "" - ): - param_value = arguments[p.name] + else: + name_value = arguments.get(p.name) + if ( + p.name in arguments + and name_value is not None + and name_value != "" + and not ( + isinstance(name_value, list | dict) and len(name_value) == 0 + ) + ): + param_value = arguments[p.name] if param_value is not None: # Handle different parameter styles and types diff --git a/tests/server/openapi/test_openapi_path_parameters.py b/tests/server/openapi/test_openapi_path_parameters.py index 8d6f1e820..a9ec8348d 100644 --- a/tests/server/openapi/test_openapi_path_parameters.py +++ b/tests/server/openapi/test_openapi_path_parameters.py @@ -448,6 +448,154 @@ async def test_array_query_parameter_exploded_format(mock_client): ) +async def test_empty_array_parameter_exclusion(mock_client): + """Test that empty array parameters are excluded from requests.""" + # Create a route with array query parameter + route = HTTPRoute( + path="/search", + method="GET", + operation_id="search-operation", + parameters=[ + ParameterInfo( + name="tags", + location="query", + required=False, + schema={ + "type": "array", + "items": {"type": "string"}, + }, + ), + ParameterInfo( + name="categories", + location="query", + required=False, + schema={ + "type": "array", + "items": {"type": "string"}, + }, + ), + ParameterInfo( + name="limit", + location="query", + required=False, + schema={"type": "integer"}, + ), + ], + ) + + # Create the tool + tool = OpenAPITool( + client=mock_client, + route=route, + name="search-operation", + description="Search operation", + parameters={}, + ) + + # Test with empty array - should be excluded + await tool.run( + { + "tags": [], # Empty array should be excluded + "categories": ["tech", "news"], # Non-empty array should be included + "limit": 10, # Non-array param should be included + } + ) + + # Check that empty array is excluded, but others are included + mock_client.request.assert_called_with( + method="GET", + url="/search", + params={ + "categories": ["tech", "news"], # Only non-empty array included + "limit": 10, + }, + headers={}, + json=None, + timeout=None, + ) + + +async def test_empty_deep_object_parameter_exclusion(mock_client): + """Test that empty dict parameters with deepObject style are excluded from requests.""" + # Create a route with deepObject query parameter + route = HTTPRoute( + path="/filter", + method="GET", + operation_id="filter-operation", + parameters=[ + ParameterInfo( + name="filters", + location="query", + required=False, + style="deepObject", + explode=True, + schema={ + "type": "object", + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + }, + }, + ), + ParameterInfo( + name="options", + location="query", + required=False, + style="deepObject", + explode=True, + schema={ + "type": "object", + "properties": { + "sort": {"type": "string"}, + "order": {"type": "string"}, + }, + }, + ), + ParameterInfo( + name="page", + location="query", + required=False, + schema={"type": "integer"}, + ), + ], + ) + + # Create the tool + tool = OpenAPITool( + client=mock_client, + route=route, + name="filter-operation", + description="Filter operation", + parameters={}, + ) + + # Test with empty dict - should be excluded + await tool.run( + { + "filters": {}, # Empty dict should be excluded + "options": { + "sort": "name", + "order": "asc", + }, # Non-empty dict should be included + "page": 1, # Non-dict param should be included + } + ) + + # Check that empty dict is excluded, but others are included + mock_client.request.assert_called_with( + method="GET", + url="/filter", + params={ + "options[sort]": "name", # Deep object style for non-empty dict + "options[order]": "asc", + "page": 1, + }, + headers={}, + json=None, + timeout=None, + ) + + def test_parameter_location_enum_handling(): """Test that ParameterLocation enum values are handled correctly (issue #950).""" from enum import Enum