Jeremiah Lowin commited on
Commit
dc4ddbc
·
unverified ·
2 Parent(s): bc43da540feb9b

Merge pull request #566 from jlowin/route_map_fn

Browse files
README.md CHANGED
@@ -15,11 +15,11 @@
15
  > [!NOTE]
16
  > #### FastMCP 2.0 & The Official MCP SDK
17
  >
18
- > Recognize the `FastMCP` name? You might have seen the version that was contributed to the [official MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk), which was based on **FastMCP 1.0**.
19
  >
20
- > **Welcome to FastMCP 2.0!** This is the actively developed successor, and it significantly expands on 1.0 by introducing powerful client capabilities, server proxying & composition, OpenAPI/FastAPI integration, and more advanced features.
21
  >
22
- > FastMCP 2.0 is the recommended path for building modern, powerful MCP applications. Ready to upgrade or get started? Follow the [installation instructions](https://gofastmcp.com/getting-started/installation), which include specific steps for upgrading from the official MCP SDK.
23
 
24
  ---
25
 
 
15
  > [!NOTE]
16
  > #### FastMCP 2.0 & The Official MCP SDK
17
  >
18
+ > FastMCP is the standard framework for building MCP servers and clients. FastMCP 1.0 was incorporated into the [official MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk).
19
  >
20
+ > **This is FastMCP 2.0,** the actively maintained version that significantly expands on 1.0's basic server-building capabilities by introducing full client support, server composition, OpenAPI/FastAPI integration, remote server proxying, built-in testing tools, and more.
21
  >
22
+ > FastMCP 2.0 is the complete toolkit for modern AI applications. Ready to upgrade or get started? Follow the [installation instructions](https://gofastmcp.com/getting-started/installation), which include specific steps for upgrading from the official MCP SDK.
23
 
24
  ---
25
 
docs/getting-started/welcome.mdx CHANGED
@@ -24,17 +24,13 @@ if __name__ == "__main__":
24
  ```
25
 
26
 
27
- ## FastMCP 2.0 and the Official MCP SDK
28
 
29
- <Tip>
30
- Recognize the `FastMCP` name? You might have seen the version that was contributed to the [official MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk), which was based on **FastMCP 1.0**.
31
 
 
32
 
33
- **Welcome to FastMCP 2.0!** This is the [actively developed successor](https://github.com/jlowin/fastmcp), and it significantly expands on 1.0 by introducing powerful client capabilities, server proxying & composition, OpenAPI/FastAPI integration, and more advanced features.
34
-
35
- FastMCP 2.0 is the recommended path for building modern, powerful MCP applications. Ready to upgrade or get started? Follow the [installation instructions](/getting-started/installation), which include specific steps for upgrading.
36
- </Tip>
37
-
38
 
39
 
40
  ## What is MCP?
 
24
  ```
25
 
26
 
27
+ ## FastMCP and the Official MCP SDK
28
 
29
+ FastMCP is the standard framework for building MCP servers and clients. FastMCP 1.0 was incorporated into the [official MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk).
 
30
 
31
+ **This is FastMCP 2.0,** the [actively maintained version](https://github.com/jlowin/fastmcp) that significantly expands on 1.0's basic server-building capabilities by introducing full client support, server composition, OpenAPI/FastAPI integration, remote server proxying, built-in testing tools, and more.
32
 
33
+ FastMCP 2.0 is the complete toolkit for modern AI applications. Ready to upgrade or get started? Follow the [installation instructions](/getting-started/installation), which include specific steps for upgrading from the official MCP SDK.
 
 
 
 
34
 
35
 
36
  ## What is MCP?
docs/servers/openapi.mdx CHANGED
@@ -1,78 +1,96 @@
1
  ---
2
  title: OpenAPI Integration
3
  sidebarTitle: OpenAPI Integration
4
- description: Generate MCP servers from OpenAPI specs
5
  icon: code-branch
6
  ---
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 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
15
  from fastmcp import FastMCP
16
 
17
- # Create a client for your API
18
- api_client = httpx.AsyncClient(base_url="https://api.example.com")
19
 
20
- # Load your OpenAPI spec
21
- spec = {...}
22
 
23
- # Create an MCP server from your OpenAPI spec
24
- mcp = FastMCP.from_openapi(openapi_spec=spec, client=api_client)
 
 
 
 
25
 
26
  if __name__ == "__main__":
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. An empty set (`{}`) means no tag filtering, so the route matches regardless of its 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
  ]
@@ -80,112 +98,272 @@ DEFAULT_ROUTE_MAPPINGS = [
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
 
130
- For example, if you call a tool with these parameters:
131
  ```python
 
132
  await client.call_tool("search_products", {
133
- "category": "electronics", # Will be included
134
- "min_price": 100, # Will be included
135
- "max_price": None, # Will be excluded
136
- "brand": "", # Will be excluded
137
  })
138
- ```
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
 
146
  ```python
147
- # This will work
148
- await client.call_tool("get_product", {"product_id": 123})
 
 
 
149
 
150
- # This will raise ValueError: "Missing required path parameters: {'product_id'}"
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
160
  from fastmcp import FastMCP
161
 
162
- # Create a client with authentication
163
  api_client = httpx.AsyncClient(
164
  base_url="https://api.example.com",
165
  headers={"Authorization": "Bearer YOUR_TOKEN"}
166
  )
167
 
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.
@@ -195,8 +373,8 @@ FastMCP does *not* include FastAPI as a dependency; you must install it separate
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():
@@ -210,41 +388,46 @@ def get_item(item_id: int):
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
 
 
1
  ---
2
  title: OpenAPI Integration
3
  sidebarTitle: OpenAPI Integration
4
+ description: Generate MCP servers from OpenAPI specs and FastAPI apps
5
  icon: code-branch
6
  ---
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 or FastAPI app. Instead of manually creating tools and resources, you provide an OpenAPI spec and FastMCP intelligently converts your API endpoints into the appropriate MCP components.
12
 
13
+ ## Quick Start
14
+
15
+ To convert an OpenAPI specification to an MCP server, you can use the `FastMCP.from_openapi` class method. This method takes an OpenAPI specification and an async HTTPX client that can be used to make requests to the API, and returns an MCP server.
16
+
17
+ Here's an example:
18
+ ```python {11-15}
19
  import httpx
20
  from fastmcp import FastMCP
21
 
22
+ # Create an HTTP client for your API
23
+ client = httpx.AsyncClient(base_url="https://api.example.com")
24
 
25
+ # Load your OpenAPI spec
26
+ openapi_spec = httpx.get("https://api.example.com/openapi.json").json()
27
 
28
+ # Create the MCP server
29
+ mcp = FastMCP.from_openapi(
30
+ openapi_spec=openapi_spec,
31
+ client=client,
32
+ name="My API Server"
33
+ )
34
 
35
  if __name__ == "__main__":
36
  mcp.run()
37
  ```
38
 
39
+ That's it! Your entire API is now available as an MCP server. Clients can discover and interact with your API endpoints through the MCP protocol, with full schema validation and type safety.
40
+
41
+
42
  ## Route Mapping
43
 
 
44
 
 
45
 
46
+ FastMCP analyzes your API specification and automatically creates MCP components based on HTTP semantics and REST conventions. By default, the following rules are used to determine what MCP component to create for each route:
47
+
48
+ | OpenAPI Route | Example | MCP Component |
49
+ |---------------|---------|---------------|
50
+ | `GET` with path params | `GET /users/{id}` | **Resource Template** |
51
+ | `GET` without path params | `GET /stats` | **Resource** |
52
+ | `POST`, `PUT`, `PATCH`, `DELETE`, etc. | `POST /users` | **Tool** |
53
+
54
+
55
+
56
+ ### Custom Route Maps
57
+
58
+ <VersionBadge version="2.5.0" />
59
 
60
+ FastMCP uses an ordered list of `RouteMap` objects to determine how to map OpenAPI routes to various MCP component types.
61
 
62
+ Each `RouteMap` specifies a combination of methods, patterns, and tags, as well as a corresponding MCP component type. Each OpenAPI route is checked against each `RouteMap` in order, and the first one that matches every criteria is used to determine its converted MCP type. A special type, `EXCLUDE`, can be used to exclude routes from the MCP server entirely.
63
 
64
  - **Methods**: HTTP methods to match (e.g. `["GET", "POST"]` or `"*"` for all)
65
  - **Pattern**: Regex pattern to match the route path (e.g. `r"^/users/.*"` or `r".*"` for all)
66
  - **Tags**: A set of OpenAPI tags that must all be present. An empty set (`{}`) means no tag filtering, so the route matches regardless of its tags.
67
+ - **MCP type**: What MCP component type to create (`TOOL`, `RESOURCE`, `RESOURCE_TEMPLATE`, or `EXCLUDE`)
68
 
69
+ To illustrate this in practice, here are FastMCP's default route mappings as a list of `RouteMap` objects:
70
 
71
  ```python
72
  from fastmcp.server.openapi import RouteMap, MCPType
73
 
 
74
  DEFAULT_ROUTE_MAPPINGS = [
75
+
76
+ # GET with path parameters → ResourceTemplate
77
  RouteMap(
78
  methods=["GET"],
79
  pattern=r".*\{.*\}.*",
 
80
  mcp_type=MCPType.RESOURCE_TEMPLATE
81
  ),
82
+
83
+ # GET without path parameters → Resource
84
  RouteMap(
85
  methods=["GET"],
86
  pattern=r".*",
 
87
  mcp_type=MCPType.RESOURCE
88
  ),
89
+
90
+ # All other methods → Tool
91
  RouteMap(
92
+ methods=["*"],
93
  pattern=r".*",
 
94
  mcp_type=MCPType.TOOL
95
  ),
96
  ]
 
98
 
99
  ### Custom Route Maps
100
 
101
+ When creating your FastMCP server, you can customize routing behavior by providing your own list of `RouteMap` objects. Your custom maps are processed before the default route maps, and routes will be assigned to the first matching custom map.
102
+
103
+ For example, the following simple rule will treat every OpenAPI route as a tool:
104
 
105
+ ```python {7}
106
+ from fastmcp import FastMCP
107
  from fastmcp.server.openapi import RouteMap, MCPType
108
 
109
  mcp = FastMCP.from_openapi(
110
+ ...,
111
+ route_maps=[
112
+ RouteMap(mcp_type=MCPType.TOOL),
113
+ ],
114
+ )
115
+ ```
116
+
117
+ Here is a more complete example that uses custom route maps to convert all `GET` endpoints under `/analytics/` to tools while excluding all admin endpoints and all routes tagged "internal". All other routes will be handled by the default rules:
118
+
119
+ ```python
120
+ from fastmcp import FastMCP
121
+ from fastmcp.server.openapi import RouteMap, MCPType
122
+
123
+ mcp = FastMCP.from_openapi(
124
+ ...,
125
  route_maps=[
126
+
127
+ # Analytics `GET` endpoints are tools
128
  RouteMap(
129
  methods=["GET"],
130
  pattern=r"^/analytics/.*",
131
  mcp_type=MCPType.TOOL,
132
  ),
133
+
134
  # Exclude all admin endpoints
135
  RouteMap(
136
  pattern=r"^/admin/.*",
137
  mcp_type=MCPType.EXCLUDE,
138
+ ),
139
+
140
+ # Exclude all routes tagged "internal"
141
+ RouteMap(
142
+ tags={"internal"},
143
+ mcp_type=MCPType.EXCLUDE,
144
+ ),
145
+ ],
146
+ )
147
+ ```
148
+
149
+ <Tip>
150
+ The default route maps are always applied after your custom maps, so you do not have to create route maps for every possible route.
151
+ </Tip>
152
+
153
+ ### Excluding Routes
154
+
155
+ To exclude routes from the MCP server, use a route map to assign them to `MCPType.EXCLUDE`.
156
+
157
+ You can use this to remove sensitive or internal routes by targeting them specifically:
158
+
159
+ ```python {7,8}
160
+ from fastmcp import FastMCP
161
+ from fastmcp.server.openapi import RouteMap, MCPType
162
+
163
+ mcp = FastMCP.from_openapi(
164
+ ...,
165
+ route_maps=[
166
+ RouteMap(pattern=r"^/admin/.*", mcp_type=MCPType.EXCLUDE),
167
+ RouteMap(tags={"internal"}, mcp_type=MCPType.EXCLUDE),
168
+ ],
169
  )
170
  ```
171
 
172
+ Or you can use a catch-all rule to exclude everything that your maps don't handle explicitly:
173
+ ```python {10}
174
+ from fastmcp import FastMCP
175
+ from fastmcp.server.openapi import RouteMap, MCPType
176
+
177
+ mcp = FastMCP.from_openapi(
178
+ ...,
179
+ route_maps=[
180
+ # custom mapping logic goes here
181
+ ...,
182
+ # exclude all remaining routes
183
+ RouteMap(mcp_type=MCPType.EXCLUDE),
184
+ ],
185
+ )
186
+ ```
187
+
188
+ <Tip>
189
+ Using a catch-all exclusion rule will prevent the default route mappings from being applied, since it will match every remaining route. This is useful if you want to explicitly allow-list certain routes.
190
+ </Tip>
191
+
192
+
193
+ ### Advanced Route Mapping
194
+
195
+ <VersionBadge version="2.5.0" />
196
 
197
+ For advanced use cases that require more complex logic, you can provide a `route_map_fn` callable. After the route map logic is applied, this function is called on each matched route and its assigned MCP component type. It can optionally return a different component type to override the mapped assignment. If it returns `None`, the assigned type is used.
198
 
199
+ In addition to more precise targeting of methods, patterns, and tags, this function can access any additional OpenAPI metadata about the route.
200
 
201
+ <Tip>
202
+ The `route_map_fn` **is** called on routes that matched `MCPType.EXCLUDE` in your custom maps, giving you an opportunity to override the exclusion.
203
+ </Tip>
204
 
205
+
206
+ ```python
207
+ from fastmcp import FastMCP
208
+ from fastmcp.server.openapi import RouteMap, MCPType, HTTPRoute
209
+
210
+ def custom_route_mapper(route: HTTPRoute, mcp_type: MCPType) -> MCPType | None:
211
+ """Advanced route type mapping."""
212
+ # Convert all admin routes to tools regardless of HTTP method
213
+ if "/admin/" in route.path:
214
+ return MCPType.TOOL
215
+
216
+ elif "internal" in route.tags:
217
+ return MCPType.EXCLUDE
218
+
219
+ # Convert user detail routes to templates even if they're POST
220
+ elif route.path.startswith("/users/") and route.method == "POST":
221
+ return MCPType.RESOURCE_TEMPLATE
222
+
223
+ # Use defaults for all other routes
224
+ return None
225
+
226
+ mcp = FastMCP.from_openapi(
227
+ ...,
228
+ route_map_fn=custom_route_mapper,
229
+ )
230
+ ```
231
+
232
+ ## Customizing MCP Components
233
 
234
  <VersionBadge version="2.5.0" />
235
 
236
+ By default, FastMCP creates MCP components using a variety of metadata from the OpenAPI spec, such as incorporating the OpenAPI description into the MCP component description.
237
+
238
+ At times you may want to modify those MCP components in a variety of ways, such as adding LLM-specific instructions or tags. For fine-grained customization, you can provide a `mcp_component_fn` when creating the MCP server. After each MCP component has been created, this function is called on it and has the opportunity to modify it in-place.
239
+
240
+ <Tip>
241
+ Your `mcp_component_fn` is expected to modify the component in-place, not to return a new component. The result of the function is ignored.
242
+ </Tip>
243
 
244
+ ```python {27}
245
+ from fastmcp import FastMCP
246
+ from fastmcp.server.openapi import (
247
+ HTTPRoute,
248
+ OpenAPITool,
249
+ OpenAPIResource,
250
+ OpenAPIResourceTemplate,
251
+ )
252
+
253
+ def customize_components(
254
+ route: HTTPRoute,
255
+ component: OpenAPITool | OpenAPIResource | OpenAPIResourceTemplate,
256
+ ) -> None:
257
+
258
+ # Add custom tags to all components
259
+ component.tags.add("openapi")
260
+
261
+ # Customize based on component type
262
+ if isinstance(component, OpenAPITool):
263
+ component.description = f"🔧 {component.description} (via API)"
264
+
265
+ if isinstance(component, OpenAPIResource):
266
+ component.description = f"📊 {component.description}"
267
+ component.tags.add("data")
268
+
269
+ mcp = FastMCP.from_openapi(
270
+ ...,
271
+ mcp_component_fn=customize_components,
272
+ )
273
+ ```
274
 
275
  ## Request Parameter Handling
276
 
277
+ FastMCP intelligently handles different types of parameters in OpenAPI requests:
278
 
279
  ### Query Parameters
280
 
281
+ By default, FastMCP only includes query parameters that have non-empty values. Parameters with `None` values or empty strings are automatically filtered out.
282
 
 
283
  ```python
284
+ # When calling this tool...
285
  await client.call_tool("search_products", {
286
+ "category": "electronics", # Included
287
+ "min_price": 100, # Included
288
+ "max_price": None, # Excluded
289
+ "brand": "", # Excluded
290
  })
 
291
 
292
+ # The HTTP request will be: GET /products?category=electronics&min_price=100
293
+ ```
294
 
295
  ### Path Parameters
296
 
297
+ Path parameters are typically required by REST APIs. FastMCP:
298
+ - Filters out `None` values
299
+ - Validates that all required path parameters are provided
300
+ - Raises clear errors for missing required parameters
301
+
302
+ ```python
303
+ # ✅ This works
304
+ await client.call_tool("get_user", {"user_id": 123})
305
+
306
+ # ❌ This raises: "Missing required path parameters: {'user_id'}"
307
+ await client.call_tool("get_user", {"user_id": None})
308
+ ```
309
+
310
+ ### Array Parameters
311
+
312
+ FastMCP handles array parameters according to OpenAPI specifications:
313
+
314
+ - **Query arrays**: Serialized based on the `explode` parameter (default: `True`)
315
+ - **Path arrays**: Serialized as comma-separated values (OpenAPI 'simple' style)
316
 
317
  ```python
318
+ # Query array with explode=true (default)
319
+ # ?tags=red&tags=blue&tags=green
320
+
321
+ # Query array with explode=false
322
+ # ?tags=red,blue,green
323
 
324
+ # Path array (always comma-separated)
325
+ # /items/red,blue,green
326
  ```
327
 
328
+ ### Headers
329
+
330
+ Header parameters are automatically converted to strings and included in the HTTP request.
331
 
332
+ ## Auth
333
+
334
+ If your API requires authentication, configure it on the HTTP client before creating the MCP server:
335
 
336
  ```python
337
  import httpx
338
  from fastmcp import FastMCP
339
 
340
+ # Bearer token authentication
341
  api_client = httpx.AsyncClient(
342
  base_url="https://api.example.com",
343
  headers={"Authorization": "Bearer YOUR_TOKEN"}
344
  )
345
 
346
+ # Create MCP server with authenticated client
347
+ mcp = FastMCP.from_openapi(..., client=api_client)
348
  ```
 
349
  ## Timeouts
350
 
351
+ Set a timeout for all API requests:
352
 
353
  ```python
354
  mcp = FastMCP.from_openapi(
355
  openapi_spec=spec,
356
  client=api_client,
357
+ timeout=30.0 # 30 second timeout for all requests
358
  )
359
  ```
360
 
361
+
362
  ## FastAPI Integration
363
 
364
  <VersionBadge version="2.0.0" />
365
 
366
+ FastMCP can directly convert FastAPI applications into MCP servers by extracting their OpenAPI specifications:
367
 
368
  <Tip>
369
  FastMCP does *not* include FastAPI as a dependency; you must install it separately to use this integration.
 
373
  from fastapi import FastAPI
374
  from fastmcp import FastMCP
375
 
376
+ # Your FastAPI app
377
+ app = FastAPI(title="My API", version="1.0.0")
378
 
379
  @app.get("/items", tags=["items"])
380
  def list_items():
 
388
  def create_item(name: str):
389
  return {"id": 3, "name": name}
390
 
391
+ # Convert FastAPI app to MCP server
392
  mcp = FastMCP.from_fastapi(app=app)
393
 
394
  if __name__ == "__main__":
395
+ mcp.run() # Run as MCP server
396
  ```
397
 
398
+ <Warning>
399
+ FastMCP servers are not FastAPI apps, even when created from one. To learn how to deploy them as an ASGI app, see the [ASGI Integration](/deployment/asgi) documentation.
400
+ </Warning>
401
 
 
402
 
 
 
 
 
403
 
404
+ ### FastAPI Configuration
405
+
406
+ All OpenAPI integration features work with FastAPI apps:
407
 
408
  ```python
409
  from fastmcp.server.openapi import RouteMap, MCPType
410
 
411
+ # Custom route mapping with FastAPI
412
  mcp = FastMCP.from_fastapi(
413
  app=app,
414
+ name="My Custom Server",
415
+ timeout=5.0,
416
  route_maps=[
417
+ # Admin endpoints become tools
418
+ RouteMap(methods="*", pattern=r"^/admin/.*", mcp_type=MCPType.TOOL),
419
+ # Internal endpoints are excluded
420
  RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.EXCLUDE, tags={"internal"}),
421
+ ],
422
+ route_map_fn=my_route_mapper,
423
+ mcp_component_fn=my_component_customizer,
424
  )
425
  ```
426
 
427
+ ### FastAPI Benefits
428
 
429
+ - **Zero code duplication**: Reuse existing FastAPI endpoints
430
+ - **Schema inheritance**: Pydantic models and validation are preserved
431
+ - **ASGI transport**: Direct in-memory communication (no HTTP overhead)
432
+ - **Full FastAPI features**: Dependencies, middleware, authentication all work
433
 
src/fastmcp/server/openapi.py CHANGED
@@ -22,6 +22,7 @@ from fastmcp.tools.tool import Tool, _convert_to_content
22
  from fastmcp.utilities import openapi
23
  from fastmcp.utilities.logging import get_logger
24
  from fastmcp.utilities.openapi import (
 
25
  _combine_schemas,
26
  format_description_with_responses,
27
  )
@@ -33,6 +34,16 @@ logger = get_logger(__name__)
33
 
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.
@@ -41,7 +52,6 @@ class MCPType(enum.Enum):
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
  """
@@ -49,7 +59,7 @@ class MCPType(enum.Enum):
49
  TOOL = "TOOL"
50
  RESOURCE = "RESOURCE"
51
  RESOURCE_TEMPLATE = "RESOURCE_TEMPLATE"
52
- PROMPT = "PROMPT"
53
  EXCLUDE = "EXCLUDE"
54
 
55
 
@@ -64,7 +74,6 @@ class RouteType(enum.Enum):
64
  TOOL = "TOOL"
65
  RESOURCE = "RESOURCE"
66
  RESOURCE_TEMPLATE = "RESOURCE_TEMPLATE"
67
- PROMPT = "PROMPT"
68
  IGNORE = "IGNORE"
69
 
70
 
@@ -614,7 +623,7 @@ class FastMCPOpenAPI(FastMCP):
614
 
615
  Example:
616
  ```python
617
- from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap, RouteType
618
  import httpx
619
 
620
  # Define custom route mappings
@@ -633,7 +642,7 @@ class FastMCPOpenAPI(FastMCP):
633
  ),
634
  ]
