Jeremiah Lowin commited on
Commit
a9af6b1
·
unverified ·
2 Parent(s): f4d1162f4a3ff2

Merge pull request #564 from jlowin/custom-routes

Browse files

Enhance route map logic for include/exclude OpenAPI routes

docs/patterns/openapi.mdx CHANGED
@@ -31,21 +31,20 @@ 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
 
51
  | OpenAPI Route | Example |MCP Component | Notes |
@@ -54,7 +53,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
@@ -64,75 +62,195 @@ DEFAULT_ROUTE_MAPPINGS = [
64
  RouteMap(
65
  methods=["GET"],
66
  pattern=r".*\{.*\}.*",
67
- route_type=RouteType.RESOURCE_TEMPLATE,
68
  ),
69
 
70
  # GET without path parameters -> Resource
71
  RouteMap(
72
  methods=["GET"],
73
  pattern=r".*",
74
- route_type=RouteType.RESOURCE,
75
  ),
76
 
77
  # All other methods -> Tool
78
- RouteMap(
79
- methods="*",
80
- pattern=r".*",
81
- route_type=RouteType.TOOL,
82
- ),
83
  ]
84
  ```
85
- ### Custom Route Maps
 
86
 
87
  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.
88
 
89
  ```python
90
- from fastmcp.server.openapi import RouteMap, RouteType
91
 
92
  # Custom mapping rules
93
  custom_maps = [
94
  # Force all analytics endpoints to be Tools
95
  RouteMap(methods=["GET"],
96
  pattern=r"^/analytics/.*",
97
- route_type=RouteType.TOOL)
98
  ]
99
 
100
  # Apply custom mappings
101
- mcp = await FastMCP.from_openapi(
102
  openapi_spec=spec,
103
  client=api_client,
104
  route_maps=custom_maps
105
  )
106
  ```
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,
129
  route_maps=[
130
- RouteMap(methods="*", pattern=r".*", route_type=RouteType.TOOL)
 
 
 
 
 
 
 
131
  ]
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
  ## How It Works
138
 
@@ -175,89 +293,20 @@ await client.call_tool("get_product", {"product_id": 123})
175
  await client.call_tool("get_product", {"product_id": None})
176
  ```
177
 
178
- ## Complete Example
179
 
180
- ```python [expandable]
181
- import asyncio
182
 
 
183
  import httpx
184
-
185
  from fastmcp import FastMCP
186
 
187
- # Sample OpenAPI spec for a Pet Store API
188
- petstore_spec = {
189
- "openapi": "3.0.0",
190
- "info": {
191
- "title": "Pet Store API",
192
- "version": "1.0.0",
193
- "description": "A sample API for managing pets",
194
- },
195
- "paths": {
196
- "/pets": {
197
- "get": {
198
- "operationId": "listPets",
199
- "summary": "List all pets",
200
- "responses": {"200": {"description": "A list of pets"}},
201
- },
202
- "post": {
203
- "operationId": "createPet",
204
- "summary": "Create a new pet",
205
- "responses": {"201": {"description": "Pet created successfully"}},
206
- },
207
- },
208
- "/pets/{petId}": {
209
- "get": {
210
- "operationId": "getPet",
211
- "summary": "Get a pet by ID",
212
- "parameters": [
213
- {
214
- "name": "petId",
215
- "in": "path",
216
- "required": True,
217
- "schema": {"type": "string"},
218
- }
219
- ],
220
- "responses": {
221
- "200": {"description": "Pet details"},
222
- "404": {"description": "Pet not found"},
223
- },
224
- }
225
- },
226
- },
227
- }
228
-
229
-
230
- async def check_mcp(mcp: FastMCP):
231
- # List what components were created
232
- tools = await mcp.get_tools()
233
- resources = await mcp.get_resources()
234
- templates = await mcp.get_resource_templates()
235
-
236
- print(
237
- f"{len(tools)} Tool(s): {', '.join([t.name for t in tools.values()])}"
238
- ) # Should include createPet
239
- print(
240
- f"{len(resources)} Resource(s): {', '.join([r.name for r in resources.values()])}"
241
- ) # Should include listPets
242
- print(
243
- f"{len(templates)} Resource Template(s): {', '.join([t.name for t in templates.values()])}"
244
- ) # Should include getPet
245
-
246
- return mcp
247
-
248
-
249
- if __name__ == "__main__":
250
- # Client for the Pet Store API
251
- client = httpx.AsyncClient(base_url="https://petstore.example.com/api")
252
-
253
- # Create the MCP server
254
- mcp = FastMCP.from_openapi(
255
- openapi_spec=petstore_spec, client=client, name="PetStore"
256
- )
257
-
258
- asyncio.run(check_mcp(mcp))
259
 
260
- # Start the MCP server
261
- mcp.run()
262
  ```
263
-
 
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
  ## 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 |
 
53
  | `GET` with path params | `GET /users/{id}` | Resource Template | Path parameters become template parameters |
54
  | `POST`, `PUT`, `PATCH`, `DELETE`, etc. | `POST /users` | Tool | Operations that modify data |
55
 
 
56
  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:
57
 
58
  ```python
 
62
  RouteMap(
63
  methods=["GET"],
64
  pattern=r".*\{.*\}.*",
65
+ mcp_type=MCPType.RESOURCE_TEMPLATE,
66
  ),
67
 
68
  # GET without path parameters -> Resource
69
  RouteMap(
70
  methods=["GET"],
71
  pattern=r".*",
72
+ mcp_type=MCPType.RESOURCE,
73
  ),
74
 
75
  # All other methods -> Tool
76
+ ALL_TOOLS(),
 
 
 
 
77
  ]
78
  ```
