Jeremiah Lowin commited on
Commit
ca8cdf5
·
1 Parent(s): d8b6bed

Split into routemapfn and mcpcomponentfn

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,153 +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
  ### Advanced Route Mapping
122
 
123
  <VersionBadge version="2.5.0" />
124
 
125
- For advanced users who need fine-grained control over route mapping, you can provide a `route_map_fn` callable. This function receives each route that was matched by a route map (and wasn't excluded) along with the assigned MCP type and name, and can return either `None` to accept the defaults or a `(mcp_type, name)` tuple to override the type and/or object name.
 
 
 
 
 
 
 
126
 
127
  ```python
128
- from fastmcp.server.openapi import MCPType
 
129
 
130
- def custom_route_mapper(route, mcp_type, name):
131
- """Custom route mapping function for advanced control."""
132
  # Convert all admin routes to tools regardless of HTTP method
133
  if "/admin/" in route.path:
134
- return MCPType.TOOL, f"admin_{name}"
 
 
 
135
 
136
- # Rename all user-specific routes to have the prefix "user_"
137
- if "/users/{id}" in route.path:
138
- return mcp_type, f"user_{name}"
139
 
140
- # Accept defaults for all other routes
141
  return None
142
 
143
  mcp = FastMCP.from_openapi(
144
- openapi_spec=spec,
145
- client=api_client,
146
  route_map_fn=custom_route_mapper,
147
  )
148
  ```
149
 
150
- The `route_map_fn` receives:
151
- - `route`: The OpenAPI route object with properties like `.method`, `.path`, `.operation_id`, etc.
152
- - `mcp_type`: The assigned `MCPType` (based on route maps)
153
- - `name`: The assigned component name (derived from operation ID or path)
154
 
155
- It should return either:
156
- - `None` to accept the defaults
157
- - `(mcp_type, name)` tuple to override the type and/or name
158
 
159
- <Warning>
160
- The `route_map_fn` is only called for routes that matched a route map and were **not** excluded. It will not be called for routes with `MCPType.EXCLUDE`.
161
- </Warning>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
  ## Request Parameter Handling
164
 
165
- FastMCP carefully handles different types of parameters in OpenAPI requests:
166
 
167
  ### Query Parameters
168
 
169
- 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.
170
 
171
- For example, if you call a tool with these parameters:
172
  ```python
 
173
  await client.call_tool("search_products", {
174
- "category": "electronics", # Will be included
175
- "min_price": 100, # Will be included
176
- "max_price": None, # Will be excluded
177
- "brand": "", # Will be excluded
178
  })
179
- ```
180
 
181
- The resulting HTTP request will only include `category=electronics&min_price=100`.
 
182
 
183
  ### Path Parameters
184
 
185
- 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.
 
 
 
186
 
187
  ```python
188
- # This will work
189
- await client.call_tool("get_product", {"product_id": 123})
190
 
191
- # This will raise ValueError: "Missing required path parameters: {'product_id'}"
192
- await client.call_tool("get_product", {"product_id": None})
193
  ```
194
 
195
- ## Authorization
 
 
196
 
197
- If your API requires authentication, set headers on the client before creating the MCP server.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
  ```python
200
  import httpx
201
  from fastmcp import FastMCP
202
 
203
- # Create a client with authentication
204
  api_client = httpx.AsyncClient(
205
  base_url="https://api.example.com",
206
  headers={"Authorization": "Bearer YOUR_TOKEN"}
207
  )
208
 
209
- # Create an MCP server from your OpenAPI spec
210
- mcp = FastMCP.from_openapi(openapi_spec=spec, client=api_client)
211
  ```
212
-
213
  ## Timeouts
214
 
215
- You can set a timeout for all requests by providing a `timeout` parameter (in seconds):
216
 
217
  ```python
218
  mcp = FastMCP.from_openapi(
219
  openapi_spec=spec,
220
  client=api_client,
221
- timeout=30.0 # 30 second timeout
222
  )
223
  ```
224
 
 
225
  ## FastAPI Integration
226
 
227
  <VersionBadge version="2.0.0" />
228
 
229
- 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)).
230
 
231
  <Tip>