635
 
636
- # Create server with custom mappings
637
  server = FastMCPOpenAPI(
638
  openapi_spec=spec,
639
  client=httpx.AsyncClient(),
@@ -649,6 +658,8 @@ class FastMCPOpenAPI(FastMCP):
649
  client: httpx.AsyncClient,
650
  name: str | None = None,
651
  route_maps: list[RouteMap] | None = None,
 
 
652
  timeout: float | None = None,
653
  **settings: Any,
654
  ):
@@ -660,6 +671,12 @@ class FastMCPOpenAPI(FastMCP):
660
  client: httpx AsyncClient for making HTTP requests
661
  name: Optional name for the server
662
  route_maps: Optional list of RouteMap objects defining route mappings
 
 
 
 
 
 
663
  timeout: Optional timeout (in seconds) for all requests
664
  **settings: Additional settings for FastMCP
665
  """
@@ -667,6 +684,8 @@ class FastMCPOpenAPI(FastMCP):
667
 
668
  self._client = client
669
  self._timeout = timeout
 
 
670
 
671
  # Keep track of names to detect collisions
672
  self._used_names = {"tools": set(), "resources": set(), "templates": set()}
@@ -679,6 +698,22 @@ class FastMCPOpenAPI(FastMCP):
679
  # Determine route type based on mappings or default rules
680
  route_type = _determine_route_type(route, route_maps)
681
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
682
  # Generate a default name from the route
683
  component_name = self._generate_default_name(route, route_type)
684
 
@@ -688,11 +723,6 @@ class FastMCPOpenAPI(FastMCP):
688
  self._create_openapi_resource(route, component_name)
689
  elif route_type == MCPType.RESOURCE_TEMPLATE:
690
  self._create_openapi_template(route, component_name)
691
- elif route_type == MCPType.PROMPT:
692
- # Not implemented yet
693
- logger.warning(
694
- f"PROMPT route type not implemented: {route.method} {route.path}"
695
- )
696
  elif route_type == MCPType.EXCLUDE:
697
  logger.info(f"Excluding route: {route.method} {route.path}")
698
 
@@ -795,6 +825,18 @@ class FastMCPOpenAPI(FastMCP):
795
  tags=set(route.tags or []),
796
  timeout=self._timeout,
797
  )
 
 
 
 
 
 
 
 
 
 
 
 
798
  # Register the tool by directly assigning to the tools dictionary
799
  self._tool_manager._tools[tool_name] = tool
800
  logger.debug(
@@ -828,6 +870,18 @@ class FastMCPOpenAPI(FastMCP):
828
  tags=set(route.tags or []),
829
  timeout=self._timeout,
830
  )
 
 
 
 
 
 
 
 
 
 
 
 
831
  # Register the resource by directly assigning to the resources dictionary
832
  self._resource_manager._resources[str(resource.uri)] = resource
833
  logger.debug(
@@ -890,6 +944,18 @@ class FastMCPOpenAPI(FastMCP):
890
  tags=set(route.tags or []),
891
  timeout=self._timeout,
892
  )
 
 
 
 
 
 
 
 
 
 
 
 
893
  # Register the template by directly assigning to the templates dictionary
894
  self._resource_manager._templates[uri_template_str] = template
895
  logger.debug(
 
22
  from fastmcp.utilities import openapi
23
  from fastmcp.utilities.logging import get_logger
24
  from fastmcp.utilities.openapi import (
25
+ HTTPRoute,
26
  _combine_schemas,
27
  format_description_with_responses,
28
  )
 
34
 
35
  HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
36
 
37
+ # Type definitions for the mapping functions
38
+ RouteMapFn = Callable[[HTTPRoute, "MCPType"], "MCPType | None"]
39
+ ComponentFn = Callable[
40
+ [
41
+ HTTPRoute,
42
+ "OpenAPITool | OpenAPIResource | OpenAPIResourceTemplate",
43
+ ],
44
+ None,
45
+ ]
46
+
47
 
48
  class MCPType(enum.Enum):
49
  """Type of FastMCP component to create from a route.
 
