From 09fae7541e9993a00a6c1742b4ef7f728d0d6dac Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Tue, 8 Jul 2025 21:28:46 -0400 Subject: [PATCH] Fix OpenAPI tool name registration when modified by mcp_component_fn (#1096) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves issue where tools modified by mcp_component_fn were registered with original names but accessible with modified names, causing "Unknown tool" errors. Now tools are registered using their final modified names. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude --- src/fastmcp/server/openapi.py | 21 ++++-- tests/server/openapi/test_route_map_fn.py | 78 +++++++++++++++++++++++ 2 files changed, 93 insertions(+), 6 deletions(-) diff --git a/src/fastmcp/server/openapi.py b/src/fastmcp/server/openapi.py index 30b9abf78..d3b30cad9 100644 --- a/src/fastmcp/server/openapi.py +++ b/src/fastmcp/server/openapi.py @@ -847,10 +847,13 @@ class FastMCPOpenAPI(FastMCP): f"Using component as-is." ) + # Use the potentially modified tool name as the registration key + final_tool_name = tool.name + # Register the tool by directly assigning to the tools dictionary - self._tool_manager._tools[tool_name] = tool + self._tool_manager._tools[final_tool_name] = tool logger.debug( - f"Registered TOOL: {tool_name} ({route.method} {route.path}) with tags: {route.tags}" + f"Registered TOOL: {final_tool_name} ({route.method} {route.path}) with tags: {route.tags}" ) def _create_openapi_resource( @@ -897,10 +900,13 @@ class FastMCPOpenAPI(FastMCP): f"Using component as-is." ) + # Use the potentially modified resource URI as the registration key + final_resource_uri = str(resource.uri) + # Register the resource by directly assigning to the resources dictionary - self._resource_manager._resources[str(resource.uri)] = resource + self._resource_manager._resources[final_resource_uri] = resource logger.debug( - f"Registered RESOURCE: {resource_uri} ({route.method} {route.path}) with tags: {route.tags}" + f"Registered RESOURCE: {final_resource_uri} ({route.method} {route.path}) with tags: {route.tags}" ) def _create_openapi_template( @@ -976,8 +982,11 @@ class FastMCPOpenAPI(FastMCP): f"Using component as-is." ) + # Use the potentially modified template URI as the registration key + final_template_uri = template.uri_template + # Register the template by directly assigning to the templates dictionary - self._resource_manager._templates[uri_template_str] = template + self._resource_manager._templates[final_template_uri] = template logger.debug( - f"Registered TEMPLATE: {uri_template_str} ({route.method} {route.path}) with tags: {route.tags}" + f"Registered TEMPLATE: {final_template_uri} ({route.method} {route.path}) with tags: {route.tags}" ) diff --git a/tests/server/openapi/test_route_map_fn.py b/tests/server/openapi/test_route_map_fn.py index f0cc8eef3..2689300ac 100644 --- a/tests/server/openapi/test_route_map_fn.py +++ b/tests/server/openapi/test_route_map_fn.py @@ -1,5 +1,7 @@ """Tests for the route_map_fn and component_fn functionality in FastMCPOpenAPI.""" +from unittest.mock import AsyncMock + import httpx import pytest @@ -372,3 +374,79 @@ def test_route_map_fn_can_rescue_excluded_routes(sample_openapi_spec, http_clien assert "getAdminSettings" not in tools assert "updateAdminSettings" not in tools assert "getData" not in tools + + +class TestComponentFnToolNameModificationBug: + """Test that mcp_component_fn can modify tool names without breaking access (Issue #1091).""" + + @pytest.fixture + def mocked_http_client(self): + """Mock HTTP client that returns successful responses.""" + from unittest.mock import MagicMock + + mock_client = AsyncMock(spec=httpx.AsyncClient) + + # Mock a successful response + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"result": "success"} + mock_response.raise_for_status.return_value = None + + mock_client.request.return_value = mock_response + return mock_client + + @pytest.fixture + def server_with_modified_tool_names(self, sample_openapi_spec, mocked_http_client): + """Server with tool names modified by mcp_component_fn.""" + + def modify_tool_names(route, component): + """Modify tool names by adding v1_removed_ prefix.""" + from fastmcp.server.openapi import OpenAPITool + + if isinstance(component, OpenAPITool): + if component.name.startswith("get"): + component.name = "v1_removed_" + component.name + + return FastMCPOpenAPI( + openapi_spec=sample_openapi_spec, + client=mocked_http_client, + name="Test Server", + mcp_component_fn=modify_tool_names, + ) + + def test_registration(self, server_with_modified_tool_names): + """Test that modified tool names are properly registered.""" + tools = server_with_modified_tool_names._tool_manager._tools + + # Tool should be registered with the modified name + assert "v1_removed_getUserById" in tools + assert "v1_removed_getAdminSettings" in tools + assert "v1_removed_getData" in tools + + # The tool object should have the same name as the registration key + for key, tool in tools.items(): + if key.startswith("v1_removed_"): + assert tool.name == key + + async def test_client_access(self, server_with_modified_tool_names): + """Test that modified tool names are accessible via client.""" + from fastmcp.client import Client + + async with Client(server_with_modified_tool_names) as client: + # List tools to verify they are exposed correctly + available_tools = await client.list_tools() + tool_names = [tool.name for tool in available_tools] + + # Verify the modified tool names are available + assert "v1_removed_getUserById" in tool_names + assert "v1_removed_getAdminSettings" in tool_names + assert "v1_removed_getData" in tool_names + + async def test_client_call(self, server_with_modified_tool_names): + """Test that modified tool names can be called via client.""" + from fastmcp.client import Client + + async with Client(server_with_modified_tool_names) as client: + # This should work without "Unknown tool" error + result = await client.call_tool("v1_removed_getData", {}) + assert result.data == {"result": "success"}