Jeremiah Lowin commited on
Commit
d625dbe
·
1 Parent(s): c997d33

Add support for RouteMap tags, update docs

Browse files
docs/docs.json CHANGED
@@ -50,6 +50,7 @@
50
  "servers/resources",
51
  "servers/prompts",
52
  "servers/context",
 
53
  "servers/proxy",
54
  "servers/composition"
55
  ]
@@ -76,8 +77,6 @@
76
  "pages": [
77
  "patterns/decorating-methods",
78
  "patterns/http-requests",
79
- "patterns/openapi",
80
- "patterns/fastapi",
81
  "patterns/contrib",
82
  "patterns/testing"
83
  ]
 
50
  "servers/resources",
51
  "servers/prompts",
52
  "servers/context",
53
+ "servers/openapi",
54
  "servers/proxy",
55
  "servers/composition"
56
  ]
 
77
  "pages": [
78
  "patterns/decorating-methods",
79
  "patterns/http-requests",
 
 
80
  "patterns/contrib",
81
  "patterns/testing"
82
  ]
docs/patterns/fastapi.mdx CHANGED
@@ -8,19 +8,18 @@ import { VersionBadge } from '/snippets/version-badge.mdx'
8
 
9
  <VersionBadge version="2.0.0" />
10
 
 
 
 
11
 
12
- FastMCP can automatically convert FastAPI applications into MCP servers.
13
-
14
- <Tip>
15
- FastMCP does *not* include FastAPI as a dependency; you must install it separately to run these examples.
16
- </Tip>
17
 
 
18
 
19
- ```python {2, 22, 25}
20
  from fastapi import FastAPI
21
  from fastmcp import FastMCP
22
 
23
-
24
  # A FastAPI app
25
  app = FastAPI()
26
 
@@ -36,7 +35,6 @@ def get_item(item_id: int):
36
  def create_item(name: str):
37
  return {"id": 3, "name": name}
38
 
39
-
40
  # Create an MCP server from your FastAPI app
41
  mcp = FastMCP.from_fastapi(app=app)
42
 
@@ -44,101 +42,6 @@ if __name__ == "__main__":
44
  mcp.run() # Start the MCP server
45
  ```
46
 