52
  TOOL: Convert the route to a callable Tool
53
  RESOURCE: Convert the route to a Resource (typically GET endpoints)
54
  RESOURCE_TEMPLATE: Convert the route to a ResourceTemplate (typically GET with path params)
 
55
  EXCLUDE: Exclude the route from being converted to any MCP component
56
  IGNORE: Deprecated, use EXCLUDE instead
57
  """
 
59
  TOOL = "TOOL"
60
  RESOURCE = "RESOURCE"
61
  RESOURCE_TEMPLATE = "RESOURCE_TEMPLATE"
62
+ # PROMPT = "PROMPT"
63
  EXCLUDE = "EXCLUDE"
64
 
65
 
 
74
  TOOL = "TOOL"
75
  RESOURCE = "RESOURCE"
76
  RESOURCE_TEMPLATE = "RESOURCE_TEMPLATE"
 
77
  IGNORE = "IGNORE"
78
 
79
 
 
623
 
624
  Example:
625
  ```python
626
+ from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap, MCPType
627
  import httpx
628
 
629
  # Define custom route mappings
 
642
  ),
643
  ]
644
 
645
+ # Create server with custom mappings and route mapper
646
  server = FastMCPOpenAPI(
647
  openapi_spec=spec,
648
  client=httpx.AsyncClient(),
 
658
  client: httpx.AsyncClient,
659
  name: str | None = None,
660
  route_maps: list[RouteMap] | None = None,
661
+ route_map_fn: RouteMapFn | None = None,
662
+ mcp_component_fn: ComponentFn | None = None,
663
  timeout: float | None = None,
664
  **settings: Any,
665
  ):
 
671
  client: httpx AsyncClient for making HTTP requests
672
  name: Optional name for the server
673
  route_maps: Optional list of RouteMap objects defining route mappings
674
+ route_map_fn: Optional callable for advanced route type mapping.
675
+ Receives (route, mcp_type) and returns MCPType or None.
676
+ Called on every route, including excluded ones.
677
+ mcp_component_fn: Optional callable for component customization.
678
+ Receives (route, component) and can modify the component in-place.
679
+ Called on every created component.
680
  timeout: Optional timeout (in seconds) for all requests
681
  **settings: Additional settings for FastMCP
682
  """
 
