Jeremiah Lowin commited on
Commit
b4e78d2
·
1 Parent(s): 33f7965

Ensure openapi path params are handled properly

Browse files
src/fastmcp/server/openapi.py CHANGED
@@ -171,6 +171,24 @@ class OpenAPITool(Tool):
171
  raise ToolError(f"Missing required path parameters: {missing_params}")
172
 
173
  for param_name, param_value in path_params.items():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  path = path.replace(f"{{{param_name}}}", str(param_value))
175
 
176
  # Prepare query parameters - filter out None and empty strings
 
171
  raise ToolError(f"Missing required path parameters: {missing_params}")
172
 
173
  for param_name, param_value in path_params.items():
174
+ # Handle array path parameters with style 'simple' (comma-separated)
175
+ # In OpenAPI, 'simple' is the default style for path parameters
176
+ # and explode=False behavior for arrays
177
+ param_info = next(
178
+ (p for p in self._route.parameters if p.name == param_name), None
179
+ )
180
+
181
+ if param_info and isinstance(param_value, list):
182
+ # Check if schema indicates an array type
183
+ schema = param_info.schema_
184
+ is_array = schema.get("type") == "array"
185
+
186
+ if is_array:
187
+ # Format array values as comma-separated string
188
+ # This follows the OpenAPI 'simple' style (default for path)
189
+ # and explode=False behavior for arrays
190
+ param_value = ",".join(str(item) for item in param_value)
191
+
192
  path = path.replace(f"{{{param_name}}}", str(param_value))
193
 
194
  # Prepare query parameters - filter out None and empty strings
