fastmcp/tests/experimental/openapi_parser/utilities/test_parameter_explode.py
Jeremiah Lowin e9aad2eacb feat: Add --workspace flag to fastmcp install cursor (#1522)
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2025-08-19 20:10:12 +00:00

158 lines
5.8 KiB
Python

"""Tests for OpenAPI parameter explode handling in the experimental parser."""
import httpx
from fastmcp.experimental.server.openapi import FastMCPOpenAPI
from fastmcp.experimental.utilities.openapi import convert_openapi_schema_to_json_schema
def _make_server_and_capture_urls(openapi_dict: dict, args: dict) -> list[str]:
"""Helper function to capture URLs generated by FastMCPOpenAPI."""
calls: list[str] = []
async def handler(request: httpx.Request):
calls.append(str(request.url))
return httpx.Response(200, json={"ok": True})
transport = httpx.MockTransport(handler)
client = httpx.AsyncClient(base_url="https://api.test", transport=transport)
spec = convert_openapi_schema_to_json_schema(openapi_dict)
server = FastMCPOpenAPI(openapi_spec=spec, client=client, name="t")
# Use the MCP call path to exercise the generated tool
# Note: _mcp_call_tool is an internal API but adequate for this repro
import anyio
anyio.run(server._mcp_call_tool, "echo", args) # type: ignore[arg-type]
return calls
def test_query_array_form_explode_false_is_not_respected():
"""Test that form style with explode=false generates comma-delimited values."""
# Minimal OpenAPI spec: array query param with style=form, explode=false
openapi = {
"openapi": "3.1.0",
"info": {"title": "T", "version": "1.0.0"},
"paths": {
"/echo": {
"get": {
"operationId": "echo",
"parameters": [
{
"name": "ids",
"in": "query",
"style": "form",
"explode": False,
"schema": {"type": "array", "items": {"type": "string"}},
}
],
"responses": {"200": {"description": "ok"}},
}
}
},
}
urls = _make_server_and_capture_urls(openapi, {"ids": ["1", "2", "3"]})
# Expected per OpenAPI (form+explode=false): `ids=1,2,3` (URL encoded as `ids=1%2C2%2C3`)
# Actual (bug): multiple entries: `ids=1&ids=2&ids=3`
assert any(
url.endswith("/echo?ids=1%2C2%2C3")
for url in urls # URL-encoded commas
), f"Expected comma-delimited value, got: {urls}"
def test_query_array_pipe_explode_false_is_not_respected():
"""Test that pipeDelimited style with explode=false generates pipe-delimited values."""
# pipeDelimited example: expect ids=1|2|3 when explode=false
openapi = {
"openapi": "3.1.0",
"info": {"title": "T", "version": "1.0.0"},
"paths": {
"/echo": {
"get": {
"operationId": "echo",
"parameters": [
{
"name": "ids",
"in": "query",
"style": "pipeDelimited",
"explode": False,
"schema": {"type": "array", "items": {"type": "string"}},
}
],
"responses": {"200": {"description": "ok"}},
}
}
},
}
urls = _make_server_and_capture_urls(openapi, {"ids": ["1", "2", "3"]})
assert any(
url.endswith("/echo?ids=1%7C2%7C3")
for url in urls # URL-encoded pipes
), f"Expected pipe-delimited value, got: {urls}"
def test_query_array_space_explode_false_is_not_respected():
"""Test that spaceDelimited style with explode=false generates space-delimited values."""
# spaceDelimited example: expect ids=1%202%203 (URL encoded spaces) when explode=false
openapi = {
"openapi": "3.1.0",
"info": {"title": "T", "version": "1.0.0"},
"paths": {
"/echo": {
"get": {
"operationId": "echo",
"parameters": [
{
"name": "ids",
"in": "query",
"style": "spaceDelimited",
"explode": False,
"schema": {"type": "array", "items": {"type": "string"}},
}
],
"responses": {"200": {"description": "ok"}},
}
}
},
}
urls = _make_server_and_capture_urls(openapi, {"ids": ["1", "2", "3"]})
assert any(
"ids=1+2+3" in url
for url in urls # URL-encoded spaces (+ encoding)
), f"Expected space-delimited value, got: {urls}"
def test_query_array_form_explode_true_works_correctly():
"""Test that form style with explode=true generates repeated parameters (current behavior)."""
openapi = {
"openapi": "3.1.0",
"info": {"title": "T", "version": "1.0.0"},
"paths": {
"/echo": {
"get": {
"operationId": "echo",
"parameters": [
{
"name": "ids",
"in": "query",
"style": "form",
"explode": True, # This should work correctly
"schema": {"type": "array", "items": {"type": "string"}},
}
],
"responses": {"200": {"description": "ok"}},
}
}
},
}
urls = _make_server_and_capture_urls(openapi, {"ids": ["1", "2", "3"]})
# With explode=true, should get repeated parameters: ids=1&ids=2&ids=3
assert any("ids=1" in url and "ids=2" in url and "ids=3" in url for url in urls), (
f"Expected repeated parameters, got: {urls}"
)