79
+
80
+ #### Custom Route Maps
81
 
82
  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.
83
 
84
  ```python
85
+ from fastmcp.server.openapi import RouteMap, MCPType
86
 
87
  # Custom mapping rules
88
  custom_maps = [
89
  # Force all analytics endpoints to be Tools
90
  RouteMap(methods=["GET"],
91
  pattern=r"^/analytics/.*",
92
+ mcp_type=MCPType.TOOL)
93
  ]
94
 
95
  # Apply custom mappings
96
+ mcp = FastMCP.from_openapi(
97
  openapi_spec=spec,
98
  client=api_client,
99
  route_maps=custom_maps
100
  )
101
  ```
102
 
103
+ <Info>
104
+ 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.
105
+ </Info>
106
+
107
+ #### All Routes as Tools
108
+
109
+ 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:
110
+
111
+ ```python
112
+ # Make all endpoints tools using the shortcut
113
+ mcp = FastMCP.from_openapi(
114
+ openapi_spec=spec,
115
+ client=api_client,
116
+ route_maps=[ALL_TOOLS()]
117
+ )
118
+
119
+ # Same effect using a custom route map
120
+ mcp = FastMCP.from_openapi(
121
+ openapi_spec=spec,
122
+ client=api_client,
123
+ route_maps=[
124
+ RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)
125
+ ]
126
+ )
127
+ ```
128
 
129
+ #### Excluding Routes
130
 
131
+ 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.
132
 
133
  ```python
134
+ from fastmcp.server.openapi import RouteMap, MCPType
135
+
136
+ # Custom mapping rules to exclude specific routes
137
+ custom_maps = [
138
+ # Exclude all admin endpoints
139
+ RouteMap(
140
+ methods="*",
141
+ pattern=r"^/admin/.*",
142
+ mcp_type=MCPType.EXCLUDE
143
+ ),
144
+ # Exclude analytics GET endpoints
145
+ RouteMap(
146
+ methods=["GET"],
147
+ pattern=r"^/analytics/.*",
148
+ mcp_type=MCPType.EXCLUDE
149
+ )
150
+ ]
151
+
152
+ # Apply custom mappings
153
  mcp = FastMCP.from_openapi(
154
  openapi_spec=spec,
155
  client=api_client,
156
+ route_maps=custom_maps
157
+ )
158
+ ```
159
+
160
+ 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.
161
+
162
+ You can customize this behavior by providing a list of `RouteMap` objects:
163
+
164
+ ```python
165
+ from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap, MCPType
166
+
167
+ # Custom route mappings
168
+ custom_mappings = [
169
+ # Convert all user-related routes to tools
170
+ RouteMap(
171
+ methods=["GET", "POST", "PUT", "DELETE"],
172
+ pattern=r"^/users.*",
173
+ mcp_type=MCPType.TOOL
174
+ ),
175
+ # Exclude analytics routes
176
+ RouteMap(
177
+ methods=["*"], # All methods
178
+ pattern=r"^/analytics.*",
179
+ mcp_type=MCPType.EXCLUDE
180
+ ),
181
+ ]
182
+
183
+ # Create server with custom mappings
184
+ mcp = FastMCPOpenAPI(
185
+ openapi_spec=spec,
186
+ client=httpx.AsyncClient(),
187
+ route_maps=custom_mappings,
188
  )
189
  ```
190
 
191
+ #### Route Map Shortcuts
192
+
193
+
194
+ FastMCP provides several shortcut functions to create common route maps more easily:
195
 
196
  ```python
197
+ from fastmcp.server.openapi import (
198
+ ALL_TOOLS,
199
+ EXCLUDE_ALL,
200
+ EXCLUDE_PATTERN,
201
+ PATTERN_AS_TOOLS,
202
+ )
203
+
204
+ # Create an MCP server with custom route maps using shortcuts
205
  mcp = FastMCP.from_openapi(
206
  openapi_spec=spec,
207
  client=api_client,
208
  route_maps=[
209
+ # First exclude all admin endpoints
210
+ EXCLUDE_PATTERN(r"^/admin/.*"),
211
+
212
+ # Make all /api/v1 endpoints tools
213
+ PATTERN_AS_TOOLS(r"^/api/v1/.*"),
214
+
215
+ # Make all remaining routes tools
216
+ ALL_TOOLS(),
217
  ]
218
  )
219
  ```
220
 
221
+ Available shortcuts:
222
+
223
+ | Shortcut Function | Description |
224
+ |------------------|-------------|
225
+ | `ALL_TOOLS()` | Converts all matching routes to tools |
226
+ | `EXCLUDE_ALL()` | Excludes all matching routes from being converted to any component |
227
+ | `PATTERN_AS_TOOLS(pattern)` | Converts routes matching a specific pattern to tools |
228
+ | `EXCLUDE_PATTERN(pattern)` | Excludes routes matching a specific pattern |
229
+
230
+ These shortcuts are particularly useful for:
231
+
232
+ 1. Converting all remaining unmatched routes to tools (use `ALL_TOOLS()`)
233
+ 2. Excluding whole sections of your API (use `EXCLUDE_PATTERN("/path/.*")`)
234
+ 3. Converting routes matching specific patterns to tools (use `PATTERN_AS_TOOLS("/path/.*")`)
235
+
236
+ <Tip>
237
+ 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.
238
+
239
+ ```python
240
+ # Create server that only uses custom route maps, ignoring defaults
241
+ mcp = FastMCP.from_openapi(
242
+ openapi_spec=spec,
243
+ client=api_client,
244
+ route_maps=[
245
+ # Routes to keep as tools
246
+ PATTERN_AS_TOOLS(r"^/api/v1/.*"),
247
+
248
+ # Exclude everything else (ignores default route maps)
249
+ EXCLUDE_ALL(),
250
+ ]
251
+ )
252
+ ```
253
+ </Tip>
254
 
