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