232
  FastMCP does *not* include FastAPI as a dependency; you must install it separately to use this integration.
@@ -236,8 +373,8 @@ FastMCP does *not* include FastAPI as a dependency; you must install it separate
236
  from fastapi import FastAPI
237
  from fastmcp import FastMCP
238
 
239
- # A FastAPI app
240
- app = FastAPI()
241
 
242
  @app.get("/items", tags=["items"])
243
  def list_items():
@@ -251,41 +388,46 @@ def get_item(item_id: int):
251
  def create_item(name: str):
252
  return {"id": 3, "name": name}
253
 
254
- # Create an MCP server from your FastAPI app
255
  mcp = FastMCP.from_fastapi(app=app)
256
 
257
  if __name__ == "__main__":
258
- mcp.run() # Start the MCP server
259
  ```
260
 
261
- ### Configuration Options
 
 
 
262
 
263
- **Timeout**: You can set a timeout for all API requests:
264
 
265
- ```python
266
- # Set a 5 second timeout for all requests
267
- mcp = FastMCP.from_fastapi(app=app, timeout=5.0)
268
- ```
269
 
270
- **Route Mapping**: All the route mapping features (including tags) work with FastAPI apps:
271
 
272
  ```python
273
  from fastmcp.server.openapi import RouteMap, MCPType
274
 
275
- # Use tag-based routing with FastAPI
276
  mcp = FastMCP.from_fastapi(
277
  app=app,
 
 
278
  route_maps=[
279
- RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL, tags={"admin"}),
 
 
280
  RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.EXCLUDE, tags={"internal"}),
281
- ]
 
 
282
  )