684
 
685
  self._client = client
686
  self._timeout = timeout
687
+ self._route_map_fn = route_map_fn
688
+ self._mcp_component_fn = mcp_component_fn
689
 
690
  # Keep track of names to detect collisions
691
  self._used_names = {"tools": set(), "resources": set(), "templates": set()}
 
698
  # Determine route type based on mappings or default rules
699
  route_type = _determine_route_type(route, route_maps)
700
 
701
+ # Call route_map_fn if provided
702
+ if self._route_map_fn is not None:
703
+ try:
704
+ result = self._route_map_fn(route, route_type)
705
+ if result is not None:
706
+ route_type = result
707
+ logger.debug(
708
+ f"Route {route.method} {route.path} mapping customized by route_map_fn: "
709
+ f"type={route_type.name}"
710
+ )
711
+ except Exception as e:
712
+ logger.warning(
713
+ f"Error in route_map_fn for {route.method} {route.path}: {e}. "
714
+ f"Using default values."
715
+ )
716
+
717
  # Generate a default name from the route
718
  component_name = self._generate_default_name(route, route_type)
719
 
 
723
  self._create_openapi_resource(route, component_name)
724
  elif route_type == MCPType.RESOURCE_TEMPLATE:
725
  self._create_openapi_template(route, component_name)
 
 
 
 
 