255
  ## How It Works
256
 
 
293
  await client.call_tool("get_product", {"product_id": None})
294
  ```
295
 
296
+ ## Example: Custom Authentication
297
 
298
+ If your API requires authentication, you can set headers on the client:
 
299
 
300
+ ```python
301
  import httpx
 
302
  from fastmcp import FastMCP
303
 
304
+ # Create a client with authentication
305
+ api_client = httpx.AsyncClient(
306
+ base_url="https://api.example.com",
307
+ headers={"Authorization": "Bearer YOUR_TOKEN"}
308
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
 
310
+ # Create an MCP server from your OpenAPI spec
311
+ mcp = FastMCP.from_openapi(openapi_spec=spec, client=api_client)
312
  ```
 
src/fastmcp/server/openapi.py CHANGED
@@ -5,8 +5,9 @@ from __future__ import annotations
5
  import enum
6
  import json
7
  import re
 
8
  from collections.abc import Callable
9
- from dataclasses import dataclass
10
  from re import Pattern
11
  from typing import TYPE_CHECKING, Any, Literal
12
 
@@ -33,8 +34,32 @@ logger = get_logger(__name__)
33
  HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
34
 
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  class RouteType(enum.Enum):
37
- """Type of FastMCP component to create from a route."""
 
 
 
 
38
 
39
  TOOL = "TOOL"
40
  RESOURCE = "RESOURCE"
@@ -47,32 +72,121 @@ class RouteType(enum.Enum):
47
  class RouteMap:
48
  """Mapping configuration for HTTP routes to FastMCP component types."""
49
 
50
- methods: list[HttpMethod] | Literal["*"]
51
- pattern: Pattern[str] | str
52
- route_type: RouteType
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
 
55
  # Default route mappings as a list, where order determines priority
56
  DEFAULT_ROUTE_MAPPINGS = [
57
  # GET requests with path parameters go to ResourceTemplate
58
  RouteMap(
59
- methods=["GET"], pattern=r".*\{.*\}.*", route_type=RouteType.RESOURCE_TEMPLATE
60
  ),
61
  # GET requests without path parameters go to Resource
62
- RouteMap(methods=["GET"], pattern=r".*", route_type=RouteType.RESOURCE),
63
  # All other HTTP methods go to Tool
64
- RouteMap(
65
- methods=["POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"],
66
- pattern=r".*",
67
- route_type=RouteType.TOOL,
68
- ),
69
  ]
70
 
71
 
72
  def _determine_route_type(
73
  route: openapi.HTTPRoute,
74
  mappings: list[RouteMap],
75
- ) -> RouteType:
76
  """
77
  Determines the FastMCP component type based on the route and mappings.
78
 
@@ -81,7 +195,7 @@ def _determine_route_type(
81
  mappings: List of RouteMap objects in priority order
82
 
83
  Returns:
84
- RouteType for this route
85
  """
86
  # Check mappings in priority order (first match wins)
87
  for route_map in mappings:
@@ -94,20 +208,15 @@ def _determine_route_type(
94
  pattern_matches = re.search(route_map.pattern, route.path)
95
 
96
  if pattern_matches:
 
 
97
  logger.debug(
98
- f"Route {route.method} {route.path} matched mapping to {route_map.route_type.name}"
99
  )
100
- return route_map.route_type
101
 
102
  # Default fallback
103
- return RouteType.TOOL
104
-
105
-
106
- # Placeholder function to provide function metadata
107
- async def _openapi_passthrough(*args, **kwargs):
108
- """Placeholder function for OpenAPI endpoints."""
109
- # This is kept for metadata generation purposes
110
- pass
111
 
112
 
113
  class OpenAPITool(Tool):
@@ -555,13 +664,13 @@ class FastMCPOpenAPI(FastMCP):
555
  RouteMap(
556
  methods=["GET", "POST", "PATCH"],
557
  pattern=r".*/users/.*",
558
- route_type=RouteType.RESOURCE_TEMPLATE
559
  ),
560
  # Map all analytics endpoints to Tool
561
  RouteMap(
562
  methods=["GET"],
563
  pattern=r".*/analytics/.*",
564
- route_type=RouteType.TOOL
565
  ),
566
  ]
567
 
@@ -599,6 +708,10 @@ class FastMCPOpenAPI(FastMCP):
599
 
600
  self._client = client
601
  self._timeout = timeout
 
 
 
 
602
  http_routes = openapi.parse_openapi_to_http_routes(openapi_spec)
603
 
604
  # Process routes
@@ -607,34 +720,99 @@ class FastMCPOpenAPI(FastMCP):
607
  # Determine route type based on mappings or default rules
608
  route_type = _determine_route_type(route, route_maps)
609
 
610
- # Use operation_id if available, otherwise generate a name
611
- operation_id = route.operation_id
612
- if not operation_id:
613
- # Generate operation ID from method and path
614
- path_parts = route.path.strip("/").split("/")
615
- path_name = "_".join(p for p in path_parts if not p.startswith("{"))
616
- operation_id = f"{route.method.lower()}_{path_name}"
617
-
618
- if route_type == RouteType.TOOL:
619
- self._create_openapi_tool(route, operation_id)
620
- elif route_type == RouteType.RESOURCE:
621
- self._create_openapi_resource(route, operation_id)
622
- elif route_type == RouteType.RESOURCE_TEMPLATE:
623
- self._create_openapi_template(route, operation_id)
624
- elif route_type == RouteType.PROMPT:
625
  # Not implemented yet
626
  logger.warning(
627
  f"PROMPT route type not implemented: {route.method} {route.path}"
628
  )
629
- elif route_type == RouteType.IGNORE:
630
- logger.info(f"Ignoring route: {route.method} {route.path}")
631
 
632
  logger.info(f"Created FastMCP OpenAPI server with {len(http_routes)} routes")
633
 
634
- def _create_openapi_tool(self, route: openapi.HTTPRoute, operation_id: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
635
  """Creates and registers an OpenAPITool with enhanced description."""
636
  combined_schema = _combine_schemas(route)
637
- tool_name = operation_id
 
 
 
638
  base_description = (
639
  route.description
640
  or route.summary
@@ -664,9 +842,11 @@ class FastMCPOpenAPI(FastMCP):
664
  f"Registered TOOL: {tool_name} ({route.method} {route.path}) with tags: {route.tags}"
665
  )
666
 
667
- def _create_openapi_resource(self, route: openapi.HTTPRoute, operation_id: str):
668
  """Creates and registers an OpenAPIResource with enhanced description."""
669
- resource_name = operation_id
 
 
670
  resource_uri = f"resource://openapi/{resource_name}"
671
  base_description = (
672
  route.description or route.summary or f"Represents {route.path}"
@@ -695,9 +875,11 @@ class FastMCPOpenAPI(FastMCP):
695
  f"Registered RESOURCE: {resource_uri} ({route.method} {route.path}) with tags: {route.tags}"
696
  )
697
 
698
- def _create_openapi_template(self, route: openapi.HTTPRoute, operation_id: str):
699
  """Creates and registers an OpenAPIResourceTemplate with enhanced description."""
700
- template_name = operation_id
 
 
701
  path_params = [p.name for p in route.parameters if p.location == "path"]
702
  path_params.sort() # Sort for consistent URIs
703
 
 
5
  import enum
6
  import json
7
  import re
8
+ import warnings
9
  from collections.abc import Callable
10
+ from dataclasses import dataclass, field
11
  from re import Pattern
12
  from typing import TYPE_CHECKING, Any, Literal
13
 
 
34
  HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
35
 
36
 
37
+ class MCPType(enum.Enum):
38
+ """Type of FastMCP component to create from a route.
39
+
40
+ Enum values:
41
+ TOOL: Convert the route to a callable Tool
42
+ RESOURCE: Convert the route to a Resource (typically GET endpoints)
43
+ RESOURCE_TEMPLATE: Convert the route to a ResourceTemplate (typically GET with path params)
44
+ PROMPT: Convert the route to a Prompt (not yet implemented)
45
+ EXCLUDE: Exclude the route from being converted to any MCP component
46
+ IGNORE: Deprecated, use EXCLUDE instead
47
+ """
48
+
49
+ TOOL = "TOOL"
50
+ RESOURCE = "RESOURCE"
51
+ RESOURCE_TEMPLATE = "RESOURCE_TEMPLATE"
52
+ PROMPT = "PROMPT"
53
+ EXCLUDE = "EXCLUDE"
54
+
55
+
56
+ # Keep RouteType as an alias to MCPType for backward compatibility
57
  class RouteType(enum.Enum):
58
+ """
59
+ Deprecated: Use MCPType instead.
60
+
61
+ This enum is kept for backward compatibility and will be removed in a future version.
62
+ """
63
 
64
  TOOL = "TOOL"
65
  RESOURCE = "RESOURCE"
 
72
  class RouteMap:
73
  """Mapping configuration for HTTP routes to FastMCP component types."""
74
 
75
+ methods: list[HttpMethod] | Literal["*"] = field(default="*")
76
+ pattern: Pattern[str] | str = field(default=r".*")
77
+ mcp_type: MCPType | None = field(default=None)
78
+ route_type: RouteType | MCPType | None = field(default=None)
79
+
80
+ def __post_init__(self):
81
+ """Validate and process the route map after initialization."""
82
+ # Handle backward compatibility for route_type, deprecated in 2.5.0
83
+ if self.mcp_type is None and self.route_type is not None:
84
+ warnings.warn(
85
+ "The 'route_type' parameter is deprecated and will be removed in a future version. "
86
+ "Use 'mcp_type' instead with the appropriate MCPType value.",
87
+ DeprecationWarning,
88
+ stacklevel=2,
89
+ )
90
+ if isinstance(self.route_type, RouteType):
91
+ warnings.warn(
92
+ "The RouteType class is deprecated and will be removed in a future version. "
93
+ "Use MCPType instead.",
94
+ DeprecationWarning,
95
+ stacklevel=2,
96
+ )
97
+ # Check for the deprecated IGNORE value
98
+ if self.route_type == RouteType.IGNORE:
99
+ warnings.warn(
100
+ "RouteType.IGNORE is deprecated and will be removed in a future version. "
101
+ "Use MCPType.EXCLUDE instead.",
102
+ DeprecationWarning,
103
+ stacklevel=2,
104
+ )
105
+
106
+ # Convert from RouteType to MCPType if needed
107
+ if isinstance(self.route_type, RouteType):
108
+ route_type_name = self.route_type.name
109
+ if route_type_name == "IGNORE":
110
+ route_type_name = "EXCLUDE"
111
+ self.mcp_type = getattr(MCPType, route_type_name)
112
+ else:
113
+ self.mcp_type = self.route_type
114
+ elif self.mcp_type is None:
115
+ raise ValueError("`mcp_type` must be provided")
116
+
117
+ # Set route_type to match mcp_type for backward compatibility
118
+ if self.route_type is None:
119
+ self.route_type = self.mcp_type
120
+
121
+
122
+ # Common route map pattern functions
123
+ def EXCLUDE_ALL() -> RouteMap:
124
+ """
125
+ Create a RouteMap that excludes all routes that haven't been matched by earlier rules.
126
+
127
+ This is useful as the last route map to exclude any routes that don't match specific patterns.
128
+
129
+ Returns:
130
+ RouteMap: A route map that excludes all routes
131
+ """
132
+ return RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.EXCLUDE)
133
+
134
+
135
+ def ALL_TOOLS() -> RouteMap:
136
+ """
137
+ Create a RouteMap that converts all routes to tools that haven't been matched by earlier rules.
138
+
139
+ This is useful to replace the last item in the default route mappings to make all unmatched routes tools.
140
+
141
+ Returns:
142
+ RouteMap: A route map that converts all routes to tools
143
+ """
144
+ return RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)
145
+
146
+
147
+ def PATTERN_AS_TOOLS(pattern: str) -> RouteMap:
148
+ """
149
+ Create a RouteMap that converts routes matching a specific pattern to tools.
150
+
151
+ Args:
152
+ pattern: Regex pattern to match routes
153
+
154
+ Returns:
155
+ RouteMap: A route map that converts routes matching the pattern to tools
156
+ """
157
+ return RouteMap(methods="*", pattern=pattern, mcp_type=MCPType.TOOL)
158
+
159
+
160
+ def EXCLUDE_PATTERN(pattern: str) -> RouteMap:
161
+ """
162
+ Create a RouteMap that excludes routes matching a specific pattern.
163
+
164
+ Args:
165
+ pattern: Regex pattern to match routes to exclude
166
+
167
+ Returns:
168
+ RouteMap: A route map that excludes routes matching the pattern
169
+ """
170
+ return RouteMap(methods="*", pattern=pattern, mcp_type=MCPType.EXCLUDE)
171
 
172
 
173
  # Default route mappings as a list, where order determines priority
174
  DEFAULT_ROUTE_MAPPINGS = [
175
  # GET requests with path parameters go to ResourceTemplate
176
  RouteMap(
177
+ methods=["GET"], pattern=r".*\{.*\}.*", mcp_type=MCPType.RESOURCE_TEMPLATE
178
  ),
179
  # GET requests without path parameters go to Resource
180
+ RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE),
181
  # All other HTTP methods go to Tool
182
+ ALL_TOOLS(),
 
 
 
 
183
  ]
184
 
185
 
186
  def _determine_route_type(
187
  route: openapi.HTTPRoute,
188
  mappings: list[RouteMap],
189
+ ) -> MCPType:
190
  """
191
  Determines the FastMCP component type based on the route and mappings.
192
 
 
195
  mappings: List of RouteMap objects in priority order
196
 
197
  Returns:
198
+ MCPType for this route
199
  """
200
  # Check mappings in priority order (first match wins)
201
  for route_map in mappings:
 
208
  pattern_matches = re.search(route_map.pattern, route.path)
209
 
210
  if pattern_matches:
211
+ # We know mcp_type is not None here due to post_init validation
212
+ assert route_map.mcp_type is not None
213
  logger.debug(
214
+ f"Route {route.method} {route.path} matched mapping to {route_map.mcp_type.name}"
215
  )
216
+ return route_map.mcp_type
217
 
218
  # Default fallback
219
+ return MCPType.TOOL
 
 
 
 
 
 
 
220
 
221
 
222
  class OpenAPITool(Tool):
 
664
  RouteMap(
665
  methods=["GET", "POST", "PATCH"],
666
  pattern=r".*/users/.*",
667
+ mcp_type=MCPType.RESOURCE_TEMPLATE
668
  ),
669
  # Map all analytics endpoints to Tool
670
  RouteMap(
671
  methods=["GET"],
672
  pattern=r".*/analytics/.*",
673
+ mcp_type=MCPType.TOOL
674
  ),
675
  ]
676
 
 
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()}
714
+
715
  http_routes = openapi.parse_openapi_to_http_routes(openapi_spec)
716
 
717
  # Process routes
 
720
  # Determine route type based on mappings or default rules
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)
728
+ elif route_type == MCPType.RESOURCE:
729
+ self._create_openapi_resource(route, component_name)
730
+ elif route_type == MCPType.RESOURCE_TEMPLATE:
731
+ self._create_openapi_template(route, component_name)
732
+ elif route_type == MCPType.PROMPT:
 
 
 
 
 
733
  # Not implemented yet
734
  logger.warning(
735
  f"PROMPT route type not implemented: {route.method} {route.path}"
736
  )
737
+ elif route_type == MCPType.EXCLUDE:
738
+ logger.info(f"Excluding route: {route.method} {route.path}")
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"]
778
+ ) -> str:
779
+ """
780
+ Ensure the name is unique within its component type by appending numbers if needed.
781
+
782
+ Args:
783
+ name: The proposed name
784
+ component_type: The type of component ("tools", "resources", or "templates")
785
+
786
+ Returns:
787
+ str: A unique name for the component
788
+ """
789
+ # Check if the name is already used
790
+ if name not in self._used_names[component_type]:
791
+ self._used_names[component_type].add(name)
792
+ return name
793
+
794
+ # Find the next available number suffix
795
+ counter = 2
796
+ while f"{name}_{counter}" in self._used_names[component_type]:
797
+ counter += 1
798
+
799
+ # Create the new name
800
+ new_name = f"{name}_{counter}"
801
+ logger.debug(
802
+ f"Name collision detected: '{name}' already exists as a {component_type[:-1]}. "
803
+ f"Using '{new_name}' instead."
804
+ )
805
+
806
+ self._used_names[component_type].add(new_name)
807
+ return new_name
808
+
809
+ def _create_openapi_tool(self, route: openapi.HTTPRoute, name: str):
810
  """Creates and registers an OpenAPITool with enhanced description."""
811
  combined_schema = _combine_schemas(route)
812
+
813
+ # Get a unique tool name
814
+ tool_name = self._get_unique_name(name, "tools")
815
+
816
  base_description = (
817
  route.description
818
  or route.summary
 
842
  f"Registered TOOL: {tool_name} ({route.method} {route.path}) with tags: {route.tags}"
843
  )
844
 
845
+ def _create_openapi_resource(self, route: openapi.HTTPRoute, name: str):
846
  """Creates and registers an OpenAPIResource with enhanced description."""
847
+ # Get a unique resource name
848
+ resource_name = self._get_unique_name(name, "resources")
849
+
850
  resource_uri = f"resource://openapi/{resource_name}"
851
  base_description = (
852
  route.description or route.summary or f"Represents {route.path}"
 
875
  f"Registered RESOURCE: {resource_uri} ({route.method} {route.path}) with tags: {route.tags}"
876
  )
877
 
878
+ def _create_openapi_template(self, route: openapi.HTTPRoute, name: str):
879
  """Creates and registers an OpenAPIResourceTemplate with enhanced description."""
880
+ # Get a unique template name
881
+ template_name = self._get_unique_name(name, "templates")
882
+
883
  path_params = [p.name for p in route.parameters if p.location == "path"]
884
  path_params.sort() # Sort for consistent URIs
885
 
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/deprecated/test_route_type_ignore.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the deprecated RouteType.IGNORE."""
2
+
3
+ import warnings
4
+
5
+ import httpx
6
+ import pytest
7
+
8
+ from fastmcp.server.openapi import (
9
+ FastMCPOpenAPI,
10
+ MCPType,
11
+ RouteMap,
12
+ RouteType,
13
+ )
14
+
15
+
16
+ def test_route_type_ignore_deprecation_warning():
17
+ """Test that using RouteType.IGNORE emits a deprecation warning."""
18
+ # Let's manually capture the warnings
19
+
20
+ # Record all warnings
21
+ with warnings.catch_warnings(record=True) as recorded:
22
+ # Make sure warnings are always triggered
23
+ warnings.simplefilter("always")
24
+
25
+ # Create a RouteMap with RouteType.IGNORE
26
+ route_map = RouteMap(
27
+ methods=["GET"], pattern=r"^/analytics$", route_type=RouteType.IGNORE
28
+ )
29
+
30
+ # Check for the expected warnings in the recorded warnings
31
+ route_type_warning = False
32
+ ignore_warning = False
33
+
34
+ for w in recorded:
35
+ if issubclass(w.category, DeprecationWarning):
36
+ message = str(w.message)
37
+ if "route_type' parameter is deprecated" in message:
38
+ route_type_warning = True
39
+ if "RouteType.IGNORE is deprecated" in message:
40
+ ignore_warning = True
41
+
42
+ # Make sure both warnings were triggered
43
+ assert route_type_warning, "Missing 'route_type' deprecation warning"
44
+ assert ignore_warning, "Missing 'RouteType.IGNORE' deprecation warning"
45
+
46
+ # Verify that RouteType.IGNORE was converted to MCPType.EXCLUDE
47
+ assert route_map.mcp_type == MCPType.EXCLUDE
48
+
49
+
50
+ class TestRouteTypeIgnoreDeprecation:
51
+ """Test class for the deprecated RouteType.IGNORE."""
52
+
53
+ @pytest.fixture
54
+ def basic_openapi_spec(self) -> dict:
55
+ """Create a simple OpenAPI spec for testing."""
56
+ return {
57
+ "openapi": "3.0.0",
58
+ "info": {"title": "Test API", "version": "1.0.0"},
59
+ "paths": {
60
+ "/items": {
61
+ "get": {
62
+ "operationId": "get_items",
63
+ "summary": "Get all items",
64
+ "responses": {"200": {"description": "Success"}},
65
+ }
66
+ },
67
+ "/analytics": {
68
+ "get": {
69
+ "operationId": "get_analytics",
70
+ "summary": "Get analytics data",
71
+ "responses": {"200": {"description": "Success"}},
72
+ }
73
+ },
74
+ },
75
+ }
76
+
77
+ @pytest.fixture
78
+ async def mock_client(self) -> httpx.AsyncClient:
79
+ """Create a mock client for testing."""
80
+
81
+ async def _responder(request):
82
+ return httpx.Response(200, json={"success": True})
83
+
84
+ return httpx.AsyncClient(transport=httpx.MockTransport(_responder))
85
+
86
+ async def test_route_type_ignore_conversion(self, basic_openapi_spec, mock_client):
87
+ """Test that routes with RouteType.IGNORE are properly excluded."""
88
+ # Capture the deprecation warning without checking the exact message
89
+ with pytest.warns(DeprecationWarning):
90
+ server = FastMCPOpenAPI(
91
+ openapi_spec=basic_openapi_spec,
92
+ client=mock_client,
93
+ route_maps=[
94
+ # Use the deprecated RouteType.IGNORE
95
+ RouteMap(
96
+ methods=["GET"],
97
+ pattern=r"^/analytics$",
98
+ route_type=RouteType.IGNORE,
99
+ ),
100
+ # Make everything else a resource
101
+ RouteMap(
102
+ methods=["GET"], pattern=r".*", route_type=RouteType.RESOURCE
103
+ ),
104
+ ],
105
+ )
106
+
107
+ # Check that the analytics route was excluded (converted from IGNORE to EXCLUDE)
108
+ resources = await server.get_resources()
109
+ resource_uris = [str(r.uri) for r in resources.values()]
110
+
111
+ # Analytics should be excluded
112
+ assert "resource://openapi/get_items" in resource_uris
113
+ assert "resource://openapi/get_analytics" not in resource_uris
tests/server/{test_openapi.py → openapi/test_openapi.py} RENAMED
@@ -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,92 +2059,169 @@ 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
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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:
2163
+ @pytest.fixture
2164
+ def basic_openapi_spec(self) -> dict:
2165
+ return {
2166
+ "openapi": "3.0.0",
2167
+ "info": {"title": "Test API", "version": "1.0.0"},
2168
+ "paths": {
2169
+ "/items": {
2170
+ "get": {
2171
+ "operationId": "get_items",
2172
+ "summary": "Get all items",
2173
+ "responses": {"200": {"description": "Success"}},
2174
+ }
2175
+ },
2176
+ "/users": {
2177
+ "get": {
2178
+ "operationId": "get_users",
2179
+ "summary": "Get all users",
2180
+ "responses": {"200": {"description": "Success"}},
2181
+ }
2182
+ },
2183
+ "/analytics": {
2184
+ "get": {
2185
+ "operationId": "get_analytics",
2186
+ "summary": "Get analytics data",
2187
+ "responses": {"200": {"description": "Success"}},
2188
+ }
2189
+ },
2190
+ },
2191
+ }
2192
+
2193
+ @pytest.fixture
2194
+ async def mock_client(self) -> httpx.AsyncClient:
2195
+ async def _responder(request):
2196
+ return httpx.Response(200, json={"success": True})
2197
+
2198
+ return httpx.AsyncClient(transport=httpx.MockTransport(_responder))
2199
+
2200
+ async def test_exclude_routes(self, basic_openapi_spec, mock_client):
2201
+ # Create a server with custom mappings that exclude specific routes
2202
+ server = FastMCPOpenAPI(
2203
+ openapi_spec=basic_openapi_spec,
2204
+ client=mock_client,
2205
+ route_maps=[
2206
+ # Exclude analytics endpoints
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
+
2217
+ # Check that resources were created for non-excluded routes
2218
+ resources = await server.get_resources()
2219
+ resource_uris = [str(r.uri) for r in resources.values()]
2220
+
2221
+ # The /analytics endpoint should be excluded
2222
+ assert "resource://openapi/get_items" in resource_uris
2223
+ assert "resource://openapi/get_users" in resource_uris
2224
+ assert "resource://openapi/get_analytics" not in resource_uris
2225
+
2226
+ # Should only have 2 resources (analytics is excluded)
2227
+ assert len(resources) == 2
tests/server/{test_openapi_path_parameters.py → openapi/test_openapi_path_parameters.py} RENAMED
@@ -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 ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the route map shortcut functions."""
2
+
3
+ import httpx
4
+ import pytest
5
+
6
+ from fastmcp.server.openapi import (
7
+ ALL_TOOLS,
8
+ EXCLUDE_ALL,
9
+ EXCLUDE_PATTERN,
10
+ PATTERN_AS_TOOLS,
11
+ FastMCPOpenAPI,
12
+ MCPType,
13
+ RouteMap,
14
+ )
15
+
16
+
17
+ class TestRouteMapShortcuts:
18
+ """Tests for the route map shortcut functions."""
19
+
20
+ def test_functions_return_correct_route_maps(self):
21
+ """Test that each shortcut function returns a RouteMap with the expected properties."""
22
+ # Test EXCLUDE_ALL
23
+ exclude_all = EXCLUDE_ALL()
24
+ assert isinstance(exclude_all, RouteMap)
25
+ assert exclude_all.methods == "*"
26
+ assert exclude_all.pattern == ".*"
27
+ assert exclude_all.mcp_type == MCPType.EXCLUDE
28
+
29
+ # Test ALL_TOOLS
30
+ all_tools = ALL_TOOLS()
31
+ assert isinstance(all_tools, RouteMap)
32
+ assert all_tools.methods == "*"
33
+ assert all_tools.pattern == ".*"
34
+ assert all_tools.mcp_type == MCPType.TOOL
35
+
36
+ # Test PATTERN_AS_TOOLS
37
+ pattern = r"^/api/.*"
38
+ pattern_as_tools = PATTERN_AS_TOOLS(pattern)
39
+ assert isinstance(pattern_as_tools, RouteMap)
40
+ assert pattern_as_tools.methods == "*"
41
+ assert pattern_as_tools.pattern == pattern
42
+ assert pattern_as_tools.mcp_type == MCPType.TOOL
43
+
44
+ # Test EXCLUDE_PATTERN
45
+ pattern = r"^/admin/.*"
46
+ exclude_pattern = EXCLUDE_PATTERN(pattern)
47
+ assert isinstance(exclude_pattern, RouteMap)
48
+ assert exclude_pattern.methods == "*"
49
+ assert exclude_pattern.pattern == pattern
50
+ assert exclude_pattern.mcp_type == MCPType.EXCLUDE
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(
59
+ methods=["GET"], pattern=r".*", route_type=RouteType.TOOL
60
+ )
61
+ assert route_map.mcp_type == MCPType.TOOL
62
+
63
+ # Test accessing fields on RouteType directly
64
+ # Note: importing RouteType already causes the deprecation warning,
65
+ # so we don't need to check for it again here
66
+ rt = RouteType.RESOURCE
67
+ assert rt.value == "RESOURCE"
68
+ assert rt.name == "RESOURCE"
69
+
70
+
71
+ class TestRouteMapShortcutsIntegration:
72
+ """Integration tests for the route map shortcut functions with FastMCPOpenAPI."""
73
+
74
+ @pytest.fixture
75
+ def basic_openapi_spec(self) -> dict:
76
+ """Create a simple OpenAPI spec for testing."""
77
+ return {
78
+ "openapi": "3.0.0",
79
+ "info": {"title": "Test API", "version": "1.0.0"},
80
+ "paths": {
81
+ "/items": {
82
+ "get": {
83
+ "operationId": "get_items",
84
+ "summary": "Get all items",
85
+ "responses": {"200": {"description": "Success"}},
86
+ },
87
+ "post": {
88
+ "operationId": "create_item",
89
+ "summary": "Create an item",
90
+ "responses": {"201": {"description": "Created"}},
91
+ },
92
+ },
93
+ "/users": {
94
+ "get": {
95
+ "operationId": "get_users",
96
+ "summary": "Get all users",
97
+ "responses": {"200": {"description": "Success"}},
98
+ },
99
+ },
100
+ "/admin": {
101
+ "get": {
102
+ "operationId": "get_admin",
103
+ "summary": "Admin endpoint",
104
+ "responses": {"200": {"description": "Success"}},
105
+ },
106
+ },
107
+ "/items/{item_id}": {
108
+ "get": {
109
+ "operationId": "get_item",
110
+ "summary": "Get an item by ID",
111
+ "parameters": [
112
+ {
113
+ "name": "item_id",
114
+ "in": "path",
115
+ "required": True,
116
+ "schema": {"type": "string"},
117
+ }
118
+ ],
119
+ "responses": {"200": {"description": "Success"}},
120
+ },
121
+ },
122
+ },
123
+ }
124
+
125
+ @pytest.fixture
126
+ async def mock_client(self) -> httpx.AsyncClient:
127
+ """Create a mock client for testing."""
128
+
129
+ async def _responder(request):
130
+ return httpx.Response(200, json={"success": True})
131
+
132
+ return httpx.AsyncClient(transport=httpx.MockTransport(_responder))
133
+
134
+ async def test_all_tools(self, basic_openapi_spec, mock_client):
135
+ """Test using ALL_TOOLS() to convert all routes to tools."""
136
+ server = FastMCPOpenAPI(
137
+ openapi_spec=basic_openapi_spec,
138
+ client=mock_client,
139
+ route_maps=[ALL_TOOLS()],
140
+ )
141
+
142
+ # Check that all routes are tools
143
+ tools = await server.get_tools()
144
+ resources = await server.get_resources()
145
+ templates = await server.get_resource_templates()
146
+
147
+ # All 5 routes should be tools
148
+ assert len(tools) == 5
149
+ assert len(resources) == 0
150
+ assert len(templates) == 0
151
+
152
+ # Check that all expected tools exist
153
+ tool_names = [t.name for t in tools.values()]
154
+ assert "get_items" in tool_names
155
+ assert "create_item" in tool_names
156
+ assert "get_users" in tool_names
157
+ assert "get_admin" in tool_names
158
+ assert "get_item" in tool_names
159
+
160
+ async def test_exclude_pattern(self, basic_openapi_spec, mock_client):
161
+ """Test using EXCLUDE_PATTERN() to exclude specific routes."""
162
+ server = FastMCPOpenAPI(
163
+ openapi_spec=basic_openapi_spec,
164
+ client=mock_client,
165
+ route_maps=[
166
+ # Exclude admin endpoints
167
+ EXCLUDE_PATTERN(r"^/admin"),
168
+ # Make everything else a tool
169
+ ALL_TOOLS(),
170
+ ],
171
+ )
172
+
173
+ # Check that admin route is excluded
174
+ tools = await server.get_tools()
175
+ tool_names = [t.name for t in tools.values()]
176
+
177
+ # All routes except admin should be tools
178
+ assert "get_items" in tool_names
179
+ assert "create_item" in tool_names
180
+ assert "get_users" in tool_names
181
+ assert "get_item" in tool_names
182
+ assert "get_admin" not in tool_names # This should be excluded
183
+
184
+ async def test_pattern_as_tools(self, basic_openapi_spec, mock_client):
185
+ """Test using PATTERN_AS_TOOLS() to convert routes matching a pattern to tools."""
186
+ server = FastMCPOpenAPI(
187
+ openapi_spec=basic_openapi_spec,
188
+ client=mock_client,
189
+ route_maps=[
190
+ # Make /items routes tools regardless of method
191
+ PATTERN_AS_TOOLS(r"^/items"),
192
+ # Make everything else a resource
193
+ RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.RESOURCE),
194
+ ],
195
+ )
196
+
197
+ # Check that /items routes are tools
198
+ tools = await server.get_tools()
199
+ tool_names = [t.name for t in tools.values()]
200
+ assert "get_items" in tool_names
201
+ assert "create_item" in tool_names
202
+ assert "get_item" in tool_names
203
+
204
+ # Check that other routes are resources
205
+ resources = await server.get_resources()
206
+ resource_names = [r.name for r in resources.values()]
207
+ assert "get_users" in resource_names
208
+ assert "get_admin" in resource_names