Spaces:
Running
Running
Merge pull request #566 from jlowin/route_map_fn
Browse files- README.md +3 -3
- docs/getting-started/welcome.mdx +4 -8
- docs/servers/openapi.mdx +265 -82
- src/fastmcp/server/openapi.py +76 -10
- src/fastmcp/server/server.py +10 -0
- tests/server/openapi/test_route_map_fn.py +377 -0
README.md
CHANGED
|
@@ -15,11 +15,11 @@
|
|
| 15 |
> [!NOTE]
|
| 16 |
> #### FastMCP 2.0 & The Official MCP SDK
|
| 17 |
>
|
| 18 |
-
>
|
| 19 |
>
|
| 20 |
-
> **
|
| 21 |
>
|
| 22 |
-
> FastMCP 2.0 is the
|
| 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
|
| 28 |
|
| 29 |
-
|
| 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 |
-
|
| 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.
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
import httpx
|
| 15 |
from fastmcp import FastMCP
|
| 16 |
|
| 17 |
-
# Create
|
| 18 |
-
|
| 19 |
|
| 20 |
-
# Load your OpenAPI spec
|
| 21 |
-
|
| 22 |
|
| 23 |
-
# Create
|
| 24 |
-
mcp = FastMCP.from_openapi(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 37 |
-
|
| 38 |
-
|
|
| 39 |
-
|
|
| 40 |
-
| `
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
|
|
|
| 42 |
|
| 43 |
-
|
| 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 (
|
| 49 |
|
| 50 |
-
|
| 51 |
|
| 52 |
```python
|
| 53 |
from fastmcp.server.openapi import RouteMap, MCPType
|
| 54 |
|
| 55 |
-
# Default route mappings
|
| 56 |
DEFAULT_ROUTE_MAPPINGS = [
|
| 57 |
-
|
|
|
|
| 58 |
RouteMap(
|
| 59 |
methods=["GET"],
|
| 60 |
pattern=r".*\{.*\}.*",
|
| 61 |
-
tags={},
|
| 62 |
mcp_type=MCPType.RESOURCE_TEMPLATE
|
| 63 |
),
|
| 64 |
-
|
|
|
|
| 65 |
RouteMap(
|
| 66 |
methods=["GET"],
|
| 67 |
pattern=r".*",
|
| 68 |
-
tags={},
|
| 69 |
mcp_type=MCPType.RESOURCE
|
| 70 |
),
|
| 71 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 84 |
|
| 85 |
-
```python {
|
|
|
|
| 86 |
from fastmcp.server.openapi import RouteMap, MCPType
|
| 87 |
|
| 88 |
mcp = FastMCP.from_openapi(
|
| 89 |
-
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
route_maps=[
|
| 92 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
-
|
| 110 |
|
| 111 |
-
|
| 112 |
|
| 113 |
-
|
|
|
|
|
|
|
| 114 |
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
<VersionBadge version="2.5.0" />
|
| 118 |
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
## Request Parameter Handling
|
| 123 |
|
| 124 |
-
FastMCP
|
| 125 |
|
| 126 |
### Query Parameters
|
| 127 |
|
| 128 |
-
By default, FastMCP
|
| 129 |
|
| 130 |
-
For example, if you call a tool with these parameters:
|
| 131 |
```python
|
|
|
|
| 132 |
await client.call_tool("search_products", {
|
| 133 |
-
"category": "electronics", #
|
| 134 |
-
"min_price": 100, #
|
| 135 |
-
"max_price": None, #
|
| 136 |
-
"brand": "", #
|
| 137 |
})
|
| 138 |
-
```
|
| 139 |
|
| 140 |
-
The
|
|
|
|
| 141 |
|
| 142 |
### Path Parameters
|
| 143 |
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
```python
|
| 147 |
-
#
|
| 148 |
-
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
-
#
|
| 151 |
-
|
| 152 |
```
|
| 153 |
|
| 154 |
-
##
|
|
|
|
|
|
|
| 155 |
|
| 156 |
-
|
|
|
|
|
|
|
| 157 |
|
| 158 |
```python
|
| 159 |
import httpx
|
| 160 |
from fastmcp import FastMCP
|
| 161 |
|
| 162 |
-
#
|
| 163 |
api_client = httpx.AsyncClient(
|
| 164 |
base_url="https://api.example.com",
|
| 165 |
headers={"Authorization": "Bearer YOUR_TOKEN"}
|
| 166 |
)
|
| 167 |
|
| 168 |
-
# Create
|
| 169 |
-
mcp = FastMCP.from_openapi(
|
| 170 |
```
|
| 171 |
-
|
| 172 |
## Timeouts
|
| 173 |
|
| 174 |
-
|
| 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
|
| 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 |
-
#
|
| 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 |
-
#
|
| 214 |
mcp = FastMCP.from_fastapi(app=app)
|
| 215 |
|
| 216 |
if __name__ == "__main__":
|
| 217 |
-
mcp.run() #
|
| 218 |
```
|
| 219 |
|
| 220 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 230 |
|
| 231 |
```python
|
| 232 |
from fastmcp.server.openapi import RouteMap, MCPType
|
| 233 |
|
| 234 |
-
#
|
| 235 |
mcp = FastMCP.from_fastapi(
|
| 236 |
app=app,
|
|
|
|
|
|
|
| 237 |
route_maps=[
|
| 238 |
-
|
|
|
|
|
|
|
| 239 |
RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.EXCLUDE, tags={"internal"}),
|
| 240 |
-
]
|
|
|
|
|
|
|
| 241 |
)
|
| 242 |
```
|
| 243 |
|
| 244 |
-
### Benefits
|
| 245 |
|
| 246 |
-
- **
|
| 247 |
-
- **Schema
|
| 248 |
-
- **
|
| 249 |
-
- **
|
| 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,
|
| 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
|