Merge pull request #278 from jeger-at/bug/267

#267 Fix openapi template resource to support multiple path parameters
This commit is contained in:
Jeremiah Lowin 2025-04-29 18:08:04 -04:00 committed by GitHub
commit 2dc71971a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 55 additions and 11 deletions

View file

@ -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():

View file

@ -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}"