From b684c0dbbfc15352faebae0984da786e623ed7d5 Mon Sep 17 00:00:00 2001 From: Juergen Eger <11678629+jeger-at@users.noreply.github.com> Date: Tue, 29 Apr 2025 07:57:40 +0000 Subject: [PATCH] #267 Fixed openapi template resource only supporting single param --- src/fastmcp/server/openapi.py | 19 ++++++++-------- tests/server/test_openapi.py | 41 +++++++++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/src/fastmcp/server/openapi.py b/src/fastmcp/server/openapi.py index b3396b368..88539ff65 100644 --- a/src/fastmcp/server/openapi.py +++ b/src/fastmcp/server/openapi.py @@ -278,21 +278,22 @@ class OpenAPIResource(Resource): if "{" in path and "}" in path: # Extract the resource ID from the URI (the last part after the last slash) parts = resource_uri.split("/") + if len(parts) > 1: # Find all path parameters in the route path path_params = {} - # Extract parameters from the URI - param_value = parts[ - -1 - ] # The last part contains the parameter value - - # Find the path parameter name from the route path + # Find the path parameter names from the route path param_matches = re.findall(r"\{([^}]+)\}", path) if param_matches: - # Assume the last parameter in the URI is for the first path parameter in the route - path_param_name = param_matches[0] - path_params[path_param_name] = param_value + # Number of sent parameters is number of parts -1 (assuming first part is resource identifier) + expected_param_count = len(parts) -1 + # Map parameters from the end of the URI to the parameters in the path + # Last parameter in URI (parts[-1]) maps to last parameter in path, and so on + for i, param_name in enumerate(param_matches): + if i < len(expected_param_count): # Ensure we don't use resource identifier as parameter + param_value = parts[-1-i] # Get values from the end of parts + path_params[param_name] = param_value # Replace path parameters with their values for param_name, param_value in path_params.items(): diff --git a/tests/server/test_openapi.py b/tests/server/test_openapi.py index 8b6e828d7..5d0506648 100644 --- a/tests/server/test_openapi.py +++ b/tests/server/test_openapi.py @@ -50,6 +50,14 @@ def fastapi_app(users_db: dict[int, User]) -> FastAPI: async def get_user(user_id: int) -> User | None: """Get a user by ID.""" return users_db.get(user_id) + + @app.get("/users/{user_id}/{is_active}", tags=["users", "detail"]) + async def get_user_active_state(user_id: int, is_active: bool) -> User | None: + """Get a user by ID.""" + user = users_db.get(user_id) + if user is not None and user.active == is_active: + return user + return None @app.post("/users", tags=["users", "create"]) async def create_user(user: UserCreate) -> User: @@ -303,12 +311,17 @@ class TestResourceTemplates: """ async with Client(fastmcp_openapi_server) as client: resource_templates = await client.list_resource_templates() - assert len(resource_templates) == 1 + assert len(resource_templates) == 2 assert resource_templates[0].name == "get_user_users__user_id__get" assert ( resource_templates[0].uriTemplate == r"resource://openapi/get_user_users__user_id__get/{user_id}" ) + assert resource_templates[1].name == "get_user_active_state_users__user_id___is_active__get" + assert ( + resource_templates[1].uriTemplate + == r"resource://openapi/get_user_active_state_users__user_id___is_active__get/{is_active}/{user_id}" + ) async def test_get_resource_template( self, @@ -333,6 +346,30 @@ class TestResourceTemplates: assert resource == response.json() + async def test_get_resource_template_multi_param( + self, + fastmcp_openapi_server: FastMCPOpenAPI, + api_client, + users_db: dict[int, User], + ): + """ + The resource template created by the OpenAPI server should be the same as the original + """ + user_id = 2 + is_active = True + async with Client(fastmcp_openapi_server) as client: + resource_response = await client.read_resource( + f"resource://openapi/get_user_users__user_type__user_id__get/{user_id}/{is_active}" + ) + assert isinstance(resource_response[0], TextResourceContents) + response_text = resource_response[0].text + resource = json.loads(response_text) + + assert resource == users_db[user_id].model_dump() + response = await api_client.get(f"/users/{user_id}/{is_active}") + assert resource == response.json() + + class TestPrompts: async def test_list_prompts(self, fastmcp_openapi_server: FastMCPOpenAPI): """ @@ -818,7 +855,7 @@ class TestMountFastMCP: # Check that templates are available with prefixed URIs async with Client(mcp) as client: templates = await client.list_resource_templates() - assert len(templates) == 1 + assert len(templates) == 2 assert templates[0].name == "get_user_users__user_id__get" prefixed_template_uri = ( r"fastapi+resource://openapi/get_user_users__user_id__get/{user_id}"