726
  elif route_type == MCPType.EXCLUDE:
727
  logger.info(f"Excluding route: {route.method} {route.path}")
728
 
 
825
  tags=set(route.tags or []),
826
  timeout=self._timeout,
827
  )
828
+
829
+ # Call component_fn if provided
830
+ if self._mcp_component_fn is not None:
831
+ try:
832
+ self._mcp_component_fn(route, tool)
833
+ logger.debug(f"Tool {tool_name} customized by component_fn")
834
+ except Exception as e:
835
+ logger.warning(
836
+ f"Error in component_fn for tool {tool_name}: {e}. "
837
+ f"Using component as-is."
838
+ )
839
+
840
  # Register the tool by directly assigning to the tools dictionary
841
  self._tool_manager._tools[tool_name] = tool
842
  logger.debug(
 
870
  tags=set(route.tags or []),
871
  timeout=self._timeout,
872
  )
873
+
874
+ # Call component_fn if provided
875
+ if self._mcp_component_fn is not None:
876
+ try:
877
+ self._mcp_component_fn(route, resource)
878
+ logger.debug(f"Resource {resource_uri} customized by component_fn")
879
+ except Exception as e:
880
+ logger.warning(
881
+ f"Error in component_fn for resource {resource_uri}: {e}. "
882
+ f"Using component as-is."
883
+ )
884
+
885
  # Register the resource by directly assigning to the resources dictionary
