Spaces:
Running
Running
Juergen Eger commited on
Commit ·
11780c0
1
Parent(s): 7657683
#267 Fixed openapi template resource only supporting single param
Browse files- src/fastmcp/server/openapi.py +10 -9
- tests/server/test_openapi.py +39 -2
src/fastmcp/server/openapi.py
CHANGED
|
@@ -278,21 +278,22 @@ class OpenAPIResource(Resource):
|
|
| 278 |
if "{" in path and "}" in path:
|
| 279 |
# Extract the resource ID from the URI (the last part after the last slash)
|
| 280 |
parts = resource_uri.split("/")
|
|
|
|
| 281 |
if len(parts) > 1:
|
| 282 |
# Find all path parameters in the route path
|
| 283 |
path_params = {}
|
| 284 |
|
| 285 |
-
#
|
| 286 |
-
param_value = parts[
|
| 287 |
-
-1
|
| 288 |
-
] # The last part contains the parameter value
|
| 289 |
-
|
| 290 |
-
# Find the path parameter name from the route path
|
| 291 |
param_matches = re.findall(r"\{([^}]+)\}", path)
|
| 292 |
if param_matches:
|
| 293 |
-
#
|
| 294 |
-
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
|
| 297 |
# Replace path parameters with their values
|
| 298 |
for param_name, param_value in path_params.items():
|
|
|
|
| 278 |
if "{" in path and "}" in path:
|
| 279 |
# Extract the resource ID from the URI (the last part after the last slash)
|
| 280 |
parts = resource_uri.split("/")
|
| 281 |
+
|
| 282 |
if len(parts) > 1:
|
| 283 |
# Find all path parameters in the route path
|
| 284 |
path_params = {}
|
| 285 |
|
| 286 |
+
# Find the path parameter names from the route path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
param_matches = re.findall(r"\{([^}]+)\}", path)
|
| 288 |
if param_matches:
|
| 289 |
+
# Number of sent parameters is number of parts -1 (assuming first part is resource identifier)
|
| 290 |
+
expected_param_count = len(parts) -1
|
| 291 |
+
# Map parameters from the end of the URI to the parameters in the path
|
| 292 |
+
# Last parameter in URI (parts[-1]) maps to last parameter in path, and so on
|
| 293 |
+
for i, param_name in enumerate(param_matches):
|
| 294 |
+
if i < len(expected_param_count): # Ensure we don't use resource identifier as parameter
|
| 295 |
+
param_value = parts[-1-i] # Get values from the end of parts
|
| 296 |
+
path_params[param_name] = param_value
|
| 297 |
|
| 298 |
# Replace path parameters with their values
|
| 299 |
for param_name, param_value in path_params.items():
|
tests/server/test_openapi.py
CHANGED
|
@@ -50,6 +50,14 @@ def fastapi_app(users_db: dict[int, User]) -> FastAPI:
|
|
| 50 |
async def get_user(user_id: int) -> User | None:
|
| 51 |
"""Get a user by ID."""
|
| 52 |
return users_db.get(user_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
@app.post("/users", tags=["users", "create"])
|
| 55 |
async def create_user(user: UserCreate) -> User:
|
|
@@ -303,12 +311,17 @@ class TestResourceTemplates:
|
|
| 303 |
"""
|
| 304 |
async with Client(fastmcp_openapi_server) as client:
|
| 305 |
resource_templates = await client.list_resource_templates()
|
| 306 |
-
assert len(resource_templates) ==
|
| 307 |
assert resource_templates[0].name == "get_user_users__user_id__get"
|
| 308 |
assert (
|
| 309 |
resource_templates[0].uriTemplate
|
| 310 |
== r"resource://openapi/get_user_users__user_id__get/{user_id}"
|
| 311 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
|
| 313 |
async def test_get_resource_template(
|
| 314 |
self,
|
|
@@ -333,6 +346,30 @@ class TestResourceTemplates:
|
|
| 333 |
assert resource == response.json()
|
| 334 |
|
| 335 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
class TestPrompts:
|
| 337 |
async def test_list_prompts(self, fastmcp_openapi_server: FastMCPOpenAPI):
|
| 338 |
"""
|
|
@@ -818,7 +855,7 @@ class TestMountFastMCP:
|
|
| 818 |
# Check that templates are available with prefixed URIs
|
| 819 |
async with Client(mcp) as client:
|
| 820 |
templates = await client.list_resource_templates()
|
| 821 |
-
assert len(templates) ==
|
| 822 |
assert templates[0].name == "get_user_users__user_id__get"
|
| 823 |
prefixed_template_uri = (
|
| 824 |
r"fastapi+resource://openapi/get_user_users__user_id__get/{user_id}"
|
|
|
|
| 50 |
async def get_user(user_id: int) -> User | None:
|
| 51 |
"""Get a user by ID."""
|
| 52 |
return users_db.get(user_id)
|
| 53 |
+
|
| 54 |
+
@app.get("/users/{user_id}/{is_active}", tags=["users", "detail"])
|
| 55 |
+
async def get_user_active_state(user_id: int, is_active: bool) -> User | None:
|
| 56 |
+
"""Get a user by ID."""
|
| 57 |
+
user = users_db.get(user_id)
|
| 58 |
+
if user is not None and user.active == is_active:
|
| 59 |
+
return user
|
| 60 |
+
return None
|
| 61 |
|
| 62 |
@app.post("/users", tags=["users", "create"])
|
| 63 |
async def create_user(user: UserCreate) -> User:
|
|
|
|
| 311 |
"""
|
| 312 |
async with Client(fastmcp_openapi_server) as client:
|
| 313 |
resource_templates = await client.list_resource_templates()
|
| 314 |
+
assert len(resource_templates) == 2
|
| 315 |
assert resource_templates[0].name == "get_user_users__user_id__get"
|
| 316 |
assert (
|
| 317 |
resource_templates[0].uriTemplate
|
| 318 |
== r"resource://openapi/get_user_users__user_id__get/{user_id}"
|
| 319 |
)
|
| 320 |
+
assert resource_templates[1].name == "get_user_active_state_users__user_id___is_active__get"
|
| 321 |
+
assert (
|
| 322 |
+
resource_templates[1].uriTemplate
|
| 323 |
+
== r"resource://openapi/get_user_active_state_users__user_id___is_active__get/{is_active}/{user_id}"
|
| 324 |
+
)
|
| 325 |
|
| 326 |
async def test_get_resource_template(
|
| 327 |
self,
|
|
|
|
| 346 |
assert resource == response.json()
|
| 347 |
|
| 348 |
|
| 349 |
+
async def test_get_resource_template_multi_param(
|
| 350 |
+
self,
|
| 351 |
+
fastmcp_openapi_server: FastMCPOpenAPI,
|
| 352 |
+
api_client,
|
| 353 |
+
users_db: dict[int, User],
|
| 354 |
+
):
|
| 355 |
+
"""
|
| 356 |
+
The resource template created by the OpenAPI server should be the same as the original
|
| 357 |
+
"""
|
| 358 |
+
user_id = 2
|
| 359 |
+
is_active = True
|
| 360 |
+
async with Client(fastmcp_openapi_server) as client:
|
| 361 |
+
resource_response = await client.read_resource(
|
| 362 |
+
f"resource://openapi/get_user_users__user_type__user_id__get/{user_id}/{is_active}"
|
| 363 |
+
)
|
| 364 |
+
assert isinstance(resource_response[0], TextResourceContents)
|
| 365 |
+
response_text = resource_response[0].text
|
| 366 |
+
resource = json.loads(response_text)
|
| 367 |
+
|
| 368 |
+
assert resource == users_db[user_id].model_dump()
|
| 369 |
+
response = await api_client.get(f"/users/{user_id}/{is_active}")
|
| 370 |
+
assert resource == response.json()
|
| 371 |
+
|
| 372 |
+
|
| 373 |
class TestPrompts:
|
| 374 |
async def test_list_prompts(self, fastmcp_openapi_server: FastMCPOpenAPI):
|
| 375 |
"""
|
|
|
|
| 855 |
# Check that templates are available with prefixed URIs
|
| 856 |
async with Client(mcp) as client:
|
| 857 |
templates = await client.list_resource_templates()
|
| 858 |
+
assert len(templates) == 2
|
| 859 |
assert templates[0].name == "get_user_users__user_id__get"
|
| 860 |
prefixed_template_uri = (
|
| 861 |
r"fastapi+resource://openapi/get_user_users__user_id__get/{user_id}"
|