Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
c997d33
1
Parent(s): 7af7032
Remove custom names
Browse files
docs/patterns/openapi.mdx
CHANGED
|
@@ -41,52 +41,10 @@ mcp = FastMCP.from_openapi(
|
|
| 41 |
)
|
| 42 |
```
|
| 43 |
|
| 44 |
-
##
|
| 45 |
|
| 46 |
<VersionBadge version="2.5.0" />
|
| 47 |
|
| 48 |
-
You can customize how FastMCP names the components generated from your OpenAPI spec:
|
| 49 |
-
|
| 50 |
-
```python
|
| 51 |
-
# Custom naming function
|
| 52 |
-
def my_component_namer(route, mcp_type, default_name):
|
| 53 |
-
# Create custom names based on the route and component type
|
| 54 |
-
if route.operation_id:
|
| 55 |
-
return route.operation_id
|
| 56 |
-
|
| 57 |
-
# For example, prefix with component type
|
| 58 |
-
prefix = {
|
| 59 |
-
MCPType.TOOL: "tool_",
|
| 60 |
-
MCPType.RESOURCE: "resource_",
|
| 61 |
-
MCPType.RESOURCE_TEMPLATE: "template_",
|
| 62 |
-
}.get(mcp_type, "")
|
| 63 |
-
|
| 64 |
-
path_name = route.path.replace("/", "_").strip("_")
|
| 65 |
-
return f"{prefix}{path_name}"
|
| 66 |
-
|
| 67 |
-
mcp = FastMCP.from_openapi(
|
| 68 |
-
openapi_spec=spec,
|
| 69 |
-
client=api_client,
|
| 70 |
-
component_namer=my_component_namer
|
| 71 |
-
)
|
| 72 |
-
```
|
| 73 |
-
|
| 74 |
-
By default, FastMCP generates component names as follows:
|
| 75 |
-
|
| 76 |
-
- If the route has an `operationId` in the OpenAPI spec, that is used
|
| 77 |
-
- Otherwise, the name is generated from the route path:
|
| 78 |
-
- For `GET` routes mapped to resources: Just the resource name (e.g., `/users` → `users`)
|
| 79 |
-
- For routes with path parameters mapped to templates: The path with parameter names (e.g., `/users/{id}` → `users_id`)
|
| 80 |
-
- For other methods mapped to tools: Method + resource name (e.g., `POST /users` → `post_users`)
|
| 81 |
-
|
| 82 |
-
#### Handling Name Collisions
|
| 83 |
-
|
| 84 |
-
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.
|
| 85 |
-
|
| 86 |
-
If you need more control over naming, you can provide a custom `component_namer` function that handles potential collisions in your own way.
|
| 87 |
-
|
| 88 |
-
### Route Mapping
|
| 89 |
-
|
| 90 |
By default, OpenAPI routes are mapped to MCP components based on these rules:
|
| 91 |
|
| 92 |
| OpenAPI Route | Example |MCP Component | Notes |
|
|
@@ -232,7 +190,6 @@ mcp = FastMCPOpenAPI(
|
|
| 232 |
|
| 233 |
#### Route Map Shortcuts
|
| 234 |
|
| 235 |
-
<VersionBadge version="2.5.0" />
|
| 236 |
|
| 237 |
FastMCP provides several shortcut functions to create common route maps more easily:
|
| 238 |
|
|
|
|
| 41 |
)
|
| 42 |
```
|
| 43 |
|
| 44 |
+
## Route Mapping
|
| 45 |
|
| 46 |
<VersionBadge version="2.5.0" />
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
By default, OpenAPI routes are mapped to MCP components based on these rules:
|
| 49 |
|
| 50 |
| OpenAPI Route | Example |MCP Component | Notes |
|
|
|
|
| 190 |
|
| 191 |
#### Route Map Shortcuts
|
| 192 |
|
|
|
|
| 193 |
|
| 194 |
FastMCP provides several shortcut functions to create common route maps more easily:
|
| 195 |
|
src/fastmcp/server/openapi.py
CHANGED
|
@@ -53,10 +53,6 @@ class MCPType(enum.Enum):
|
|
| 53 |
EXCLUDE = "EXCLUDE"
|
| 54 |
|
| 55 |
|
| 56 |
-
# Type for component naming function
|
| 57 |
-
ComponentNameFn = Callable[[openapi.HTTPRoute, MCPType, str], str]
|
| 58 |
-
|
| 59 |
-
|
| 60 |
# Keep RouteType as an alias to MCPType for backward compatibility
|
| 61 |
class RouteType(enum.Enum):
|
| 62 |
"""
|
|
@@ -650,55 +646,6 @@ class OpenAPIResourceTemplate(ResourceTemplate):
|
|
| 650 |
)
|
| 651 |
|
| 652 |
|
| 653 |
-
def default_component_name_fn(
|
| 654 |
-
route: openapi.HTTPRoute, mcp_type: MCPType, default_name: str
|
| 655 |
-
) -> str:
|
| 656 |
-
"""
|
| 657 |
-
Default function for generating component names from routes.
|
| 658 |
-
|
| 659 |
-
This function creates simpler names than the original method:
|
| 660 |
-
- For resources and templates: Just uses the resource name without HTTP method
|
| 661 |
-
- For tools: Uses a simpler naming convention
|
| 662 |
-
|
| 663 |
-
Args:
|
| 664 |
-
route: The OpenAPI route
|
| 665 |
-
mcp_type: The component type being created
|
| 666 |
-
default_name: The original default name that would be used
|
| 667 |
-
|
| 668 |
-
Returns:
|
| 669 |
-
str: The component name to use
|
| 670 |
-
"""
|
| 671 |
-
# First check for OpenAPI operationId which takes precedence
|
| 672 |
-
if route.operation_id:
|
| 673 |
-
return route.operation_id
|
| 674 |
-
|
| 675 |
-
# For path-based naming, clean up the path
|
| 676 |
-
path_parts = route.path.strip("/").split("/")
|
| 677 |
-
|
| 678 |
-
# Remove path parameters (parts with {})
|
| 679 |
-
clean_parts = []
|
| 680 |
-
for part in path_parts:
|
| 681 |
-
if part.startswith("{") and part.endswith("}"):
|
| 682 |
-
# For templates, include parameter name without braces
|
| 683 |
-
if mcp_type == MCPType.RESOURCE_TEMPLATE:
|
| 684 |
-
param_name = part[1:-1] # Remove braces
|
| 685 |
-
clean_parts.append(param_name)
|
| 686 |
-
else:
|
| 687 |
-
clean_parts.append(part)
|
| 688 |
-
|
| 689 |
-
# Join the parts
|
| 690 |
-
resource_name = "_".join(clean_parts)
|
| 691 |
-
|
| 692 |
-
# For tools, might be useful to keep the method for clarity on what it does
|
| 693 |
-
if mcp_type == MCPType.TOOL:
|
| 694 |
-
# Only include method if it helps distinguish (POST, PUT, PATCH, DELETE)
|
| 695 |
-
# For GET we don't need the method as it's implied for resources
|
| 696 |
-
if route.method != "GET":
|
| 697 |
-
resource_name = f"{route.method.lower()}_{resource_name}"
|
| 698 |
-
|
| 699 |
-
return resource_name
|
| 700 |
-
|
| 701 |
-
|
| 702 |
class FastMCPOpenAPI(FastMCP):
|
| 703 |
"""
|
| 704 |
FastMCP server implementation that creates components from an OpenAPI schema.
|
|
@@ -744,7 +691,6 @@ class FastMCPOpenAPI(FastMCP):
|
|
| 744 |
name: str | None = None,
|
| 745 |
route_maps: list[RouteMap] | None = None,
|
| 746 |
timeout: float | None = None,
|
| 747 |
-
component_namer: ComponentNameFn | None = None,
|
| 748 |
**settings: Any,
|
| 749 |
):
|
| 750 |
"""
|
|
@@ -756,14 +702,12 @@ class FastMCPOpenAPI(FastMCP):
|
|
| 756 |
name: Optional name for the server
|
| 757 |
route_maps: Optional list of RouteMap objects defining route mappings
|
| 758 |
timeout: Optional timeout (in seconds) for all requests
|
| 759 |
-
component_namer: Optional function to customize component names
|
| 760 |
**settings: Additional settings for FastMCP
|
| 761 |
"""
|
| 762 |
super().__init__(name=name or "OpenAPI FastMCP", **settings)
|
| 763 |
|
| 764 |
self._client = client
|
| 765 |
self._timeout = timeout
|
| 766 |
-
self._component_namer = component_namer or default_component_name_fn
|
| 767 |
|
| 768 |
# Keep track of names to detect collisions
|
| 769 |
self._used_names = {"tools": set(), "resources": set(), "templates": set()}
|
|
@@ -777,10 +721,7 @@ class FastMCPOpenAPI(FastMCP):
|
|
| 777 |
route_type = _determine_route_type(route, route_maps)
|
| 778 |
|
| 779 |
# Generate a default name from the route
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
# Get the component name using the namer function
|
| 783 |
-
component_name = self._component_namer(route, route_type, default_name)
|
| 784 |
|
| 785 |
if route_type == MCPType.TOOL:
|
| 786 |
self._create_openapi_tool(route, component_name)
|
|
@@ -798,18 +739,39 @@ class FastMCPOpenAPI(FastMCP):
|
|
| 798 |
|
| 799 |
logger.info(f"Created FastMCP OpenAPI server with {len(http_routes)} routes")
|
| 800 |
|
| 801 |
-
def _generate_default_name(
|
|
|
|
|
|
|
| 802 |
"""Generate a default name from the route path."""
|
| 803 |
-
#
|
| 804 |
if route.operation_id:
|
| 805 |
return route.operation_id
|
| 806 |
|
| 807 |
-
#
|
| 808 |
path_parts = route.path.strip("/").split("/")
|
| 809 |
-
path_name = "_".join(p for p in path_parts if not p.startswith("{"))
|
| 810 |
|
| 811 |
-
#
|
| 812 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 813 |
|
| 814 |
def _get_unique_name(
|
| 815 |
self, name: str, component_type: Literal["tools", "resources", "templates"]
|
|
|
|
| 53 |
EXCLUDE = "EXCLUDE"
|
| 54 |
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
# Keep RouteType as an alias to MCPType for backward compatibility
|
| 57 |
class RouteType(enum.Enum):
|
| 58 |
"""
|
|
|
|
| 646 |
)
|
| 647 |
|
| 648 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 649 |
class FastMCPOpenAPI(FastMCP):
|
| 650 |
"""
|
| 651 |
FastMCP server implementation that creates components from an OpenAPI schema.
|
|
|
|
| 691 |
name: str | None = None,
|
| 692 |
route_maps: list[RouteMap] | None = None,
|
| 693 |
timeout: float | None = None,
|
|
|
|
| 694 |
**settings: Any,
|
| 695 |
):
|
| 696 |
"""
|
|
|
|
| 702 |
name: Optional name for the server
|
| 703 |
route_maps: Optional list of RouteMap objects defining route mappings
|
| 704 |
timeout: Optional timeout (in seconds) for all requests
|
|
|
|
| 705 |
**settings: Additional settings for FastMCP
|
| 706 |
"""
|
| 707 |
super().__init__(name=name or "OpenAPI FastMCP", **settings)
|
| 708 |
|
| 709 |
self._client = client
|
| 710 |
self._timeout = timeout
|
|
|
|
| 711 |
|
| 712 |
# Keep track of names to detect collisions
|
| 713 |
self._used_names = {"tools": set(), "resources": set(), "templates": set()}
|
|
|
|
| 721 |
route_type = _determine_route_type(route, route_maps)
|
| 722 |
|
| 723 |
# Generate a default name from the route
|
| 724 |
+
component_name = self._generate_default_name(route, route_type)
|
|
|
|
|
|
|
|
|
|
| 725 |
|
| 726 |
if route_type == MCPType.TOOL:
|
| 727 |
self._create_openapi_tool(route, component_name)
|
|
|
|
| 739 |
|
| 740 |
logger.info(f"Created FastMCP OpenAPI server with {len(http_routes)} routes")
|
| 741 |
|
| 742 |
+
def _generate_default_name(
|
| 743 |
+
self, route: openapi.HTTPRoute, mcp_type: MCPType
|
| 744 |
+
) -> str:
|
| 745 |
"""Generate a default name from the route path."""
|
| 746 |
+
# First check for OpenAPI operationId which takes precedence
|
| 747 |
if route.operation_id:
|
| 748 |
return route.operation_id
|
| 749 |
|
| 750 |
+
# For path-based naming, clean up the path
|
| 751 |
path_parts = route.path.strip("/").split("/")
|
|
|
|
| 752 |
|
| 753 |
+
# Remove path parameters (parts with {})
|
| 754 |
+
clean_parts = []
|
| 755 |
+
for part in path_parts:
|
| 756 |
+
if part.startswith("{") and part.endswith("}"):
|
| 757 |
+
# For templates, include parameter name without braces
|
| 758 |
+
if mcp_type == MCPType.RESOURCE_TEMPLATE:
|
| 759 |
+
param_name = part[1:-1] # Remove braces
|
| 760 |
+
clean_parts.append(param_name)
|
| 761 |
+
else:
|
| 762 |
+
clean_parts.append(part)
|
| 763 |
+
|
| 764 |
+
# Join the parts
|
| 765 |
+
resource_name = "_".join(clean_parts)
|
| 766 |
+
|
| 767 |
+
# For tools, might be useful to keep the method for clarity on what it does
|
| 768 |
+
if mcp_type == MCPType.TOOL:
|
| 769 |
+
# Only include method if it helps distinguish (POST, PUT, PATCH, DELETE)
|
| 770 |
+
# For GET we don't need the method as it's implied for resources
|
| 771 |
+
if route.method != "GET":
|
| 772 |
+
resource_name = f"{route.method.lower()}_{resource_name}"
|
| 773 |
+
|
| 774 |
+
return resource_name
|
| 775 |
|
| 776 |
def _get_unique_name(
|
| 777 |
self, name: str, component_type: Literal["tools", "resources", "templates"]
|
tests/server/{test_openapi.py → openapi/test_openapi.py}
RENAMED
|
File without changes
|
tests/server/{test_openapi_path_parameters.py → openapi/test_openapi_path_parameters.py}
RENAMED
|
File without changes
|
tests/server/test_openapi_naming.py
DELETED
|
@@ -1,231 +0,0 @@
|
|
| 1 |
-
"""Tests for OpenAPI component naming in FastMCP."""
|
| 2 |
-
|
| 3 |
-
from unittest.mock import MagicMock, patch
|
| 4 |
-
|
| 5 |
-
import httpx
|
| 6 |
-
import pytest
|
| 7 |
-
|
| 8 |
-
from fastmcp.server.openapi import FastMCPOpenAPI, MCPType
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
@pytest.fixture
|
| 12 |
-
def simple_openapi_spec():
|
| 13 |
-
"""A simple OpenAPI spec with some routes for testing."""
|
| 14 |
-
return {
|
| 15 |
-
"openapi": "3.0.0",
|
| 16 |
-
"info": {"title": "Test API", "version": "1.0.0"},
|
| 17 |
-
"paths": {
|
| 18 |
-
"/users": {
|
| 19 |
-
"get": {
|
| 20 |
-
"summary": "Get all users",
|
| 21 |
-
"responses": {"200": {"description": "OK"}},
|
| 22 |
-
},
|
| 23 |
-
"post": {
|
| 24 |
-
"summary": "Create a user",
|
| 25 |
-
"responses": {"201": {"description": "Created"}},
|
| 26 |
-
},
|
| 27 |
-
},
|
| 28 |
-
"/users/{id}": {
|
| 29 |
-
"get": {
|
| 30 |
-
"summary": "Get a user",
|
| 31 |
-
"parameters": [
|
| 32 |
-
{
|
| 33 |
-
"name": "id",
|
| 34 |
-
"in": "path",
|
| 35 |
-
"required": True,
|
| 36 |
-
"schema": {"type": "string"},
|
| 37 |
-
}
|
| 38 |
-
],
|
| 39 |
-
"responses": {"200": {"description": "OK"}},
|
| 40 |
-
},
|
| 41 |
-
"put": {
|
| 42 |
-
"summary": "Update a user",
|
| 43 |
-
"parameters": [
|
| 44 |
-
{
|
| 45 |
-
"name": "id",
|
| 46 |
-
"in": "path",
|
| 47 |
-
"required": True,
|
| 48 |
-
"schema": {"type": "string"},
|
| 49 |
-
}
|
| 50 |
-
],
|
| 51 |
-
"responses": {"200": {"description": "OK"}},
|
| 52 |
-
},
|
| 53 |
-
},
|
| 54 |
-
"/users/{id}/orders": {
|
| 55 |
-
"get": {
|
| 56 |
-
"summary": "Get user orders",
|
| 57 |
-
"parameters": [
|
| 58 |
-
{
|
| 59 |
-
"name": "id",
|
| 60 |
-
"in": "path",
|
| 61 |
-
"required": True,
|
| 62 |
-
"schema": {"type": "string"},
|
| 63 |
-
}
|
| 64 |
-
],
|
| 65 |
-
"responses": {"200": {"description": "OK"}},
|
| 66 |
-
}
|
| 67 |
-
},
|
| 68 |
-
"/products": {
|
| 69 |
-
"get": {
|
| 70 |
-
"operationId": "listProducts",
|
| 71 |
-
"summary": "Get all products",
|
| 72 |
-
"responses": {"200": {"description": "OK"}},
|
| 73 |
-
}
|
| 74 |
-
},
|
| 75 |
-
},
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
class TestOpenAPIComponentNaming:
|
| 80 |
-
"""Tests for OpenAPI component naming functionality."""
|
| 81 |
-
|
| 82 |
-
@patch("fastmcp.server.openapi._combine_schemas")
|
| 83 |
-
def test_default_naming(self, mock_combine, simple_openapi_spec):
|
| 84 |
-
"""Test the default component naming behavior."""
|
| 85 |
-
# Mock the HTTP client
|
| 86 |
-
mock_client = MagicMock(spec=httpx.AsyncClient)
|
| 87 |
-
|
| 88 |
-
# Mock the combine schemas function to return empty dict
|
| 89 |
-
mock_combine.return_value = {}
|
| 90 |
-
|
| 91 |
-
# Create a server with the default naming
|
| 92 |
-
# Instead of mocking the creation methods, we'll just override them to
|
| 93 |
-
# add the names to _used_names without actually creating components
|
| 94 |
-
class TestServer(FastMCPOpenAPI):
|
| 95 |
-
def _create_openapi_tool(self, route, name):
|
| 96 |
-
_tool_name = self._get_unique_name(name, "tools")
|
| 97 |
-
# Don't actually create the tool, just record that the name was used
|
| 98 |
-
|
| 99 |
-
def _create_openapi_resource(self, route, name):
|
| 100 |
-
_resource_name = self._get_unique_name(name, "resources")
|
| 101 |
-
# Don't actually create the resource, just record that the name was used
|
| 102 |
-
|
| 103 |
-
def _create_openapi_template(self, route, name):
|
| 104 |
-
_template_name = self._get_unique_name(name, "templates")
|
| 105 |
-
# Don't actually create the template, just record that the name was used
|
| 106 |
-
|
| 107 |
-
# Create the server with our test subclass
|
| 108 |
-
server = TestServer(
|
| 109 |
-
openapi_spec=simple_openapi_spec,
|
| 110 |
-
client=mock_client,
|
| 111 |
-
)
|
| 112 |
-
|
| 113 |
-
# Check that the correct names were generated
|
| 114 |
-
expected_names = {
|
| 115 |
-
"tools": {"post_users", "put_users"},
|
| 116 |
-
"resources": {
|
| 117 |
-
"users",
|
| 118 |
-
"listProducts",
|
| 119 |
-
}, # GET /users, GET /products (from operationId)
|
| 120 |
-
"templates": {
|
| 121 |
-
"users_id",
|
| 122 |
-
"users_id_orders",
|
| 123 |
-
}, # GET /users/{id}, GET /users/{id}/orders
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
# The "tools" set in the server might contain more than our expected names
|
| 127 |
-
# because all HTTP methods could be converted to tools - we just check for inclusion
|
| 128 |
-
assert expected_names["tools"].issubset(server._used_names["tools"])
|
| 129 |
-
assert expected_names["resources"].issubset(server._used_names["resources"])
|
| 130 |
-
assert expected_names["templates"].issubset(server._used_names["templates"])
|
| 131 |
-
|
| 132 |
-
# Check that the operationId is preferred for naming
|
| 133 |
-
assert "listProducts" in server._used_names["resources"]
|
| 134 |
-
|
| 135 |
-
@patch("fastmcp.server.openapi._combine_schemas")
|
| 136 |
-
def test_custom_naming(self, mock_combine, simple_openapi_spec):
|
| 137 |
-
"""Test custom component naming function."""
|
| 138 |
-
# Mock the HTTP client
|
| 139 |
-
mock_client = MagicMock(spec=httpx.AsyncClient)
|
| 140 |
-
|
| 141 |
-
# Mock the combine schemas function to return empty dict
|
| 142 |
-
mock_combine.return_value = {}
|
| 143 |
-
|
| 144 |
-
# Create a custom naming function
|
| 145 |
-
def custom_namer(route, mcp_type, default_name):
|
| 146 |
-
# Always prefix with component type
|
| 147 |
-
if mcp_type == MCPType.TOOL:
|
| 148 |
-
prefix = "tool"
|
| 149 |
-
elif mcp_type == MCPType.RESOURCE:
|
| 150 |
-
prefix = "res"
|
| 151 |
-
elif mcp_type == MCPType.RESOURCE_TEMPLATE:
|
| 152 |
-
prefix = "tmpl"
|
| 153 |
-
else:
|
| 154 |
-
prefix = "other"
|
| 155 |
-
|
| 156 |
-
# Use operationId if available
|
| 157 |
-
if route.operation_id:
|
| 158 |
-
return f"{prefix}_{route.operation_id}"
|
| 159 |
-
|
| 160 |
-
# Otherwise use the path
|
| 161 |
-
path_name = route.path.replace("/", "_").replace("{", "").replace("}", "")
|
| 162 |
-
return f"{prefix}{path_name}"
|
| 163 |
-
|
| 164 |
-
# Create a custom testing server subclass
|
| 165 |
-
class TestServer(FastMCPOpenAPI):
|
| 166 |
-
def _create_openapi_tool(self, route, name):
|
| 167 |
-
_tool_name = self._get_unique_name(name, "tools")
|
| 168 |
-
# Don't actually create the tool, just record that the name was used
|
| 169 |
-
|
| 170 |
-
def _create_openapi_resource(self, route, name):
|
| 171 |
-
_resource_name = self._get_unique_name(name, "resources")
|
| 172 |
-
# Don't actually create the resource, just record that the name was used
|
| 173 |
-
|
| 174 |
-
def _create_openapi_template(self, route, name):
|
| 175 |
-
_template_name = self._get_unique_name(name, "templates")
|
| 176 |
-
# Don't actually create the template, just record that the name was used
|
| 177 |
-
|
| 178 |
-
# Create a server with the custom naming
|
| 179 |
-
server = TestServer(
|
| 180 |
-
openapi_spec=simple_openapi_spec,
|
| 181 |
-
client=mock_client,
|
| 182 |
-
component_namer=custom_namer,
|
| 183 |
-
)
|
| 184 |
-
|
| 185 |
-
# Check some of the generated names
|
| 186 |
-
assert "tool_users" in server._used_names["tools"]
|
| 187 |
-
assert "res_users" in server._used_names["resources"]
|
| 188 |
-
assert "tmpl_users_id" in server._used_names["templates"]
|
| 189 |
-
assert "res_listProducts" in server._used_names["resources"]
|
| 190 |
-
|
| 191 |
-
@patch("fastmcp.server.openapi._combine_schemas")
|
| 192 |
-
def test_collision_handling(self, mock_combine, simple_openapi_spec):
|
| 193 |
-
"""Test how name collisions are handled by appending numbers."""
|
| 194 |
-
# Mock the HTTP client
|
| 195 |
-
mock_client = MagicMock(spec=httpx.AsyncClient)
|
| 196 |
-
|
| 197 |
-
# Mock the combine schemas function to return empty dict
|
| 198 |
-
mock_combine.return_value = {}
|
| 199 |
-
|
| 200 |
-
# Create a custom naming function that always returns the same name
|
| 201 |
-
def collision_namer(route, mcp_type, default_name):
|
| 202 |
-
return "same_name"
|
| 203 |
-
|
| 204 |
-
# Create a custom testing server subclass
|
| 205 |
-
class TestServer(FastMCPOpenAPI):
|
| 206 |
-
def _create_openapi_tool(self, route, name):
|
| 207 |
-
_tool_name = self._get_unique_name(name, "tools")
|
| 208 |
-
# Don't actually create the tool, just record that the name was used
|
| 209 |
-
|
| 210 |
-
def _create_openapi_resource(self, route, name):
|
| 211 |
-
_resource_name = self._get_unique_name(name, "resources")
|
| 212 |
-
# Don't actually create the resource, just record that the name was used
|
| 213 |
-
|
| 214 |
-
def _create_openapi_template(self, route, name):
|
| 215 |
-
_template_name = self._get_unique_name(name, "templates")
|
| 216 |
-
# Don't actually create the template, just record that the name was used
|
| 217 |
-
|
| 218 |
-
# Create a server with the collision namer
|
| 219 |
-
server = TestServer(
|
| 220 |
-
openapi_spec=simple_openapi_spec,
|
| 221 |
-
client=mock_client,
|
| 222 |
-
component_namer=collision_namer,
|
| 223 |
-
)
|
| 224 |
-
|
| 225 |
-
# Check that names were renamed with numbers
|
| 226 |
-
assert "same_name" in server._used_names["tools"]
|
| 227 |
-
assert "same_name_2" in server._used_names["tools"]
|
| 228 |
-
assert "same_name" in server._used_names["resources"]
|
| 229 |
-
assert "same_name_2" in server._used_names["resources"]
|
| 230 |
-
assert "same_name" in server._used_names["templates"]
|
| 231 |
-
assert "same_name_2" in server._used_names["templates"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|