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
- ### Component Naming
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
- default_name = self._generate_default_name(route)
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(self, route: openapi.HTTPRoute) -> str:
 
 
802
  """Generate a default name from the route path."""
803
- # Use OpenAPI operationId if available
804
  if route.operation_id:
805
  return route.operation_id
806
 
807
- # Generate a name from the path
808
  path_parts = route.path.strip("/").split("/")
809
- path_name = "_".join(p for p in path_parts if not p.startswith("{"))
810
 
811
- # The original default naming included the HTTP method
812
- return f"{route.method.lower()}_{path_name}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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"]