Jeremiah Lowin commited on
Commit
7af7032
·
1 Parent(s): f477a70

Add custom naming and deprecate all_routes_as_tools

Browse files
docs/patterns/openapi.mdx CHANGED
@@ -31,20 +31,61 @@ if __name__ == "__main__":
31
 
32
  ### Timeout
33
 
34
- You can set a timeout for all API requests:
35
 
36
  ```python
37
- # Set a 5 second timeout for all requests
38
  mcp = FastMCP.from_openapi(
39
  openapi_spec=spec,
40
- client=api_client,
41
- timeout=5.0
42
  )
43
  ```
44
 
45
- This timeout is applied to all requests made by tools, resources, and resource templates.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
- ## Route Mapping
 
 
48
 
49
  By default, OpenAPI routes are mapped to MCP components based on these rules:
50
 
@@ -54,7 +95,6 @@ By default, OpenAPI routes are mapped to MCP components based on these rules:
54
  | `GET` with path params | `GET /users/{id}` | Resource Template | Path parameters become template parameters |
55
  | `POST`, `PUT`, `PATCH`, `DELETE`, etc. | `POST /users` | Tool | Operations that modify data |
56
 
57
-
58
  Internally, FastMCP uses a priority-ordered set of `RouteMap` objects to determine the component type. Route maps indicate that a specific HTTP method (or methods) and path pattern should be treated as a specific component type. This is the default set of route maps:
59
 
60
  ```python
@@ -79,7 +119,7 @@ DEFAULT_ROUTE_MAPPINGS = [
79
  ]
80
  ```
81
 
82
- ### Custom Route Maps
83
 
84
  Users can add custom route maps to override the default mapping behavior. User-supplied route maps are always applied first, before the default route maps.
85
 
@@ -95,7 +135,7 @@ custom_maps = [
95
  ]
96
 
97
  # Apply custom mappings
