diff --git a/docs/servers/openapi.mdx b/docs/servers/openapi.mdx index 580502a49..ff7d6460c 100644 --- a/docs/servers/openapi.mdx +++ b/docs/servers/openapi.mdx @@ -41,17 +41,9 @@ That's it! Your entire API is now available as an MCP server. Clients can discov ## Route Mapping +By default, FastMCP converts **every endpoint** in your OpenAPI specification into an MCP **Tool**. This provides a simple, predictable starting point that ensures all your API's functionality is immediately available to the vast majority of LLM clients which only support MCP tools. - -FastMCP analyzes your API specification and automatically creates MCP components based on HTTP semantics and REST conventions. By default, the following rules are used to determine what MCP component to create for each route: - -| OpenAPI Route | Example | MCP Component | -|---------------|---------|---------------| -| `GET` with path params | `GET /users/{id}` | **Resource Template** | -| `GET` without path params | `GET /stats` | **Resource** | -| `POST`, `PUT`, `PATCH`, `DELETE`, etc. | `POST /users` | **Tool** | - -Interally, FastMCP uses an ordered list of `RouteMap` objects to determine how to map OpenAPI routes to various MCP component types. +While this is a pragmatic default for maximum compatibility, you can easily customize this behavior. Internally, FastMCP uses an ordered list of `RouteMap` objects to determine how to map OpenAPI routes to various MCP component types. Each `RouteMap` specifies a combination of methods, patterns, and tags, as well as a corresponding MCP component type. Each OpenAPI route is checked against each `RouteMap` in order, and the first one that matches every criteria is used to determine its converted MCP type. A special type, `EXCLUDE`, can be used to exclude routes from the MCP server entirely. @@ -60,33 +52,14 @@ Each `RouteMap` specifies a combination of methods, patterns, and tags, as well - **Tags**: A set of OpenAPI tags that must all be present. An empty set (`{}`) means no tag filtering, so the route matches regardless of its tags. - **MCP type**: What MCP component type to create (`TOOL`, `RESOURCE`, `RESOURCE_TEMPLATE`, or `EXCLUDE`) -To illustrate this in practice, here are FastMCP's default rules as a list of `RouteMap` objects: +Here is FastMCP's default rule: ```python from fastmcp.server.openapi import RouteMap, MCPType DEFAULT_ROUTE_MAPPINGS = [ - - # GET with path parameters → ResourceTemplate - RouteMap( - methods=["GET"], - pattern=r".*\{.*\}.*", - mcp_type=MCPType.RESOURCE_TEMPLATE - ), - - # GET without path parameters → Resource - RouteMap( - methods=["GET"], - pattern=r".*", - mcp_type=MCPType.RESOURCE - ), - - # All other methods → Tool - RouteMap( - methods=["*"], - pattern=r".*", - mcp_type=MCPType.TOOL - ), + # All routes become tools + RouteMap(mcp_type=MCPType.TOOL), ] ``` @@ -94,20 +67,28 @@ DEFAULT_ROUTE_MAPPINGS = [ When creating your FastMCP server, you can customize routing behavior by providing your own list of `RouteMap` objects. Your custom maps are processed before the default route maps, and routes will be assigned to the first matching custom map. -For example, the following simple rule will treat every OpenAPI route as a tool: +For example, prior to FastMCP 2.8.0, GET requests were automatically mapped to `Resource` and `ResourceTemplate` components based on whether they had path parameters. (This was changed solely for client compatibility reasons.) You can restore this behavior by providing custom route maps: -```python {7} +```python {2, 5-10} from fastmcp import FastMCP from fastmcp.server.openapi import RouteMap, MCPType +# Restore pre-2.8.0 semantic mapping +semantic_maps = [ + # GET requests with path parameters become ResourceTemplates + RouteMap(methods=["GET"], pattern=r".*\{.*\}.*", mcp_type=MCPType.RESOURCE_TEMPLATE), + # All other GET requests become Resources + RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE), +] + mcp = FastMCP.from_openapi( ..., - route_maps=[ - RouteMap(mcp_type=MCPType.TOOL), - ], + route_maps=semantic_maps, ) ``` +With these maps, `GET` requests are handled semantically, and all other methods (`POST`, `PUT`, etc.) will fall through to the default rule and become `Tool`s. + Here is a more complete example that uses custom route maps to convert all `GET` endpoints under `/analytics/` to tools while excluding all admin endpoints and all routes tagged "internal". All other routes will be handled by the default rules: ```python diff --git a/src/fastmcp/server/openapi.py b/src/fastmcp/server/openapi.py index 58a0d5037..8f6682799 100644 --- a/src/fastmcp/server/openapi.py +++ b/src/fastmcp/server/openapi.py @@ -155,16 +155,10 @@ class RouteMap: self.route_type = self.mcp_type -# Default route mappings as a list, where order determines priority +# Default route mapping: all routes become tools. +# Users can provide custom route_maps to override this behavior. DEFAULT_ROUTE_MAPPINGS = [ - # GET requests with path parameters go to ResourceTemplate - RouteMap( - methods=["GET"], pattern=r".*\{.*\}.*", mcp_type=MCPType.RESOURCE_TEMPLATE - ), - # GET requests without path parameters go to Resource - RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE), - # All other HTTP methods go to Tool - RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL), + RouteMap(mcp_type=MCPType.TOOL), ] diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 04997426c..dadc0e4f7 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -1551,28 +1551,12 @@ class FastMCP(Generic[LifespanResultT]): route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, - all_routes_as_tools: bool = False, **settings: Any, ) -> FastMCPOpenAPI: """ Create a FastMCP server from an OpenAPI specification. """ - from .openapi import FastMCPOpenAPI, MCPType, RouteMap - - # Deprecated since 2.5.0 - if all_routes_as_tools: - warnings.warn( - "The 'all_routes_as_tools' parameter is deprecated and will be removed in a future version. " - 'Use \'route_maps=[RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]\' instead.', - DeprecationWarning, - stacklevel=2, - ) - - if all_routes_as_tools and route_maps: - raise ValueError("Cannot specify both all_routes_as_tools and route_maps") - - elif all_routes_as_tools: - route_maps = [RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)] + from .openapi import FastMCPOpenAPI return FastMCPOpenAPI( openapi_spec=openapi_spec, @@ -1593,7 +1577,6 @@ class FastMCP(Generic[LifespanResultT]): route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, - all_routes_as_tools: bool = False, httpx_client_kwargs: dict[str, Any] | None = None, **settings: Any, ) -> FastMCPOpenAPI: @@ -1601,22 +1584,7 @@ class FastMCP(Generic[LifespanResultT]): Create a FastMCP server from a FastAPI application. """ - from .openapi import FastMCPOpenAPI, MCPType, RouteMap - - # Deprecated since 2.5.0 - if all_routes_as_tools: - warnings.warn( - "The 'all_routes_as_tools' parameter is deprecated and will be removed in a future version. " - 'Use \'route_maps=[RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]\' instead.', - DeprecationWarning, - stacklevel=2, - ) - - if all_routes_as_tools and route_maps: - raise ValueError("Cannot specify both all_routes_as_tools and route_maps") - - elif all_routes_as_tools: - route_maps = [RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)] + from .openapi import FastMCPOpenAPI if httpx_client_kwargs is None: httpx_client_kwargs = {} diff --git a/tests/client/test_openapi.py b/tests/client/test_openapi.py index 000895e87..d97f89eb9 100644 --- a/tests/client/test_openapi.py +++ b/tests/client/test_openapi.py @@ -6,6 +6,7 @@ from fastapi import FastAPI, Request from fastmcp import Client, FastMCP from fastmcp.client.transports import SSETransport, StreamableHttpTransport +from fastmcp.server.openapi import MCPType, RouteMap from fastmcp.utilities.tests import run_server_in_process @@ -27,6 +28,16 @@ def fastmcp_server_for_headers() -> FastMCP: mcp = FastMCP.from_fastapi( app, httpx_client_kwargs={"headers": {"x-server-header": "test-abc"}}, + route_maps=[ + # GET requests with path parameters go to ResourceTemplate + RouteMap( + methods=["GET"], + pattern=r".*\{.*\}.*", + mcp_type=MCPType.RESOURCE_TEMPLATE, + ), + # GET requests without path parameters go to Resource + RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE), + ], ) return mcp diff --git a/tests/server/openapi/test_openapi.py b/tests/server/openapi/test_openapi.py index 97b5297a6..2f5fad773 100644 --- a/tests/server/openapi/test_openapi.py +++ b/tests/server/openapi/test_openapi.py @@ -46,6 +46,20 @@ def users_db() -> dict[int, User]: } +# route maps for GET requests +# use these to create components of all types instead of just tools +GET_ROUTE_MAPS = [ + # GET requests with path parameters go to ResourceTemplate + RouteMap( + methods=["GET"], + pattern=r".*\{.*\}.*", + mcp_type=MCPType.RESOURCE_TEMPLATE, + ), + # GET requests without path parameters go to Resource + RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE), +] + + @pytest.fixture def fastapi_app(users_db: dict[int, User]) -> FastAPI: app = FastAPI(title="FastAPI App") @@ -122,7 +136,7 @@ def api_client(fastapi_app: FastAPI) -> AsyncClient: @pytest.fixture -async def fastmcp_openapi_server( +async def fastmcp_openapi_server_with_all_types( fastapi_app: FastAPI, api_client: httpx.AsyncClient ) -> FastMCPOpenAPI: openapi_spec = fastapi_app.openapi() @@ -131,6 +145,7 @@ async def fastmcp_openapi_server( openapi_spec=openapi_spec, client=api_client, name="Test App", + route_maps=GET_ROUTE_MAPS, ) @@ -169,6 +184,7 @@ async def test_create_openapi_server_with_timeout( client=api_client, name="Test App", timeout=1.0, + route_maps=GET_ROUTE_MAPS, ) assert server._timeout == 1.0 @@ -186,11 +202,24 @@ async def test_create_openapi_server_with_timeout( class TestTools: - async def test_list_tools(self, fastmcp_openapi_server: FastMCPOpenAPI): + async def test_default_behavior_converts_everything_to_tools( + self, fastapi_app: FastAPI + ): """ By default, tools exclude GET methods """ - async with Client(fastmcp_openapi_server) as client: + server = FastMCPOpenAPI.from_fastapi(fastapi_app) + assert len(await server.get_tools()) == 8 + assert len(await server.get_resources()) == 0 + assert len(await server.get_resource_templates()) == 0 + + async def test_list_tools( + self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI + ): + """ + By default, tools exclude GET methods + """ + async with Client(fastmcp_openapi_server_with_all_types) as client: tools = await client.list_tools() assert len(tools) == 2 @@ -224,12 +253,14 @@ class TestTools: ) async def test_call_create_user_tool( - self, fastmcp_openapi_server: FastMCPOpenAPI, api_client + self, + fastmcp_openapi_server_with_all_types: FastMCPOpenAPI, + api_client, ): """ The tool created by the OpenAPI server should be the same as the original """ - async with Client(fastmcp_openapi_server) as client: + async with Client(fastmcp_openapi_server_with_all_types) as client: tool_response = await client.call_tool( "create_user_users_post", {"name": "David", "active": False} ) @@ -243,19 +274,21 @@ class TestTools: assert len(response.json()) == 4 # Check that the user was created via MCP - async with Client(fastmcp_openapi_server) as client: + async with Client(fastmcp_openapi_server_with_all_types) as client: user_response = await client.read_resource("resource://get_user_users/4") response_text = user_response[0].text # type: ignore[attr-defined] user = json.loads(response_text) assert user == expected_user async def test_call_update_user_name_tool( - self, fastmcp_openapi_server: FastMCPOpenAPI, api_client + self, + fastmcp_openapi_server_with_all_types: FastMCPOpenAPI, + api_client, ): """ The tool created by the OpenAPI server should be the same as the original """ - async with Client(fastmcp_openapi_server) as client: + async with Client(fastmcp_openapi_server_with_all_types) as client: tool_response = await client.call_tool( "update_user_name_users", {"user_id": 1, "name": "XYZ"}, @@ -270,7 +303,7 @@ class TestTools: assert expected_data in response.json() # Check that the user was updated via MCP - async with Client(fastmcp_openapi_server) as client: + async with Client(fastmcp_openapi_server_with_all_types) as client: user_response = await client.read_resource("resource://get_user_users/1") response_text = user_response[0].text # type: ignore[attr-defined] user = json.loads(response_text) @@ -302,11 +335,13 @@ class TestTools: class TestResources: - async def test_list_resources(self, fastmcp_openapi_server: FastMCPOpenAPI): + async def test_list_resources( + self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI + ): """ By default, resources exclude GET methods without parameters """ - async with Client(fastmcp_openapi_server) as client: + async with Client(fastmcp_openapi_server_with_all_types) as client: resources = await client.list_resources() assert len(resources) == 4 assert resources[0].uri == AnyUrl("resource://get_users_users_get") @@ -314,7 +349,7 @@ class TestResources: async def test_get_resource( self, - fastmcp_openapi_server: FastMCPOpenAPI, + fastmcp_openapi_server_with_all_types: FastMCPOpenAPI, api_client, users_db: dict[int, User], ): @@ -325,7 +360,7 @@ class TestResources: json_users = TypeAdapter(list[User]).dump_python( sorted(users_db.values(), key=lambda x: x.id) ) - async with Client(fastmcp_openapi_server) as client: + async with Client(fastmcp_openapi_server_with_all_types) as client: resource_response = await client.read_resource( "resource://get_users_users_get" ) @@ -337,11 +372,11 @@ class TestResources: async def test_get_bytes_resource( self, - fastmcp_openapi_server: FastMCPOpenAPI, + fastmcp_openapi_server_with_all_types: FastMCPOpenAPI, api_client, ): """Test reading a resource that returns bytes.""" - async with Client(fastmcp_openapi_server) as client: + async with Client(fastmcp_openapi_server_with_all_types) as client: resource_response = await client.read_resource( "resource://ping_bytes_ping_bytes_get" ) @@ -350,23 +385,23 @@ class TestResources: async def test_get_str_resource( self, - fastmcp_openapi_server: FastMCPOpenAPI, + fastmcp_openapi_server_with_all_types: FastMCPOpenAPI, api_client, ): """Test reading a resource that returns a string.""" - async with Client(fastmcp_openapi_server) as client: + async with Client(fastmcp_openapi_server_with_all_types) as client: resource_response = await client.read_resource("resource://ping_ping_get") assert resource_response[0].text == "pong" # type: ignore[attr-defined] class TestResourceTemplates: async def test_list_resource_templates( - self, fastmcp_openapi_server: FastMCPOpenAPI + self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI ): """ By default, resource templates exclude GET methods without parameters """ - async with Client(fastmcp_openapi_server) as client: + async with Client(fastmcp_openapi_server_with_all_types) as client: resource_templates = await client.list_resource_templates() assert len(resource_templates) == 2 assert resource_templates[0].name == "get_user_users" @@ -381,7 +416,7 @@ class TestResourceTemplates: async def test_get_resource_template( self, - fastmcp_openapi_server: FastMCPOpenAPI, + fastmcp_openapi_server_with_all_types: FastMCPOpenAPI, api_client, users_db: dict[int, User], ): @@ -389,7 +424,7 @@ class TestResourceTemplates: The resource template created by the OpenAPI server should be the same as the original """ user_id = 2 - async with Client(fastmcp_openapi_server) as client: + async with Client(fastmcp_openapi_server_with_all_types) as client: resource_response = await client.read_resource( f"resource://get_user_users/{user_id}" ) @@ -402,7 +437,7 @@ class TestResourceTemplates: async def test_get_resource_template_multi_param( self, - fastmcp_openapi_server: FastMCPOpenAPI, + fastmcp_openapi_server_with_all_types: FastMCPOpenAPI, api_client, users_db: dict[int, User], ): @@ -411,7 +446,7 @@ class TestResourceTemplates: """ user_id = 2 is_active = True - async with Client(fastmcp_openapi_server) as client: + async with Client(fastmcp_openapi_server_with_all_types) as client: resource_response = await client.read_resource( f"resource://get_user_active_state_users/{is_active}/{user_id}" ) @@ -424,11 +459,13 @@ class TestResourceTemplates: class TestPrompts: - async def test_list_prompts(self, fastmcp_openapi_server: FastMCPOpenAPI): + async def test_list_prompts( + self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI + ): """ By default, there are no prompts. """ - async with Client(fastmcp_openapi_server) as client: + async with Client(fastmcp_openapi_server_with_all_types) as client: prompts = await client.list_prompts() assert len(prompts) == 0 @@ -437,11 +474,11 @@ class TestTagTransfer: """Tests for transferring tags from OpenAPI routes to MCP objects.""" async def test_tags_transferred_to_tools( - self, fastmcp_openapi_server: FastMCPOpenAPI + self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI ): """Test that tags from OpenAPI routes are correctly transferred to Tools.""" # Get internal tools directly (not the public API which returns MCP.Content) - tools = fastmcp_openapi_server._tool_manager.list_tools() + tools = fastmcp_openapi_server_with_all_types._tool_manager.list_tools() # Find the create_user and update_user_name tools create_user_tool = next( @@ -465,12 +502,12 @@ class TestTagTransfer: assert len(update_user_tool.tags) == 2 async def test_tags_transferred_to_resources( - self, fastmcp_openapi_server: FastMCPOpenAPI + self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI ): """Test that tags from OpenAPI routes are correctly transferred to Resources.""" # Get internal resources directly resources = list( - fastmcp_openapi_server._resource_manager.get_resources().values() + fastmcp_openapi_server_with_all_types._resource_manager.get_resources().values() ) # Find the get_users resource @@ -486,12 +523,12 @@ class TestTagTransfer: assert len(get_users_resource.tags) == 2 async def test_tags_transferred_to_resource_templates( - self, fastmcp_openapi_server: FastMCPOpenAPI + self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI ): """Test that tags from OpenAPI routes are correctly transferred to ResourceTemplates.""" # Get internal resource templates directly templates = list( - fastmcp_openapi_server._resource_manager.get_templates().values() + fastmcp_openapi_server_with_all_types._resource_manager.get_templates().values() ) # Find the get_user template @@ -507,12 +544,12 @@ class TestTagTransfer: assert len(get_user_template.tags) == 2 async def test_tags_preserved_in_resources_created_from_templates( - self, fastmcp_openapi_server: FastMCPOpenAPI + self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI ): """Test that tags are preserved when creating resources from templates.""" # Get internal resource templates directly templates = list( - fastmcp_openapi_server._resource_manager.get_templates().values() + fastmcp_openapi_server_with_all_types._resource_manager.get_templates().values() ) # Find the get_user template @@ -624,46 +661,49 @@ class TestOpenAPI30Compatibility: return httpx.AsyncClient(transport=transport, base_url="http://test") @pytest.fixture - async def openapi_30_server( + async def openapi_30_server_with_all_types( self, openapi_30_spec, mock_30_client ) -> FastMCPOpenAPI: """Create a FastMCPOpenAPI server from the OpenAPI 3.0 spec.""" return FastMCPOpenAPI( - openapi_spec=openapi_30_spec, client=mock_30_client, name="Product API 3.0" + openapi_spec=openapi_30_spec, + client=mock_30_client, + name="Product API 3.0", + route_maps=GET_ROUTE_MAPS, ) - async def test_server_creation(self, openapi_30_server): + async def test_server_creation(self, openapi_30_server_with_all_types): """Test that a server can be created from an OpenAPI 3.0 spec.""" - assert isinstance(openapi_30_server, FastMCP) - assert openapi_30_server.name == "Product API 3.0" + assert isinstance(openapi_30_server_with_all_types, FastMCP) + assert openapi_30_server_with_all_types.name == "Product API 3.0" - async def test_resource_discovery(self, openapi_30_server): + async def test_resource_discovery(self, openapi_30_server_with_all_types): """Test that resources are correctly discovered from an OpenAPI 3.0 spec.""" - async with Client(openapi_30_server) as client: + async with Client(openapi_30_server_with_all_types) as client: resources = await client.list_resources() assert len(resources) == 1 assert resources[0].uri == AnyUrl("resource://listProducts") - async def test_resource_template_discovery(self, openapi_30_server): + async def test_resource_template_discovery(self, openapi_30_server_with_all_types): """Test that resource templates are correctly discovered from an OpenAPI 3.0 spec.""" - async with Client(openapi_30_server) as client: + async with Client(openapi_30_server_with_all_types) as client: templates = await client.list_resource_templates() assert len(templates) == 1 assert templates[0].name == "getProduct" assert templates[0].uriTemplate == r"resource://getProduct/{product_id}" - async def test_tool_discovery(self, openapi_30_server): + async def test_tool_discovery(self, openapi_30_server_with_all_types): """Test that tools are correctly discovered from an OpenAPI 3.0 spec.""" - async with Client(openapi_30_server) as client: + async with Client(openapi_30_server_with_all_types) as client: tools = await client.list_tools() assert len(tools) == 1 assert tools[0].name == "createProduct" assert "name" in tools[0].inputSchema["properties"] assert "price" in tools[0].inputSchema["properties"] - async def test_resource_access(self, openapi_30_server): + async def test_resource_access(self, openapi_30_server_with_all_types): """Test reading a resource from an OpenAPI 3.0 server.""" - async with Client(openapi_30_server) as client: + async with Client(openapi_30_server_with_all_types) as client: resource_response = await client.read_resource("resource://listProducts") response_text = resource_response[0].text # type: ignore[attr-defined] content = json.loads(response_text) @@ -671,9 +711,9 @@ class TestOpenAPI30Compatibility: assert content[0]["name"] == "Product 1" assert content[1]["name"] == "Product 2" - async def test_resource_template_access(self, openapi_30_server): + async def test_resource_template_access(self, openapi_30_server_with_all_types): """Test reading a resource from template from an OpenAPI 3.0 server.""" - async with Client(openapi_30_server) as client: + async with Client(openapi_30_server_with_all_types) as client: resource_response = await client.read_resource("resource://getProduct/p1") response_text = resource_response[0].text # type: ignore[attr-defined] content = json.loads(response_text) @@ -681,9 +721,9 @@ class TestOpenAPI30Compatibility: assert content["name"] == "Product 1" assert content["price"] == 19.99 - async def test_tool_execution(self, openapi_30_server): + async def test_tool_execution(self, openapi_30_server_with_all_types): """Test executing a tool from an OpenAPI 3.0 server.""" - async with Client(openapi_30_server) as client: + async with Client(openapi_30_server_with_all_types) as client: result = await client.call_tool( "createProduct", {"name": "New Product", "price": 39.99} ) @@ -797,46 +837,49 @@ class TestOpenAPI31Compatibility: return httpx.AsyncClient(transport=transport, base_url="http://test") @pytest.fixture - async def openapi_31_server( + async def openapi_31_server_with_all_types( self, openapi_31_spec, mock_31_client ) -> FastMCPOpenAPI: """Create a FastMCPOpenAPI server from the OpenAPI 3.1 spec.""" return FastMCPOpenAPI( - openapi_spec=openapi_31_spec, client=mock_31_client, name="Order API 3.1" + openapi_spec=openapi_31_spec, + client=mock_31_client, + name="Order API 3.1", + route_maps=GET_ROUTE_MAPS, ) - async def test_server_creation(self, openapi_31_server): + async def test_server_creation(self, openapi_31_server_with_all_types): """Test that a server can be created from an OpenAPI 3.1 spec.""" - assert isinstance(openapi_31_server, FastMCP) - assert openapi_31_server.name == "Order API 3.1" + assert isinstance(openapi_31_server_with_all_types, FastMCP) + assert openapi_31_server_with_all_types.name == "Order API 3.1" - async def test_resource_discovery(self, openapi_31_server): + async def test_resource_discovery(self, openapi_31_server_with_all_types): """Test that resources are correctly discovered from an OpenAPI 3.1 spec.""" - async with Client(openapi_31_server) as client: + async with Client(openapi_31_server_with_all_types) as client: resources = await client.list_resources() assert len(resources) == 1 assert resources[0].uri == AnyUrl("resource://listOrders") - async def test_resource_template_discovery(self, openapi_31_server): + async def test_resource_template_discovery(self, openapi_31_server_with_all_types): """Test that resource templates are correctly discovered from an OpenAPI 3.1 spec.""" - async with Client(openapi_31_server) as client: + async with Client(openapi_31_server_with_all_types) as client: templates = await client.list_resource_templates() assert len(templates) == 1 assert templates[0].name == "getOrder" assert templates[0].uriTemplate == r"resource://getOrder/{order_id}" - async def test_tool_discovery(self, openapi_31_server): + async def test_tool_discovery(self, openapi_31_server_with_all_types): """Test that tools are correctly discovered from an OpenAPI 3.1 spec.""" - async with Client(openapi_31_server) as client: + async with Client(openapi_31_server_with_all_types) as client: tools = await client.list_tools() assert len(tools) == 1 assert tools[0].name == "createOrder" assert "customer" in tools[0].inputSchema["properties"] assert "items" in tools[0].inputSchema["properties"] - async def test_resource_access(self, openapi_31_server): + async def test_resource_access(self, openapi_31_server_with_all_types): """Test reading a resource from an OpenAPI 3.1 server.""" - async with Client(openapi_31_server) as client: + async with Client(openapi_31_server_with_all_types) as client: resource_response = await client.read_resource("resource://listOrders") response_text = resource_response[0].text # type: ignore[attr-defined] content = json.loads(response_text) @@ -844,9 +887,9 @@ class TestOpenAPI31Compatibility: assert content[0]["customer"] == "Alice" assert content[1]["customer"] == "Bob" - async def test_resource_template_access(self, openapi_31_server): + async def test_resource_template_access(self, openapi_31_server_with_all_types): """Test reading a resource from template from an OpenAPI 3.1 server.""" - async with Client(openapi_31_server) as client: + async with Client(openapi_31_server_with_all_types) as client: resource_response = await client.read_resource("resource://getOrder/o1") response_text = resource_response[0].text # type: ignore[attr-defined] content = json.loads(response_text) @@ -854,9 +897,9 @@ class TestOpenAPI31Compatibility: assert content["customer"] == "Alice" assert content["items"] == ["item1", "item2"] - async def test_tool_execution(self, openapi_31_server): + async def test_tool_execution(self, openapi_31_server_with_all_types): """Test executing a tool from an OpenAPI 3.1 server.""" - async with Client(openapi_31_server) as client: + async with Client(openapi_31_server_with_all_types) as client: result = await client.call_tool( "createOrder", {"customer": "Charlie", "items": ["item4", "item5"]} ) @@ -871,11 +914,13 @@ class TestOpenAPI31Compatibility: class TestMountFastMCP: """Tests for mounting FastMCP servers.""" - async def test_mount_fastmcp(self, fastmcp_openapi_server: FastMCPOpenAPI): + async def test_mount_fastmcp( + self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI + ): """Test mounting an OpenAPI server.""" mcp = FastMCP("MainApp") - await mcp.import_server("fastapi", fastmcp_openapi_server) + await mcp.import_server("fastapi", fastmcp_openapi_server_with_all_types) # Check that resources are available with prefixed URIs async with Client(mcp) as client: @@ -1144,19 +1189,24 @@ class TestDescriptionPropagation: return httpx.AsyncClient(transport=transport, base_url="http://test") @pytest.fixture - async def test_server(self, simple_openapi_spec, mock_client): + async def simple_server_with_all_types(self, simple_openapi_spec, mock_client): """Create a FastMCPOpenAPI server with the simple test spec.""" return FastMCPOpenAPI( openapi_spec=simple_openapi_spec, client=mock_client, name="Test API", + route_maps=GET_ROUTE_MAPS, ) # --- RESOURCE TESTS --- - async def test_resource_includes_route_description(self, test_server): + async def test_resource_includes_route_description( + self, simple_server_with_all_types + ): """Test that a Resource includes the route description.""" - resources = list(test_server._resource_manager.get_resources().values()) + resources = list( + simple_server_with_all_types._resource_manager.get_resources().values() + ) list_resource = next((r for r in resources if r.name == "listItems"), None) assert list_resource is not None, "listItems resource wasn't created" @@ -1164,9 +1214,13 @@ class TestDescriptionPropagation: "Route description missing from Resource" ) - async def test_resource_includes_response_description(self, test_server): + async def test_resource_includes_response_description( + self, simple_server_with_all_types + ): """Test that a Resource includes the response description.""" - resources = list(test_server._resource_manager.get_resources().values()) + resources = list( + simple_server_with_all_types._resource_manager.get_resources().values() + ) list_resource = next((r for r in resources if r.name == "listItems"), None) assert list_resource is not None, "listItems resource wasn't created" @@ -1174,9 +1228,13 @@ class TestDescriptionPropagation: "Response description missing from Resource" ) - async def test_resource_includes_response_model_fields(self, test_server): + async def test_resource_includes_response_model_fields( + self, simple_server_with_all_types + ): """Test that a Resource description includes response model field descriptions.""" - resources = list(test_server._resource_manager.get_resources().values()) + resources = list( + simple_server_with_all_types._resource_manager.get_resources().values() + ) list_resource = next((r for r in resources if r.name == "listItems"), None) assert list_resource is not None, "listItems resource wasn't created" @@ -1193,9 +1251,13 @@ class TestDescriptionPropagation: # --- RESOURCE TEMPLATE TESTS --- - async def test_template_includes_route_description(self, test_server): + async def test_template_includes_route_description( + self, simple_server_with_all_types + ): """Test that a ResourceTemplate includes the route description.""" - templates = list(test_server._resource_manager.get_templates().values()) + templates = list( + simple_server_with_all_types._resource_manager.get_templates().values() + ) get_template = next((t for t in templates if t.name == "getItem"), None) assert get_template is not None, "getItem template wasn't created" @@ -1203,9 +1265,13 @@ class TestDescriptionPropagation: "Route description missing from ResourceTemplate" ) - async def test_template_includes_function_docstring(self, test_server): + async def test_template_includes_function_docstring( + self, simple_server_with_all_types + ): """Test that a ResourceTemplate includes the function docstring.""" - templates = list(test_server._resource_manager.get_templates().values()) + templates = list( + simple_server_with_all_types._resource_manager.get_templates().values() + ) get_template = next((t for t in templates if t.name == "getItem"), None) assert get_template is not None, "getItem template wasn't created" @@ -1213,9 +1279,13 @@ class TestDescriptionPropagation: "Function docstring missing from ResourceTemplate" ) - async def test_template_includes_path_parameter_description(self, test_server): + async def test_template_includes_path_parameter_description( + self, simple_server_with_all_types + ): """Test that a ResourceTemplate includes path parameter descriptions.""" - templates = list(test_server._resource_manager.get_templates().values()) + templates = list( + simple_server_with_all_types._resource_manager.get_templates().values() + ) get_template = next((t for t in templates if t.name == "getItem"), None) assert get_template is not None, "getItem template wasn't created" @@ -1223,9 +1293,13 @@ class TestDescriptionPropagation: "Path parameter description missing from ResourceTemplate description" ) - async def test_template_includes_query_parameter_description(self, test_server): + async def test_template_includes_query_parameter_description( + self, simple_server_with_all_types + ): """Test that a ResourceTemplate includes query parameter descriptions.""" - templates = list(test_server._resource_manager.get_templates().values()) + templates = list( + simple_server_with_all_types._resource_manager.get_templates().values() + ) get_template = next((t for t in templates if t.name == "getItem"), None) assert get_template is not None, "getItem template wasn't created" @@ -1233,9 +1307,13 @@ class TestDescriptionPropagation: "Query parameter description missing from ResourceTemplate description" ) - async def test_template_parameter_schema_includes_description(self, test_server): + async def test_template_parameter_schema_includes_description( + self, simple_server_with_all_types + ): """Test that a ResourceTemplate's parameter schema includes parameter descriptions.""" - templates = list(test_server._resource_manager.get_templates().values()) + templates = list( + simple_server_with_all_types._resource_manager.get_templates().values() + ) get_template = next((t for t in templates if t.name == "getItem"), None) assert get_template is not None, "getItem template wasn't created" @@ -1255,9 +1333,9 @@ class TestDescriptionPropagation: # --- TOOL TESTS --- - async def test_tool_includes_route_description(self, test_server): + async def test_tool_includes_route_description(self, simple_server_with_all_types): """Test that a Tool includes the route description.""" - tools = test_server._tool_manager.list_tools() + tools = simple_server_with_all_types._tool_manager.list_tools() create_tool = next((t for t in tools if t.name == "createItem"), None) assert create_tool is not None, "createItem tool wasn't created" @@ -1265,9 +1343,9 @@ class TestDescriptionPropagation: "Route description missing from Tool" ) - async def test_tool_includes_function_docstring(self, test_server): + async def test_tool_includes_function_docstring(self, simple_server_with_all_types): """Test that a Tool includes the function docstring.""" - tools = test_server._tool_manager.list_tools() + tools = simple_server_with_all_types._tool_manager.list_tools() create_tool = next((t for t in tools if t.name == "createItem"), None) assert create_tool is not None, "createItem tool wasn't created" @@ -1277,10 +1355,10 @@ class TestDescriptionPropagation: ) async def test_tool_parameter_schema_includes_property_description( - self, test_server + self, simple_server_with_all_types ): """Test that a Tool's parameter schema includes property descriptions from request model.""" - tools = test_server._tool_manager.list_tools() + tools = simple_server_with_all_types._tool_manager.list_tools() create_tool = next((t for t in tools if t.name == "createItem"), None) assert create_tool is not None, "createItem tool wasn't created" @@ -1300,9 +1378,9 @@ class TestDescriptionPropagation: # --- CLIENT API TESTS --- - async def test_client_api_resource_description(self, test_server): + async def test_client_api_resource_description(self, simple_server_with_all_types): """Test that Resource descriptions are accessible via the client API.""" - async with Client(test_server) as client: + async with Client(simple_server_with_all_types) as client: resources = await client.list_resources() list_resource = next((r for r in resources if r.name == "listItems"), None) @@ -1314,9 +1392,9 @@ class TestDescriptionPropagation: "Route description missing in Resource from client API" ) - async def test_client_api_template_description(self, test_server): + async def test_client_api_template_description(self, simple_server_with_all_types): """Test that ResourceTemplate descriptions are accessible via the client API.""" - async with Client(test_server) as client: + async with Client(simple_server_with_all_types) as client: templates = await client.list_resource_templates() get_template = next((t for t in templates if t.name == "getItem"), None) @@ -1328,9 +1406,9 @@ class TestDescriptionPropagation: "Route description missing in ResourceTemplate from client API" ) - async def test_client_api_tool_description(self, test_server): + async def test_client_api_tool_description(self, simple_server_with_all_types): """Test that Tool descriptions are accessible via the client API.""" - async with Client(test_server) as client: + async with Client(simple_server_with_all_types) as client: tools = await client.list_tools() create_tool = next((t for t in tools if t.name == "createItem"), None) @@ -1342,9 +1420,9 @@ class TestDescriptionPropagation: "Function docstring missing in Tool from client API" ) - async def test_client_api_tool_parameter_schema(self, test_server): + async def test_client_api_tool_parameter_schema(self, simple_server_with_all_types): """Test that Tool parameter schemas are accessible via the client API.""" - async with Client(test_server) as client: + async with Client(simple_server_with_all_types) as client: tools = await client.list_tools() create_tool = next((t for t in tools if t.name == "createItem"), None) @@ -1699,9 +1777,11 @@ class TestFastAPIDescriptionPropagation: class TestReprMethods: """Tests for the custom __repr__ methods of OpenAPI objects.""" - async def test_openapi_tool_repr(self, fastmcp_openapi_server: FastMCPOpenAPI): + async def test_openapi_tool_repr( + self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI + ): """Test that OpenAPITool's __repr__ method works without recursion errors.""" - tools = fastmcp_openapi_server._tool_manager.list_tools() + tools = fastmcp_openapi_server_with_all_types._tool_manager.list_tools() tool = next(iter(tools)) # Verify repr doesn't cause recursion and contains expected elements @@ -1711,10 +1791,12 @@ class TestReprMethods: assert "method=" in tool_repr assert "path=" in tool_repr - async def test_openapi_resource_repr(self, fastmcp_openapi_server: FastMCPOpenAPI): + async def test_openapi_resource_repr( + self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI + ): """Test that OpenAPIResource's __repr__ method works without recursion errors.""" resources = list( - fastmcp_openapi_server._resource_manager.get_resources().values() + fastmcp_openapi_server_with_all_types._resource_manager.get_resources().values() ) resource = next(iter(resources)) @@ -1726,11 +1808,11 @@ class TestReprMethods: assert "path=" in resource_repr async def test_openapi_resource_template_repr( - self, fastmcp_openapi_server: FastMCPOpenAPI + self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI ): """Test that OpenAPIResourceTemplate's __repr__ method works without recursion errors.""" templates = list( - fastmcp_openapi_server._resource_manager.get_templates().values() + fastmcp_openapi_server_with_all_types._resource_manager.get_templates().values() ) template = next(iter(templates)) @@ -2182,6 +2264,7 @@ class TestMCPNames: openapi_spec=mcp_names_openapi_spec, client=mock_client, mcp_names=mcp_names, + route_maps=GET_ROUTE_MAPS, ) # Check tools use custom names @@ -2212,6 +2295,7 @@ class TestMCPNames: openapi_spec=mcp_names_openapi_spec, client=mock_client, mcp_names=mcp_names, + route_maps=GET_ROUTE_MAPS, ) tools = server._tool_manager.list_tools() @@ -2235,6 +2319,7 @@ class TestMCPNames: server = FastMCPOpenAPI( openapi_spec=mcp_names_openapi_spec, client=mock_client, + route_maps=GET_ROUTE_MAPS, ) resources = list(server._resource_manager.get_resources().values()) @@ -2261,6 +2346,7 @@ class TestMCPNames: server = FastMCPOpenAPI( openapi_spec=mcp_names_openapi_spec, client=mock_client, + route_maps=GET_ROUTE_MAPS, ) # Check all component types @@ -2299,9 +2385,9 @@ class TestMCPNames: mcp_names=mcp_names, ) - resources = list(server._resource_manager.get_resources().values()) - resource_names = {resource.name for resource in resources} - assert "openapi_user_list" in resource_names + tools = server._tool_manager.list_tools() + tool_names = {tool.name for tool in tools} + assert "openapi_user_list" in tool_names async def test_mcp_names_with_from_fastapi_classmethod(self): """Test mcp_names works with FastMCP.from_fastapi() classmethod.""" @@ -2334,11 +2420,8 @@ class TestMCPNames: tools = server._tool_manager.list_tools() tool_names = {tool.name for tool in tools} - resources = list(server._resource_manager.get_resources().values()) - resource_names = {resource.name for resource in resources} - assert "fastapi_create_user" in tool_names - assert "fastapi_user_list" in resource_names + assert "fastapi_user_list" in tool_names async def test_mcp_names_custom_names_are_also_truncated( self, mcp_names_openapi_spec, mock_client @@ -2355,6 +2438,7 @@ class TestMCPNames: openapi_spec=mcp_names_openapi_spec, client=mock_client, mcp_names=mcp_names, + route_maps=GET_ROUTE_MAPS, ) resources = list(server._resource_manager.get_resources().values()) diff --git a/tests/server/openapi/test_route_map_fn.py b/tests/server/openapi/test_route_map_fn.py index 44acde060..2cac7e36b 100644 --- a/tests/server/openapi/test_route_map_fn.py +++ b/tests/server/openapi/test_route_map_fn.py @@ -161,15 +161,15 @@ def test_route_map_fn_returns_none(sample_openapi_spec, http_client): # Should have default behavior assert server.name == "Test Server" - # Check that components were created with default types + # Check that components were created with default mapping tools = server._tool_manager._tools resources = server._resource_manager._resources templates = server._resource_manager._templates # Should have tools, resources, and templates based on default mapping assert len(tools) > 0 - assert len(resources) > 0 - assert len(templates) > 0 + assert len(resources) == 0 + assert len(templates) == 0 def test_route_map_fn_called_for_excluded_routes(sample_openapi_spec, http_client):