886
  self._resource_manager._resources[str(resource.uri)] = resource
887
  logger.debug(
 
944
  tags=set(route.tags or []),
945
  timeout=self._timeout,
946
  )
947
+
948
+ # Call component_fn if provided
949
+ if self._mcp_component_fn is not None:
950
+ try:
951
+ self._mcp_component_fn(route, template)
952
+ logger.debug(f"Template {uri_template_str} customized by component_fn")
953
+ except Exception as e:
954
+ logger.warning(
955
+ f"Error in component_fn for template {uri_template_str}: {e}. "
956
+ f"Using component as-is."
957
+ )
958
+
959
  # Register the template by directly assigning to the templates dictionary
960
  self._resource_manager._templates[uri_template_str] = template
961
  logger.debug(
src/fastmcp/server/server.py CHANGED
@@ -63,7 +63,9 @@ from fastmcp.utilities.mcp_config import MCPConfig
63
  if TYPE_CHECKING:
64
  from fastmcp.client import Client
65
  from fastmcp.client.transports import ClientTransport
 
66
  from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
 
67
  from fastmcp.server.proxy import FastMCPProxy
68
  logger = get_logger(__name__)
69
 
@@ -1141,6 +1143,8 @@ class FastMCP(Generic[LifespanResultT]):
1141
  openapi_spec: dict[str, Any],
1142
  client: httpx.AsyncClient,
1143
  route_maps: list[RouteMap] | None = None,
 
 
1144
  all_routes_as_tools: bool = False,
1145
  **settings: Any,
1146
  ) -> FastMCPOpenAPI:
@@ -1168,6 +1172,8 @@ class FastMCP(Generic[LifespanResultT]):
1168
  openapi_spec=openapi_spec,
1169
  client=client,
1170
  route_maps=route_maps,
 
 
1171
  **settings,
1172
  )
1173
 
@@ -1177,6 +1183,8 @@ class FastMCP(Generic[LifespanResultT]):
1177
  app: Any,
1178
  name: str | None = None,
1179
  route_maps: list[RouteMap] | None = None,
 
 
1180
  all_routes_as_tools: bool = False,
1181
  **settings: Any,
1182
  ) -> FastMCPOpenAPI:
@@ -1212,6 +1220,8 @@ class FastMCP(Generic[LifespanResultT]):
1212
  client=client,
1213
  name=name,
1214
  route_maps=route_maps,
 
 
1215
  **settings,
1216
  )
1217
 
 
63
  if TYPE_CHECKING:
64
  from fastmcp.client import Client
65
  from fastmcp.client.transports import ClientTransport
66
+ from fastmcp.server.openapi import ComponentFn as OpenAPIComponentFn
67
  from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
68
+ from fastmcp.server.openapi import RouteMapFn as OpenAPIRouteMapFn
69
  from fastmcp.server.proxy import FastMCPProxy
70
  logger = get_logger(__name__)
71
 
 
1143
  openapi_spec: dict[str, Any],
1144
  client: httpx.AsyncClient,
1145
  route_maps: list[RouteMap] | None = None,
1146
+ route_map_fn: OpenAPIRouteMapFn | None = None,
1147
+ mcp_component_fn: OpenAPIComponentFn | None = None,
1148
  all_routes_as_tools: bool = False,
1149
  **settings: Any,
1150
  ) -> FastMCPOpenAPI:
 
1172
  openapi_spec=openapi_spec,
1173
  client=client,
1174
  route_maps=route_maps,
1175
+ route_map_fn=route_map_fn,
1176
+ mcp_component_fn=mcp_component_fn,
1177
  **settings,
1178
  )
1179
 
 
1183
  app: Any,
1184
  name: str | None = None,
1185
  route_maps: list[RouteMap] | None = None,
1186
+ route_map_fn: OpenAPIRouteMapFn | None = None,
1187
+ mcp_component_fn: OpenAPIComponentFn | None = None,
1188
  all_routes_as_tools: bool = False,
1189
  **settings: Any,
1190
  ) -> FastMCPOpenAPI:
 
1220
  client=client,
1221
  name=name,
1222
  route_maps=route_maps,
1223
+ route_map_fn=route_map_fn,
1224
+ mcp_component_fn=mcp_component_fn,
1225
  **settings,
1226
  )
1227
 