98
- mcp = await FastMCP.from_openapi(
99
  openapi_spec=spec,
100
  client=api_client,
101
  route_maps=custom_maps
@@ -106,23 +146,19 @@ mcp = await FastMCP.from_openapi(
106
  For backward compatibility, FastMCP still supports the `route_type` parameter and `RouteType` enum, but they are deprecated and will be removed in a future version. You will see deprecation warnings if you use them.
107
  </Info>
108
 
109
- ### All Routes as Tools
110
 
111
- When building AI agent backends, it's often useful to treat all routes as callable tools regardless of their HTTP method. You can use the `all_routes_as_tools` parameter to automatically map every route to a Tool:
112
 
113
  ```python
114
- # Make all endpoints tools, regardless of HTTP method
115
  mcp = FastMCP.from_openapi(
116
  openapi_spec=spec,
117
  client=api_client,
118
- all_routes_as_tools=True
119
  )
120
- ```
121
-
122
- This is equivalent to defining a single route map that matches all routes:
123
 
124
- ```python
125
- # Same effect as all_routes_as_tools=True
126
  mcp = FastMCP.from_openapi(
127
  openapi_spec=spec,
128
  client=api_client,
@@ -132,9 +168,7 @@ mcp = FastMCP.from_openapi(
132
  )
133
  ```
134
 
135
- Note that `all_routes_as_tools` and `route_maps` cannot be used together - if you need more complex mapping rules, use `route_maps` instead.
136
-
137
- ### Excluding Routes
138
 
139
  If you want to exclude certain routes from being converted to MCP components, you can map them to `MCPType.EXCLUDE`. This is useful for endpoints that should not be accessible to the agent.
140
 
@@ -167,134 +201,38 @@ mcp = FastMCP.from_openapi(
167
 
168
  When a route is mapped to `MCPType.EXCLUDE`, FastMCP will log its presence but won't create any MCP component for it, effectively making it invisible to clients and agents using the MCP server.
169
 
170
- ## How It Works
171
-
172
- 1. FastMCP parses your OpenAPI spec to extract routes and schemas
173
- 2. It applies mapping rules to categorize each route
174
- 3. When an MCP client calls a tool or accesses a resource:
175
- - FastMCP constructs an HTTP request based on the OpenAPI definition
176
- - It sends the request through the provided httpx client
177
- - It translates the HTTP response to the appropriate MCP format
178
-
179
- ### Request Parameter Handling
180
-
181
- FastMCP carefully handles different types of parameters in OpenAPI requests:
182
 
183
- #### Query Parameters
184
-
185
- By default, FastMCP will only include query parameters that have non-empty values. Parameters with `None` values or empty strings (`""`) are automatically filtered out of requests. This ensures that API servers don't receive unnecessary empty parameters that might cause issues.
186
-
187
- For example, if you call a tool with these parameters:
188
  ```python
189
- await client.call_tool("search_products", {
190
- "category": "electronics", # Will be included
191
- "min_price": 100, # Will be included
192
- "max_price": None, # Will be excluded
193
- "brand": "", # Will be excluded
194
- })
195
- ```
196
-
197
- The resulting HTTP request will only include `category=electronics&min_price=100`.
198
-
199
- #### Path Parameters
200
-
201
- For path parameters, which are typically required by REST APIs, FastMCP filters out `None` values and checks that all required path parameters are provided. If a required path parameter is missing or `None`, an error will be raised.
202
 
203
- ```python
204
- # This will work
205
- await client.call_tool("get_product", {"product_id": 123})
 
 
 
 
 
 
 
 
 
 
 
 
206
 
207
- # This will raise ValueError: "Missing required path parameters: {'product_id'}"
208
- await client.call_tool("get_product", {"product_id": None})
 
 
 
 
209
  ```
210
 
211
- ## Complete Example
212
 
213
- ```python [expandable]
214
- import asyncio
215
-
216
- import httpx
217
-
218
- from fastmcp import FastMCP
219
-
220
- # Sample OpenAPI spec for a Pet Store API
221
- petstore_spec = {
222
- "openapi": "3.0.0",
223
- "info": {
224
- "title": "Pet Store API",
225
- "version": "1.0.0",
226
- "description": "A sample API for managing pets",
227
- },
228
- "paths": {
229
- "/pets": {
230
- "get": {
231
- "operationId": "listPets",
232
- "summary": "List all pets",
233
- "responses": {"200": {"description": "A list of pets"}},
234
- },
235
- "post": {
236
- "operationId": "createPet",
237
- "summary": "Create a new pet",
238
- "responses": {"201": {"description": "Pet created successfully"}},
239
- },
240
- },
241
- "/pets/{petId}": {
242
- "get": {
243
- "operationId": "getPet",
244
- "summary": "Get a pet by ID",
245
- "parameters": [
246
- {
247
- "name": "petId",
248
- "in": "path",
249
- "required": True,
250
- "schema": {"type": "string"},
251
- }
252
- ],
253
- "responses": {
254
- "200": {"description": "Pet details"},
255
- "404": {"description": "Pet not found"},
256
- },
257
- }
258
- },
259
- },
260
- }
261
-
262
-
263
- async def check_mcp(mcp: FastMCP):
264
- # List what components were created
265
- tools = await mcp.get_tools()
266
- resources = await mcp.get_resources()
267
- templates = await mcp.get_resource_templates()
268
-
269
- print(
270
- f"{len(tools)} Tool(s): {', '.join([t.name for t in tools.values()])}"
271
- ) # Should include createPet
272
- print(
273
- f"{len(resources)} Resource(s): {', '.join([r.name for r in resources.values()])}"
274
- ) # Should include listPets
275
- print(
276
- f"{len(templates)} Resource Template(s): {', '.join([t.name for t in templates.values()])}"
277
- ) # Should include getPet
278
-
279
- return mcp
280
-
281
-
282
- if __name__ == "__main__":
283
- # Client for the Pet Store API
284
- client = httpx.AsyncClient(base_url="https://petstore.example.com/api")
285
-
286
- # Create the MCP server
287
- mcp = FastMCP.from_openapi(
288
- openapi_spec=petstore_spec, client=client, name="PetStore"
289
- )
290
-
291
- asyncio.run(check_mcp(mcp))
292
-
293
- # Start the MCP server
294
- mcp.run()
295
- ```
296
-
297
- ### Route Map Shortcuts
298
 
299
  FastMCP provides several shortcut functions to create common route maps more easily:
300
 
@@ -338,8 +276,6 @@ These shortcuts are particularly useful for:
338
  2. Excluding whole sections of your API (use `EXCLUDE_PATTERN("/path/.*")`)
339
  3. Converting routes matching specific patterns to tools (use `PATTERN_AS_TOOLS("/path/.*")`)
340
 
341
- The `all_routes_as_tools=True` parameter is equivalent to using just `[ALL_TOOLS()]` as your route maps.
342
-
343
  <Tip>
344
  You can use `EXCLUDE_ALL()` as the last entry in your custom route maps to completely ignore the default route maps. Since custom route maps are applied first and default maps are appended afterward, having `EXCLUDE_ALL()` at the end of your custom maps will match any routes that your earlier custom rules didn't match, preventing the default maps from having any effect.
345
 
@@ -359,3 +295,61 @@ mcp = FastMCP.from_openapi(
359
  ```
360
  </Tip>
361
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
  ### Timeout
33
 
34
+ You can set a timeout for all requests by providing a `timeout` parameter (in seconds):
35
 
36
  ```python
 
37
  mcp = FastMCP.from_openapi(
38
  openapi_spec=spec,
39
+ client=api_client,
40
+ timeout=30.0 # 30 second timeout
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
 
 
95
  | `GET` with path params | `GET /users/{id}` | Resource Template | Path parameters become template parameters |
96
  | `POST`, `PUT`, `PATCH`, `DELETE`, etc. | `POST /users` | Tool | Operations that modify data |
97
 
 
98
  Internally, FastMCP uses a priority-ordered set of `RouteMap` objects to determine the component type. Route maps indicate that a specific HTTP method (or methods) and path pattern should be treated as a specific component type. This is the default set of route maps:
99
 
100
  ```python
 
119
  ]
120
  ```
121
 
122
+ #### Custom Route Maps
123
 
124
  Users can add custom route maps to override the default mapping behavior. User-supplied route maps are always applied first, before the default route maps.
125
 
 
135
  ]
136
 
137
  # Apply custom mappings
138
+ mcp = FastMCP.from_openapi(
139
  openapi_spec=spec,
140
  client=api_client,
141
  route_maps=custom_maps
 
146
  For backward compatibility, FastMCP still supports the `route_type` parameter and `RouteType` enum, but they are deprecated and will be removed in a future version. You will see deprecation warnings if you use them.
147
  </Info>
148
 
149
+ #### All Routes as Tools
150
 
151
+ When building AI agent backends, it's often useful to treat all routes as callable tools regardless of their HTTP method. You can use the `ALL_TOOLS()` shortcut or create a custom route map:
152
 
153
  ```python
154
+ # Make all endpoints tools using the shortcut
155
  mcp = FastMCP.from_openapi(
156
  openapi_spec=spec,
157
  client=api_client,
158
+ route_maps=[ALL_TOOLS()]
159
  )
 
 
 
160
 
161
+ # Same effect using a custom route map
 
162
  mcp = FastMCP.from_openapi(
163
  openapi_spec=spec,
164
  client=api_client,
 
168
  )
169
  ```
170
 
171
+ #### Excluding Routes
 
 
172
 
173
  If you want to exclude certain routes from being converted to MCP components, you can map them to `MCPType.EXCLUDE`. This is useful for endpoints that should not be accessible to the agent.
174
 
 
201
 
202
  When a route is mapped to `MCPType.EXCLUDE`, FastMCP will log its presence but won't create any MCP component for it, effectively making it invisible to clients and agents using the MCP server.
203
 
204
+ You can customize this behavior by providing a list of `RouteMap` objects:
 
 
 
 
 
 
 
 
 
 
 
205
 
 
 
 
 
 
206
  ```python
207
+ from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap, MCPType
 
 
 
 
 
 
 
 
 
 
 
 
208
 
209
+ # Custom route mappings
210
+ custom_mappings = [
211
+ # Convert all user-related routes to tools
212
+ RouteMap(
213
+ methods=["GET", "POST", "PUT", "DELETE"],
214
+ pattern=r"^/users.*",
215
+ mcp_type=MCPType.TOOL
216
+ ),
217
+ # Exclude analytics routes
218
+ RouteMap(
219
+ methods=["*"], # All methods
220
+ pattern=r"^/analytics.*",
221
+ mcp_type=MCPType.EXCLUDE
222
+ ),
223
+ ]
224
 
225
+ # Create server with custom mappings
226
+ mcp = FastMCPOpenAPI(
227
+ openapi_spec=spec,
228
+ client=httpx.AsyncClient(),
229
+ route_maps=custom_mappings,
230
+ )
231
  ```
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
 
 
276
  2. Excluding whole sections of your API (use `EXCLUDE_PATTERN("/path/.*")`)
277
  3. Converting routes matching specific patterns to tools (use `PATTERN_AS_TOOLS("/path/.*")`)
278
 
 
 
279
  <Tip>
280
  You can use `EXCLUDE_ALL()` as the last entry in your custom route maps to completely ignore the default route maps. Since custom route maps are applied first and default maps are appended afterward, having `EXCLUDE_ALL()` at the end of your custom maps will match any routes that your earlier custom rules didn't match, preventing the default maps from having any effect.
281
 
 
295
  ```
296
  </Tip>
297
 
298
+ ## How It Works
299
+
300
+ 1. FastMCP parses your OpenAPI spec to extract routes and schemas
301
+ 2. It applies mapping rules to categorize each route
302
+ 3. When an MCP client calls a tool or accesses a resource:
303
+ - FastMCP constructs an HTTP request based on the OpenAPI definition
304
+ - It sends the request through the provided httpx client
305
+ - It translates the HTTP response to the appropriate MCP format
306
+
307
+ ### Request Parameter Handling
308
+
309
+ FastMCP carefully handles different types of parameters in OpenAPI requests:
310
+
311
+ #### Query Parameters
312
+
313
+ By default, FastMCP will only include query parameters that have non-empty values. Parameters with `None` values or empty strings (`""`) are automatically filtered out of requests. This ensures that API servers don't receive unnecessary empty parameters that might cause issues.
314
+
315
+ For example, if you call a tool with these parameters:
316
+ ```python
317
+ await client.call_tool("search_products", {
318
+ "category": "electronics", # Will be included
319
+ "min_price": 100, # Will be included
320
+ "max_price": None, # Will be excluded
321
+ "brand": "", # Will be excluded
322
+ })
323
+ ```
324
+
325
+ The resulting HTTP request will only include `category=electronics&min_price=100`.
326
+
327
+ #### Path Parameters
328
+
329
+ For path parameters, which are typically required by REST APIs, FastMCP filters out `None` values and checks that all required path parameters are provided. If a required path parameter is missing or `None`, an error will be raised.
330
+
331
+ ```python
332
+ # This will work
333
+ await client.call_tool("get_product", {"product_id": 123})
334
+
335
+ # This will raise ValueError: "Missing required path parameters: {'product_id'}"
336
+ await client.call_tool("get_product", {"product_id": None})
337
+ ```
338
+
339
+ ## Example: Custom Authentication
340
+
341
+ If your API requires authentication, you can set headers on the client:
342
+
343
+ ```python
344
+ import httpx
345
+ from fastmcp import FastMCP
346
+
347
+ # Create a client with authentication
348
+ api_client = httpx.AsyncClient(
349
+ base_url="https://api.example.com",
350
+ headers={"Authorization": "Bearer YOUR_TOKEN"}
351
+ )
352
+
353
+ # Create an MCP server from your OpenAPI spec
354
+ mcp = FastMCP.from_openapi(openapi_spec=spec, client=api_client)
355
+ ```
src/fastmcp/server/openapi.py CHANGED
@@ -53,6 +53,10 @@ class MCPType(enum.Enum):
53
  EXCLUDE = "EXCLUDE"
54
 
55
 
 
 
 
 
56
  # Keep RouteType as an alias to MCPType for backward compatibility
57
  class RouteType(enum.Enum):
58
  """
@@ -65,30 +69,7 @@ class RouteType(enum.Enum):
65
  RESOURCE = "RESOURCE"
66
  RESOURCE_TEMPLATE = "RESOURCE_TEMPLATE"
67
  PROMPT = "PROMPT"
68
- EXCLUDE = "EXCLUDE"
69
- IGNORE = "IGNORE" # Deprecated, use EXCLUDE instead
70
-
71
- def __new__(cls, value):
72
- # Deprecated in 2.4.1
73
- warnings.warn(
74
- "RouteType is deprecated and will be removed in a future version. "
75
- "Use MCPType instead.",
76
- DeprecationWarning,
77
- stacklevel=2,
78
- )
79
-
80
- # Add a specific warning for the deprecated IGNORE value
81
- if value == "IGNORE":
82
- warnings.warn(
83
- "RouteType.IGNORE is deprecated and will be removed in a future version. "
84
- "Use MCPType.EXCLUDE instead.",
85
- DeprecationWarning,
86
- stacklevel=2,
87
- )
88
-
89
- instance = object.__new__(cls)
90
- instance._value_ = value
91
- return instance
92
 
93
 
94
  @dataclass
@@ -102,7 +83,7 @@ class RouteMap:
102
 
103
  def __post_init__(self):
104
  """Validate and process the route map after initialization."""
105
- # Handle backward compatibility for route_type
106
  if self.mcp_type is None and self.route_type is not None:
107
  warnings.warn(
108
  "The 'route_type' parameter is deprecated and will be removed in a future version. "
@@ -110,7 +91,13 @@ class RouteMap:
110
  DeprecationWarning,
111
  stacklevel=2,
112
  )
113
-
 
 
 
 
 
 
114
  # Check for the deprecated IGNORE value
115
  if self.route_type == RouteType.IGNORE:
116
  warnings.warn(
@@ -236,13 +223,6 @@ def _determine_route_type(
236
  return MCPType.TOOL
237
 
238
 
239
- # Placeholder function to provide function metadata
240
- async def _openapi_passthrough(*args, **kwargs):
241
- """Placeholder function for OpenAPI endpoints."""
242
- # This is kept for metadata generation purposes
243
- pass
244
-
245
-
246
  class OpenAPITool(Tool):
247
  """Tool implementation for OpenAPI endpoints."""
248
 
@@ -670,6 +650,55 @@ class OpenAPIResourceTemplate(ResourceTemplate):
670
  )
671
 
672
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
673
  class FastMCPOpenAPI(FastMCP):
674
  """
675
  FastMCP server implementation that creates components from an OpenAPI schema.
@@ -715,6 +744,7 @@ class FastMCPOpenAPI(FastMCP):
715
  name: str | None = None,
716
  route_maps: list[RouteMap] | None = None,
717
  timeout: float | None = None,
 
718
  **settings: Any,
719
  ):
720
  """
@@ -726,12 +756,18 @@ class FastMCPOpenAPI(FastMCP):
726
  name: Optional name for the server
727
  route_maps: Optional list of RouteMap objects defining route mappings
728
  timeout: Optional timeout (in seconds) for all requests
 
729
  **settings: Additional settings for FastMCP
730
  """
731
  super().__init__(name=name or "OpenAPI FastMCP", **settings)
732
 
733
  self._client = client
734
  self._timeout = timeout
 
 
 
 
 
735
  http_routes = openapi.parse_openapi_to_http_routes(openapi_spec)
736
 
737
  # Process routes
@@ -740,20 +776,18 @@ class FastMCPOpenAPI(FastMCP):
740
  # Determine route type based on mappings or default rules
741
  route_type = _determine_route_type(route, route_maps)
742
 
743
- # Use operation_id if available, otherwise generate a name
744
- operation_id = route.operation_id
745
- if not operation_id:
746
- # Generate operation ID from method and path
747
- path_parts = route.path.strip("/").split("/")
748
- path_name = "_".join(p for p in path_parts if not p.startswith("{"))
749
- operation_id = f"{route.method.lower()}_{path_name}"
750
 
751
  if route_type == MCPType.TOOL:
752
- self._create_openapi_tool(route, operation_id)
753
  elif route_type == MCPType.RESOURCE:
754
- self._create_openapi_resource(route, operation_id)
755
  elif route_type == MCPType.RESOURCE_TEMPLATE:
756
- self._create_openapi_template(route, operation_id)
757
  elif route_type == MCPType.PROMPT:
758
  # Not implemented yet
759
  logger.warning(
@@ -764,10 +798,59 @@ class FastMCPOpenAPI(FastMCP):
764
 
765
  logger.info(f"Created FastMCP OpenAPI server with {len(http_routes)} routes")
766
 
767
- def _create_openapi_tool(self, route: openapi.HTTPRoute, operation_id: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
768
  """Creates and registers an OpenAPITool with enhanced description."""
769
  combined_schema = _combine_schemas(route)
770
- tool_name = operation_id
 
 
 
771
  base_description = (
772
  route.description
773
  or route.summary
@@ -797,9 +880,11 @@ class FastMCPOpenAPI(FastMCP):
797
  f"Registered TOOL: {tool_name} ({route.method} {route.path}) with tags: {route.tags}"
798
  )
799
 
800
- def _create_openapi_resource(self, route: openapi.HTTPRoute, operation_id: str):
801
  """Creates and registers an OpenAPIResource with enhanced description."""
802
- resource_name = operation_id
 
 
803
  resource_uri = f"resource://openapi/{resource_name}"
804
  base_description = (
805
  route.description or route.summary or f"Represents {route.path}"
@@ -828,9 +913,11 @@ class FastMCPOpenAPI(FastMCP):
828
  f"Registered RESOURCE: {resource_uri} ({route.method} {route.path}) with tags: {route.tags}"
829
  )
830
 
831
- def _create_openapi_template(self, route: openapi.HTTPRoute, operation_id: str):
832
  """Creates and registers an OpenAPIResourceTemplate with enhanced description."""
833
- template_name = operation_id
 
 
834
  path_params = [p.name for p in route.parameters if p.location == "path"]
835
  path_params.sort() # Sort for consistent URIs
836
 
 
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
  """
 
69
  RESOURCE = "RESOURCE"
70
  RESOURCE_TEMPLATE = "RESOURCE_TEMPLATE"
71
  PROMPT = "PROMPT"
72
+ IGNORE = "IGNORE"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
 
75
  @dataclass
 
83
 
84
  def __post_init__(self):
85
  """Validate and process the route map after initialization."""
86
+ # Handle backward compatibility for route_type, deprecated in 2.5.0
87
  if self.mcp_type is None and self.route_type is not None:
88
  warnings.warn(
89
  "The 'route_type' parameter is deprecated and will be removed in a future version. "
 
91
  DeprecationWarning,
92
  stacklevel=2,
93
  )
94
+ if isinstance(self.route_type, RouteType):
95
+ warnings.warn(
96
+ "The RouteType class is deprecated and will be removed in a future version. "
97
+ "Use MCPType instead.",
98
+ DeprecationWarning,
99
+ stacklevel=2,
100
+ )
101
  # Check for the deprecated IGNORE value
102
  if self.route_type == RouteType.IGNORE:
103
  warnings.warn(
 
223
  return MCPType.TOOL
224
 
225
 
 
 
 
 
 
 
 
226
  class OpenAPITool(Tool):
227
  """Tool implementation for OpenAPI endpoints."""
228
 
 
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
  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
  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()}
770
+
771
  http_routes = openapi.parse_openapi_to_http_routes(openapi_spec)
772
 
773
  # Process routes
 
776
  # Determine route type based on mappings or default rules
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)
787
  elif route_type == MCPType.RESOURCE:
788
+ self._create_openapi_resource(route, component_name)
789
  elif route_type == MCPType.RESOURCE_TEMPLATE:
790
+ self._create_openapi_template(route, component_name)
791
  elif route_type == MCPType.PROMPT:
792
  # Not implemented yet
793
  logger.warning(
 
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"]
816
+ ) -> str:
817
+ """
818
+ Ensure the name is unique within its component type by appending numbers if needed.
819
+
820
+ Args:
821
+ name: The proposed name
822
+ component_type: The type of component ("tools", "resources", or "templates")
823
+
824
+ Returns:
825
+ str: A unique name for the component
826
+ """
827
+ # Check if the name is already used
828
+ if name not in self._used_names[component_type]:
829
+ self._used_names[component_type].add(name)
830
+ return name
831
+
832
+ # Find the next available number suffix
833
+ counter = 2
834
+ while f"{name}_{counter}" in self._used_names[component_type]:
835
+ counter += 1
836
+
837
+ # Create the new name
838
+ new_name = f"{name}_{counter}"
839
+ logger.debug(
840
+ f"Name collision detected: '{name}' already exists as a {component_type[:-1]}. "
841
+ f"Using '{new_name}' instead."
842
+ )
843
+
844
+ self._used_names[component_type].add(new_name)
845
+ return new_name
846
+
847
+ def _create_openapi_tool(self, route: openapi.HTTPRoute, name: str):
848
  """Creates and registers an OpenAPITool with enhanced description."""
849
  combined_schema = _combine_schemas(route)
850
+
851
+ # Get a unique tool name
852
+ tool_name = self._get_unique_name(name, "tools")
853
+
854
  base_description = (
855
  route.description
856
  or route.summary
 
880
  f"Registered TOOL: {tool_name} ({route.method} {route.path}) with tags: {route.tags}"
881
  )
882
 
883
+ def _create_openapi_resource(self, route: openapi.HTTPRoute, name: str):
884
  """Creates and registers an OpenAPIResource with enhanced description."""
885
+ # Get a unique resource name
886
+ resource_name = self._get_unique_name(name, "resources")
887
+
888
  resource_uri = f"resource://openapi/{resource_name}"
889
  base_description = (
890
  route.description or route.summary or f"Represents {route.path}"
 
913
  f"Registered RESOURCE: {resource_uri} ({route.method} {route.path}) with tags: {route.tags}"
914
  )
915
 
916
+ def _create_openapi_template(self, route: openapi.HTTPRoute, name: str):
917
  """Creates and registers an OpenAPIResourceTemplate with enhanced description."""
918
+ # Get a unique template name
919
+ template_name = self._get_unique_name(name, "templates")
920
+
921
  path_params = [p.name for p in route.parameters if p.location == "path"]
922
  path_params.sort() # Sort for consistent URIs
923
 
src/fastmcp/server/server.py CHANGED
@@ -1147,19 +1147,22 @@ class FastMCP(Generic[LifespanResultT]):
1147
  """
1148
  Create a FastMCP server from an OpenAPI specification.
1149
  """
1150
- from .openapi import FastMCPOpenAPI, RouteMap, RouteType
 
 
 
 
 
 
 
 
 
1151
 
1152
  if all_routes_as_tools and route_maps:
1153
  raise ValueError("Cannot specify both all_routes_as_tools and route_maps")
1154
 
1155
  elif all_routes_as_tools:
1156
- route_maps = [
1157
- RouteMap(
1158
- methods="*",
1159
- pattern=r".*",
1160
- route_type=RouteType.TOOL,
1161
- )
1162
- ]
1163
 
1164
  return FastMCPOpenAPI(
1165
  openapi_spec=openapi_spec,
@@ -1181,15 +1184,21 @@ class FastMCP(Generic[LifespanResultT]):
1181
  Create a FastMCP server from a FastAPI application.
1182
  """
1183
 
1184
- from .openapi import FastMCPOpenAPI, RouteMap, RouteType
 
 
 
 
 
 
 
 
1185
 
1186
  if all_routes_as_tools and route_maps:
1187
  raise ValueError("Cannot specify both all_routes_as_tools and route_maps")
1188
 
1189
  elif all_routes_as_tools:
1190
- route_maps = [
1191
- RouteMap(methods="*", pattern=r".*", route_type=RouteType.TOOL)
1192
- ]
1193
 
1194
  client = httpx.AsyncClient(
1195
  transport=httpx.ASGITransport(app=app), base_url="http://fastapi"
 
1147
  """
1148
  Create a FastMCP server from an OpenAPI specification.
1149
  """
1150
+ from .openapi import ALL_TOOLS, FastMCPOpenAPI
1151
+
1152
+ # Deprecated since 2.5.0
1153
+ if all_routes_as_tools:
1154
+ warnings.warn(
1155
+ "The 'all_routes_as_tools' parameter is deprecated and will be removed in a future version. "
1156
+ "Use 'route_maps=[ALL_TOOLS()]' instead.",
1157
+ DeprecationWarning,
1158
+ stacklevel=2,
1159
+ )
1160
 
1161
  if all_routes_as_tools and route_maps:
1162
  raise ValueError("Cannot specify both all_routes_as_tools and route_maps")
1163
 
1164
  elif all_routes_as_tools:
1165
+ route_maps = [ALL_TOOLS()]
 
 
 
 
 
 
1166
 
1167
  return FastMCPOpenAPI(
1168
  openapi_spec=openapi_spec,
 
1184
  Create a FastMCP server from a FastAPI application.
1185
  """
1186
 
1187
+ from .openapi import ALL_TOOLS, FastMCPOpenAPI
1188
+
1189
+ if all_routes_as_tools:
1190
+ warnings.warn(
1191
+ "The 'all_routes_as_tools' parameter is deprecated and will be removed in a future version. "
1192
+ "Use 'route_maps=[ALL_TOOLS()]' instead.",
1193
+ DeprecationWarning,
1194
+ stacklevel=2,
1195
+ )
1196
 
1197
  if all_routes_as_tools and route_maps:
1198
  raise ValueError("Cannot specify both all_routes_as_tools and route_maps")
1199
 
1200
  elif all_routes_as_tools:
1201
+ route_maps = [ALL_TOOLS()]
 
 
1202
 
1203
  client = httpx.AsyncClient(
1204
  transport=httpx.ASGITransport(app=app), base_url="http://fastapi"
tests/server/test_openapi.py CHANGED
@@ -18,11 +18,11 @@ from fastmcp.client import Client
18
  from fastmcp.exceptions import ToolError
19
  from fastmcp.server.openapi import (
20
  FastMCPOpenAPI,
 
21
  OpenAPIResource,
22
  OpenAPIResourceTemplate,
23
  OpenAPITool,
24
  RouteMap,
25
- RouteType,
26
  )
27
 
28
 
@@ -304,7 +304,7 @@ class TestTools:
304
  openapi_spec=openapi_spec,
305
  client=api_client,
306
  route_maps=[
307
- RouteMap(methods=["GET"], pattern=r".*", route_type=RouteType.TOOL)
308
  ],
309
  )
310
  async with Client(mcp_server) as client:
@@ -956,9 +956,7 @@ async def test_empty_query_parameters_not_sent(
956
  mcp_server = FastMCPOpenAPI(
957
  openapi_spec=openapi_spec,
958
  client=api_client,
959
- route_maps=[
960
- RouteMap(methods=["GET"], pattern=r".*", route_type=RouteType.TOOL)
961
- ],
962
  )
963
 
964
  # Call the search tool with mixed parameter values
@@ -1499,17 +1497,15 @@ class TestFastAPIDescriptionPropagation:
1499
  # Create custom route mappings
1500
  route_maps = [
1501
  # Map GET /items to Resource
1502
- RouteMap(
1503
- methods=["GET"], pattern=r"^/items$", route_type=RouteType.RESOURCE
1504
- ),
1505
  # Map GET /items/{item_id} to ResourceTemplate
1506
  RouteMap(
1507
  methods=["GET"],
1508
  pattern=r"^/items/\{.*\}$",
1509
- route_type=RouteType.RESOURCE_TEMPLATE,
1510
  ),
1511
  # Map POST /items to Tool
1512
- RouteMap(methods=["POST"], pattern=r"^/items$", route_type=RouteType.TOOL),
1513
  ]
1514
 
1515
  # Create FastMCP server with the OpenAPI spec and custom route mappings
@@ -1918,7 +1914,7 @@ class TestRouteMapWildcard:
1918
  ):
1919
  """Test that a RouteMap with methods='*' matches all HTTP methods."""
1920
  # Create a single route map with wildcard method
1921
- route_maps = [RouteMap(methods="*", pattern=r".*", route_type=RouteType.TOOL)]
1922
 
1923
  mcp = FastMCPOpenAPI(
1924
  openapi_spec=basic_openapi_spec,
@@ -1947,9 +1943,9 @@ class TestRouteMapWildcard:
1947
  # Create route maps with specific method first, then wildcard
1948
  route_maps = [
1949
  # GET operations should be mapped to resources
1950
- RouteMap(methods=["GET"], pattern=r".*", route_type=RouteType.RESOURCE),
1951
  # All other operations should be mapped to tools
1952
- RouteMap(methods="*", pattern=r".*", route_type=RouteType.TOOL),
1953
  ]
1954
 
1955
  mcp = FastMCPOpenAPI(
@@ -1977,9 +1973,9 @@ class TestRouteMapWildcard:
1977
  # Create route maps with wildcard first, then specific methods
1978
  route_maps = [
1979
  # Wildcard first matches everything
1980
- RouteMap(methods="*", pattern=r".*", route_type=RouteType.TOOL),
1981
  # This should never be reached
1982
- RouteMap(methods=["GET"], pattern=r".*", route_type=RouteType.RESOURCE),
1983
  ]
1984
 
1985
  mcp = FastMCPOpenAPI(
@@ -2002,9 +1998,9 @@ class TestRouteMapWildcard:
2002
  """Test wildcard methods combined with specific path patterns."""
2003
  route_maps = [
2004
  # All methods on /users path -> Resources
2005
- RouteMap(methods="*", pattern=r".*/users$", route_type=RouteType.RESOURCE),
2006
  # All methods on /posts path -> Tools
2007
- RouteMap(methods="*", pattern=r".*/posts$", route_type=RouteType.TOOL),
2008
  ]
2009
 
2010
  mcp = FastMCPOpenAPI(
@@ -2063,95 +2059,104 @@ class TestAllRoutesAsTools:
2063
 
2064
  async def test_from_openapi_all_routes_as_tools(self, simple_api_spec, mock_client):
2065
  """Test FastMCP.from_openapi with all_routes_as_tools=True."""
2066
- # Create server with all routes as tools
2067
- server = FastMCP.from_openapi(
2068
- openapi_spec=simple_api_spec, client=mock_client, all_routes_as_tools=True
2069
- )
2070
 
2071
- # All operations (GET and POST) should be mapped to tools
2072
- tools = server._tool_manager.list_tools()
2073
- tool_names = {t.name for t in tools}
 
 
 
2074
 
2075
- assert "getItems" in tool_names
2076
- assert "createItem" in tool_names
2077
- assert len(tools) == 2
2078
 
2079
- # No resources or templates should be created
2080
- resources = server._resource_manager.get_resources()
2081
- templates = server._resource_manager.get_templates()
2082
  assert len(resources) == 0
 
 
 
2083
  assert len(templates) == 0
2084
 
2085
  async def test_from_openapi_all_routes_as_tools_conflicting_args(
2086
  self, simple_api_spec, mock_client
2087
  ):
2088
  """Test FastMCP.from_openapi raises error when both route_maps and all_routes_as_tools are provided."""
2089
- # Try to create server with conflicting args
2090
  with pytest.raises(
2091
  ValueError, match="Cannot specify both all_routes_as_tools and route_maps"
2092
  ):
2093
- FastMCP.from_openapi(
2094
- openapi_spec=simple_api_spec,
2095
- client=mock_client,
2096
- all_routes_as_tools=True,
2097
- route_maps=[
2098
- RouteMap(
2099
- methods=["GET"], pattern=r".*", route_type=RouteType.RESOURCE
2100
- )
2101
- ],
2102
- )
 
 
 
2103
 
2104
  async def test_from_fastapi_all_routes_as_tools(self):
2105
  """Test FastMCP.from_fastapi with all_routes_as_tools=True."""
2106
- # Create a simple FastAPI app
2107
- app = FastAPI(title="Test FastAPI")
2108
 
2109
- @app.get("/items")
2110
- async def get_items():
2111
- return [{"id": 1, "name": "Item 1"}]
 
2112
 
2113
- @app.post("/items")
2114
- async def create_item(item: dict):
2115
- return {"id": 2, **item}
2116
 
2117
- # Create server with all routes as tools
2118
- server = FastMCP.from_fastapi(app=app, all_routes_as_tools=True)
 
2119
 
2120
- # Both GET and POST operations should be mapped to tools
2121
- tools = server._tool_manager.list_tools()
 
2122
 
2123
- # Get tool names from the generated operation IDs
2124
- tool_names = {t.name for t in tools}
2125
 
2126
- # Check that both routes were mapped to tools
2127
- # The exact names depend on FastAPI's operation ID generation
2128
- assert len(tools) == 2
2129
- assert any("get" in name.lower() for name in tool_names)
2130
- assert any("post" in name.lower() for name in tool_names)
2131
 
2132
- # No resources or templates should be created
2133
- resources = server._resource_manager.get_resources()
2134
- templates = server._resource_manager.get_templates()
2135
  assert len(resources) == 0
 
 
 
2136
  assert len(templates) == 0
2137
 
2138
  async def test_from_fastapi_all_routes_as_tools_conflicting_args(self):
2139
  """Test FastMCP.from_fastapi raises error when both route_maps and all_routes_as_tools are provided."""
2140
- app = FastAPI(title="Test FastAPI")
 
 
 
 
 
2141
 
2142
- # Try to create server with conflicting args
2143
  with pytest.raises(
2144
  ValueError, match="Cannot specify both all_routes_as_tools and route_maps"
2145
  ):
2146
- FastMCP.from_fastapi(
2147
- app=app,
2148
- all_routes_as_tools=True,
2149
- route_maps=[
2150
- RouteMap(
2151
- methods=["GET"], pattern=r".*", route_type=RouteType.RESOURCE
2152
- )
2153
- ],
2154
- )
 
 
 
2155
 
2156
 
2157
  class TestRouteTypeExclude:
@@ -2202,10 +2207,10 @@ class TestRouteTypeExclude:
2202
  RouteMap(
2203
  methods=["GET"],
2204
  pattern=r"^/analytics$",
2205
- route_type=RouteType.IGNORE,
2206
  ),
2207
  # Make everything else a resource
2208
- RouteMap(methods=["GET"], pattern=r".*", route_type=RouteType.RESOURCE),
2209
  ],
2210
  )
2211
 
 
18
  from fastmcp.exceptions import ToolError
19
  from fastmcp.server.openapi import (
20
  FastMCPOpenAPI,
21
+ MCPType,
22
  OpenAPIResource,
23
  OpenAPIResourceTemplate,
24
  OpenAPITool,
25
  RouteMap,
 
26
  )
27
 
28
 
 
304
  openapi_spec=openapi_spec,
305
  client=api_client,
306
  route_maps=[
307
+ RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.TOOL)
308
  ],
309
  )
310
  async with Client(mcp_server) as client:
 
956
  mcp_server = FastMCPOpenAPI(
957
  openapi_spec=openapi_spec,
958
  client=api_client,
959
+ route_maps=[RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.TOOL)],
 
 
960
  )
961
 
962
  # Call the search tool with mixed parameter values
 
1497
  # Create custom route mappings
1498
  route_maps = [
1499
  # Map GET /items to Resource
1500
+ RouteMap(methods=["GET"], pattern=r"^/items$", mcp_type=MCPType.RESOURCE),
 
 
1501
  # Map GET /items/{item_id} to ResourceTemplate
1502
  RouteMap(
1503
  methods=["GET"],
1504
  pattern=r"^/items/\{.*\}$",
1505
+ mcp_type=MCPType.RESOURCE_TEMPLATE,
1506
  ),
1507
  # Map POST /items to Tool
1508
+ RouteMap(methods=["POST"], pattern=r"^/items$", mcp_type=MCPType.TOOL),
1509
  ]
1510
 
1511
  # Create FastMCP server with the OpenAPI spec and custom route mappings
 
1914
  ):
1915
  """Test that a RouteMap with methods='*' matches all HTTP methods."""
1916
  # Create a single route map with wildcard method
1917
+ route_maps = [RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]
1918
 
1919
  mcp = FastMCPOpenAPI(
1920
  openapi_spec=basic_openapi_spec,
 
1943
  # Create route maps with specific method first, then wildcard
1944
  route_maps = [
1945
  # GET operations should be mapped to resources
1946
+ RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE),
1947
  # All other operations should be mapped to tools
1948
+ RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL),
1949
  ]
1950
 
1951
  mcp = FastMCPOpenAPI(
 
1973
  # Create route maps with wildcard first, then specific methods
1974
  route_maps = [
1975
  # Wildcard first matches everything
1976
+ RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL),
1977
  # This should never be reached
1978
+ RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE),
1979
  ]
1980
 
1981
  mcp = FastMCPOpenAPI(
 
1998
  """Test wildcard methods combined with specific path patterns."""
1999
  route_maps = [
2000
  # All methods on /users path -> Resources
2001
+ RouteMap(methods="*", pattern=r".*/users$", mcp_type=MCPType.RESOURCE),
2002
  # All methods on /posts path -> Tools
2003
+ RouteMap(methods="*", pattern=r".*/posts$", mcp_type=MCPType.TOOL),
2004
  ]
2005
 
2006
  mcp = FastMCPOpenAPI(
 
2059
 
2060
  async def test_from_openapi_all_routes_as_tools(self, simple_api_spec, mock_client):
2061
  """Test FastMCP.from_openapi with all_routes_as_tools=True."""
 
 
 
 
2062
 
2063
+ with pytest.warns(DeprecationWarning, match="all_routes_as_tools.*deprecated"):
2064
+ server = FastMCP.from_openapi(
2065
+ openapi_spec=simple_api_spec,
2066
+ client=mock_client,
2067
+ all_routes_as_tools=True,
2068
+ )
2069
 
2070
+ # Check that all routes are tools
2071
+ tools = await server.get_tools()
2072
+ assert len(tools) >= 2 # Should have at least the two endpoints as tools
2073
 
2074
+ # Should have no resources since all routes are tools
2075
+ resources = await server.get_resources()
 
2076
  assert len(resources) == 0
2077
+
2078
+ # Should have no resource templates since all routes are tools
2079
+ templates = await server.get_resource_templates()
2080
  assert len(templates) == 0
2081
 
2082
  async def test_from_openapi_all_routes_as_tools_conflicting_args(
2083
  self, simple_api_spec, mock_client
2084
  ):
2085
  """Test FastMCP.from_openapi raises error when both route_maps and all_routes_as_tools are provided."""
 
2086
  with pytest.raises(
2087
  ValueError, match="Cannot specify both all_routes_as_tools and route_maps"
2088
  ):
2089
+ with pytest.warns(
2090
+ DeprecationWarning, match="all_routes_as_tools.*deprecated"
2091
+ ):
2092
+ FastMCP.from_openapi(
2093
+ openapi_spec=simple_api_spec,
2094
+ client=mock_client,
2095
+ route_maps=[
2096
+ RouteMap(
2097
+ methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE
2098
+ )
2099
+ ],
2100
+ all_routes_as_tools=True,
2101
+ )
2102
 
2103
  async def test_from_fastapi_all_routes_as_tools(self):
2104
  """Test FastMCP.from_fastapi with all_routes_as_tools=True."""
 
 
2105
 
2106
+ try:
2107
+ import fastapi
2108
+ except ImportError:
2109
+ pytest.skip("FastAPI not available")
2110
 
2111
+ app = fastapi.FastAPI()
 
 
2112
 
2113
+ @app.get("/items")
2114
+ def get_items():
2115
+ return {"items": []}
2116
 
2117
+ @app.post("/items")
2118
+ def create_item():
2119
+ return {"item": "created"}
2120
 
2121
+ with pytest.warns(DeprecationWarning, match="all_routes_as_tools.*deprecated"):
2122
+ server = FastMCP.from_fastapi(app=app, all_routes_as_tools=True)
2123
 
2124
+ # Check that all routes are tools
2125
+ tools = await server.get_tools()
2126
+ assert len(tools) >= 2 # Should have at least the two endpoints as tools
 
 
2127
 
2128
+ # Should have no resources since all routes are tools
2129
+ resources = await server.get_resources()
 
2130
  assert len(resources) == 0
2131
+
2132
+ # Should have no resource templates since all routes are tools
2133
+ templates = await server.get_resource_templates()
2134
  assert len(templates) == 0
2135
 
2136
  async def test_from_fastapi_all_routes_as_tools_conflicting_args(self):
2137
  """Test FastMCP.from_fastapi raises error when both route_maps and all_routes_as_tools are provided."""
2138
+ try:
2139
+ import fastapi
2140
+ except ImportError:
2141
+ pytest.skip("FastAPI not available")
2142
+
2143
+ app = fastapi.FastAPI()
2144
 
 
2145
  with pytest.raises(
2146
  ValueError, match="Cannot specify both all_routes_as_tools and route_maps"
2147
  ):
2148
+ with pytest.warns(
2149
+ DeprecationWarning, match="all_routes_as_tools.*deprecated"
2150
+ ):
2151
+ FastMCP.from_fastapi(
2152
+ app=app,
2153
+ route_maps=[
2154
+ RouteMap(
2155
+ methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE
2156
+ )
2157
+ ],
2158
+ all_routes_as_tools=True,
2159
+ )
2160
 
2161
 
2162
  class TestRouteTypeExclude:
 
2207
  RouteMap(
2208
  methods=["GET"],
2209
  pattern=r"^/analytics$",
2210
+ mcp_type=MCPType.EXCLUDE,
2211
  ),
2212
  # Make everything else a resource
2213
+ RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE),
2214
  ],
2215
  )
2216
 
tests/server/test_openapi_naming.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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"]
tests/server/test_openapi_path_parameters.py CHANGED
@@ -6,7 +6,7 @@ import pytest
6
  from fastapi import FastAPI, Query
7
 
8
  from fastmcp import Client, FastMCP
9
- from fastmcp.server.openapi import OpenAPITool, RouteMap, RouteType
10
  from fastmcp.utilities.openapi import HTTPRoute, ParameterInfo
11
 
12
 
@@ -286,9 +286,7 @@ async def test_array_query_param_with_fastapi():
286
  # Create a FastMCP server from the FastAPI app
287
  mcp = FastMCP.from_fastapi(
288
  app,
289
- route_maps=[
290
- RouteMap(methods=["GET"], pattern=r".*", route_type=RouteType.TOOL)
291
- ],
292
  )
293
 
294
  # Test with the client
 
6
  from fastapi import FastAPI, Query
7
 
8
  from fastmcp import Client, FastMCP
9
+ from fastmcp.server.openapi import MCPType, OpenAPITool, RouteMap
10
  from fastmcp.utilities.openapi import HTTPRoute, ParameterInfo
11
 
12
 
 
286
  # Create a FastMCP server from the FastAPI app
287
  mcp = FastMCP.from_fastapi(
288
  app,
289
+ route_maps=[RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.TOOL)],
 
 
290
  )
291
 
292
  # Test with the client
tests/server/test_route_map_shortcuts.py CHANGED
@@ -11,7 +11,6 @@ from fastmcp.server.openapi import (
11
  FastMCPOpenAPI,
12
  MCPType,
13
  RouteMap,
14
- RouteType,
15
  )
16
 
17
 
@@ -52,6 +51,8 @@ class TestRouteMapShortcuts:
52
 
53
  def test_backward_compatibility(self):
54
  """Test that backward compatibility with RouteType and route_type works."""
 
 
55
  # Test creating a RouteMap with route_type
56
  with pytest.warns(DeprecationWarning):
57
  route_map = RouteMap(
 
11
  FastMCPOpenAPI,
12
  MCPType,
13
  RouteMap,
 
14
  )
15
 
16
 
 
51
 
52
  def test_backward_compatibility(self):
53
  """Test that backward compatibility with RouteType and route_type works."""
54
+ from fastmcp.server.openapi import RouteType
55
+
56
  # Test creating a RouteMap with route_type
57
  with pytest.warns(DeprecationWarning):
58
  route_map = RouteMap(