Repository: jlowin/fastmcp
Pull request #278 (2025-04-29 19:45:56)

#267 Fix openapi template resource to support multiple path parameters
The implementation now correctly handles multiple path parameters in OpenAPI endpoints. Previously, only the last parameter was recognized, leading to incorrect behavior when accessing resources with multiple parameters. The changes include updates to the parameter extraction logic and the addition of tests to ensure proper functionality. This addresses issue https://github.com/jlowin/fastmcp/issues/267.


Patch:
diff --git a/src/fastmcp/server/openapi.py b/src/fastmcp/server/openapi.py
index b3396b3..ea8388e 100644
--- a/src/fastmcp/server/openapi.py
+++ b/src/fastmcp/server/openapi.py
@@ -278,21 +278,26 @@ 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
+                        # Reverse sorting from creation order (traversal is backwards)
+                        param_matches.sort(reverse=True)
+                        # 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):
+                            # Ensure we don't use resource identifier as parameter
+                            if i < expected_param_count:
+                                # Get values from the end of parts
+                                param_value = parts[-1 - i]
+                                path_params[param_name] = param_value
 
                     # Replace path parameters with their values
                     for param_name, param_value in path_params.items():


Test patch:
diff --git a/tests/server/test_openapi.py b/tests/server/test_openapi.py
index 8b6e828..df46138 100644
--- a/tests/server/test_openapi.py
+++ b/tests/server/test_openapi.py
@@ -51,6 +51,14 @@ def fastapi_app(users_db: dict[int, User]) -> FastAPI:
         """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 and filter by active state."""
+        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:
         """Create a new user."""
@@ -303,12 +311,20 @@ 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,
@@ -332,6 +348,29 @@ class TestResourceTemplates:
         response = await api_client.get(f"/users/{user_id}")
         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_active_state_users__user_id___is_active__get/{is_active}/{user_id}"
+            )
+            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 +857,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}"