tests/server/openapi/test_route_map_fn.py ADDED
@@ -0,0 +1,377 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the route_map_fn and component_fn functionality in FastMCPOpenAPI."""
2
+
3
+ import httpx
4
+ import pytest
5
+
6
+ from fastmcp.server.openapi import FastMCPOpenAPI, MCPType
7
+
8
+
9
+ @pytest.fixture
10
+ def sample_openapi_spec():
11
+ """Sample OpenAPI spec for testing."""
12
+ return {
13
+ "openapi": "3.0.0",
14
+ "info": {"title": "Test API", "version": "1.0.0"},
15
+ "paths": {
16
+ "/users": {
17
+ "get": {
18
+ "summary": "List users",
19
+ "operationId": "listUsers",
20
+ "responses": {"200": {"description": "Success"}},
21
+ }
22
+ },
23
+ "/users/{id}": {
24
+ "get": {
25
+ "summary": "Get user by ID",
26
+ "operationId": "getUserById",
27
+ "parameters": [
28
+ {
29
+ "name": "id",
30
+ "in": "path",
31
+ "required": True,
32
+ "schema": {"type": "string"},
33
+ }
34
+ ],
35
+ "responses": {"200": {"description": "Success"}},
36
+ }
37
+ },
38
+ "/admin/settings": {
39
+ "get": {
40
+ "summary": "Get admin settings",
41
+ "operationId": "getAdminSettings",
42
+ "responses": {"200": {"description": "Success"}},
43
+ },
44
+ "post": {
45
+ "summary": "Update admin settings",
46
+ "operationId": "updateAdminSettings",
47
+ "requestBody": {
48
+ "content": {"application/json": {"schema": {"type": "object"}}}
49
+ },
50
+ "responses": {"200": {"description": "Success"}},
51
+ },
52
+ },
53
+ "/api/data": {
54
+ "get": {
55
+ "summary": "Get data",
56
+ "operationId": "getData",
57
+ "responses": {"200": {"description": "Success"}},
58
+ }
59
+ },
60
+ },
61
+ }
62
+
63
+
64
+ @pytest.fixture
65
+ def http_client():
66
+ """HTTP client for testing."""
67
+ return httpx.AsyncClient()
68
+
69
+
70
+ def test_route_map_fn_none(sample_openapi_spec, http_client):
71
+ """Test that server works correctly when route_map_fn is None."""
72
+ server = FastMCPOpenAPI(
73
+ openapi_spec=sample_openapi_spec,
74
+ client=http_client,
75
+ name="Test Server",
76
+ route_map_fn=None, # Explicitly set to None
77
+ )
78
+
79
+ assert server.name == "Test Server"
80
+
81
+
82
+ def test_route_map_fn_custom_type_conversion(sample_openapi_spec, http_client):
83
+ """Test that route_map_fn can convert route types."""
84
+
85
+ def admin_routes_to_tools(route, mcp_type):
86
+ """Convert all admin routes to tools."""
87
+ if "/admin/" in route.path:
88
+ return MCPType.TOOL
89
+ return None
90
+
91
+ server = FastMCPOpenAPI(
92
+ openapi_spec=sample_openapi_spec,
93
+ client=http_client,
94
+ name="Test Server",
95
+ route_map_fn=admin_routes_to_tools,
96
+ )
97
+
98
+ # Admin GET route should be converted to tool instead of resource
99
+ tools = server._tool_manager._tools
100
+ assert "getAdminSettings" in tools
101
+
102
+ # Admin POST route should still be a tool (was already)
103
+ assert "updateAdminSettings" in tools
104
+
105
+
106
+ def test_component_fn_customization(sample_openapi_spec, http_client):
107
+ """Test that component_fn can customize components."""
108
+
109
+ def customize_components(route, component):
110
+ """Customize components based on route."""
111
+ from fastmcp.server.openapi import OpenAPIResource, OpenAPITool
112
+
113
+ # Add custom tags to all components
114
+ component.tags.add("custom")
115
+
116
+ # Modify tool descriptions
117
+ if isinstance(component, OpenAPITool):
118
+ component.description = (component.description or "") + " [CUSTOMIZED TOOL]"
119
+
120
+ # Modify resource descriptions
121
+ if isinstance(component, OpenAPIResource):
122
+ component.description = (
123
+ component.description or ""
124
+ ) + " [CUSTOMIZED RESOURCE]"
125
+
126
+ server = FastMCPOpenAPI(
127
+ openapi_spec=sample_openapi_spec,
128
+ client=http_client,
129
+ name="Test Server",
130
+ mcp_component_fn=customize_components,
131
+ )
132
+
133
+ # Check that components were customized
134
+ tools = server._tool_manager._tools
135
+ resources = server._resource_manager._resources
136
+
137
+ # Tools should have custom tags and modified descriptions
138
+ for tool in tools.values():
139
+ assert "custom" in tool.tags
140
+ assert "[CUSTOMIZED TOOL]" in (tool.description or "")
141
+
142
+ # Resources should have custom tags and modified descriptions
143
+ for resource in resources.values():
144
+ assert "custom" in resource.tags
145
+ assert "[CUSTOMIZED RESOURCE]" in (resource.description or "")
146
+
147
+
148
+ def test_route_map_fn_returns_none(sample_openapi_spec, http_client):
149
+ """Test that route_map_fn returning None uses defaults."""
150
+
151
+ def always_return_none(route, mcp_type):
152
+ """Always return None to use defaults."""
153
+ return None
154
+
155
+ server = FastMCPOpenAPI(
156
+ openapi_spec=sample_openapi_spec,
157
+ client=http_client,
158
+ name="Test Server",
159
+ route_map_fn=always_return_none,
160
+ )
161
+
162
+ # Should have default behavior
163
+ assert server.name == "Test Server"
164
+ # Check that components were created with default types
165
+ tools = server._tool_manager._tools
166
+ resources = server._resource_manager._resources
167
+ templates = server._resource_manager._templates
168
+
169
+ # Should have tools, resources, and templates based on default mapping
170
+ assert len(tools) > 0
171
+ assert len(resources) > 0
172
+ assert len(templates) > 0
173
+
174
+
175
+ def test_route_map_fn_called_for_excluded_routes(sample_openapi_spec, http_client):
176
+ """Test that route_map_fn is called for excluded routes and can rescue them."""
177
+
178
+ from fastmcp.server.openapi import RouteMap
179
+
180
+ # Exclude all admin routes
181
+ route_maps = [
182
+ RouteMap(
183
+ methods=["GET", "POST"], pattern=r".*/admin/.*", mcp_type=MCPType.EXCLUDE
184
+ )
185
+ ]
186
+
187
+ called_routes = []
188
+
189
+ def track_calls_and_rescue(route, mcp_type):
190
+ """Track which routes the function is called for and rescue some excluded routes."""
191
+ called_routes.append(route.path)
192
+
193
+ # Rescue the admin GET route by converting it to a tool
194
+ if route.path == "/admin/settings" and route.method == "GET":
195
+ return MCPType.TOOL
196
+
197
+ return None # Accept the assignment for other routes
198
+
199
+ server = FastMCPOpenAPI(
200
+ openapi_spec=sample_openapi_spec,
201
+ client=http_client,
202
+ name="Test Server",
203
+ route_maps=route_maps,
204
+ route_map_fn=track_calls_and_rescue,
205
+ )
206
+
207
+ # route_map_fn should now be called for all routes, including excluded admin routes
208
+ assert "/admin/settings" in called_routes
209
+ assert "/users" in called_routes
210
+ assert "/users/{id}" in called_routes
211
+ assert "/api/data" in called_routes
212
+
213
+ # The rescued admin GET route should now be a tool
214
+ tools = server._tool_manager._tools
215
+ assert "getAdminSettings" in tools
216
+
217
+ # The admin POST route should still be excluded (not rescued)
218
+ assert "updateAdminSettings" not in tools
219
+
220
+
221
+ def test_route_map_fn_error_handling(sample_openapi_spec, http_client):
222
+ """Test that errors in route_map_fn are handled gracefully."""
223
+
224
+ def error_function(route, mcp_type):
225
+ """Function that raises an error."""
226
+ if route.path == "/users":
227
+ raise ValueError("Test error")
228
+ return None
229
+
230
+ # Should not raise an error, but log a warning
231
+ server = FastMCPOpenAPI(
232
+ openapi_spec=sample_openapi_spec,
233
+ client=http_client,
234
+ name="Test Server",
235
+ route_map_fn=error_function,
236
+ )
237
+
238
+ # Server should still be created successfully
239
+ assert server.name == "Test Server"
240
+
241
+
242
+ def test_component_fn_error_handling(sample_openapi_spec, http_client):
243
+ """Test that errors in component_fn are handled gracefully."""
244
+
245
+ def error_function(route, component):
246
+ """Function that raises an error."""
247
+ if route.path == "/users":
248
+ raise ValueError("Test error in component_fn")
249
+
250
+ # Should not raise an error, but log a warning
251
+ server = FastMCPOpenAPI(
252
+ openapi_spec=sample_openapi_spec,
253
+ client=http_client,
254
+ name="Test Server",
255
+ mcp_component_fn=error_function,
256
+ )
257
+
258
+ # Server should still be created successfully
259
+ assert server.name == "Test Server"
260
+
261
+
262
+ def test_combined_route_map_fn_and_component_fn(sample_openapi_spec, http_client):
263
+ """Test using both route_map_fn and component_fn together."""
264
+
265
+ def route_mapper(route, mcp_type):
266
+ """Convert admin routes to tools."""
267
+ if "/admin/" in route.path:
268
+ return MCPType.TOOL
269
+ return None
270
+
271
+ def component_customizer(route, component):
272
+ """Add admin tag to admin components."""
273
+ if "/admin/" in route.path:
274
+ component.tags.add("admin")
275
+
276
+ server = FastMCPOpenAPI(
277
+ openapi_spec=sample_openapi_spec,
278
+ client=http_client,
279
+ name="Test Server",
280
+ route_map_fn=route_mapper,
281
+ mcp_component_fn=component_customizer,
282
+ )
283
+
284
+ # Check that both functions worked
285
+ tools = server._tool_manager._tools
286
+
287
+ # Admin GET route should be converted to tool
288
+ assert "getAdminSettings" in tools
289
+ admin_tool = tools["getAdminSettings"]
290
+ assert "admin" in admin_tool.tags
291
+
292
+ # Admin POST route should have admin tag
293
+ admin_post_tool = tools["updateAdminSettings"]
294
+ assert "admin" in admin_post_tool.tags
295
+
296
+
297
+ def test_route_map_fn_signature_validation():
298
+ """Test that route_map_fn has the correct signature."""
299
+ from fastmcp.server.openapi import RouteMapFn
300
+ from fastmcp.utilities import openapi
301
+
302
+ # This is more of a type checking test
303
+ def valid_route_map_fn(
304
+ route: openapi.HTTPRoute, mcp_type: MCPType
305
+ ) -> MCPType | None:
306
+ return None
307
+
308
+ # Should be assignable to RouteMapFn type
309
+ fn: RouteMapFn = valid_route_map_fn
310
+ assert callable(fn)
311
+
312
+
313
+ def test_component_fn_signature_validation():
314
+ """Test that component_fn has the correct signature."""
315
+ from fastmcp.server.openapi import (
316
+ ComponentFn,
317
+ OpenAPIResource,
318
+ OpenAPIResourceTemplate,
319
+ OpenAPITool,
320
+ )
321
+ from fastmcp.utilities import openapi
322
+
323
+ # This is more of a type checking test
324
+ def valid_component_fn(
325
+ route: openapi.HTTPRoute,
326
+ component: OpenAPITool | OpenAPIResource | OpenAPIResourceTemplate,
327
+ ) -> None:
328
+ pass
329
+
330
+ # Should be assignable to ComponentFn type
331
+ fn: ComponentFn = valid_component_fn
332
+ assert callable(fn)
333
+
334
+
335
+ def test_route_map_fn_can_rescue_excluded_routes(sample_openapi_spec, http_client):
336
+ """Test that route_map_fn can rescue routes that were excluded by RouteMap."""
337
+
338
+ from fastmcp.server.openapi import RouteMap
339
+
340
+ # Exclude ALL routes by default
341
+ route_maps = [
342
+ RouteMap(mcp_type=MCPType.EXCLUDE) # Catch-all exclusion
343
+ ]
344
+
345
+ def rescue_users_routes(route, mcp_type):
346
+ """Rescue only user-related routes."""
347
+ if "/users" in route.path:
348
+ # Rescue user routes as tools
349
+ return MCPType.TOOL
350
+ # Let everything else stay excluded
351
+ return None
352
+
353
+ server = FastMCPOpenAPI(
354
+ openapi_spec=sample_openapi_spec,
355
+ client=http_client,
356
+ name="Test Server",
357
+ route_maps=route_maps,
358
+ route_map_fn=rescue_users_routes,
359
+ )
360
+
361
+ # Only user routes should be rescued as tools
362
+ tools = server._tool_manager._tools
363
+ resources = server._resource_manager._resources
364
+ templates = server._resource_manager._templates
365
+
366
+ # Should have user-related tools
367
+ assert "listUsers" in tools
368
+ assert "getUserById" in tools
369
+
370
+ # Should have no resources or templates (everything excluded except rescued tools)
371
+ assert len(resources) == 0
372
+ assert len(templates) == 0
373
+
374
+ # Admin and API routes should still be excluded
375
+ assert "getAdminSettings" not in tools
376
+ assert "updateAdminSettings" not in tools
377
+ assert "getData" not in tools