Spaces:
Running
Running
Merge pull request #1129 from jlowin/expand-empty-params-fix-1128
Browse files
src/fastmcp/server/openapi.py
CHANGED
|
@@ -344,18 +344,28 @@ class OpenAPITool(Tool):
|
|
| 344 |
suffixed_name = f"{p.name}__{p.location}"
|
| 345 |
param_value = None
|
| 346 |
|
|
|
|
| 347 |
if (
|
| 348 |
suffixed_name in arguments
|
| 349 |
-
and
|
| 350 |
-
and
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
):
|
| 352 |
param_value = arguments[suffixed_name]
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
|
| 360 |
if param_value is not None:
|
| 361 |
# Handle different parameter styles and types
|
|
|
|
| 344 |
suffixed_name = f"{p.name}__{p.location}"
|
| 345 |
param_value = None
|
| 346 |
|
| 347 |
+
suffixed_value = arguments.get(suffixed_name)
|
| 348 |
if (
|
| 349 |
suffixed_name in arguments
|
| 350 |
+
and suffixed_value is not None
|
| 351 |
+
and suffixed_value != ""
|
| 352 |
+
and not (
|
| 353 |
+
isinstance(suffixed_value, list | dict)
|
| 354 |
+
and len(suffixed_value) == 0
|
| 355 |
+
)
|
| 356 |
):
|
| 357 |
param_value = arguments[suffixed_name]
|
| 358 |
+
else:
|
| 359 |
+
name_value = arguments.get(p.name)
|
| 360 |
+
if (
|
| 361 |
+
p.name in arguments
|
| 362 |
+
and name_value is not None
|
| 363 |
+
and name_value != ""
|
| 364 |
+
and not (
|
| 365 |
+
isinstance(name_value, list | dict) and len(name_value) == 0
|
| 366 |
+
)
|
| 367 |
+
):
|
| 368 |
+
param_value = arguments[p.name]
|
| 369 |
|
| 370 |
if param_value is not None:
|
| 371 |
# Handle different parameter styles and types
|
tests/server/openapi/test_openapi_path_parameters.py
CHANGED
|
@@ -448,6 +448,154 @@ async def test_array_query_parameter_exploded_format(mock_client):
|
|
| 448 |
)
|
| 449 |
|
| 450 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 451 |
def test_parameter_location_enum_handling():
|
| 452 |
"""Test that ParameterLocation enum values are handled correctly (issue #950)."""
|
| 453 |
from enum import Enum
|
|
|
|
| 448 |
)
|
| 449 |
|
| 450 |
|
| 451 |
+
async def test_empty_array_parameter_exclusion(mock_client):
|
| 452 |
+
"""Test that empty array parameters are excluded from requests."""
|
| 453 |
+
# Create a route with array query parameter
|
| 454 |
+
route = HTTPRoute(
|
| 455 |
+
path="/search",
|
| 456 |
+
method="GET",
|
| 457 |
+
operation_id="search-operation",
|
| 458 |
+
parameters=[
|
| 459 |
+
ParameterInfo(
|
| 460 |
+
name="tags",
|
| 461 |
+
location="query",
|
| 462 |
+
required=False,
|
| 463 |
+
schema={
|
| 464 |
+
"type": "array",
|
| 465 |
+
"items": {"type": "string"},
|
| 466 |
+
},
|
| 467 |
+
),
|
| 468 |
+
ParameterInfo(
|
| 469 |
+
name="categories",
|
| 470 |
+
location="query",
|
| 471 |
+
required=False,
|
| 472 |
+
schema={
|
| 473 |
+
"type": "array",
|
| 474 |
+
"items": {"type": "string"},
|
| 475 |
+
},
|
| 476 |
+
),
|
| 477 |
+
ParameterInfo(
|
| 478 |
+
name="limit",
|
| 479 |
+
location="query",
|
| 480 |
+
required=False,
|
| 481 |
+
schema={"type": "integer"},
|
| 482 |
+
),
|
| 483 |
+
],
|
| 484 |
+
)
|
| 485 |
+
|
| 486 |
+
# Create the tool
|
| 487 |
+
tool = OpenAPITool(
|
| 488 |
+
client=mock_client,
|
| 489 |
+
route=route,
|
| 490 |
+
name="search-operation",
|
| 491 |
+
description="Search operation",
|
| 492 |
+
parameters={},
|
| 493 |
+
)
|
| 494 |
+
|
| 495 |
+
# Test with empty array - should be excluded
|
| 496 |
+
await tool.run(
|
| 497 |
+
{
|
| 498 |
+
"tags": [], # Empty array should be excluded
|
| 499 |
+
"categories": ["tech", "news"], # Non-empty array should be included
|
| 500 |
+
"limit": 10, # Non-array param should be included
|
| 501 |
+
}
|
| 502 |
+
)
|
| 503 |
+
|
| 504 |
+
# Check that empty array is excluded, but others are included
|
| 505 |
+
mock_client.request.assert_called_with(
|
| 506 |
+
method="GET",
|
| 507 |
+
url="/search",
|
| 508 |
+
params={
|
| 509 |
+
"categories": ["tech", "news"], # Only non-empty array included
|
| 510 |
+
"limit": 10,
|
| 511 |
+
},
|
| 512 |
+
headers={},
|
| 513 |
+
json=None,
|
| 514 |
+
timeout=None,
|
| 515 |
+
)
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
async def test_empty_deep_object_parameter_exclusion(mock_client):
|
| 519 |
+
"""Test that empty dict parameters with deepObject style are excluded from requests."""
|
| 520 |
+
# Create a route with deepObject query parameter
|
| 521 |
+
route = HTTPRoute(
|
| 522 |
+
path="/filter",
|
| 523 |
+
method="GET",
|
| 524 |
+
operation_id="filter-operation",
|
| 525 |
+
parameters=[
|
| 526 |
+
ParameterInfo(
|
| 527 |
+
name="filters",
|
| 528 |
+
location="query",
|
| 529 |
+
required=False,
|
| 530 |
+
style="deepObject",
|
| 531 |
+
explode=True,
|
| 532 |
+
schema={
|
| 533 |
+
"type": "object",
|
| 534 |
+
"properties": {
|
| 535 |
+
"name": {"type": "string"},
|
| 536 |
+
"age": {"type": "integer"},
|
| 537 |
+
},
|
| 538 |
+
},
|
| 539 |
+
),
|
| 540 |
+
ParameterInfo(
|
| 541 |
+
name="options",
|
| 542 |
+
location="query",
|
| 543 |
+
required=False,
|
| 544 |
+
style="deepObject",
|
| 545 |
+
explode=True,
|
| 546 |
+
schema={
|
| 547 |
+
"type": "object",
|
| 548 |
+
"properties": {
|
| 549 |
+
"sort": {"type": "string"},
|
| 550 |
+
"order": {"type": "string"},
|
| 551 |
+
},
|
| 552 |
+
},
|
| 553 |
+
),
|
| 554 |
+
ParameterInfo(
|
| 555 |
+
name="page",
|
| 556 |
+
location="query",
|
| 557 |
+
required=False,
|
| 558 |
+
schema={"type": "integer"},
|
| 559 |
+
),
|
| 560 |
+
],
|
| 561 |
+
)
|
| 562 |
+
|
| 563 |
+
# Create the tool
|
| 564 |
+
tool = OpenAPITool(
|
| 565 |
+
client=mock_client,
|
| 566 |
+
route=route,
|
| 567 |
+
name="filter-operation",
|
| 568 |
+
description="Filter operation",
|
| 569 |
+
parameters={},
|
| 570 |
+
)
|
| 571 |
+
|
| 572 |
+
# Test with empty dict - should be excluded
|
| 573 |
+
await tool.run(
|
| 574 |
+
{
|
| 575 |
+
"filters": {}, # Empty dict should be excluded
|
| 576 |
+
"options": {
|
| 577 |
+
"sort": "name",
|
| 578 |
+
"order": "asc",
|
| 579 |
+
}, # Non-empty dict should be included
|
| 580 |
+
"page": 1, # Non-dict param should be included
|
| 581 |
+
}
|
| 582 |
+
)
|
| 583 |
+
|
| 584 |
+
# Check that empty dict is excluded, but others are included
|
| 585 |
+
mock_client.request.assert_called_with(
|
| 586 |
+
method="GET",
|
| 587 |
+
url="/filter",
|
| 588 |
+
params={
|
| 589 |
+
"options[sort]": "name", # Deep object style for non-empty dict
|
| 590 |
+
"options[order]": "asc",
|
| 591 |
+
"page": 1,
|
| 592 |
+
},
|
| 593 |
+
headers={},
|
| 594 |
+
json=None,
|
| 595 |
+
timeout=None,
|
| 596 |
+
)
|
| 597 |
+
|
| 598 |
+
|
| 599 |
def test_parameter_location_enum_handling():
|
| 600 |
"""Test that ParameterLocation enum values are handled correctly (issue #950)."""
|
| 601 |
from enum import Enum
|