tests/server/test_openapi_array_params.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from unittest.mock import AsyncMock, MagicMock
2
+
3
+ import httpx
4
+ import pytest
5
+
6
+ from fastmcp.server.openapi import OpenAPITool
7
+ from fastmcp.utilities.openapi import HTTPRoute, ParameterInfo
8
+
9
+
10
+ @pytest.fixture
11
+ def mock_client():
12
+ """Create a mock httpx.AsyncClient."""
13
+ client = AsyncMock(spec=httpx.AsyncClient)
14
+ # Set up a mock response
15
+ mock_response = MagicMock()
16
+ mock_response.json.return_value = {"result": "success"}
17
+ mock_response.raise_for_status.return_value = None
18
+ client.request.return_value = mock_response
19
+ return client
20
+
21
+
22
+ @pytest.mark.asyncio
23
+ async def test_array_path_parameter_handling(mock_client):
24
+ """Test how array path parameters are handled."""
25
+ # Create a simple route with array path parameter
26
+ route = HTTPRoute(
27
+ path="/select/{days}",
28
+ method="PUT",
29
+ operation_id="test-operation",
30
+ parameters=[
31
+ ParameterInfo(
32
+ name="days",
33
+ location="path",
34
+ required=True,
35
+ schema={
36
+ "type": "array",
37
+ "items": {
38
+ "type": "string",
39
+ "enum": [
40
+ "monday",
41
+ "tuesday",
42
+ "wednesday",
43
+ "thursday",
44
+ "friday",
45
+ "saturday",
46
+ "sunday",
47
+ ],
48
+ },
49
+ },
50
+ )
51
+ ],
52
+ )
53
+
54
+ # Create the tool
55
+ tool = OpenAPITool(
56
+ client=mock_client,
57
+ route=route,
58
+ name="test-operation",
59
+ description="Test operation",
60
+ parameters={},
61
+ )
62
+
63
+ # Test with a single value
64
+ await tool._execute_request(days=["monday"])
65
+
66
+ # Check that the path parameter is formatted correctly
67
+ # This is where the bug is: it should be '/select/monday' not '/select/[\'monday\']'
68
+ mock_client.request.assert_called_with(
69
+ method="PUT",
70
+ url="/select/monday", # This is the expected format
71
+ params={},
72
+ headers={},
73
+ json=None,
74
+ timeout=None,
75
+ )
76
+ mock_client.request.reset_mock()
77
+
78
+ # Test with multiple values
79
+ await tool._execute_request(days=["monday", "tuesday"])
80
+
81
+ # Check that the path parameter is formatted correctly
82
+ # It should be '/select/monday,tuesday' not '/select/[\'monday\', \'tuesday\']'
83
+ mock_client.request.assert_called_with(
84
+ method="PUT",
85
+ url="/select/monday,tuesday", # This is the expected format
86
+ params={},
87
+ headers={},
88
+ json=None,
89
+ timeout=None,
90
+ )
tests/server/test_openapi_path_parameters.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from unittest.mock import AsyncMock, MagicMock
2
+
3
+ import httpx
4
+ import pytest
5
+
6
+ from fastmcp import FastMCP
7
+ from fastmcp.server.openapi import OpenAPITool
8
+ from fastmcp.utilities.openapi import HTTPRoute, ParameterInfo
9
+
10
+
11
+ @pytest.fixture
12
+ def array_path_spec():
13
+ """Load a minimal OpenAPI spec with an array path parameter."""
14
+ return {
15
+ "openapi": "3.1.0",
16
+ "info": {"title": "Test API", "version": "1.0.0"},
17
+ "paths": {
18
+ "/select/{days}": {
19
+ "put": {
20
+ "operationId": "test-operation",
21
+ "parameters": [
22
+ {
23
+ "name": "days",
24
+ "in": "path",
25
+ "required": True,
26
+ "style": "simple",
27
+ "explode": False,
28
+ "schema": {
29
+ "type": "array",
30
+ "items": {
31
+ "type": "string",
32
+ "enum": [
33
+ "monday",
34
+ "tuesday",
35
+ "wednesday",
36
+ "thursday",
37
+ "friday",
38
+ "saturday",
39
+ "sunday",
40
+ ],
41
+ },
42
+ },
43
+ }
44
+ ],
45
+ "responses": {
46
+ "200": {
47
+ "description": "Success",
48
+ "content": {
49
+ "application/json": {
50
+ "schema": {
51
+ "type": "object",
52
+ "properties": {"result": {"type": "string"}},
53
+ "required": ["result"],
54
+ }
55
+ }
56
+ },
57
+ }
58
+ },
59
+ }
60
+ }
61
+ },
62
+ }
63
+
64
+
65
+ @pytest.fixture
66
+ def mock_client():
67
+ """Create a mock httpx.AsyncClient."""
68
+ client = AsyncMock(spec=httpx.AsyncClient)
69
+ # Set up a mock response
70
+ mock_response = MagicMock()
71
+ mock_response.json.return_value = {"result": "success"}
72
+ mock_response.raise_for_status.return_value = None
73
+ client.request.return_value = mock_response
74
+ return client
75
+
76
+
77
+ async def test_fastmcp_from_openapi(array_path_spec, mock_client):
78
+ """Test creating FastMCP from OpenAPI spec with array path parameter."""
79
+ # Create FastMCP from the spec
80
+ mcp = FastMCP.from_openapi(array_path_spec, client=mock_client)
81
+
82
+ # Verify the tool was created using the MCP protocol method
83
+ tools_result = await mcp.get_tools()
84
+ tool_names = [tool.name for tool in tools_result.values()]
85
+ assert "test-operation" in tool_names
86
+
87
+
88
+ @pytest.mark.asyncio
89
+ async def test_array_path_parameter_handling(mock_client):
90
+ """Test how array path parameters are handled."""
91
+ # Create a simple route with array path parameter
92
+ route = HTTPRoute(
93
+ path="/select/{days}",
94
+ method="PUT",
95
+ operation_id="test-operation",
96
+ parameters=[
97
+ ParameterInfo(
98
+ name="days",
99
+ location="path",
100
+ required=True,
101
+ schema={
102
+ "type": "array",
103
+ "items": {
104
+ "type": "string",
105
+ "enum": [
106
+ "monday",
107
+ "tuesday",
108
+ "wednesday",
109
+ "thursday",
110
+ "friday",
111
+ "saturday",
112
+ "sunday",
113
+ ],
114
+ },
115
+ },
116
+ )
117
+ ],
118
+ )
119
+
120
+ # Create the tool
121
+ tool = OpenAPITool(
122
+ client=mock_client,
123
+ route=route,
124
+ name="test-operation",
125
+ description="Test operation",
126
+ parameters={},
127
+ )
128
+
129
+ # Test with a single value
130
+ await tool._execute_request(days=["monday"])
131
+
132
+ # Check that the path parameter is formatted correctly
133
+ # This is where the bug is: it should be '/select/monday' not '/select/[\'monday\']'
134
+ mock_client.request.assert_called_with(
135
+ method="PUT",
136
+ url="/select/monday", # This is the expected format
137
+ params={},
138
+ headers={},
139
+ json=None,
140
+ timeout=None,
141
+ )
142
+ mock_client.request.reset_mock()
143
+
144
+ # Test with multiple values
145
+ await tool._execute_request(days=["monday", "tuesday"])
146
+
147
+ # Check that the path parameter is formatted correctly
148
+ # It should be '/select/monday,tuesday' not '/select/[\'monday\', \'tuesday\']'
149
+ mock_client.request.assert_called_with(
150
+ method="PUT",
151
+ url="/select/monday,tuesday", # This is the expected format
152
+ params={},
153
+ headers={},
154
+ json=None,
155
+ timeout=None,
156
+ )
157
+
158
+
159
+ @pytest.mark.asyncio
160
+ async def test_integration_array_path_parameter(array_path_spec, mock_client):
161
+ """Integration test for array path parameters."""
162
+ # Create FastMCP from the spec
163
+ mcp = FastMCP.from_openapi(array_path_spec, client=mock_client)
164
+
165
+ # Call the tool with a single value
166
+ await mcp._mcp_call_tool("test-operation", {"days": ["monday"]})
167
+
168
+ # Check the request was made correctly
169
+ mock_client.request.assert_called_with(
170
+ method="PUT",
171
+ url="/select/monday",
172
+ params={},
173
+ headers={},
174
+ json=None,
175
+ timeout=None,
176
+ )
177
+ mock_client.request.reset_mock()
178
+
179
+ # Call the tool with multiple values
180
+ await mcp._mcp_call_tool("test-operation", {"days": ["monday", "tuesday"]})
181
+
182
+ # Check the request was made correctly
183
+ mock_client.request.assert_called_with(
184
+ method="PUT",
185
+ url="/select/monday,tuesday",
186
+ params={},
187
+ headers={},
188
+ json=None,
189
+ timeout=None,
190
+ )