From 702412e28b0b197502788a73cd652e4e2bbab783 Mon Sep 17 00:00:00 2001
From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Date: Thu, 22 May 2025 21:11:46 -0400
Subject: [PATCH] Remove custom names
---
docs/patterns/openapi.mdx | 45 +---
src/fastmcp/server/openapi.py | 94 +++----
tests/server/{ => openapi}/test_openapi.py | 0
.../test_openapi_path_parameters.py | 0
tests/server/test_openapi_naming.py | 231 ------------------
5 files changed, 29 insertions(+), 341 deletions(-)
rename tests/server/{ => openapi}/test_openapi.py (100%)
rename tests/server/{ => openapi}/test_openapi_path_parameters.py (100%)
delete mode 100644 tests/server/test_openapi_naming.py
diff --git a/docs/patterns/openapi.mdx b/docs/patterns/openapi.mdx
index 68d5c6bd4..c2a586ccd 100644
--- a/docs/patterns/openapi.mdx
+++ b/docs/patterns/openapi.mdx
@@ -41,52 +41,10 @@ mcp = FastMCP.from_openapi(
)
```
-### Component Naming
+## Route Mapping
-You can customize how FastMCP names the components generated from your OpenAPI spec:
-
-```python
-# Custom naming function
-def my_component_namer(route, mcp_type, default_name):
- # Create custom names based on the route and component type
- if route.operation_id:
- return route.operation_id
-
- # For example, prefix with component type
- prefix = {
- MCPType.TOOL: "tool_",
- MCPType.RESOURCE: "resource_",
- MCPType.RESOURCE_TEMPLATE: "template_",
- }.get(mcp_type, "")
-
- path_name = route.path.replace("/", "_").strip("_")
- return f"{prefix}{path_name}"
-
-mcp = FastMCP.from_openapi(
- openapi_spec=spec,
- client=api_client,
- component_namer=my_component_namer
-)
-```
-
-By default, FastMCP generates component names as follows:
-
-- If the route has an `operationId` in the OpenAPI spec, that is used
-- Otherwise, the name is generated from the route path:
- - For `GET` routes mapped to resources: Just the resource name (e.g., `/users` → `users`)
- - For routes with path parameters mapped to templates: The path with parameter names (e.g., `/users/{id}` → `users_id`)
- - For other methods mapped to tools: Method + resource name (e.g., `POST /users` → `post_users`)
-
-#### Handling Name Collisions
-
-When multiple routes would generate the same component name, FastMCP automatically appends a number suffix to ensure uniqueness (e.g., `users`, `users_2`, `users_3`). You'll see these numbered suffixes in the component names returned by `get_tools()`, `get_resources()`, etc.
-
-If you need more control over naming, you can provide a custom `component_namer` function that handles potential collisions in your own way.
-
-### Route Mapping
-
By default, OpenAPI routes are mapped to MCP components based on these rules:
| OpenAPI Route | Example |MCP Component | Notes |
@@ -232,7 +190,6 @@ mcp = FastMCPOpenAPI(
#### Route Map Shortcuts
-
FastMCP provides several shortcut functions to create common route maps more easily:
diff --git a/src/fastmcp/server/openapi.py b/src/fastmcp/server/openapi.py
index 9af330f35..0bed5c537 100644
--- a/src/fastmcp/server/openapi.py
+++ b/src/fastmcp/server/openapi.py
@@ -53,10 +53,6 @@ class MCPType(enum.Enum):
EXCLUDE = "EXCLUDE"
-# Type for component naming function
-ComponentNameFn = Callable[[openapi.HTTPRoute, MCPType, str], str]
-
-
# Keep RouteType as an alias to MCPType for backward compatibility
class RouteType(enum.Enum):
"""
@@ -650,55 +646,6 @@ class OpenAPIResourceTemplate(ResourceTemplate):
)
-def default_component_name_fn(
- route: openapi.HTTPRoute, mcp_type: MCPType, default_name: str
-) -> str:
- """
- Default function for generating component names from routes.
-
- This function creates simpler names than the original method:
- - For resources and templates: Just uses the resource name without HTTP method
- - For tools: Uses a simpler naming convention
-
- Args:
- route: The OpenAPI route
- mcp_type: The component type being created
- default_name: The original default name that would be used
-
- Returns:
- str: The component name to use
- """
- # First check for OpenAPI operationId which takes precedence
- if route.operation_id:
- return route.operation_id
-
- # For path-based naming, clean up the path
- path_parts = route.path.strip("/").split("/")
-
- # Remove path parameters (parts with {})
- clean_parts = []
- for part in path_parts:
- if part.startswith("{") and part.endswith("}"):
- # For templates, include parameter name without braces
- if mcp_type == MCPType.RESOURCE_TEMPLATE:
- param_name = part[1:-1] # Remove braces
- clean_parts.append(param_name)
- else:
- clean_parts.append(part)
-
- # Join the parts
- resource_name = "_".join(clean_parts)
-
- # For tools, might be useful to keep the method for clarity on what it does
- if mcp_type == MCPType.TOOL:
- # Only include method if it helps distinguish (POST, PUT, PATCH, DELETE)
- # For GET we don't need the method as it's implied for resources
- if route.method != "GET":
- resource_name = f"{route.method.lower()}_{resource_name}"
-
- return resource_name
-
-
class FastMCPOpenAPI(FastMCP):
"""
FastMCP server implementation that creates components from an OpenAPI schema.
@@ -744,7 +691,6 @@ class FastMCPOpenAPI(FastMCP):
name: str | None = None,
route_maps: list[RouteMap] | None = None,
timeout: float | None = None,
- component_namer: ComponentNameFn | None = None,
**settings: Any,
):
"""
@@ -756,14 +702,12 @@ class FastMCPOpenAPI(FastMCP):
name: Optional name for the server
route_maps: Optional list of RouteMap objects defining route mappings
timeout: Optional timeout (in seconds) for all requests
- component_namer: Optional function to customize component names
**settings: Additional settings for FastMCP
"""
super().__init__(name=name or "OpenAPI FastMCP", **settings)
self._client = client
self._timeout = timeout
- self._component_namer = component_namer or default_component_name_fn
# Keep track of names to detect collisions
self._used_names = {"tools": set(), "resources": set(), "templates": set()}
@@ -777,10 +721,7 @@ class FastMCPOpenAPI(FastMCP):
route_type = _determine_route_type(route, route_maps)
# Generate a default name from the route
- default_name = self._generate_default_name(route)
-
- # Get the component name using the namer function
- component_name = self._component_namer(route, route_type, default_name)
+ component_name = self._generate_default_name(route, route_type)
if route_type == MCPType.TOOL:
self._create_openapi_tool(route, component_name)
@@ -798,18 +739,39 @@ class FastMCPOpenAPI(FastMCP):
logger.info(f"Created FastMCP OpenAPI server with {len(http_routes)} routes")
- def _generate_default_name(self, route: openapi.HTTPRoute) -> str:
+ def _generate_default_name(
+ self, route: openapi.HTTPRoute, mcp_type: MCPType
+ ) -> str:
"""Generate a default name from the route path."""
- # Use OpenAPI operationId if available
+ # First check for OpenAPI operationId which takes precedence
if route.operation_id:
return route.operation_id
- # Generate a name from the path
+ # For path-based naming, clean up the path
path_parts = route.path.strip("/").split("/")
- path_name = "_".join(p for p in path_parts if not p.startswith("{"))
- # The original default naming included the HTTP method
- return f"{route.method.lower()}_{path_name}"
+ # Remove path parameters (parts with {})
+ clean_parts = []
+ for part in path_parts:
+ if part.startswith("{") and part.endswith("}"):
+ # For templates, include parameter name without braces
+ if mcp_type == MCPType.RESOURCE_TEMPLATE:
+ param_name = part[1:-1] # Remove braces
+ clean_parts.append(param_name)
+ else:
+ clean_parts.append(part)
+
+ # Join the parts
+ resource_name = "_".join(clean_parts)
+
+ # For tools, might be useful to keep the method for clarity on what it does
+ if mcp_type == MCPType.TOOL:
+ # Only include method if it helps distinguish (POST, PUT, PATCH, DELETE)
+ # For GET we don't need the method as it's implied for resources
+ if route.method != "GET":
+ resource_name = f"{route.method.lower()}_{resource_name}"
+
+ return resource_name
def _get_unique_name(
self, name: str, component_type: Literal["tools", "resources", "templates"]
diff --git a/tests/server/test_openapi.py b/tests/server/openapi/test_openapi.py
similarity index 100%
rename from tests/server/test_openapi.py
rename to tests/server/openapi/test_openapi.py
diff --git a/tests/server/test_openapi_path_parameters.py b/tests/server/openapi/test_openapi_path_parameters.py
similarity index 100%
rename from tests/server/test_openapi_path_parameters.py
rename to tests/server/openapi/test_openapi_path_parameters.py
diff --git a/tests/server/test_openapi_naming.py b/tests/server/test_openapi_naming.py
deleted file mode 100644
index 52ffe00e3..000000000
--- a/tests/server/test_openapi_naming.py
+++ /dev/null
@@ -1,231 +0,0 @@
-"""Tests for OpenAPI component naming in FastMCP."""
-
-from unittest.mock import MagicMock, patch
-
-import httpx
-import pytest
-
-from fastmcp.server.openapi import FastMCPOpenAPI, MCPType
-
-
-@pytest.fixture
-def simple_openapi_spec():
- """A simple OpenAPI spec with some routes for testing."""
- return {
- "openapi": "3.0.0",
- "info": {"title": "Test API", "version": "1.0.0"},
- "paths": {
- "/users": {
- "get": {
- "summary": "Get all users",
- "responses": {"200": {"description": "OK"}},
- },
- "post": {
- "summary": "Create a user",
- "responses": {"201": {"description": "Created"}},
- },
- },
- "/users/{id}": {
- "get": {
- "summary": "Get a user",
- "parameters": [
- {
- "name": "id",
- "in": "path",
- "required": True,
- "schema": {"type": "string"},
- }
- ],
- "responses": {"200": {"description": "OK"}},
- },
- "put": {
- "summary": "Update a user",
- "parameters": [
- {
- "name": "id",
- "in": "path",
- "required": True,
- "schema": {"type": "string"},
- }
- ],
- "responses": {"200": {"description": "OK"}},
- },
- },
- "/users/{id}/orders": {
- "get": {
- "summary": "Get user orders",
- "parameters": [
- {
- "name": "id",
- "in": "path",
- "required": True,
- "schema": {"type": "string"},
- }
- ],
- "responses": {"200": {"description": "OK"}},
- }
- },
- "/products": {
- "get": {
- "operationId": "listProducts",
- "summary": "Get all products",
- "responses": {"200": {"description": "OK"}},
- }
- },
- },
- }
-
-
-class TestOpenAPIComponentNaming:
- """Tests for OpenAPI component naming functionality."""
-
- @patch("fastmcp.server.openapi._combine_schemas")
- def test_default_naming(self, mock_combine, simple_openapi_spec):
- """Test the default component naming behavior."""
- # Mock the HTTP client
- mock_client = MagicMock(spec=httpx.AsyncClient)
-
- # Mock the combine schemas function to return empty dict
- mock_combine.return_value = {}
-
- # Create a server with the default naming
- # Instead of mocking the creation methods, we'll just override them to
- # add the names to _used_names without actually creating components
- class TestServer(FastMCPOpenAPI):
- def _create_openapi_tool(self, route, name):
- _tool_name = self._get_unique_name(name, "tools")
- # Don't actually create the tool, just record that the name was used
-
- def _create_openapi_resource(self, route, name):
- _resource_name = self._get_unique_name(name, "resources")
- # Don't actually create the resource, just record that the name was used
-
- def _create_openapi_template(self, route, name):
- _template_name = self._get_unique_name(name, "templates")
- # Don't actually create the template, just record that the name was used
-
- # Create the server with our test subclass
- server = TestServer(
- openapi_spec=simple_openapi_spec,
- client=mock_client,
- )
-
- # Check that the correct names were generated
- expected_names = {
- "tools": {"post_users", "put_users"},
- "resources": {
- "users",
- "listProducts",
- }, # GET /users, GET /products (from operationId)
- "templates": {
- "users_id",
- "users_id_orders",
- }, # GET /users/{id}, GET /users/{id}/orders
- }
-
- # The "tools" set in the server might contain more than our expected names
- # because all HTTP methods could be converted to tools - we just check for inclusion
- assert expected_names["tools"].issubset(server._used_names["tools"])
- assert expected_names["resources"].issubset(server._used_names["resources"])
- assert expected_names["templates"].issubset(server._used_names["templates"])
-
- # Check that the operationId is preferred for naming
- assert "listProducts" in server._used_names["resources"]
-
- @patch("fastmcp.server.openapi._combine_schemas")
- def test_custom_naming(self, mock_combine, simple_openapi_spec):
- """Test custom component naming function."""
- # Mock the HTTP client
- mock_client = MagicMock(spec=httpx.AsyncClient)
-
- # Mock the combine schemas function to return empty dict
- mock_combine.return_value = {}
-
- # Create a custom naming function
- def custom_namer(route, mcp_type, default_name):
- # Always prefix with component type
- if mcp_type == MCPType.TOOL:
- prefix = "tool"
- elif mcp_type == MCPType.RESOURCE:
- prefix = "res"
- elif mcp_type == MCPType.RESOURCE_TEMPLATE:
- prefix = "tmpl"
- else:
- prefix = "other"
-
- # Use operationId if available
- if route.operation_id:
- return f"{prefix}_{route.operation_id}"
-
- # Otherwise use the path
- path_name = route.path.replace("/", "_").replace("{", "").replace("}", "")
- return f"{prefix}{path_name}"
-
- # Create a custom testing server subclass
- class TestServer(FastMCPOpenAPI):
- def _create_openapi_tool(self, route, name):
- _tool_name = self._get_unique_name(name, "tools")
- # Don't actually create the tool, just record that the name was used
-
- def _create_openapi_resource(self, route, name):
- _resource_name = self._get_unique_name(name, "resources")
- # Don't actually create the resource, just record that the name was used
-
- def _create_openapi_template(self, route, name):
- _template_name = self._get_unique_name(name, "templates")
- # Don't actually create the template, just record that the name was used
-
- # Create a server with the custom naming
- server = TestServer(
- openapi_spec=simple_openapi_spec,
- client=mock_client,
- component_namer=custom_namer,
- )
-
- # Check some of the generated names
- assert "tool_users" in server._used_names["tools"]
- assert "res_users" in server._used_names["resources"]
- assert "tmpl_users_id" in server._used_names["templates"]
- assert "res_listProducts" in server._used_names["resources"]
-
- @patch("fastmcp.server.openapi._combine_schemas")
- def test_collision_handling(self, mock_combine, simple_openapi_spec):
- """Test how name collisions are handled by appending numbers."""
- # Mock the HTTP client
- mock_client = MagicMock(spec=httpx.AsyncClient)
-
- # Mock the combine schemas function to return empty dict
- mock_combine.return_value = {}
-
- # Create a custom naming function that always returns the same name
- def collision_namer(route, mcp_type, default_name):
- return "same_name"
-
- # Create a custom testing server subclass
- class TestServer(FastMCPOpenAPI):
- def _create_openapi_tool(self, route, name):
- _tool_name = self._get_unique_name(name, "tools")
- # Don't actually create the tool, just record that the name was used
-
- def _create_openapi_resource(self, route, name):
- _resource_name = self._get_unique_name(name, "resources")
- # Don't actually create the resource, just record that the name was used
-
- def _create_openapi_template(self, route, name):
- _template_name = self._get_unique_name(name, "templates")
- # Don't actually create the template, just record that the name was used
-
- # Create a server with the collision namer
- server = TestServer(
- openapi_spec=simple_openapi_spec,
- client=mock_client,
- component_namer=collision_namer,
- )
-
- # Check that names were renamed with numbers
- assert "same_name" in server._used_names["tools"]
- assert "same_name_2" in server._used_names["tools"]
- assert "same_name" in server._used_names["resources"]
- assert "same_name_2" in server._used_names["resources"]
- assert "same_name" in server._used_names["templates"]
- assert "same_name_2" in server._used_names["templates"]