283
  ```
284
 
285
- ### Benefits
286
 
287
- - **Leverage existing FastAPI apps** - No need to rewrite your API logic
288
- - **Schema reuse** - FastAPI's Pydantic models and validation are inherited
289
- - **Full feature support** - Works with FastAPI's authentication, dependencies, etc.
290
- - **ASGI transport** - Direct communication without additional HTTP overhead
291
 
 
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,8 +34,15 @@ logger = get_logger(__name__)
33
 
34
  HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
35
 
36
- # Type definition for the route mapping function
37
- RouteMapFn = Callable[[openapi.HTTPRoute, "MCPType", str], tuple["MCPType", str] | None]
 
 
 
 
 
 
 
38
 
39
 
40
  class MCPType(enum.Enum):
@@ -44,7 +52,6 @@ class MCPType(enum.Enum):
44
  TOOL: Convert the route to a callable Tool
45
  RESOURCE: Convert the route to a Resource (typically GET endpoints)
46
  RESOURCE_TEMPLATE: Convert the route to a ResourceTemplate (typically GET with path params)
47
- PROMPT: Convert the route to a Prompt (not yet implemented)
48
  EXCLUDE: Exclude the route from being converted to any MCP component
49
  IGNORE: Deprecated, use EXCLUDE instead
50
  """
@@ -52,7 +59,7 @@ class MCPType(enum.Enum):
52
  TOOL = "TOOL"
53
  RESOURCE = "RESOURCE"
54
  RESOURCE_TEMPLATE = "RESOURCE_TEMPLATE"
55
- PROMPT = "PROMPT"
56
  EXCLUDE = "EXCLUDE"
57
 
58
 
@@ -67,7 +74,6 @@ class RouteType(enum.Enum):
67
  TOOL = "TOOL"
68
  RESOURCE = "RESOURCE"
69
  RESOURCE_TEMPLATE = "RESOURCE_TEMPLATE"
70
- PROMPT = "PROMPT"
71
  IGNORE = "IGNORE"
72
 
73
 
@@ -667,6 +673,7 @@ class FastMCPOpenAPI(FastMCP):
667
  name: str | None = None,
668
  route_maps: list[RouteMap] | None = None,
669
  route_map_fn: RouteMapFn | None = None,
 
670
  timeout: float | None = None,
671
  **settings: Any,
672
  ):
@@ -678,9 +685,12 @@ class FastMCPOpenAPI(FastMCP):
678
  client: httpx AsyncClient for making HTTP requests
679
  name: Optional name for the server
680
  route_maps: Optional list of RouteMap objects defining route mappings
681
- route_map_fn: Optional callable for advanced users to customize route mapping.
682
- Receives (route, mcp_type, name) and returns (mcp_type, name) tuple or None.
683
  Only called on routes that matched a route_map and were not excluded.
 
 
 
684
  timeout: Optional timeout (in seconds) for all requests
685
  **settings: Additional settings for FastMCP
686
  """
@@ -689,6 +699,7 @@ class FastMCPOpenAPI(FastMCP):
689
  self._client = client
690
  self._timeout = timeout
691
  self._route_map_fn = route_map_fn
 
692
 
693
  # Keep track of names to detect collisions
694
  self._used_names = {"tools": set(), "resources": set(), "templates": set()}
@@ -701,18 +712,15 @@ class FastMCPOpenAPI(FastMCP):
701
  # Determine route type based on mappings or default rules
702
  route_type = _determine_route_type(route, route_maps)
703
 
704
- # Generate a default name from the route
705
- component_name = self._generate_default_name(route, route_type)
706
-
707
- # Call route_map_fn if provided and route is not excluded
708
- if self._route_map_fn is not None and route_type != MCPType.EXCLUDE:
709
  try:
710
- result = self._route_map_fn(route, route_type, component_name)
711
  if result is not None:
712
- route_type, component_name = result
713
  logger.debug(
714
  f"Route {route.method} {route.path} mapping customized by route_map_fn: "
715
- f"type={route_type.name}, name={component_name}"
716
  )
717
  except Exception as e:
718
  logger.warning(
@@ -720,17 +728,15 @@ class FastMCPOpenAPI(FastMCP):
720
  f"Using default values."
721
  )
722
 
 
 
 
723
  if route_type == MCPType.TOOL:
724
  self._create_openapi_tool(route, component_name)
725
  elif route_type == MCPType.RESOURCE:
726
  self._create_openapi_resource(route, component_name)
727
  elif route_type == MCPType.RESOURCE_TEMPLATE:
728
  self._create_openapi_template(route, component_name)
729
- elif route_type == MCPType.PROMPT:
730
- # Not implemented yet
731
- logger.warning(
732
- f"PROMPT route type not implemented: {route.method} {route.path}"
733
- )
734
  elif route_type == MCPType.EXCLUDE:
735
  logger.info(f"Excluding route: {route.method} {route.path}")
736
 
@@ -833,6 +839,18 @@ class FastMCPOpenAPI(FastMCP):
833
  tags=set(route.tags or []),
834
  timeout=self._timeout,
835
  )
 
 
 
 
 
 
 
 
 
 
 
 
836
  # Register the tool by directly assigning to the tools dictionary
837
  self._tool_manager._tools[tool_name] = tool
838
  logger.debug(
@@ -866,6 +884,18 @@ class FastMCPOpenAPI(FastMCP):
866
  tags=set(route.tags or []),
867
  timeout=self._timeout,
868
  )
 
 
 
 
 
 
 
 
 
 
 
 
869
  # Register the resource by directly assigning to the resources dictionary
870
  self._resource_manager._resources[str(resource.uri)] = resource
871
  logger.debug(
@@ -928,6 +958,18 @@ class FastMCPOpenAPI(FastMCP):
928
  tags=set(route.tags or []),
929
  timeout=self._timeout,
930
  )
 
 
 
 
 
 
 
 
 
 
 
 
931
  # Register the template by directly assigning to the templates dictionary
932
  self._resource_manager._templates[uri_template_str] = template
933
  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):
 
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
 
 
673
  name: str | None = None,
674
  route_maps: list[RouteMap] | None = None,
675
  route_map_fn: RouteMapFn | None = None,
676
+ mcp_component_fn: ComponentFn | None = None,
677
  timeout: float | None = None,
678
  **settings: Any,
679
  ):
 
685
  client: httpx AsyncClient for making HTTP requests
686
  name: Optional name for the server
687
  route_maps: Optional list of RouteMap objects defining route mappings
688
+ route_map_fn: Optional callable for advanced route type mapping.
689
+ Receives (route, mcp_type) and returns MCPType or None.
690
  Only called on routes that matched a route_map and were not excluded.
691
+ component_fn: Optional callable for component customization.
692
+ Receives (route, component) and can modify the component in-place.
693
+ Called on every created component.
694
  timeout: Optional timeout (in seconds) for all requests
695
  **settings: Additional settings for FastMCP
696
  """
 
699
  self._client = client
700
  self._timeout = timeout
701
  self._route_map_fn = route_map_fn
702
+ self._mcp_component_fn = mcp_component_fn
703
 
704
  # Keep track of names to detect collisions
705
  self._used_names = {"tools": set(), "resources": set(), "templates": set()}
 
712
  # Determine route type based on mappings or default rules
713
  route_type = _determine_route_type(route, route_maps)
714
 
715
+ # Call route_map_fn if provided
716
+ if self._route_map_fn is not None:
 
 
 
717
  try:
718
+ result = self._route_map_fn(route, route_type)
719
  if result is not None:
720
+ route_type = result
721
  logger.debug(
722
  f"Route {route.method} {route.path} mapping customized by route_map_fn: "
723
+ f"type={route_type.name}"
724
  )
725
  except Exception as e:
726
  logger.warning(
 
728
  f"Using default values."
729
  )
730
 
731
+ # Generate a default name from the route
732
+ component_name = self._generate_default_name(route, route_type)
733
+
734
  if route_type == MCPType.TOOL:
735
  self._create_openapi_tool(route, component_name)
736
  elif route_type == MCPType.RESOURCE:
737
  self._create_openapi_resource(route, component_name)
738
  elif route_type == MCPType.RESOURCE_TEMPLATE:
739
  self._create_openapi_template(route, component_name)
 
 
 
 
 
740
  elif route_type == MCPType.EXCLUDE:
741
  logger.info(f"Excluding route: {route.method} {route.path}")
742
 
 
839
  tags=set(route.tags or []),
840
  timeout=self._timeout,
841
  )
842
+
843
+ # Call component_fn if provided
844
+ if self._mcp_component_fn is not None:
845
+ try:
846
+ self._mcp_component_fn(route, tool)
847
+ logger.debug(f"Tool {tool_name} customized by component_fn")
848
+ except Exception as e:
849
+ logger.warning(
850
+ f"Error in component_fn for tool {tool_name}: {e}. "
851
+ f"Using component as-is."
852
+ )
853
+
854
  # Register the tool by directly assigning to the tools dictionary
855
  self._tool_manager._tools[tool_name] = tool
856
  logger.debug(
 
884
  tags=set(route.tags or []),
885
  timeout=self._timeout,
886
  )
887
+
888
+ # Call component_fn if provided
889
+ if self._mcp_component_fn is not None:
890
+ try:
891
+ self._mcp_component_fn(route, resource)
892
+ logger.debug(f"Resource {resource_uri} customized by component_fn")
893
+ except Exception as e:
894
+ logger.warning(
895
+ f"Error in component_fn for resource {resource_uri}: {e}. "
896
+ f"Using component as-is."
897
+ )
898
+
899
  # Register the resource by directly assigning to the resources dictionary
900
  self._resource_manager._resources[str(resource.uri)] = resource
901
  logger.debug(
 
958
  tags=set(route.tags or []),
959
  timeout=self._timeout,
960
  )
961
+
962
+ # Call component_fn if provided
963
+ if self._mcp_component_fn is not None:
964
+ try:
965
+ self._mcp_component_fn(route, template)
966
+ logger.debug(f"Template {uri_template_str} customized by component_fn")
967
+ except Exception as e:
968
+ logger.warning(
969
+ f"Error in component_fn for template {uri_template_str}: {e}. "
970
+ f"Using component as-is."
971
+ )
972
+
973
  # Register the template by directly assigning to the templates dictionary
974
  self._resource_manager._templates[uri_template_str] = template
975
  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, RouteMapFn
 
 
67
  from fastmcp.server.proxy import FastMCPProxy
68
  logger = get_logger(__name__)
69
 
@@ -1141,7 +1143,8 @@ class FastMCP(Generic[LifespanResultT]):
1141
  openapi_spec: dict[str, Any],
1142
  client: httpx.AsyncClient,
1143
  route_maps: list[RouteMap] | None = None,
1144
- route_map_fn: RouteMapFn | None = None,
 
1145
  all_routes_as_tools: bool = False,
1146
  **settings: Any,
1147
  ) -> FastMCPOpenAPI:
@@ -1170,6 +1173,7 @@ class FastMCP(Generic[LifespanResultT]):
1170
  client=client,
1171
  route_maps=route_maps,
1172
  route_map_fn=route_map_fn,
 
1173
  **settings,
1174
  )
1175
 
@@ -1179,7 +1183,8 @@ class FastMCP(Generic[LifespanResultT]):
1179
  app: Any,
1180
  name: str | None = None,
1181
  route_maps: list[RouteMap] | None = None,
1182
- route_map_fn: RouteMapFn | None = None,
 
1183
  all_routes_as_tools: bool = False,
1184
  **settings: Any,
1185
  ) -> FastMCPOpenAPI:
@@ -1216,6 +1221,7 @@ class FastMCP(Generic[LifespanResultT]):
1216
  name=name,
1217
  route_maps=route_maps,
1218
  route_map_fn=route_map_fn,
 
1219
  **settings,
1220
  )
1221
 
 
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:
 
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:
 
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 CHANGED
@@ -1,4 +1,4 @@
1
- """Tests for the route_map_fn functionality in FastMCPOpenAPI."""
2
 
3
  import httpx
4
  import pytest
@@ -82,10 +82,10 @@ def test_route_map_fn_none(sample_openapi_spec, http_client):
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, name):
86
  """Convert all admin routes to tools."""
87
  if "/admin/" in route.path:
88
- return MCPType.TOOL, f"admin_{name}"
89
  return None
90
 
91
  server = FastMCPOpenAPI(
@@ -97,45 +97,58 @@ def test_route_map_fn_custom_type_conversion(sample_openapi_spec, http_client):
97
 
98
  # Admin GET route should be converted to tool instead of resource
99
  tools = server._tool_manager._tools
100
- assert "admin_getAdminSettings" in tools
101
 
102
- # Admin POST route should be renamed
103
- assert "admin_updateAdminSettings" in tools
104
 
105
 
106
- def test_route_map_fn_custom_naming(sample_openapi_spec, http_client):
107
- """Test that route_map_fn can customize naming."""
108
 
109
- def prefix_user_routes(route, mcp_type, name):
110
- """Add user_ prefix to user-related routes."""
111
- if "/users/" in route.path:
112
- return mcp_type, f"user_{name}"
113
- return None
 
 
 
 
 
 
 
 
 
 
 
114
 
115
  server = FastMCPOpenAPI(
116
  openapi_spec=sample_openapi_spec,
117
  client=http_client,
118
  name="Test Server",
119
- route_map_fn=prefix_user_routes,
120
  )
121
 
122
- # Check that user routes got renamed
123
- templates = server._resource_manager._templates
124
- template_names = list(templates.keys())
 
 
 
 
 
125
 
126
- # The getUserById template should be renamed to user_getUserById
127
- found_user_template = False
128
- for uri in template_names:
129
- if "user_getUserById" in uri:
130
- found_user_template = True
131
- break
132
- assert found_user_template
133
 
134
 
135
  def test_route_map_fn_returns_none(sample_openapi_spec, http_client):
136
  """Test that route_map_fn returning None uses defaults."""
137
 
138
- def always_return_none(route, mcp_type, name):
139
  """Always return None to use defaults."""
140
  return None
141
 
@@ -148,7 +161,7 @@ def test_route_map_fn_returns_none(sample_openapi_spec, http_client):
148
 
149
  # Should have default behavior
150
  assert server.name == "Test Server"
151
- # Check that components were created with default names
152
  tools = server._tool_manager._tools
153
  resources = server._resource_manager._resources
154
  templates = server._resource_manager._templates
@@ -159,8 +172,8 @@ def test_route_map_fn_returns_none(sample_openapi_spec, http_client):
159
  assert len(templates) > 0
160
 
161
 
162
- def test_route_map_fn_not_called_for_excluded_routes(sample_openapi_spec, http_client):
163
- """Test that route_map_fn is not called for excluded routes."""
164
 
165
  from fastmcp.server.openapi import RouteMap
166
 
@@ -173,31 +186,42 @@ def test_route_map_fn_not_called_for_excluded_routes(sample_openapi_spec, http_c
173
 
174
  called_routes = []
175
 
176
- def track_calls(route, mcp_type, name):
177
- """Track which routes the function is called for."""
178
  called_routes.append(route.path)
179
- return None
180
 
181
- FastMCPOpenAPI(
 
 
 
 
 
 
182
  openapi_spec=sample_openapi_spec,
183
  client=http_client,
184
  name="Test Server",
185
  route_maps=route_maps,
186
- route_map_fn=track_calls,
187
  )
188
 
189
- # route_map_fn should not be called for excluded admin routes
190
- assert "/admin/settings" not in called_routes
191
- # But should be called for other routes
192
  assert "/users" in called_routes
193
  assert "/users/{id}" in called_routes
194
  assert "/api/data" in called_routes
195
 
 
 
 
 
 
 
 
196
 
197
  def test_route_map_fn_error_handling(sample_openapi_spec, http_client):
198
  """Test that errors in route_map_fn are handled gracefully."""
199
 
200
- def error_function(route, mcp_type, name):
201
  """Function that raises an error."""
202
  if route.path == "/users":
203
  raise ValueError("Test error")
@@ -215,57 +239,59 @@ def test_route_map_fn_error_handling(sample_openapi_spec, http_client):
215
  assert server.name == "Test Server"
216
 
217
 
218
- def test_route_map_fn_with_complex_logic(sample_openapi_spec, http_client):
219
- """Test route_map_fn with complex conditional logic."""
220
 
221
- def complex_mapper(route, mcp_type, name):
222
- """Complex mapping logic."""
223
- # Convert admin routes to tools
224
- if "/admin/" in route.path:
225
- return MCPType.TOOL, f"admin_{name}"
 
 
 
 
 
 
 
 
 
 
226
 
227
- # Convert user parameter routes to templates with custom naming
228
- if "/users/{" in route.path:
229
- return MCPType.RESOURCE_TEMPLATE, f"user_template_{name}"
230
 
231
- # Convert list routes to resources with custom naming
232
- if route.path.endswith("/users") or route.path.endswith("/data"):
233
- return MCPType.RESOURCE, f"list_{name}"
234
 
235
- # Use defaults for everything else
 
 
 
236
  return None
237
 
 
 
 
 
 
238
  server = FastMCPOpenAPI(
239
  openapi_spec=sample_openapi_spec,
240
  client=http_client,
241
  name="Test Server",
242
- route_map_fn=complex_mapper,
 
243
  )
244
 
245
- # Check that the complex logic was applied correctly
246
  tools = server._tool_manager._tools
247
- resources = server._resource_manager._resources
248
- templates = server._resource_manager._templates
249
-
250
- # Admin routes should be tools
251
- assert "admin_getAdminSettings" in tools
252
- assert "admin_updateAdminSettings" in tools
253
 
254
- # List routes should be resources with custom names
255
- found_list_resource = False
256
- for uri in resources.keys():
257
- if "list_listUsers" in uri or "list_getData" in uri:
258
- found_list_resource = True
259
- break
260
- assert found_list_resource
261
 
262
- # User parameter route should be template with custom name
263
- found_user_template = False
264
- for uri in templates.keys():
265
- if "user_template_getUserById" in uri:
266
- found_user_template = True
267
- break
268
- assert found_user_template
269
 
270
 
271
  def test_route_map_fn_signature_validation():
@@ -275,10 +301,77 @@ def test_route_map_fn_signature_validation():
275
 
276
  # This is more of a type checking test
277
  def valid_route_map_fn(
278
- route: openapi.HTTPRoute, mcp_type: MCPType, name: str
279
- ) -> tuple[MCPType, str] | None:
280
  return None
281
 
282
  # Should be assignable to RouteMapFn type
283
  fn: RouteMapFn = valid_route_map_fn
284
  assert callable(fn)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the route_map_fn and component_fn functionality in FastMCPOpenAPI."""
2
 
3
  import httpx
4
  import pytest
 
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(
 
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
 
 
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
 
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
 
 
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")
 
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():
 
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