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 01/10] #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}" From 87c63ec343a1a51e8ce67ddf4763eb6384eb05c0 Mon Sep 17 00:00:00 2001 From: Juergen Eger <11678629+jeger-at@users.noreply.github.com> Date: Tue, 29 Apr 2025 08:04:04 +0000 Subject: [PATCH 02/10] openapi template resources with more than 1 path parameter is not supported #267 --- src/fastmcp/server/openapi.py | 4 +++- tests/server/test_openapi.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/fastmcp/server/openapi.py b/src/fastmcp/server/openapi.py index 88539ff65..a2dd3e21e 100644 --- a/src/fastmcp/server/openapi.py +++ b/src/fastmcp/server/openapi.py @@ -286,12 +286,14 @@ class OpenAPIResource(Resource): # Find the path parameter names from the route path param_matches = re.findall(r"\{([^}]+)\}", path) if param_matches: + # 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): - if i < len(expected_param_count): # Ensure we don't use resource identifier as parameter + if i < 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 diff --git a/tests/server/test_openapi.py b/tests/server/test_openapi.py index 5d0506648..091b158db 100644 --- a/tests/server/test_openapi.py +++ b/tests/server/test_openapi.py @@ -53,7 +53,7 @@ def fastapi_app(users_db: dict[int, User]) -> FastAPI: @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.""" + """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 @@ -359,7 +359,7 @@ class TestResourceTemplates: 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}" + 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 From da4b42659efe895aadceae92cbf4eaed0fa15c8a Mon Sep 17 00:00:00 2001 From: Juergen Eger <11678629+jeger-at@users.noreply.github.com> Date: Tue, 29 Apr 2025 08:10:36 +0000 Subject: [PATCH 03/10] openapi template resources with more than 1 path parameter is not supported #267 - fixing ruff findings --- src/fastmcp/server/openapi.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/fastmcp/server/openapi.py b/src/fastmcp/server/openapi.py index a2dd3e21e..f16ac4fe5 100644 --- a/src/fastmcp/server/openapi.py +++ b/src/fastmcp/server/openapi.py @@ -289,12 +289,14 @@ class OpenAPIResource(Resource): # 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 + 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 < expected_param_count: # Ensure we don't use resource identifier as parameter - param_value = parts[-1-i] # Get values from the end of parts + # 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 From 08e850441a9277b3f576a0b158776bebcf8df417 Mon Sep 17 00:00:00 2001 From: Juergen Eger <11678629+jeger-at@users.noreply.github.com> Date: Tue, 29 Apr 2025 08:19:45 +0000 Subject: [PATCH 04/10] fixing ruff --- tests/server/test_openapi.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/server/test_openapi.py b/tests/server/test_openapi.py index 091b158db..79d0103c0 100644 --- a/tests/server/test_openapi.py +++ b/tests/server/test_openapi.py @@ -317,7 +317,10 @@ class TestResourceTemplates: 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].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}" From 2a08b831adf879e9e8e20adde889b2caa3522f38 Mon Sep 17 00:00:00 2001 From: Juergen Eger <11678629+jeger-at@users.noreply.github.com> Date: Tue, 29 Apr 2025 08:21:07 +0000 Subject: [PATCH 05/10] fixing ruff --- tests/server/test_openapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/server/test_openapi.py b/tests/server/test_openapi.py index 79d0103c0..97c8201a0 100644 --- a/tests/server/test_openapi.py +++ b/tests/server/test_openapi.py @@ -318,7 +318,7 @@ class TestResourceTemplates: == r"resource://openapi/get_user_users__user_id__get/{user_id}" ) assert ( - resource_templates[1].name + resource_templates[1].name == "get_user_active_state_users__user_id___is_active__get" ) assert ( From 587c48f27c4b6a36dfa80557b4c4b5d5a223a219 Mon Sep 17 00:00:00 2001 From: Juergen Eger <11678629+jeger-at@users.noreply.github.com> Date: Tue, 29 Apr 2025 08:24:53 +0000 Subject: [PATCH 06/10] fix ruff --- src/fastmcp/server/openapi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fastmcp/server/openapi.py b/src/fastmcp/server/openapi.py index f16ac4fe5..b53dae111 100644 --- a/src/fastmcp/server/openapi.py +++ b/src/fastmcp/server/openapi.py @@ -294,9 +294,9 @@ class OpenAPIResource(Resource): # 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] + 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 From 2591836d86bc4e5260d4a3996f95f19ea28c857e Mon Sep 17 00:00:00 2001 From: Juergen Eger <11678629+jeger-at@users.noreply.github.com> Date: Tue, 29 Apr 2025 08:25:57 +0000 Subject: [PATCH 07/10] fix ruff --- src/fastmcp/server/openapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fastmcp/server/openapi.py b/src/fastmcp/server/openapi.py index b53dae111..931edd00b 100644 --- a/src/fastmcp/server/openapi.py +++ b/src/fastmcp/server/openapi.py @@ -296,7 +296,7 @@ class OpenAPIResource(Resource): # 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] + param_value = parts[-1 - i] path_params[param_name] = param_value # Replace path parameters with their values From ba7b44879a717386b37532dfa3c2bd228df07a6d Mon Sep 17 00:00:00 2001 From: Juergen Eger <11678629+jeger-at@users.noreply.github.com> Date: Tue, 29 Apr 2025 08:27:34 +0000 Subject: [PATCH 08/10] fix ruff number 123 --- src/fastmcp/server/openapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fastmcp/server/openapi.py b/src/fastmcp/server/openapi.py index 931edd00b..ea8388ec0 100644 --- a/src/fastmcp/server/openapi.py +++ b/src/fastmcp/server/openapi.py @@ -278,7 +278,7 @@ 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 = {} From 670047d674cf2c590f7a8a532967660b62d81774 Mon Sep 17 00:00:00 2001 From: Juergen Eger <11678629+jeger-at@users.noreply.github.com> Date: Tue, 29 Apr 2025 08:29:00 +0000 Subject: [PATCH 09/10] ruff fix once again --- tests/server/test_openapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/server/test_openapi.py b/tests/server/test_openapi.py index 97c8201a0..90226d405 100644 --- a/tests/server/test_openapi.py +++ b/tests/server/test_openapi.py @@ -50,7 +50,7 @@ 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 and filter by active state.""" From 92d0e5705f453c7400b22c7f1d084ef767ac3eb6 Mon Sep 17 00:00:00 2001 From: Juergen Eger <11678629+jeger-at@users.noreply.github.com> Date: Tue, 29 Apr 2025 08:44:14 +0000 Subject: [PATCH 10/10] fix ruff --- tests/server/test_openapi.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/server/test_openapi.py b/tests/server/test_openapi.py index 90226d405..df4613898 100644 --- a/tests/server/test_openapi.py +++ b/tests/server/test_openapi.py @@ -348,7 +348,6 @@ 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,