47
- ## Configuration Options
48
-
49
- ### Timeout
50
-
51
- You can set a timeout for all API requests:
52
-
53
- ```python
54
- # Set a 5 second timeout for all requests
55
- mcp = FastMCP.from_fastapi(app=app, timeout=5.0)
56
- ```
57
-
58
- This timeout is applied to all requests made by tools, resources, and resource templates.
59
-
60
- ## Route Mapping
61
-
62
- By default, FastMCP will map FastAPI routes to MCP components according to the following rules:
63
-
64
- | FastAPI Route Type | FastAPI Example | MCP Component | Notes |
65
- |--------------------|--------------|---------|-------|
66
- | GET without path params | `@app.get("/stats")` | Resource | Simple resources for fetching data |
67
- | GET with path params | `@app.get("/users/{id}")` | Resource Template | Path parameters become template parameters |
68
- | POST, PUT, DELETE, etc. | `@app.post("/users")` | Tool | Operations that modify data |
69
-
70
- For more details on route mapping or custom mapping rules, see the [OpenAPI integration documentation](/patterns/openapi#route-mapping); FastMCP uses the same mapping rules for both FastAPI and OpenAPI integrations.
71
-
72
- ## Complete Example
73
-
74
- Here's a more detailed example with a data model:
75
-
76
- ```python [expandable]
77
- import asyncio
78
- from fastapi import FastAPI, HTTPException
79
- from pydantic import BaseModel
80
- from fastmcp import FastMCP, Client
81
-
82
- # Define your Pydantic model
83
- class Item(BaseModel):
84
- name: str
85
- price: float
86
-
87
- # Create your FastAPI app
88
- app = FastAPI()
89
- items = {} # In-memory database
90
-
91
- @app.get("/items")
92
- def list_items():
93
- """List all items"""
94
- return list(items.values())
95
-
96
- @app.get("/items/{item_id}")
97
- def get_item(item_id: int):
98
- """Get item by ID"""
99
- if item_id not in items:
100
- raise HTTPException(404, "Item not found")
101
- return items[item_id]
102
-
103
- @app.post("/items")
104
- def create_item(item: Item):
105
- """Create a new item"""
106
- item_id = len(items) + 1
107
- items[item_id] = {"id": item_id, **item.model_dump()}
108
- return items[item_id]
109
-
110
- # Test your MCP server with a client
111
- async def check_mcp(mcp: FastMCP):
112
- # List the components that were created
113
- tools = await mcp.get_tools()
114
- resources = await mcp.get_resources()
115
- templates = await mcp.get_resource_templates()
116
-
117
- print(
118
- f"{len(tools)} Tool(s): {', '.join([t.name for t in tools.values()])}"
119
- )
120
- print(
121
- f"{len(resources)} Resource(s): {', '.join([r.name for r in resources.values()])}"
122
- )
123
- print(
124
- f"{len(templates)} Resource Template(s): {', '.join([t.name for t in templates.values()])}"
125
- )
126
-
127
- return mcp
128
-
129
- if __name__ == "__main__":
130
- # Create MCP server from FastAPI app
131
- mcp = FastMCP.from_fastapi(app=app)
132
-
133
- asyncio.run(check_mcp(mcp))
134
-
135
- # In a real scenario, you would run the server:
136
- mcp.run()
137
- ```
138
-
139
- ## Benefits
140
-
141
- - **Leverage existing FastAPI apps** - No need to rewrite your API logic
142
- - **Schema reuse** - FastAPI's Pydantic models and validation are inherited
143
- - **Full feature support** - Works with FastAPI's authentication, dependencies, etc.
144
- - **ASGI transport** - Direct communication without additional HTTP overhead
 
8
 
9
  <VersionBadge version="2.0.0" />
10
 
11
+ <Note>
12
+ **Documentation Moved**: The comprehensive FastAPI integration documentation has been moved to the [OpenAPI Integration](/patterns/openapi#fastapi-integration) page, where it's covered alongside all other OpenAPI features including route mapping and tags support.
13
+ </Note>
14
 
15
+ ## Quick Start
 
 
 
 
16
 
17
+ FastMCP can automatically convert FastAPI applications into MCP servers:
18
 
19
+ ```python
20
  from fastapi import FastAPI
21
  from fastmcp import FastMCP
22
 
 
23
  # A FastAPI app
24
  app = FastAPI()
25
 
 
35
  def create_item(name: str):
36
  return {"id": 3, "name": name}
37
 
 
38
  # Create an MCP server from your FastAPI app
39
  mcp = FastMCP.from_fastapi(app=app)
40
 
 
42
  mcp.run() # Start the MCP server
43
  ```
44
 
45
+ <Tip>
46
+ For complete documentation including tag-based routing, route mapping configuration, timeout settings, authentication examples, and advanced configuration options, see the comprehensive [OpenAPI Integration documentation](/patterns/openapi#fastapi-integration).
47
+ </Tip>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/{patterns → servers}/openapi.mdx RENAMED
@@ -1,6 +1,6 @@
1
  ---
2
  title: OpenAPI Integration
3
- sidebarTitle: OpenAPI
4
  description: Generate MCP servers from OpenAPI specs
5
  icon: code-branch
6
  ---
@@ -8,7 +8,7 @@ import { VersionBadge } from '/snippets/version-badge.mdx'
8
 
9
  <VersionBadge version="2.0.0" />
10
 
11
- FastMCP can automatically generate an MCP server from an OpenAPI specification. Users only need to provide an OpenAPI specification (3.0 or 3.1) and an API client.
12
 
13
  ```python
14
  import httpx
@@ -27,245 +27,103 @@ if __name__ == "__main__":
27
  mcp.run()
28
  ```
29
 
30
- ## Configuration Options
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 |
51
- |- | - | - | - |
52
- | `GET` without path params | `GET /stats` | Resource | Simple resources for fetching data |
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
59
- # Simplified version of the actual mapping rules
 
 
60
  DEFAULT_ROUTE_MAPPINGS = [
61
  # GET with path parameters -> ResourceTemplate
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
 
257
- 1. FastMCP parses your OpenAPI spec to extract routes and schemas
258
- 2. It applies mapping rules to categorize each route
259
- 3. When an MCP client calls a tool or accesses a resource:
260
- - FastMCP constructs an HTTP request based on the OpenAPI definition
261
- - It sends the request through the provided httpx client
262
- - It translates the HTTP response to the appropriate MCP format
263
 
264
- ### Request Parameter Handling
265
 
266
  FastMCP carefully handles different types of parameters in OpenAPI requests:
267
 
268
- #### Query Parameters
269
 
270
  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.
271
 
@@ -281,7 +139,7 @@ await client.call_tool("search_products", {
281
 
282
  The resulting HTTP request will only include `category=electronics&min_price=100`.
283
 
284
- #### Path Parameters
285
 
286
  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.
287
 
@@ -293,9 +151,9 @@ await client.call_tool("get_product", {"product_id": 123})
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
@@ -310,3 +168,83 @@ api_client = httpx.AsyncClient(
310
  # Create an MCP server from your OpenAPI spec
311
  mcp = FastMCP.from_openapi(openapi_spec=spec, client=api_client)
312
  ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: OpenAPI Integration
3
+ sidebarTitle: OpenAPI Integration
4
  description: Generate MCP servers from OpenAPI specs
5
  icon: code-branch
6
  ---
 
8
 
9
  <VersionBadge version="2.0.0" />
10
 
11
+ FastMCP can automatically generate an MCP server from an OpenAPI specification or FastAPI app. Users only need to provide an OpenAPI specification (3.0 or 3.1) and an API client, or their FastAPI app.
12
 
13
  ```python
14
  import httpx
 
27
  mcp.run()
28
  ```
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  ## Route Mapping
31
 
32
  <VersionBadge version="2.5.0" />
33
 
34
  By default, OpenAPI routes are mapped to MCP components based on these rules:
35
 
36
+ | OpenAPI Route | Example |MCP Component |
37
+ | - | - | - |
38
+ | `GET` with path params | `GET /users/{id}` | Resource Template |
39
+ | `GET` without path params | `GET /stats` | Resource |
40
+ | `POST`, `PUT`, `PATCH`, `DELETE`, etc. | `POST /users` | Tool |
41
+
42
+
43
+ Internally, FastMCP uses a priority-ordered list of `RouteMap` objects to determine the component type for each route. Each `RouteMap` specifies:
44
 
45
+ - **Methods**: HTTP methods to match (e.g. `["GET", "POST"]` or `"*"` for all)
46
+ - **Pattern**: Regex pattern to match the route path (e.g. `r"^/users/.*"` or `r".*"` for all)
47
+ - **Tags**: A set of OpenAPI tags that must all be present (`{}` means all tags)
48
+ - **MCP type**: What MCP component type to create (the options are `TOOL`, `RESOURCE`, `RESOURCE_TEMPLATE`, `PROMPT`, or `EXCLUDE` to exclude the route from the MCP server)
49
+
50
+ Each OpenAPI route is matched against `RouteMap` objects in order, and the **first match wins** to determine the MCP component type. For example, here are the default route mappings, expressed as `RouteMap` objects in priority order:
51
 
52
  ```python
53
+ from fastmcp.server.openapi import RouteMap, MCPType
54
+
55
+ # Default route mappings
56
  DEFAULT_ROUTE_MAPPINGS = [
57
  # GET with path parameters -> ResourceTemplate
58
  RouteMap(
59
  methods=["GET"],
60
  pattern=r".*\{.*\}.*",
61
+ tags={},
62
+ mcp_type=MCPType.RESOURCE_TEMPLATE
63
  ),
 
64
  # GET without path parameters -> Resource
65
  RouteMap(
66
  methods=["GET"],
67
  pattern=r".*",
68
+ tags={},
69
+ mcp_type=MCPType.RESOURCE
70
  ),
 
71
  # All other methods -> Tool
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  RouteMap(
73
  methods="*",
74
+ pattern=r".*",
75
+ tags={},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  mcp_type=MCPType.TOOL
77
  ),
 
 
 
 
 
 
78
  ]
 
 
 
 
 
 
 
79
  ```
80
 
81
+ ### Custom Route Maps
 
82
 
83
+ You can override the default behavior by providing custom route maps when creating your MCP server. Custom maps are processed **before** the default maps, so they take priority. Each OpenAPI route will be matched against your custom route maps in order, and the first match will determine the MCP component type (or exclusion!).
84
 
85
+ ```python {1, 6-18}
86
+ from fastmcp.server.openapi import RouteMap, MCPType
 
 
 
 
 
87
 
 
88
  mcp = FastMCP.from_openapi(
89
  openapi_spec=spec,
90
  client=api_client,
91
  route_maps=[
92
+ # All GET analytics endpoints should be tools
93
+ RouteMap(
94
+ methods=["GET"],
95
+ pattern=r"^/analytics/.*",
96
+ mcp_type=MCPType.TOOL,
97
+ ),
98
+ # Exclude all admin endpoints
99
+ RouteMap(
100
+ pattern=r"^/admin/.*",
101
+ mcp_type=MCPType.EXCLUDE,
102
+ )
103
  ]
104
  )
105
  ```
106
 
107
+ ### Treat All Routes as Tools
108
 
109
+ To treat all routes as tools, use `RouteMap(mcp_type=MCPType.TOOL)` as your only route map. It will match all routes and create a tool for each.
 
 
 
 
 
110
 
111
+ ### Prevent Default Mappings
112
 
113
+ To prevent the default mappings from being applied, add a catch-all exclusion routemap at the end of your custom route maps: `RouteMap(mcp_type=MCPType.EXCLUDE)`. Since it will match all routes, it will exclude any that weren't match by your previous rules and short-circuit the default mappings.
 
 
114
 
115
+ ### Tag-Based Routing
 
116
 
117
+ <VersionBadge version="2.5.0" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
+ To filter routes by OpenAPI tags, use `RouteMap(tags={...})`. The route must have ALL of the specified tags to be matched. If no tags are specified, all routes will be matched.
120
 
 
 
 
 
 
 
121
 
122
+ ## Request Parameter Handling
123
 
124
  FastMCP carefully handles different types of parameters in OpenAPI requests:
125
 
126
+ ### Query Parameters
127
 
128
  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.
129
 
 
139
 
140
  The resulting HTTP request will only include `category=electronics&min_price=100`.
141
 
142
+ ### Path Parameters
143
 
144
  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.
145
 
 
151
  await client.call_tool("get_product", {"product_id": None})
152
  ```
153
 
154
+ ## Authorization
155
 
156
+ If your API requires authentication, set headers on the client before creating the MCP server.
157
 
158
  ```python
159
  import httpx
 
168
  # Create an MCP server from your OpenAPI spec
169
  mcp = FastMCP.from_openapi(openapi_spec=spec, client=api_client)
170
  ```
171
+
172
+ ## Timeouts
173
+
174
+ You can set a timeout for all requests by providing a `timeout` parameter (in seconds):
175
+
176
+ ```python
177
+ mcp = FastMCP.from_openapi(
178
+ openapi_spec=spec,
179
+ client=api_client,
180
+ timeout=30.0 # 30 second timeout
181
+ )
182
+ ```
183
+
184
+ ## FastAPI Integration
185
+
186
+ <VersionBadge version="2.0.0" />
187
+
188
+ FastMCP can automatically convert FastAPI applications into MCP servers by extracting their OpenAPI specifications. A special client will be created that uses an in-memory ASGI transport to avoid network calls to your FastAPI app. Note that the resulting MCP server is *not* a FastAPI app itself, but can be added to one (see [ASGI integration](/deployment/asgi)).
189
+
190
+ <Tip>
191
+ FastMCP does *not* include FastAPI as a dependency; you must install it separately to use this integration.
192
+ </Tip>
193
+
194
+ ```python
195
+ from fastapi import FastAPI
196
+ from fastmcp import FastMCP
197
+
198
+ # A FastAPI app
199
+ app = FastAPI()
200
+
201
+ @app.get("/items", tags=["items"])
202
+ def list_items():
203
+ return [{"id": 1, "name": "Item 1"}, {"id": 2, "name": "Item 2"}]
204
+
205
+ @app.get("/items/{item_id}", tags=["items", "detail"])
206
+ def get_item(item_id: int):
207
+ return {"id": item_id, "name": f"Item {item_id}"}
208
+
209
+ @app.post("/items", tags=["items", "create"])
210
+ def create_item(name: str):
211
+ return {"id": 3, "name": name}
212
+
213
+ # Create an MCP server from your FastAPI app
214
+ mcp = FastMCP.from_fastapi(app=app)
215
+
216
+ if __name__ == "__main__":
217
+ mcp.run() # Start the MCP server
218
+ ```
219
+
220
+ ### Configuration Options
221
+
222
+ **Timeout**: You can set a timeout for all API requests:
223
+
224
+ ```python
225
+ # Set a 5 second timeout for all requests
226
+ mcp = FastMCP.from_fastapi(app=app, timeout=5.0)
227
+ ```
228
+
229
+ **Route Mapping**: All the route mapping features (including tags) work with FastAPI apps:
230
+
231
+ ```python
232
+ from fastmcp.server.openapi import RouteMap, MCPType
233
+
234
+ # Use tag-based routing with FastAPI
235
+ mcp = FastMCP.from_fastapi(
236
+ app=app,
237
+ route_maps=[
238
+ RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL, tags={"admin"}),
239
+ RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.EXCLUDE, tags={"internal"}),
240
+ ]
241
+ )
242
+ ```
243
+
244
+ ### Benefits
245
+
246
+ - **Leverage existing FastAPI apps** - No need to rewrite your API logic
247
+ - **Schema reuse** - FastAPI's Pydantic models and validation are inherited
248
+ - **Full feature support** - Works with FastAPI's authentication, dependencies, etc.
249
+ - **ASGI transport** - Direct communication without additional HTTP overhead
250
+
examples/tags_example.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Example demonstrating RouteMap tags functionality.
3
+
4
+ This example shows how to use the tags parameter in RouteMap
5
+ to selectively route OpenAPI endpoints based on their tags.
6
+ """
7
+
8
+ import asyncio
9
+
10
+ from fastapi import FastAPI
11
+
12
+ from fastmcp import FastMCP
13
+ from fastmcp.server.openapi import MCPType, RouteMap
14
+
15
+ # Create a FastAPI app with tagged endpoints
16
+ app = FastAPI(title="Tagged API Example")
17
+
18
+
19
+ @app.get("/users", tags=["users", "public"])
20
+ async def get_users():
21
+ """Get all users - public endpoint"""
22
+ return [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
23
+
24
+
25
+ @app.post("/users", tags=["users", "admin"])
26
+ async def create_user(name: str):
27
+ """Create a user - admin only"""
28
+ return {"id": 3, "name": name}
29
+
30
+
31
+ @app.get("/admin/stats", tags=["admin", "internal"])
32
+ async def get_admin_stats():
33
+ """Get admin statistics - internal use"""
34
+ return {"total_users": 100, "active_sessions": 25}
35
+
36
+
37
+ @app.get("/health", tags=["public"])
38
+ async def health_check():
39
+ """Public health check"""
40
+ return {"status": "healthy"}
41
+
42
+
43
+ @app.get("/metrics")
44
+ async def get_metrics():
45
+ """Metrics endpoint with no tags"""
46
+ return {"requests": 1000, "errors": 5}
47
+
48
+
49
+ async def main():
50
+ """Demonstrate different tag-based routing strategies."""
51
+
52
+ print("=== Example 1: Make admin-tagged routes tools ===")
53
+
54
+ # Strategy 1: Convert admin-tagged routes to tools
55
+ mcp1 = FastMCP.from_fastapi(
56
+ app=app,
57
+ route_maps=[
58
+ RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL, tags={"admin"}),
59
+ RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE),
60
+ ],
61
+ )
62
+
63
+ tools = await mcp1.get_tools()
64
+ resources = await mcp1.get_resources()
65
+
66
+ print(f"Tools ({len(tools)}): {', '.join(tools.keys())}")
67
+ print(f"Resources ({len(resources)}): {', '.join(resources.keys())}")
68
+
69
+ print("\n=== Example 2: Exclude internal routes ===")
70
+
71
+ # Strategy 2: Exclude internal routes entirely
72
+ mcp2 = FastMCP.from_fastapi(
73
+ app=app,
74
+ route_maps=[
75
+ RouteMap(
76
+ methods="*", pattern=r".*", mcp_type=MCPType.EXCLUDE, tags={"internal"}
77
+ ),
78
+ RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE),
79
+ RouteMap(methods=["POST"], pattern=r".*", mcp_type=MCPType.TOOL),
80
+ ],
81
+ )
82
+
83
+ tools = await mcp2.get_tools()
84
+ resources = await mcp2.get_resources()
85
+
86
+ print(f"Tools ({len(tools)}): {', '.join(tools.keys())}")
87
+ print(f"Resources ({len(resources)}): {', '.join(resources.keys())}")
88
+
89
+ print("\n=== Example 3: Pattern + Tags combination ===")
90
+
91
+ # Strategy 3: Routes matching both pattern AND tags
92
+ mcp3 = FastMCP.from_fastapi(
93
+ app=app,
94
+ route_maps=[
95
+ # Admin routes under /admin path -> tools
96
+ RouteMap(
97
+ methods="*",
98
+ pattern=r".*/admin/.*",
99
+ mcp_type=MCPType.TOOL,
100
+ tags={"admin"},
101
+ ),
102
+ # Public routes -> tools
103
+ RouteMap(
104
+ methods="*", pattern=r".*", mcp_type=MCPType.TOOL, tags={"public"}
105
+ ),
106
+ RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE),
107
+ ],
108
+ )
109
+
110
+ tools = await mcp3.get_tools()
111
+ resources = await mcp3.get_resources()
112
+
113
+ print(f"Tools ({len(tools)}): {', '.join(tools.keys())}")
114
+ print(f"Resources ({len(resources)}): {', '.join(resources.keys())}")
115
+
116
+ print("\n=== Example 4: Multiple tag AND condition ===")
117
+
118
+ # Strategy 4: Routes must have ALL specified tags
119
+ mcp4 = FastMCP.from_fastapi(
120
+ app=app,
121
+ route_maps=[
122
+ # Routes with BOTH "users" AND "admin" tags -> tools
123
+ RouteMap(
124
+ methods="*",
125
+ pattern=r".*",
126
+ mcp_type=MCPType.TOOL,
127
+ tags={"users", "admin"},
128
+ ),
129
+ RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE),
130
+ ],
131
+ )
132
+
133
+ tools = await mcp4.get_tools()
134
+ resources = await mcp4.get_resources()
135
+
136
+ print(f"Tools ({len(tools)}): {', '.join(tools.keys())}")
137
+ print(f"Resources ({len(resources)}): {', '.join(resources.keys())}")
138
+
139
+
140
+ if __name__ == "__main__":
141
+ asyncio.run(main())
src/fastmcp/server/openapi.py CHANGED
@@ -76,6 +76,7 @@ class RouteMap:
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."""
@@ -119,57 +120,6 @@ class RouteMap:
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
@@ -179,7 +129,7 @@ DEFAULT_ROUTE_MAPPINGS = [
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
 
@@ -208,6 +158,15 @@ def _determine_route_type(
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(
 
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
+ tags: set[str] = field(default_factory=set)
80
 
81
  def __post_init__(self):
82
  """Validate and process the route map after initialization."""
 
120
  self.route_type = self.mcp_type
121
 
122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  # Default route mappings as a list, where order determines priority
124
  DEFAULT_ROUTE_MAPPINGS = [
125
  # GET requests with path parameters go to ResourceTemplate
 
129
  # GET requests without path parameters go to Resource
130
  RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE),
131
  # All other HTTP methods go to Tool
132
+ RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL),
133
  ]
134
 
135
 
 
158
  pattern_matches = re.search(route_map.pattern, route.path)
159
 
160
  if pattern_matches:
161
+ # Check if tags match (if specified)
162
+ # If route_map.tags is empty, tags are not matched
163
+ # If route_map.tags is non-empty, all tags must be present in route.tags (AND condition)
164
+ if route_map.tags:
165
+ route_tags_set = set(route.tags or [])
166
+ if not route_map.tags.issubset(route_tags_set):
167
+ # Tags don't match, continue to next mapping
168
+ continue
169
+
170
  # We know mcp_type is not None here due to post_init validation
171
  assert route_map.mcp_type is not None
172
  logger.debug(
src/fastmcp/server/server.py CHANGED
@@ -1147,13 +1147,13 @@ class FastMCP(Generic[LifespanResultT]):
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
  )
@@ -1162,7 +1162,7 @@ class FastMCP(Generic[LifespanResultT]):
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,12 +1184,13 @@ class FastMCP(Generic[LifespanResultT]):
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
  )
@@ -1198,7 +1199,7 @@ class FastMCP(Generic[LifespanResultT]):
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"
 
1147
  """
1148
  Create a FastMCP server from an OpenAPI specification.
1149
  """
1150
+ from .openapi import FastMCPOpenAPI, MCPType, RouteMap
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=[RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]\' instead.',
1157
  DeprecationWarning,
1158
  stacklevel=2,
1159
  )
 
1162
  raise ValueError("Cannot specify both all_routes_as_tools and route_maps")
1163
 
1164
  elif all_routes_as_tools:
1165
+ route_maps = [RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]
1166
 
1167
  return FastMCPOpenAPI(
1168
  openapi_spec=openapi_spec,
 
1184
  Create a FastMCP server from a FastAPI application.
1185
  """
1186
 
1187
+ from .openapi import FastMCPOpenAPI, MCPType, RouteMap
1188
 
1189
+ # Deprecated since 2.5.0
1190
  if all_routes_as_tools:
1191
  warnings.warn(
1192
  "The 'all_routes_as_tools' parameter is deprecated and will be removed in a future version. "
1193
+ 'Use \'route_maps=[RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]\' instead.',
1194
  DeprecationWarning,
1195
  stacklevel=2,
1196
  )
 
1199
  raise ValueError("Cannot specify both all_routes_as_tools and route_maps")
1200
 
1201
  elif all_routes_as_tools:
1202
+ route_maps = [RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]
1203
 
1204
  client = httpx.AsyncClient(
1205
  transport=httpx.ASGITransport(app=app), base_url="http://fastapi"
tests/server/openapi/test_openapi.py CHANGED
@@ -1926,302 +1926,223 @@ class TestRouteMapWildcard:
1926
  tools = mcp._tool_manager.list_tools()
1927
  tool_names = {tool.name for tool in tools}
1928
 
1929
- # Check that all operations were mapped as tools
1930
  expected_tools = {"getUsers", "createUser", "getPosts", "createPost"}
1931
  assert tool_names == expected_tools
1932
 
1933
- # No resources or templates should be created
1934
- resources = mcp._resource_manager.get_resources()
1935
- templates = mcp._resource_manager.get_templates()
1936
- assert len(resources) == 0
1937
- assert len(templates) == 0
1938
 
1939
- async def test_priority_specific_over_wildcard(
1940
- self, basic_openapi_spec, mock_basic_client
1941
- ):
1942
- """Test that specific method maps take priority over wildcard."""
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(
1952
- openapi_spec=basic_openapi_spec,
1953
- client=mock_basic_client,
1954
- route_maps=route_maps,
1955
- )
1956
-
1957
- # Check GET operations went to resources
1958
- resources = mcp._resource_manager.get_resources()
1959
- resource_names = {r.name for r in resources.values()}
1960
- assert "getUsers" in resource_names
1961
- assert "getPosts" in resource_names
1962
- assert len(resources) == 2
1963
-
1964
- # Check other operations went to tools
1965
- tools = mcp._tool_manager.list_tools()
1966
- tool_names = {tool.name for tool in tools}
1967
- assert "createUser" in tool_names
1968
- assert "createPost" in tool_names
1969
- assert len(tools) == 2
1970
-
1971
- async def test_priority_wildcard_first(self, basic_openapi_spec, mock_basic_client):
1972
- """Test that when wildcard is first, it matches everything."""
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(
1982
- openapi_spec=basic_openapi_spec,
1983
- client=mock_basic_client,
1984
- route_maps=route_maps,
1985
- )
1986
-
1987
- # All operations should be tools
1988
- tools = mcp._tool_manager.list_tools()
1989
- assert len(tools) == 4
1990
-
1991
- # No resources should be created
1992
- resources = mcp._resource_manager.get_resources()
1993
- assert len(resources) == 0
1994
-
1995
- async def test_wildcard_with_specific_paths(
1996
- self, basic_openapi_spec, mock_basic_client
1997
- ):
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(
2007
- openapi_spec=basic_openapi_spec,
2008
- client=mock_basic_client,
2009
- route_maps=route_maps,
2010
- )
2011
-
2012
- # Check /users operations went to resources
2013
- resources = mcp._resource_manager.get_resources()
2014
- resource_names = {r.name for r in resources.values()}
2015
- assert "getUsers" in resource_names
2016
- assert "createUser" in resource_names
2017
- assert len(resources) == 2
2018
-
2019
- # Check /posts operations went to tools
2020
- tools = mcp._tool_manager.list_tools()
2021
- tool_names = {tool.name for tool in tools}
2022
- assert "getPosts" in tool_names
2023
- assert "createPost" in tool_names
2024
- assert len(tools) == 2
2025
-
2026
-
2027
- class TestAllRoutesAsTools:
2028
- """Tests for the all_routes_as_tools parameter in FastMCP class methods."""
2029
 
2030
  @pytest.fixture
2031
- def simple_api_spec(self) -> dict:
2032
- """A simple OpenAPI spec with both GET and POST methods."""
2033
  return {
2034
  "openapi": "3.1.0",
2035
- "info": {"title": "Test API", "version": "1.0.0"},
2036
  "paths": {
2037
- "/items": {
2038
  "get": {
2039
- "operationId": "getItems",
 
2040
  "responses": {"200": {"description": "Success"}},
2041
  },
2042
  "post": {
2043
- "operationId": "createItem",
 
2044
  "responses": {"201": {"description": "Created"}},
2045
  },
2046
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2047
  },
2048
  }
2049
 
2050
  @pytest.fixture
2051
  async def mock_client(self) -> httpx.AsyncClient:
2052
- """Simple mock client for testing."""
2053
 
2054
  async def _responder(request):
2055
- return httpx.Response(200, json={"result": "ok"})
2056
 
2057
  transport = httpx.MockTransport(_responder)
2058
  return httpx.AsyncClient(transport=transport, base_url="http://test")
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
 
 
 
 
 
 
 
 
1926
  tools = mcp._tool_manager.list_tools()
1927
  tool_names = {tool.name for tool in tools}
1928
 
1929
+ # Check that all 4 operations became tools
1930
  expected_tools = {"getUsers", "createUser", "getPosts", "createPost"}
1931
  assert tool_names == expected_tools
1932
 
 
 
 
 
 
1933
 
1934
+ class TestRouteMapTags:
1935
+ """Tests for RouteMap tags functionality."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1936
 
1937
  @pytest.fixture
1938
+ def tagged_openapi_spec(self) -> dict:
1939
+ """Create an OpenAPI spec with various tags for testing."""
1940
  return {
1941
  "openapi": "3.1.0",
1942
+ "info": {"title": "Tagged API", "version": "1.0.0"},
1943
  "paths": {
1944
+ "/users": {
1945
  "get": {
1946
+ "operationId": "getUsers",
1947
+ "tags": ["users", "public"],
1948
  "responses": {"200": {"description": "Success"}},
1949
  },
1950
  "post": {
1951
+ "operationId": "createUser",
1952
+ "tags": ["users", "admin"],
1953
  "responses": {"201": {"description": "Created"}},
1954
  },
1955
  },
1956
+ "/admin/stats": {
1957
+ "get": {
1958
+ "operationId": "getAdminStats",
1959
+ "tags": ["admin", "internal"],
1960
+ "responses": {"200": {"description": "Success"}},
1961
+ }
1962
+ },
1963
+ "/health": {
1964
+ "get": {
1965
+ "operationId": "getHealth",
1966
+ "tags": ["public"],
1967
+ "responses": {"200": {"description": "Success"}},
1968
+ }
1969
+ },
1970
+ "/metrics": {
1971
+ "get": {
1972
+ "operationId": "getMetrics",
1973
+ "responses": {"200": {"description": "Success"}},
1974
+ }
1975
+ },
1976
  },
1977
  }
1978
 
1979
  @pytest.fixture
1980
  async def mock_client(self) -> httpx.AsyncClient:
1981
+ """Create a simple mock client."""
1982
 
1983
  async def _responder(request):
1984
+ return httpx.Response(200, json={"status": "ok"})
1985
 
1986
  transport = httpx.MockTransport(_responder)
1987
  return httpx.AsyncClient(transport=transport, base_url="http://test")
1988
 
1989
+ async def test_tags_as_tools(self, tagged_openapi_spec, mock_client):
1990
+ """Test that routes with specific tags are converted to tools."""
1991
+ # Convert routes with "admin" tag to tools
1992
+ route_maps = [
1993
+ RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL, tags={"admin"}),
1994
+ RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE),
1995
+ ]
1996
 
1997
+ server = FastMCPOpenAPI(
1998
+ openapi_spec=tagged_openapi_spec,
1999
+ client=mock_client,
2000
+ route_maps=route_maps,
2001
+ )
 
2002
 
2003
+ # Check that admin-tagged routes are tools
2004
+ tools = server._tool_manager.get_tools()
2005
+ tool_names = {t.name for t in tools.values()}
2006
 
2007
+ resources = server._resource_manager.get_resources()
2008
+ resource_names = {r.name for r in resources.values()}
 
2009
 
2010
+ # Routes with "admin" tag should be tools
2011
+ assert "createUser" in tool_names
2012
+ assert "getAdminStats" in tool_names
2013
 
2014
+ # Routes without "admin" tag should be resources
2015
+ assert "getUsers" in resource_names
2016
+ assert "getHealth" in resource_names
2017
+ assert "getMetrics" in resource_names
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2018
 
2019
+ async def test_exclude_tags(self, tagged_openapi_spec, mock_client):
2020
+ """Test that routes with specific tags are excluded."""
2021
+ # Exclude routes with "internal" tag
2022
+ route_maps = [
2023
+ RouteMap(
2024
+ methods="*", pattern=r".*", mcp_type=MCPType.EXCLUDE, tags={"internal"}
2025
+ ),
2026
+ RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE),
2027
+ RouteMap(methods=["POST"], pattern=r".*", mcp_type=MCPType.TOOL),
2028
+ ]
2029
 
2030
+ server = FastMCPOpenAPI(
2031
+ openapi_spec=tagged_openapi_spec,
2032
+ client=mock_client,
2033
+ route_maps=route_maps,
2034
+ )
2035
 
2036
+ # Check that internal-tagged routes are excluded
2037
+ resources = server._resource_manager.get_resources()
2038
+ resource_names = {r.name for r in resources.values()}
2039
 
2040
+ tools = server._tool_manager.get_tools()
2041
+ tool_names = {t.name for t in tools.values()}
 
2042
 
2043
+ # Internal-tagged route should be excluded
2044
+ assert "getAdminStats" not in resource_names
2045
+ assert "getAdminStats" not in tool_names
2046
 
2047
+ # Other routes should still be present
2048
+ assert "getUsers" in resource_names
2049
+ assert "getHealth" in resource_names
2050
+ assert "getMetrics" in resource_names
2051
+ assert "createUser" in tool_names
2052
 
2053
+ async def test_multiple_tags_and_condition(self, tagged_openapi_spec, mock_client):
2054
+ """Test that routes must have ALL specified tags (AND condition)."""
2055
+ # Routes must have BOTH "users" AND "admin" tags
2056
+ route_maps = [
2057
+ RouteMap(
2058
+ methods="*",
2059
+ pattern=r".*",
2060
+ mcp_type=MCPType.TOOL,
2061
+ tags={"users", "admin"},
2062
+ ),
2063
+ RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE),
2064
+ ]
2065
 
2066
+ server = FastMCPOpenAPI(
2067
+ openapi_spec=tagged_openapi_spec,
2068
+ client=mock_client,
2069
+ route_maps=route_maps,
2070
+ )
2071
 
2072
+ tools = server._tool_manager.get_tools()
2073
+ tool_names = {t.name for t in tools.values()}
 
2074
 
2075
+ resources = server._resource_manager.get_resources()
2076
+ resource_names = {r.name for r in resources.values()}
 
 
 
 
2077
 
2078
+ # Only createUser has both "users" AND "admin" tags
2079
+ assert "createUser" in tool_names
2080
 
2081
+ # Other routes should be resources
2082
+ assert "getUsers" in resource_names # has "users" but not "admin"
2083
+ assert "getAdminStats" in resource_names # has "admin" but not "users"
2084
+ assert "getHealth" in resource_names
2085
+ assert "getMetrics" in resource_names
 
 
 
 
 
 
 
 
 
 
2086
 
2087
+ async def test_pattern_and_tags_combination(self, tagged_openapi_spec, mock_client):
2088
+ """Test that both pattern and tags must be satisfied."""
2089
+ # Routes matching pattern AND having specific tags
2090
+ route_maps = [
2091
+ RouteMap(
2092
+ methods="*",
2093
+ pattern=r".*/admin/.*",
2094
+ mcp_type=MCPType.TOOL,
2095
+ tags={"admin"},
2096
+ ),
2097
+ RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE),
2098
+ RouteMap(methods=["POST"], pattern=r".*", mcp_type=MCPType.TOOL),
2099
+ ]
2100
 
2101
+ server = FastMCPOpenAPI(
2102
+ openapi_spec=tagged_openapi_spec,
2103
+ client=mock_client,
2104
+ route_maps=route_maps,
2105
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2106
 
2107
+ tools = server._tool_manager.get_tools()
2108
+ tool_names = {t.name for t in tools.values()}
2109
+
2110
+ resources = server._resource_manager.get_resources()
2111
+ resource_names = {r.name for r in resources.values()}
2112
+
2113
+ # Only getAdminStats matches both /admin/ pattern AND "admin" tag
2114
+ assert "getAdminStats" in tool_names
2115
+
2116
+ # createUser has "admin" tag but doesn't match pattern, so it becomes a tool via POST rule
2117
+ assert "createUser" in tool_names
2118
+
2119
+ # Other routes should be resources (GET)
2120
+ assert "getUsers" in resource_names
2121
+ assert "getHealth" in resource_names
2122
+ assert "getMetrics" in resource_names
2123
 
2124
+ async def test_empty_tags_ignored(self, tagged_openapi_spec, mock_client):
2125
+ """Test that empty tags set is ignored (matches all routes)."""
2126
+ # Empty tags should match all routes
2127
+ route_maps = [
2128
+ RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL, tags=set()),
2129
+ ]
2130
 
 
 
2131
  server = FastMCPOpenAPI(
2132
+ openapi_spec=tagged_openapi_spec,
2133
  client=mock_client,
2134
+ route_maps=route_maps,
 
 
 
 
 
 
 
 
 
2135
  )
2136
 
2137
+ tools = server._tool_manager.get_tools()
2138
+ tool_names = {t.name for t in tools.values()}
 
 
 
 
 
 
2139
 
2140
+ # All routes should be tools since empty tags matches everything
2141
+ expected_tools = {
2142
+ "getUsers",
2143
+ "createUser",
2144
+ "getAdminStats",
2145
+ "getHealth",
2146
+ "getMetrics",
2147
+ }
2148
+ assert tool_names == expected_tools
tests/server/test_route_map_shortcuts.py DELETED
@@ -1,208 +0,0 @@
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