Jeremiah Lowin commited on
Commit
7a8ed45
·
unverified ·
2 Parent(s): 85e9cfdbc43da5

Merge branch 'main' into mask-errors

Browse files
docs/clients/client.mdx CHANGED
@@ -37,7 +37,7 @@ Clients must be initialized with a `transport`. You can either provide an alread
37
  The following inference rules are used to determine the appropriate `ClientTransport` based on the input type:
38
 
39
  1. **`ClientTransport` Instance**: If you provide an already instantiated transport object, it's used directly.
40
- 2. **`FastMCP` Instance**: Creates a `FastMCPTransport` for efficient in-memory communication (ideal for testing).
41
  3. **`Path` or `str` pointing to an existing file**:
42
  * If it ends with `.py`: Creates a `PythonStdioTransport` to run the script using `python`.
43
  * If it ends with `.js`: Creates a `NodeStdioTransport` to run the script using `node`.
@@ -91,7 +91,7 @@ For more control over connection details (like headers for SSE, environment vari
91
 
92
  ### Multi-Server Clients
93
 
94
- <VersionBadge version="2.3.6" />
95
 
96
  FastMCP supports creating clients that connect to multiple MCP servers through a single client interface using a standard MCP configuration format (`MCPConfig`). This configuration approach makes it easy to connect to multiple specialized servers or create composable systems with a simple, declarative syntax.
97
 
 
37
  The following inference rules are used to determine the appropriate `ClientTransport` based on the input type:
38
 
39
  1. **`ClientTransport` Instance**: If you provide an already instantiated transport object, it's used directly.
40
+ 2. **`FastMCP` Instance**: Creates a `FastMCPTransport` for efficient in-memory communication (ideal for testing). This also works with a **FastMCP 1.0 server** created via `mcp.server.fastmcp.FastMCP`.
41
  3. **`Path` or `str` pointing to an existing file**:
42
  * If it ends with `.py`: Creates a `PythonStdioTransport` to run the script using `python`.
43
  * If it ends with `.js`: Creates a `NodeStdioTransport` to run the script using `node`.
 
91
 
92
  ### Multi-Server Clients
93
 
94
+ <VersionBadge version="2.4.0" />
95
 
96
  FastMCP supports creating clients that connect to multiple MCP servers through a single client interface using a standard MCP configuration format (`MCPConfig`). This configuration approach makes it easy to connect to multiple specialized servers or create composable systems with a simple, declarative syntax.
97
 
docs/clients/transports.mdx CHANGED
@@ -290,8 +290,8 @@ asyncio.run(main())
290
  ### FastMCP Transport
291
 
292
  - **Class:** `fastmcp.client.transports.FastMCPTransport`
293
- - **Inferred From:** An instance of `fastmcp.server.FastMCP`
294
- - **Use Case:** Connecting directly to a `FastMCP` server instance in the same Python process
295
 
296
  This is extremely useful for testing your FastMCP servers.
297
 
@@ -323,7 +323,7 @@ Communication happens through efficient in-memory queues, making it very fast an
323
 
324
  ### MCPConfig Transport
325
 
326
- <VersionBadge version="2.3.6" />
327
 
328
  - **Class:** `fastmcp.client.transports.MCPConfigTransport`
329
  - **Inferred From:** An instance of `MCPConfig` or a dictionary matching the MCPConfig schema
 
290
  ### FastMCP Transport
291
 
292
  - **Class:** `fastmcp.client.transports.FastMCPTransport`
293
+ - **Inferred From:** An instance of `fastmcp.server.FastMCP` or a **FastMCP 1.0 server** (`mcp.server.fastmcp.FastMCP`)
294
+ - **Use Case:** Connecting directly to a FastMCP server instance in the same Python process
295
 
296
  This is extremely useful for testing your FastMCP servers.
297
 
 
323
 
324
  ### MCPConfig Transport
325
 
326
+ <VersionBadge version="2.4.0" />
327
 
328
  - **Class:** `fastmcp.client.transports.MCPConfigTransport`
329
  - **Inferred From:** An instance of `MCPConfig` or a dictionary matching the MCPConfig schema
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/getting-started/installation.mdx CHANGED
@@ -60,6 +60,20 @@ mcp = FastMCP("My MCP Server")
60
  Prior to `fastmcp==2.3.0` and `mcp==1.8.0`, the 2.x API always mirrored the 1.0 API. However, as the projects diverge, this can not be guaranteed. You may see deprecation warnings if you attempt to use 1.0 APIs in FastMCP 2.x. Please refer to this documentation for details on new capabilities.
61
  </Warning>
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  ## Installing for Development
64
 
65
  If you plan to contribute to FastMCP, you should begin by cloning the repository and using uv to install all dependencies (development dependencies are installed automatically):
 
60
  Prior to `fastmcp==2.3.0` and `mcp==1.8.0`, the 2.x API always mirrored the 1.0 API. However, as the projects diverge, this can not be guaranteed. You may see deprecation warnings if you attempt to use 1.0 APIs in FastMCP 2.x. Please refer to this documentation for details on new capabilities.
61
  </Warning>
62
 
63
+ ## Versioning and Breaking Changes
64
+
65
+ While we make every effort not to introduce backwards-incompatible changes to our public APIs and behavior, FastMCP exists in a rapidly evolving MCP landscape. We're committed to bringing the most cutting-edge features to our users, which occasionally necessitates changes to existing functionality.
66
+
67
+ As a practice, breaking changes will only occur on minor version changes (e.g., 2.3.x to 2.4.0). A minor version change indicates either:
68
+ - A significant new feature set that warrants a new minor version
69
+ - Introducing breaking changes that may affect behavior on upgrade
70
+
71
+ For users concerned about stability in production environments, we recommend pinning FastMCP to a specific version in your dependencies.
72
+
73
+ Whenever possible, FastMCP will issue deprecation warnings when users attempt to use APIs that are either deprecated or destined for future removal. These warnings will be maintained for at least 1 minor version release, and may be maintained longer.
74
+
75
+ Note that the "public API" includes the core functionality of the `FastMCP` server and its methods. It does not include private methods or objects that are stored as private attributes, as we do not expect users to rely on those implementation details.
76
+
77
  ## Installing for Development
78
 
79
  If you plan to contribute to FastMCP, you should begin by cloning the repository and using uv to install all dependencies (development dependencies are installed automatically):
docs/patterns/fastapi.mdx CHANGED
@@ -8,19 +8,18 @@ import { VersionBadge } from '/snippets/version-badge.mdx'
8
 
9
  <VersionBadge version="2.0.0" />
10
 
 
 
 
11
 
12
- FastMCP can automatically convert FastAPI applications into MCP servers.
13
-
14
- <Tip>
15
- FastMCP does *not* include FastAPI as a dependency; you must install it separately to run these examples.
16
- </Tip>
17
 
 
18
 
19
- ```python {2, 22, 25}
20
  from fastapi import FastAPI
21
  from fastmcp import FastMCP
22
 
23
-
24
  # A FastAPI app
25
  app = FastAPI()
26
 
@@ -36,7 +35,6 @@ def get_item(item_id: int):
36
  def create_item(name: str):
37
  return {"id": 3, "name": name}
38
 
39
-
40
  # Create an MCP server from your FastAPI app
41
  mcp = FastMCP.from_fastapi(app=app)
42
 
@@ -44,101 +42,6 @@ if __name__ == "__main__":
44
  mcp.run() # Start the MCP server
45
  ```
46
 
47
- ## Configuration Options
48
-
49
- ### Timeout
50
-
51
- You can set a timeout for all API requests:
52
-
53
- ```python
54
- # Set a 5 second timeout for all requests
55
- mcp = FastMCP.from_fastapi(app=app, timeout=5.0)
56
- ```
57
-
58
- This timeout is applied to all requests made by tools, resources, and resource templates.
59
-
60
- ## Route Mapping
61
-
62
- By default, FastMCP will map FastAPI routes to MCP components according to the following rules:
63
-
64
- | FastAPI Route Type | FastAPI Example | MCP Component | Notes |
65
- |--------------------|--------------|---------|-------|
66
- | GET without path params | `@app.get("/stats")` | Resource | Simple resources for fetching data |
67
- | GET with path params | `@app.get("/users/{id}")` | Resource Template | Path parameters become template parameters |
68
- | POST, PUT, DELETE, etc. | `@app.post("/users")` | Tool | Operations that modify data |
69
-
70
- For more details on route mapping or custom mapping rules, see the [OpenAPI integration documentation](/patterns/openapi#route-mapping); FastMCP uses the same mapping rules for both FastAPI and OpenAPI integrations.
71
-
72
- ## Complete Example
73
-
74
- Here's a more detailed example with a data model:
75
-
76
- ```python [expandable]
77
- import asyncio
78
- from fastapi import FastAPI, HTTPException
79
- from pydantic import BaseModel
80
- from fastmcp import FastMCP, Client
81
-
82
- # Define your Pydantic model
83
- class Item(BaseModel):
84
- name: str
85
- price: float
86
-
87
- # Create your FastAPI app
88
- app = FastAPI()
89
- items = {} # In-memory database
90
-
91
- @app.get("/items")
92
- def list_items():
93
- """List all items"""
94
- return list(items.values())
95
-
96
- @app.get("/items/{item_id}")
97
- def get_item(item_id: int):
98
- """Get item by ID"""
99
- if item_id not in items:
100
- raise HTTPException(404, "Item not found")
101
- return items[item_id]
102
-
103
- @app.post("/items")
104
- def create_item(item: Item):
105
- """Create a new item"""
106
- item_id = len(items) + 1
107
- items[item_id] = {"id": item_id, **item.model_dump()}
108
- return items[item_id]
109
-
110
- # Test your MCP server with a client
111
- async def check_mcp(mcp: FastMCP):
112
- # List the components that were created
113
- tools = await mcp.get_tools()
114
- resources = await mcp.get_resources()
115
- templates = await mcp.get_resource_templates()
116
-
117
- print(
118
- f"{len(tools)} Tool(s): {', '.join([t.name for t in tools.values()])}"
119
- )
120
- print(
121
- f"{len(resources)} Resource(s): {', '.join([r.name for r in resources.values()])}"
122
- )
123
- print(
124
- f"{len(templates)} Resource Template(s): {', '.join([t.name for t in templates.values()])}"
125
- )
126
-
127
- return mcp
128
-
129
- if __name__ == "__main__":
130
- # Create MCP server from FastAPI app
131
- mcp = FastMCP.from_fastapi(app=app)
132
-
133
- asyncio.run(check_mcp(mcp))
134
-
135
- # In a real scenario, you would run the server:
136
- mcp.run()
137
- ```
138
-
139
- ## Benefits
140
-
141
- - **Leverage existing FastAPI apps** - No need to rewrite your API logic
142
- - **Schema reuse** - FastAPI's Pydantic models and validation are inherited
143
- - **Full feature support** - Works with FastAPI's authentication, dependencies, etc.
144
- - **ASGI transport** - Direct communication without additional HTTP overhead
 
8
 
9
  <VersionBadge version="2.0.0" />
10
 
11
+ <Note>
12
+ **Documentation Moved**: The comprehensive FastAPI integration documentation has been moved to the [OpenAPI Integration](/patterns/openapi#fastapi-integration) page, where it's covered alongside all other OpenAPI features including route mapping and tags support.
13
+ </Note>
14
 
15
+ ## Quick Start
 
 
 
 
16
 
17
+ FastMCP can automatically convert FastAPI applications into MCP servers:
18
 
19
+ ```python
20
  from fastapi import FastAPI
21
  from fastmcp import FastMCP
22
 
 
23
  # A FastAPI app
24
  app = FastAPI()
25
 
 
35
  def create_item(name: str):
36
  return {"id": 3, "name": name}
37
 
 
38
  # Create an MCP server from your FastAPI app
39
  mcp = FastMCP.from_fastapi(app=app)
40
 
 
42
  mcp.run() # Start the MCP server
43
  ```
44
 
45
+ <Tip>
46
+ For complete documentation including tag-based routing, route mapping configuration, timeout settings, authentication examples, and advanced configuration options, see the comprehensive [OpenAPI Integration documentation](/patterns/openapi#fastapi-integration).
47
+ </Tip>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/servers/composition.mdx CHANGED
@@ -35,7 +35,7 @@ The choice of importing or mounting depends on your use case and requirements.
35
 
36
  FastMCP supports [MCP proxying](/patterns/proxy), which allows you to mirror a local or remote server in a local FastMCP instance. Proxies are fully compatible with both importing and mounting.
37
 
38
- <VersionBadge version="2.3.6" />
39
 
40
  You can also create proxies from configuration dictionaries that follow the MCPConfig schema, which is useful for quickly connecting to one or more remote servers. See the [Proxy Servers documentation](/servers/proxy#configuration-based-proxies) for details on configuration-based proxying. Note that MCPConfig follows an emerging standard and its format may evolve over time.
41
 
 
35
 
36
  FastMCP supports [MCP proxying](/patterns/proxy), which allows you to mirror a local or remote server in a local FastMCP instance. Proxies are fully compatible with both importing and mounting.
37
 
38
+ <VersionBadge version="2.4.0" />
39
 
40
  You can also create proxies from configuration dictionaries that follow the MCPConfig schema, which is useful for quickly connecting to one or more remote servers. See the [Proxy Servers documentation](/servers/proxy#configuration-based-proxies) for details on configuration-based proxying. Note that MCPConfig follows an emerging standard and its format may evolve over time.
41
 
docs/servers/context.mdx CHANGED
@@ -228,8 +228,8 @@ async def analyze_sentiment(text: str, ctx: Context) -> dict:
228
  # Create a sampling prompt asking for sentiment analysis
229
  prompt = f"Analyze the sentiment of the following text as positive, negative, or neutral. Just output a single word - 'positive', 'negative', or 'neutral'. Text to analyze: {text}"
230
 
231
- # Send the sampling request to the client's LLM
232
- response = await ctx.sample(prompt)
233
 
234
  # Process the LLM's response
235
  sentiment = response.text.strip().lower()
@@ -247,11 +247,12 @@ async def analyze_sentiment(text: str, ctx: Context) -> dict:
247
 
248
  **Method signature:**
249
 
250
- - **`ctx.sample(messages: str | list[str | SamplingMessage], system_prompt: str | None = None, temperature: float | None = None, max_tokens: int | None = None) -> TextContent | ImageContent`**
251
  - `messages`: A string or list of strings/message objects to send to the LLM
252
  - `system_prompt`: Optional system prompt to guide the LLM's behavior
253
  - `temperature`: Optional sampling temperature (controls randomness)
254
  - `max_tokens`: Optional maximum number of tokens to generate (defaults to 512)
 
255
  - Returns the LLM's response as TextContent or ImageContent
256
 
257
  When providing a simple string, it's treated as a user message. For more complex scenarios, you can provide a list of messages with different roles.
 
228
  # Create a sampling prompt asking for sentiment analysis
229
  prompt = f"Analyze the sentiment of the following text as positive, negative, or neutral. Just output a single word - 'positive', 'negative', or 'neutral'. Text to analyze: {text}"
230
 
231
+ # Send the sampling request to the client's LLM (provide a hint for the model you want to use)
232
+ response = await ctx.sample(prompt, model_preferences="claude-3-sonnet")
233
 
234
  # Process the LLM's response
235
  sentiment = response.text.strip().lower()
 
247
 
248
  **Method signature:**
249
 
250
+ - **`ctx.sample(messages: str | list[str | SamplingMessage], system_prompt: str | None = None, temperature: float | None = None, max_tokens: int | None = None, model_preferences: ModelPreferences | str | list[str] | None = None) -> TextContent | ImageContent`**
251
  - `messages`: A string or list of strings/message objects to send to the LLM
252
  - `system_prompt`: Optional system prompt to guide the LLM's behavior
253
  - `temperature`: Optional sampling temperature (controls randomness)
254
  - `max_tokens`: Optional maximum number of tokens to generate (defaults to 512)
255
+ - `model_preferences`: Optional model selection preferences (e.g., a model hint string, list of hints, or a ModelPreferences object)
256
  - Returns the LLM's response as TextContent or ImageContent
257
 
258
  When providing a simple string, it's treated as a user message. For more complex scenarios, you can provide a list of messages with different roles.
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,127 +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 API requests:
35
 
36
- ```python
37
- # Set a 5 second timeout for all requests
38
- mcp = FastMCP.from_openapi(
39
- openapi_spec=spec,
40
- client=api_client,
41
- timeout=5.0
42
- )
43
- ```
44
 
45
- This timeout is applied to all requests made by tools, resources, and resource templates.
46
 
47
- ## Route Mapping
48
 
49
- By default, OpenAPI routes are mapped to MCP components based on these rules:
 
 
 
50
 
51
- | OpenAPI Route | Example |MCP Component | Notes |
52
- |- | - | - | - |
53
- | `GET` without path params | `GET /stats` | Resource | Simple resources for fetching data |
54
- | `GET` with path params | `GET /users/{id}` | Resource Template | Path parameters become template parameters |
55
- | `POST`, `PUT`, `PATCH`, `DELETE`, etc. | `POST /users` | Tool | Operations that modify data |
56
-
57
-
58
- Internally, FastMCP uses a priority-ordered set of `RouteMap` objects to determine the component type. Route maps indicate that a specific HTTP method (or methods) and path pattern should be treated as a specific component type. This is the default set of route maps:
59
 
60
  ```python
61
- # Simplified version of the actual mapping rules
 
 
62
  DEFAULT_ROUTE_MAPPINGS = [
63
  # GET with path parameters -> ResourceTemplate
64
  RouteMap(
65
  methods=["GET"],
66
  pattern=r".*\{.*\}.*",
67
- route_type=RouteType.RESOURCE_TEMPLATE,
 
68
  ),
69
-
70
  # GET without path parameters -> Resource
71
  RouteMap(
72
  methods=["GET"],
73
  pattern=r".*",
74
- route_type=RouteType.RESOURCE,
 
75
  ),
76
-
77
  # All other methods -> Tool
78
  RouteMap(
79
- methods="*",
80
  pattern=r".*",
81
- route_type=RouteType.TOOL,
 
82
  ),
83
  ]
84
  ```
 
85
  ### Custom Route Maps
86
 
87
- 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.
88
 
89
- ```python
90
- from fastmcp.server.openapi import RouteMap, RouteType
91
-
92
- # Custom mapping rules
93
- custom_maps = [
94
- # Force all analytics endpoints to be Tools
95
- RouteMap(methods=["GET"],
96
- pattern=r"^/analytics/.*",
97
- route_type=RouteType.TOOL)
98
- ]
99
 
100
- # Apply custom mappings
101
- mcp = await FastMCP.from_openapi(
102
  openapi_spec=spec,
103
  client=api_client,
104
- route_maps=custom_maps
 
 
 
 
 
 
 
 
 
 
 
 
105
  )
106
  ```
107
 
 
108
 
109
- ### All Routes as Tools
110
 
111
- 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_routes_as_tools` parameter to automatically map every route to a Tool:
112
 
113
- ```python
114
- # Make all endpoints tools, regardless of HTTP method
115
- mcp = FastMCP.from_openapi(
116
- openapi_spec=spec,
117
- client=api_client,
118
- all_routes_as_tools=True
119
- )
120
- ```
121
-
122
- This is equivalent to defining a single route map that matches all routes:
123
 
124
- ```python
125
- # Same effect as all_routes_as_tools=True
126
- mcp = FastMCP.from_openapi(
127
- openapi_spec=spec,
128
- client=api_client,
129
- route_maps=[
130
- RouteMap(methods="*", pattern=r".*", route_type=RouteType.TOOL)
131
- ]
132
- )
133
- ```
134
 
135
- Note that `all_routes_as_tools` and `route_maps` cannot be used together - if you need more complex mapping rules, use `route_maps` instead.
136
 
137
- ## How It Works
138
 
139
- 1. FastMCP parses your OpenAPI spec to extract routes and schemas
140
- 2. It applies mapping rules to categorize each route
141
- 3. When an MCP client calls a tool or accesses a resource:
142
- - FastMCP constructs an HTTP request based on the OpenAPI definition
143
- - It sends the request through the provided httpx client
144
- - It translates the HTTP response to the appropriate MCP format
145
 
146
- ### Request Parameter Handling
147
 
148
  FastMCP carefully handles different types of parameters in OpenAPI requests:
149
 
150
- #### Query Parameters
151
 
152
  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.
153
 
@@ -163,7 +139,7 @@ await client.call_tool("search_products", {
163
 
164
  The resulting HTTP request will only include `category=electronics&min_price=100`.
165
 
166
- #### Path Parameters
167
 
168
  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.
169
 
@@ -175,89 +151,100 @@ await client.call_tool("get_product", {"product_id": 123})
175
  await client.call_tool("get_product", {"product_id": None})
176
  ```
177
 
178
- ## Complete Example
179
 
180
- ```python [expandable]
181
- import asyncio
182
 
 
183
  import httpx
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
 
 
185
  from fastmcp import FastMCP
186
 
187
- # Sample OpenAPI spec for a Pet Store API
188
- petstore_spec = {
189
- "openapi": "3.0.0",
190
- "info": {
191
- "title": "Pet Store API",
192
- "version": "1.0.0",
193
- "description": "A sample API for managing pets",
194
- },
195
- "paths": {
196
- "/pets": {
197
- "get": {
198
- "operationId": "listPets",
199
- "summary": "List all pets",
200
- "responses": {"200": {"description": "A list of pets"}},
201
- },
202
- "post": {
203
- "operationId": "createPet",
204
- "summary": "Create a new pet",
205
- "responses": {"201": {"description": "Pet created successfully"}},
206
- },
207
- },
208
- "/pets/{petId}": {
209
- "get": {
210
- "operationId": "getPet",
211
- "summary": "Get a pet by ID",
212
- "parameters": [
213
- {
214
- "name": "petId",
215
- "in": "path",
216
- "required": True,
217
- "schema": {"type": "string"},
218
- }
219
- ],
220
- "responses": {
221
- "200": {"description": "Pet details"},
222
- "404": {"description": "Pet not found"},
223
- },
224
- }
225
- },
226
- },
227
- }
228
-
229
-
230
- async def check_mcp(mcp: FastMCP):
231
- # List what components were created
232
- tools = await mcp.get_tools()
233
- resources = await mcp.get_resources()
234
- templates = await mcp.get_resource_templates()
235
-
236
- print(
237
- f"{len(tools)} Tool(s): {', '.join([t.name for t in tools.values()])}"
238
- ) # Should include createPet
239
- print(
240
- f"{len(resources)} Resource(s): {', '.join([r.name for r in resources.values()])}"
241
- ) # Should include listPets
242
- print(
243
- f"{len(templates)} Resource Template(s): {', '.join([t.name for t in templates.values()])}"
244
- ) # Should include getPet
245
-
246
- return mcp
247
 
 
 
 
 
 
 
248
 
249
  if __name__ == "__main__":
250
- # Client for the Pet Store API
251
- client = httpx.AsyncClient(base_url="https://petstore.example.com/api")
252
 
253
- # Create the MCP server
254
- mcp = FastMCP.from_openapi(
255
- openapi_spec=petstore_spec, client=client, name="PetStore"
256
- )
257
 
258
- asyncio.run(check_mcp(mcp))
259
 
260
- # Start the MCP server
261
- mcp.run()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  ```
263
 
 
 
 
 
 
 
 
 
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. An empty set (`{}`) means no tag filtering, so the route matches regardless of its tags.
48
+ - **MCP type**: What MCP component type to create (the options are `TOOL`, `RESOURCE`, `RESOURCE_TEMPLATE`, `PROMPT`, or `EXCLUDE` to exclude the route from the MCP server)
49
 
50
+ Each OpenAPI route is matched against `RouteMap` objects in order, and the **first match wins** to determine the MCP component type. For example, here are the default route mappings, expressed as `RouteMap` objects in priority order:
 
 
 
 
 
 
 
51
 
52
  ```python
53
+ from fastmcp.server.openapi import RouteMap, MCPType
54
+
55
+ # Default route mappings
56
  DEFAULT_ROUTE_MAPPINGS = [
57
  # GET with path parameters -> ResourceTemplate
58
  RouteMap(
59
  methods=["GET"],
60
  pattern=r".*\{.*\}.*",
61
+ tags={},
62
+ mcp_type=MCPType.RESOURCE_TEMPLATE
63
  ),
 
64
  # GET without path parameters -> Resource
65
  RouteMap(
66
  methods=["GET"],
67
  pattern=r".*",
68
+ tags={},
69
+ mcp_type=MCPType.RESOURCE
70
  ),
 
71
  # All other methods -> Tool
72
  RouteMap(
73
+ methods="*",
74
  pattern=r".*",
75
+ tags={},
76
+ mcp_type=MCPType.TOOL
77
  ),
78
  ]
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
160
+ from fastmcp import FastMCP
161
+
162
+ # Create a client with authentication
163
+ api_client = httpx.AsyncClient(
164
+ base_url="https://api.example.com",
165
+ headers={"Authorization": "Bearer YOUR_TOKEN"}
166
+ )
167
+
168
+ # Create an MCP server from your OpenAPI spec
169
+ mcp = FastMCP.from_openapi(openapi_spec=spec, client=api_client)
170
+ ```
171
+
172
+ ## Timeouts
173
+
174
+ You can set a timeout for all requests by providing a `timeout` parameter (in seconds):
175
+
176
+ ```python
177
+ mcp = FastMCP.from_openapi(
178
+ openapi_spec=spec,
179
+ client=api_client,
180
+ timeout=30.0 # 30 second timeout
181
+ )
182
+ ```
183
+
184
+ ## FastAPI Integration
185
+
186
+ <VersionBadge version="2.0.0" />
187
+
188
+ FastMCP can automatically convert FastAPI applications into MCP servers by extracting their OpenAPI specifications. A special client will be created that uses an in-memory ASGI transport to avoid network calls to your FastAPI app. Note that the resulting MCP server is *not* a FastAPI app itself, but can be added to one (see [ASGI integration](/deployment/asgi)).
189
+
190
+ <Tip>
191
+ FastMCP does *not* include FastAPI as a dependency; you must install it separately to use this integration.
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
+
docs/servers/proxy.mdx CHANGED
@@ -106,7 +106,7 @@ proxy = FastMCP.as_proxy(
106
 
107
  ### Configuration-Based Proxies
108
 
109
- <VersionBadge version="2.3.6" />
110
 
111
  You can create a proxy directly from a configuration dictionary that follows the MCPConfig schema. This is useful for quickly setting up proxies to remote servers without manually configuring each connection detail.
112
 
 
106
 
107
  ### Configuration-Based Proxies
108
 
109
+ <VersionBadge version="2.4.0" />
110
 
111
  You can create a proxy directly from a configuration dictionary that follows the MCPConfig schema. This is useful for quickly setting up proxies to remote servers without manually configuring each connection detail.
112
 
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/client/client.py CHANGED
@@ -210,6 +210,23 @@ class Client:
210
  result = await self.session.send_ping()
211
  return isinstance(result, mcp.types.EmptyResult)
212
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  async def progress(
214
  self,
215
  progress_token: str | int,
 
210
  result = await self.session.send_ping()
211
  return isinstance(result, mcp.types.EmptyResult)
212
 
213
+ async def cancel(
214
+ self,
215
+ request_id: str | int,
216
+ reason: str | None = None,
217
+ ) -> None:
218
+ """Send a cancellation notification for an in-progress request."""
219
+ notification = mcp.types.ClientNotification(
220
+ mcp.types.CancelledNotification(
221
+ method="notifications/cancelled",
222
+ params=mcp.types.CancelledNotificationParams(
223
+ requestId=request_id,
224
+ reason=reason,
225
+ ),
226
+ )
227
+ )
228
+ await self.session.send_notification(notification)
229
+
230
  async def progress(
231
  self,
232
  progress_token: str | int,
src/fastmcp/client/transports.py CHANGED
@@ -19,6 +19,7 @@ from mcp.client.sse import sse_client
19
  from mcp.client.stdio import stdio_client
20
  from mcp.client.streamable_http import streamablehttp_client
21
  from mcp.client.websocket import websocket_client
 
22
  from mcp.shared.memory import create_connected_server_and_client_session
23
  from pydantic import AnyUrl
24
  from typing_extensions import Unpack
@@ -448,15 +449,21 @@ class NpxStdioTransport(StdioTransport):
448
 
449
 
450
  class FastMCPTransport(ClientTransport):
451
- """
452
- Special transport for in-memory connections to an MCP server.
453
 
454
- This is particularly useful for testing or when client and server
455
- are in the same process.
 
 
456
  """
457
 
458
- def __init__(self, mcp: FastMCPServer):
459
- self.server = mcp # Can be FastMCP or MCPServer
 
 
 
 
 
460
 
461
  @contextlib.asynccontextmanager
462
  async def connect_session(
@@ -528,8 +535,12 @@ class MCPConfigTransport(ClientTransport):
528
  config = MCPConfig.from_dict(config)
529
  self.config = config
530
 
 
 
 
 
531
  # if there's exactly one server, create a client for that server
532
- if len(self.config.mcpServers) == 1:
533
  self.transport = list(self.config.mcpServers.values())[0].to_transport()
534
 
535
  # otherwise create a composite client
@@ -558,6 +569,7 @@ class MCPConfigTransport(ClientTransport):
558
  def infer_transport(
559
  transport: ClientTransport
560
  | FastMCPServer
 
561
  | AnyUrl
562
  | Path
563
  | MCPConfig
@@ -573,7 +585,7 @@ def infer_transport(
573
 
574
  The function supports these input types:
575
  - ClientTransport: Used directly without modification
576
- - FastMCPServer: Creates an in-memory FastMCPTransport
577
  - Path or str (file path): Creates PythonStdioTransport (.py) or NodeStdioTransport (.js)
578
  - AnyUrl or str (URL): Creates StreamableHttpTransport (default) or SSETransport (for /sse endpoints)
579
  - MCPConfig or dict: Creates MCPConfigTransport, potentially connecting to multiple servers
@@ -610,8 +622,8 @@ def infer_transport(
610
  if isinstance(transport, ClientTransport):
611
  return transport
612
 
613
- # the transport is a FastMCP server
614
- elif isinstance(transport, FastMCPServer):
615
  inferred_transport = FastMCPTransport(mcp=transport)
616
 
617
  # the transport is a path to a script
 
19
  from mcp.client.stdio import stdio_client
20
  from mcp.client.streamable_http import streamablehttp_client
21
  from mcp.client.websocket import websocket_client
22
+ from mcp.server.fastmcp import FastMCP as FastMCP1Server
23
  from mcp.shared.memory import create_connected_server_and_client_session
24
  from pydantic import AnyUrl
25
  from typing_extensions import Unpack
 
449
 
450
 
451
  class FastMCPTransport(ClientTransport):
452
+ """In-memory transport for FastMCP servers.
 
453
 
454
+ This transport connects directly to a FastMCP server instance in the same
455
+ Python process. It works with both FastMCP 2.x servers and FastMCP 1.0
456
+ servers from the low-level MCP SDK. This is particularly useful for unit
457
+ tests or scenarios where client and server run in the same runtime.
458
  """
459
 
460
+ def __init__(self, mcp: FastMCPServer | FastMCP1Server):
461
+ """Initialize a FastMCPTransport from a FastMCP server instance."""
462
+
463
+ # Accept both FastMCP 2.x and FastMCP 1.0 servers. Both expose a
464
+ # ``_mcp_server`` attribute pointing to the underlying MCP server
465
+ # implementation, so we can treat them identically.
466
+ self.server = mcp
467
 
468
  @contextlib.asynccontextmanager
469
  async def connect_session(
 
535
  config = MCPConfig.from_dict(config)
536
  self.config = config
537
 
538
+ # if there are no servers, raise an error
539
+ if len(self.config.mcpServers) == 0:
540
+ raise ValueError("No MCP servers defined in the config")
541
+
542
  # if there's exactly one server, create a client for that server
543
+ elif len(self.config.mcpServers) == 1:
544
  self.transport = list(self.config.mcpServers.values())[0].to_transport()
545
 
546
  # otherwise create a composite client
 
569
  def infer_transport(
570
  transport: ClientTransport
571
  | FastMCPServer
572
+ | FastMCP1Server
573
  | AnyUrl
574
  | Path
575
  | MCPConfig
 
585
 
586
  The function supports these input types:
587
  - ClientTransport: Used directly without modification
588
+ - FastMCPServer or FastMCP1Server: Creates an in-memory FastMCPTransport
589
  - Path or str (file path): Creates PythonStdioTransport (.py) or NodeStdioTransport (.js)
590
  - AnyUrl or str (URL): Creates StreamableHttpTransport (default) or SSETransport (for /sse endpoints)
591
  - MCPConfig or dict: Creates MCPConfigTransport, potentially connecting to multiple servers
 
622
  if isinstance(transport, ClientTransport):
623
  return transport
624
 
625
+ # the transport is a FastMCP server (2.x or 1.0)
626
+ elif isinstance(transport, FastMCPServer | FastMCP1Server):
627
  inferred_transport = FastMCPTransport(mcp=transport)
628
 
629
  # the transport is a path to a script
src/fastmcp/server/context.py CHANGED
@@ -12,6 +12,8 @@ from mcp.shared.context import RequestContext
12
  from mcp.types import (
13
  CreateMessageResult,
14
  ImageContent,
 
 
15
  Root,
16
  SamplingMessage,
17
  TextContent,
@@ -200,6 +202,7 @@ class Context:
200
  system_prompt: str | None = None,
201
  temperature: float | None = None,
202
  max_tokens: int | None = None,
 
203
  ) -> TextContent | ImageContent:
204
  """
205
  Send a sampling request to the client and await the response.
@@ -231,6 +234,7 @@ class Context:
231
  system_prompt=system_prompt,
232
  temperature=temperature,
233
  max_tokens=max_tokens,
 
234
  )
235
 
236
  return result.content
@@ -248,3 +252,45 @@ class Context:
248
  )
249
 
250
  return fastmcp.server.dependencies.get_http_request()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  from mcp.types import (
13
  CreateMessageResult,
14
  ImageContent,
15
+ ModelHint,
16
+ ModelPreferences,
17
  Root,
18
  SamplingMessage,
19
  TextContent,
 
202
  system_prompt: str | None = None,
203
  temperature: float | None = None,
204
  max_tokens: int | None = None,
205
+ model_preferences: ModelPreferences | str | list[str] | None = None,
206
  ) -> TextContent | ImageContent:
207
  """
208
  Send a sampling request to the client and await the response.
 
234
  system_prompt=system_prompt,
235
  temperature=temperature,
236
  max_tokens=max_tokens,
237
+ model_preferences=self._parse_model_preferences(model_preferences),
238
  )
239
 
240
  return result.content
 
252
  )
253
 
254
  return fastmcp.server.dependencies.get_http_request()
255
+
256
+ def _parse_model_preferences(
257
+ self, model_preferences: ModelPreferences | str | list[str] | None
258
+ ) -> ModelPreferences | None:
259
+ """
260
+ Validates and converts user input for model_preferences into a ModelPreferences object.
261
+
262
+ Args:
263
+ model_preferences (ModelPreferences | str | list[str] | None):
264
+ The model preferences to use. Accepts:
265
+ - ModelPreferences (returns as-is)
266
+ - str (single model hint)
267
+ - list[str] (multiple model hints)
268
+ - None (no preferences)
269
+
270
+ Returns:
271
+ ModelPreferences | None: The parsed ModelPreferences object, or None if not provided.
272
+
273
+ Raises:
274
+ ValueError: If the input is not a supported type or contains invalid values.
275
+ """
276
+ if model_preferences is None:
277
+ return None
278
+ elif isinstance(model_preferences, ModelPreferences):
279
+ return model_preferences
280
+ elif isinstance(model_preferences, str):
281
+ # Single model hint
282
+ return ModelPreferences(hints=[ModelHint(name=model_preferences)])
283
+ elif isinstance(model_preferences, list):
284
+ # List of model hints (strings)
285
+ if not all(isinstance(h, str) for h in model_preferences):
286
+ raise ValueError(
287
+ "All elements of model_preferences list must be"
288
+ " strings (model name hints)."
289
+ )
290
+ return ModelPreferences(
291
+ hints=[ModelHint(name=h) for h in model_preferences]
292
+ )
293
+ else:
294
+ raise ValueError(
295
+ "model_preferences must be one of: ModelPreferences, str, list[str], or None."
296
+ )
src/fastmcp/server/http.py CHANGED
@@ -241,6 +241,7 @@ def create_sse_app(
241
  # Add custom routes with lowest precedence
242
  if routes:
243
  server_routes.extend(routes)
 
244
 
245
  # Add middleware
246
  if middleware:
@@ -359,6 +360,7 @@ def create_streamable_http_app(
359
  # Add custom routes with lowest precedence
360
  if routes:
361
  server_routes.extend(routes)
 
362
 
363
  # Add middleware
364
  if middleware:
 
241
  # Add custom routes with lowest precedence
242
  if routes:
243
  server_routes.extend(routes)
244
+ server_routes.extend(server._additional_http_routes)
245
 
246
  # Add middleware
247
  if middleware:
 
360
  # Add custom routes with lowest precedence
361
  if routes:
362
  server_routes.extend(routes)
363
+ server_routes.extend(server._additional_http_routes)
364
 
365
  # Add middleware
366
  if middleware:
src/fastmcp/server/openapi.py CHANGED
@@ -5,8 +5,9 @@ from __future__ import annotations
5
  import enum
6
  import json
7
  import re
 
8
  from collections.abc import Callable
9
- from dataclasses import dataclass
10
  from re import Pattern
11
  from typing import TYPE_CHECKING, Any, Literal
12
 
@@ -33,8 +34,32 @@ logger = get_logger(__name__)
33
  HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
34
 
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  class RouteType(enum.Enum):
37
- """Type of FastMCP component to create from a route."""
 
 
 
 
38
 
39
  TOOL = "TOOL"
40
  RESOURCE = "RESOURCE"
@@ -47,32 +72,71 @@ class RouteType(enum.Enum):
47
  class RouteMap:
48
  """Mapping configuration for HTTP routes to FastMCP component types."""
49
 
50
- methods: list[HttpMethod] | Literal["*"]
51
- pattern: Pattern[str] | str
52
- route_type: RouteType
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
 
55
  # Default route mappings as a list, where order determines priority
56
  DEFAULT_ROUTE_MAPPINGS = [
57
  # GET requests with path parameters go to ResourceTemplate
58
  RouteMap(
59
- methods=["GET"], pattern=r".*\{.*\}.*", route_type=RouteType.RESOURCE_TEMPLATE
60
  ),
61
  # GET requests without path parameters go to Resource
62
- RouteMap(methods=["GET"], pattern=r".*", route_type=RouteType.RESOURCE),
63
  # All other HTTP methods go to Tool
64
- RouteMap(
65
- methods=["POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"],
66
- pattern=r".*",
67
- route_type=RouteType.TOOL,
68
- ),
69
  ]
70
 
71
 
72
  def _determine_route_type(
73
  route: openapi.HTTPRoute,
74
  mappings: list[RouteMap],
75
- ) -> RouteType:
76
  """
77
  Determines the FastMCP component type based on the route and mappings.
78
 
@@ -81,7 +145,7 @@ def _determine_route_type(
81
  mappings: List of RouteMap objects in priority order
82
 
83
  Returns:
84
- RouteType for this route
85
  """
86
  # Check mappings in priority order (first match wins)
87
  for route_map in mappings:
@@ -94,20 +158,24 @@ def _determine_route_type(
94
  pattern_matches = re.search(route_map.pattern, route.path)
95
 
96
  if pattern_matches:
 
 
 
 
 
 
 
 
 
 
 
97
  logger.debug(
98
- f"Route {route.method} {route.path} matched mapping to {route_map.route_type.name}"
99
  )
100
- return route_map.route_type
101
 
102
  # Default fallback
103
- return RouteType.TOOL
104
-
105
-
106
- # Placeholder function to provide function metadata
107
- async def _openapi_passthrough(*args, **kwargs):
108
- """Placeholder function for OpenAPI endpoints."""
109
- # This is kept for metadata generation purposes
110
- pass
111
 
112
 
113
  class OpenAPITool(Tool):
@@ -555,13 +623,13 @@ class FastMCPOpenAPI(FastMCP):
555
  RouteMap(
556
  methods=["GET", "POST", "PATCH"],
557
  pattern=r".*/users/.*",
558
- route_type=RouteType.RESOURCE_TEMPLATE
559
  ),
560
  # Map all analytics endpoints to Tool
561
  RouteMap(
562
  methods=["GET"],
563
  pattern=r".*/analytics/.*",
564
- route_type=RouteType.TOOL
565
  ),
566
  ]
567
 
@@ -599,6 +667,10 @@ class FastMCPOpenAPI(FastMCP):
599
 
600
  self._client = client
601
  self._timeout = timeout
 
 
 
 
602
  http_routes = openapi.parse_openapi_to_http_routes(openapi_spec)
603
 
604
  # Process routes
@@ -607,34 +679,99 @@ class FastMCPOpenAPI(FastMCP):
607
  # Determine route type based on mappings or default rules
608
  route_type = _determine_route_type(route, route_maps)
609
 
610
- # Use operation_id if available, otherwise generate a name
611
- operation_id = route.operation_id
612
- if not operation_id:
613
- # Generate operation ID from method and path
614
- path_parts = route.path.strip("/").split("/")
615
- path_name = "_".join(p for p in path_parts if not p.startswith("{"))
616
- operation_id = f"{route.method.lower()}_{path_name}"
617
-
618
- if route_type == RouteType.TOOL:
619
- self._create_openapi_tool(route, operation_id)
620
- elif route_type == RouteType.RESOURCE:
621
- self._create_openapi_resource(route, operation_id)
622
- elif route_type == RouteType.RESOURCE_TEMPLATE:
623
- self._create_openapi_template(route, operation_id)
624
- elif route_type == RouteType.PROMPT:
625
  # Not implemented yet
626
  logger.warning(
627
  f"PROMPT route type not implemented: {route.method} {route.path}"
628
  )
629
- elif route_type == RouteType.IGNORE:
630
- logger.info(f"Ignoring route: {route.method} {route.path}")
631
 
632
  logger.info(f"Created FastMCP OpenAPI server with {len(http_routes)} routes")
633
 
634
- def _create_openapi_tool(self, route: openapi.HTTPRoute, operation_id: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
635
  """Creates and registers an OpenAPITool with enhanced description."""
636
  combined_schema = _combine_schemas(route)
637
- tool_name = operation_id
 
 
 
638
  base_description = (
639
  route.description
640
  or route.summary
@@ -664,9 +801,11 @@ class FastMCPOpenAPI(FastMCP):
664
  f"Registered TOOL: {tool_name} ({route.method} {route.path}) with tags: {route.tags}"
665
  )
666
 
667
- def _create_openapi_resource(self, route: openapi.HTTPRoute, operation_id: str):
668
  """Creates and registers an OpenAPIResource with enhanced description."""
669
- resource_name = operation_id
 
 
670
  resource_uri = f"resource://openapi/{resource_name}"
671
  base_description = (
672
  route.description or route.summary or f"Represents {route.path}"
@@ -695,9 +834,11 @@ class FastMCPOpenAPI(FastMCP):
695
  f"Registered RESOURCE: {resource_uri} ({route.method} {route.path}) with tags: {route.tags}"
696
  )
697
 
698
- def _create_openapi_template(self, route: openapi.HTTPRoute, operation_id: str):
699
  """Creates and registers an OpenAPIResourceTemplate with enhanced description."""
700
- template_name = operation_id
 
 
701
  path_params = [p.name for p in route.parameters if p.location == "path"]
702
  path_params.sort() # Sort for consistent URIs
703
 
 
5
  import enum
6
  import json
7
  import re
8
+ import warnings
9
  from collections.abc import Callable
10
+ from dataclasses import dataclass, field
11
  from re import Pattern
12
  from typing import TYPE_CHECKING, Any, Literal
13
 
 
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.
39
+
40
+ Enum values:
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
+ """
48
+
49
+ TOOL = "TOOL"
50
+ RESOURCE = "RESOURCE"
51
+ RESOURCE_TEMPLATE = "RESOURCE_TEMPLATE"
52
+ PROMPT = "PROMPT"
53
+ EXCLUDE = "EXCLUDE"
54
+
55
+
56
+ # Keep RouteType as an alias to MCPType for backward compatibility
57
  class RouteType(enum.Enum):
58
+ """
59
+ Deprecated: Use MCPType instead.
60
+
61
+ This enum is kept for backward compatibility and will be removed in a future version.
62
+ """
63
 
64
  TOOL = "TOOL"
65
  RESOURCE = "RESOURCE"
 
72
  class RouteMap:
73
  """Mapping configuration for HTTP routes to FastMCP component types."""
74
 
75
+ methods: list[HttpMethod] | Literal["*"] = field(default="*")
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."""
83
+ # Handle backward compatibility for route_type, deprecated in 2.5.0
84
+ if self.mcp_type is None and self.route_type is not None:
85
+ warnings.warn(
86
+ "The 'route_type' parameter is deprecated and will be removed in a future version. "
87
+ "Use 'mcp_type' instead with the appropriate MCPType value.",
88
+ DeprecationWarning,
89
+ stacklevel=2,
90
+ )
91
+ if isinstance(self.route_type, RouteType):
92
+ warnings.warn(
93
+ "The RouteType class is deprecated and will be removed in a future version. "
94
+ "Use MCPType instead.",
95
+ DeprecationWarning,
96
+ stacklevel=2,
97
+ )
98
+ # Check for the deprecated IGNORE value
99
+ if self.route_type == RouteType.IGNORE:
100
+ warnings.warn(
101
+ "RouteType.IGNORE is deprecated and will be removed in a future version. "
102
+ "Use MCPType.EXCLUDE instead.",
103
+ DeprecationWarning,
104
+ stacklevel=2,
105
+ )
106
+
107
+ # Convert from RouteType to MCPType if needed
108
+ if isinstance(self.route_type, RouteType):
109
+ route_type_name = self.route_type.name
110
+ if route_type_name == "IGNORE":
111
+ route_type_name = "EXCLUDE"
112
+ self.mcp_type = getattr(MCPType, route_type_name)
113
+ else:
114
+ self.mcp_type = self.route_type
115
+ elif self.mcp_type is None:
116
+ raise ValueError("`mcp_type` must be provided")
117
+
118
+ # Set route_type to match mcp_type for backward compatibility
119
+ if self.route_type is None:
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
126
  RouteMap(
127
+ methods=["GET"], pattern=r".*\{.*\}.*", mcp_type=MCPType.RESOURCE_TEMPLATE
128
  ),
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
 
136
  def _determine_route_type(
137
  route: openapi.HTTPRoute,
138
  mappings: list[RouteMap],
139
+ ) -> MCPType:
140
  """
141
  Determines the FastMCP component type based on the route and mappings.
142
 
 
145
  mappings: List of RouteMap objects in priority order
146
 
147
  Returns:
148
+ MCPType for this route
149
  """
150
  # Check mappings in priority order (first match wins)
151
  for route_map in mappings:
 
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(
173
+ f"Route {route.method} {route.path} matched mapping to {route_map.mcp_type.name}"
174
  )
175
+ return route_map.mcp_type
176
 
177
  # Default fallback
178
+ return MCPType.TOOL
 
 
 
 
 
 
 
179
 
180
 
181
  class OpenAPITool(Tool):
 
623
  RouteMap(
624
  methods=["GET", "POST", "PATCH"],
625
  pattern=r".*/users/.*",
626
+ mcp_type=MCPType.RESOURCE_TEMPLATE
627
  ),
628
  # Map all analytics endpoints to Tool
629
  RouteMap(
630
  methods=["GET"],
631
  pattern=r".*/analytics/.*",
632
+ mcp_type=MCPType.TOOL
633
  ),
634
  ]
635
 
 
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()}
673
+
674
  http_routes = openapi.parse_openapi_to_http_routes(openapi_spec)
675
 
676
  # Process routes
 
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
+
685
+ if route_type == MCPType.TOOL:
686
+ self._create_openapi_tool(route, component_name)
687
+ elif route_type == MCPType.RESOURCE:
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
 
699
  logger.info(f"Created FastMCP OpenAPI server with {len(http_routes)} routes")
700
 
701
+ def _generate_default_name(
702
+ self, route: openapi.HTTPRoute, mcp_type: MCPType
703
+ ) -> str:
704
+ """Generate a default name from the route path."""
705
+ # First check for OpenAPI operationId which takes precedence
706
+ if route.operation_id:
707
+ return route.operation_id
708
+
709
+ # For path-based naming, clean up the path
710
+ path_parts = route.path.strip("/").split("/")
711
+
712
+ # Remove path parameters (parts with {})
713
+ clean_parts = []
714
+ for part in path_parts:
715
+ if part.startswith("{") and part.endswith("}"):
716
+ # For templates, include parameter name without braces
717
+ if mcp_type == MCPType.RESOURCE_TEMPLATE:
718
+ param_name = part[1:-1] # Remove braces
719
+ clean_parts.append(param_name)
720
+ else:
721
+ clean_parts.append(part)
722
+
723
+ # Join the parts
724
+ resource_name = "_".join(clean_parts)
725
+
726
+ # For tools, might be useful to keep the method for clarity on what it does
727
+ if mcp_type == MCPType.TOOL:
728
+ # Only include method if it helps distinguish (POST, PUT, PATCH, DELETE)
729
+ # For GET we don't need the method as it's implied for resources
730
+ if route.method != "GET":
731
+ resource_name = f"{route.method.lower()}_{resource_name}"
732
+
733
+ return resource_name
734
+
735
+ def _get_unique_name(
736
+ self, name: str, component_type: Literal["tools", "resources", "templates"]
737
+ ) -> str:
738
+ """
739
+ Ensure the name is unique within its component type by appending numbers if needed.
740
+
741
+ Args:
742
+ name: The proposed name
743
+ component_type: The type of component ("tools", "resources", or "templates")
744
+
745
+ Returns:
746
+ str: A unique name for the component
747
+ """
748
+ # Check if the name is already used
749
+ if name not in self._used_names[component_type]:
750
+ self._used_names[component_type].add(name)
751
+ return name
752
+
753
+ # Find the next available number suffix
754
+ counter = 2
755
+ while f"{name}_{counter}" in self._used_names[component_type]:
756
+ counter += 1
757
+
758
+ # Create the new name
759
+ new_name = f"{name}_{counter}"
760
+ logger.debug(
761
+ f"Name collision detected: '{name}' already exists as a {component_type[:-1]}. "
762
+ f"Using '{new_name}' instead."
763
+ )
764
+
765
+ self._used_names[component_type].add(new_name)
766
+ return new_name
767
+
768
+ def _create_openapi_tool(self, route: openapi.HTTPRoute, name: str):
769
  """Creates and registers an OpenAPITool with enhanced description."""
770
  combined_schema = _combine_schemas(route)
771
+
772
+ # Get a unique tool name
773
+ tool_name = self._get_unique_name(name, "tools")
774
+
775
  base_description = (
776
  route.description
777
  or route.summary
 
801
  f"Registered TOOL: {tool_name} ({route.method} {route.path}) with tags: {route.tags}"
802
  )
803
 
804
+ def _create_openapi_resource(self, route: openapi.HTTPRoute, name: str):
805
  """Creates and registers an OpenAPIResource with enhanced description."""
806
+ # Get a unique resource name
807
+ resource_name = self._get_unique_name(name, "resources")
808
+
809
  resource_uri = f"resource://openapi/{resource_name}"
810
  base_description = (
811
  route.description or route.summary or f"Represents {route.path}"
 
834
  f"Registered RESOURCE: {resource_uri} ({route.method} {route.path}) with tags: {route.tags}"
835
  )
836
 
837
+ def _create_openapi_template(self, route: openapi.HTTPRoute, name: str):
838
  """Creates and registers an OpenAPIResourceTemplate with enhanced description."""
839
+ # Get a unique template name
840
+ template_name = self._get_unique_name(name, "templates")
841
+
842
  path_params = [p.name for p in route.parameters if p.location == "path"]
843
  path_params.sort() # Sort for consistent URIs
844
 
src/fastmcp/server/server.py CHANGED
@@ -879,7 +879,6 @@ class FastMCP(Generic[LifespanResultT]):
879
  auth_server_provider=self._auth_server_provider,
880
  auth_settings=self.settings.auth,
881
  debug=self.settings.debug,
882
- routes=self._additional_http_routes,
883
  middleware=middleware,
884
  )
885
 
@@ -930,7 +929,6 @@ class FastMCP(Generic[LifespanResultT]):
930
  json_response=self.settings.json_response,
931
  stateless_http=self.settings.stateless_http,
932
  debug=self.settings.debug,
933
- routes=self._additional_http_routes,
934
  middleware=middleware,
935
  )
936
  elif transport == "sse":
@@ -941,7 +939,6 @@ class FastMCP(Generic[LifespanResultT]):
941
  auth_server_provider=self._auth_server_provider,
942
  auth_settings=self.settings.auth,
943
  debug=self.settings.debug,
944
- routes=self._additional_http_routes,
945
  middleware=middleware,
946
  )
947
 
@@ -1026,7 +1023,7 @@ class FastMCP(Generic[LifespanResultT]):
1026
  from fastmcp.server.proxy import FastMCPProxy
1027
 
1028
  if tool_separator is not None:
1029
- # Deprecated since 2.3.6
1030
  warnings.warn(
1031
  "The tool_separator parameter is deprecated and will be removed in a future version. "
1032
  "Tools are now prefixed using 'prefix_toolname' format.",
@@ -1035,7 +1032,7 @@ class FastMCP(Generic[LifespanResultT]):
1035
  )
1036
 
1037
  if resource_separator is not None:
1038
- # Deprecated since 2.3.6
1039
  warnings.warn(
1040
  "The resource_separator parameter is deprecated and ignored. "
1041
  "Resource prefixes are now added using the protocol://prefix/path format.",
@@ -1044,7 +1041,7 @@ class FastMCP(Generic[LifespanResultT]):
1044
  )
1045
 
1046
  if prompt_separator is not None:
1047
- # Deprecated since 2.3.6
1048
  warnings.warn(
1049
  "The prompt_separator parameter is deprecated and will be removed in a future version. "
1050
  "Prompts are now prefixed using 'prefix_promptname' format.",
@@ -1111,7 +1108,7 @@ class FastMCP(Generic[LifespanResultT]):
1111
  prompt_separator: Deprecated. Separator for prompt names.
1112
  """
1113
  if tool_separator is not None:
1114
- # Deprecated since 2.3.6
1115
  warnings.warn(
1116
  "The tool_separator parameter is deprecated and will be removed in a future version. "
1117
  "Tools are now prefixed using 'prefix_toolname' format.",
@@ -1120,7 +1117,7 @@ class FastMCP(Generic[LifespanResultT]):
1120
  )
1121
 
1122
  if resource_separator is not None:
1123
- # Deprecated since 2.3.6
1124
  warnings.warn(
1125
  "The resource_separator parameter is deprecated and ignored. "
1126
  "Resource prefixes are now added using the protocol://prefix/path format.",
@@ -1129,7 +1126,7 @@ class FastMCP(Generic[LifespanResultT]):
1129
  )
1130
 
1131
  if prompt_separator is not None:
1132
- # Deprecated since 2.3.6
1133
  warnings.warn(
1134
  "The prompt_separator parameter is deprecated and will be removed in a future version. "
1135
  "Prompts are now prefixed using 'prefix_promptname' format.",
@@ -1175,19 +1172,22 @@ class FastMCP(Generic[LifespanResultT]):
1175
  """
1176
  Create a FastMCP server from an OpenAPI specification.
1177
  """
1178
- from .openapi import FastMCPOpenAPI, RouteMap, RouteType
 
 
 
 
 
 
 
 
 
1179
 
1180
  if all_routes_as_tools and route_maps:
1181
  raise ValueError("Cannot specify both all_routes_as_tools and route_maps")
1182
 
1183
  elif all_routes_as_tools:
1184
- route_maps = [
1185
- RouteMap(
1186
- methods="*",
1187
- pattern=r".*",
1188
- route_type=RouteType.TOOL,
1189
- )
1190
- ]
1191
 
1192
  return FastMCPOpenAPI(
1193
  openapi_spec=openapi_spec,
@@ -1209,15 +1209,22 @@ class FastMCP(Generic[LifespanResultT]):
1209
  Create a FastMCP server from a FastAPI application.
1210
  """
1211
 
1212
- from .openapi import FastMCPOpenAPI, RouteMap, RouteType
 
 
 
 
 
 
 
 
 
1213
 
1214
  if all_routes_as_tools and route_maps:
1215
  raise ValueError("Cannot specify both all_routes_as_tools and route_maps")
1216
 
1217
  elif all_routes_as_tools:
1218
- route_maps = [
1219
- RouteMap(methods="*", pattern=r".*", route_type=RouteType.TOOL)
1220
- ]
1221
 
1222
  client = httpx.AsyncClient(
1223
  transport=httpx.ASGITransport(app=app), base_url="http://fastapi"
 
879
  auth_server_provider=self._auth_server_provider,
880
  auth_settings=self.settings.auth,
881
  debug=self.settings.debug,
 
882
  middleware=middleware,
883
  )
884
 
 
929
  json_response=self.settings.json_response,
930
  stateless_http=self.settings.stateless_http,
931
  debug=self.settings.debug,
 
932
  middleware=middleware,
933
  )
934
  elif transport == "sse":
 
939
  auth_server_provider=self._auth_server_provider,
940
  auth_settings=self.settings.auth,
941
  debug=self.settings.debug,
 
942
  middleware=middleware,
943
  )
944
 
 
1023
  from fastmcp.server.proxy import FastMCPProxy
1024
 
1025
  if tool_separator is not None:
1026
+ # Deprecated since 2.4.0
1027
  warnings.warn(
1028
  "The tool_separator parameter is deprecated and will be removed in a future version. "
1029
  "Tools are now prefixed using 'prefix_toolname' format.",
 
1032
  )
1033
 
1034
  if resource_separator is not None:
1035
+ # Deprecated since 2.4.0
1036
  warnings.warn(
1037
  "The resource_separator parameter is deprecated and ignored. "
1038
  "Resource prefixes are now added using the protocol://prefix/path format.",
 
1041
  )
1042
 
1043
  if prompt_separator is not None:
1044
+ # Deprecated since 2.4.0
1045
  warnings.warn(
1046
  "The prompt_separator parameter is deprecated and will be removed in a future version. "
1047
  "Prompts are now prefixed using 'prefix_promptname' format.",
 
1108
  prompt_separator: Deprecated. Separator for prompt names.
1109
  """
1110
  if tool_separator is not None:
1111
+ # Deprecated since 2.4.0
1112
  warnings.warn(
1113
  "The tool_separator parameter is deprecated and will be removed in a future version. "
1114
  "Tools are now prefixed using 'prefix_toolname' format.",
 
1117
  )
1118
 
1119
  if resource_separator is not None:
1120
+ # Deprecated since 2.4.0
1121
  warnings.warn(
1122
  "The resource_separator parameter is deprecated and ignored. "
1123
  "Resource prefixes are now added using the protocol://prefix/path format.",
 
1126
  )
1127
 
1128
  if prompt_separator is not None:
1129
+ # Deprecated since 2.4.0
1130
  warnings.warn(
1131
  "The prompt_separator parameter is deprecated and will be removed in a future version. "
1132
  "Prompts are now prefixed using 'prefix_promptname' format.",
 
1172
  """
1173
  Create a FastMCP server from an OpenAPI specification.
1174
  """
1175
+ from .openapi import FastMCPOpenAPI, MCPType, RouteMap
1176
+
1177
+ # Deprecated since 2.5.0
1178
+ if all_routes_as_tools:
1179
+ warnings.warn(
1180
+ "The 'all_routes_as_tools' parameter is deprecated and will be removed in a future version. "
1181
+ 'Use \'route_maps=[RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]\' instead.',
1182
+ DeprecationWarning,
1183
+ stacklevel=2,
1184
+ )
1185
 
1186
  if all_routes_as_tools and route_maps:
1187
  raise ValueError("Cannot specify both all_routes_as_tools and route_maps")
1188
 
1189
  elif all_routes_as_tools:
1190
+ route_maps = [RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]
 
 
 
 
 
 
1191
 
1192
  return FastMCPOpenAPI(
1193
  openapi_spec=openapi_spec,
 
1209
  Create a FastMCP server from a FastAPI application.
1210
  """
1211
 
1212
+ from .openapi import FastMCPOpenAPI, MCPType, RouteMap
1213
+
1214
+ # Deprecated since 2.5.0
1215
+ if all_routes_as_tools:
1216
+ warnings.warn(
1217
+ "The 'all_routes_as_tools' parameter is deprecated and will be removed in a future version. "
1218
+ 'Use \'route_maps=[RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]\' instead.',
1219
+ DeprecationWarning,
1220
+ stacklevel=2,
1221
+ )
1222
 
1223
  if all_routes_as_tools and route_maps:
1224
  raise ValueError("Cannot specify both all_routes_as_tools and route_maps")
1225
 
1226
  elif all_routes_as_tools:
1227
+ route_maps = [RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]
 
 
1228
 
1229
  client = httpx.AsyncClient(
1230
  transport=httpx.ASGITransport(app=app), base_url="http://fastapi"
src/fastmcp/utilities/mcp_config.py CHANGED
@@ -32,11 +32,12 @@ def infer_transport_type_from_url(
32
  return "streamable-http"
33
 
34
 
35
- class LocalMCPServer(BaseModel):
36
  command: str
37
  args: list[str] = Field(default_factory=list)
38
  env: dict[str, Any] = Field(default_factory=dict)
39
  cwd: str | None = None
 
40
 
41
  def to_transport(self) -> StdioTransport:
42
  from fastmcp.client.transports import StdioTransport
@@ -51,8 +52,8 @@ class LocalMCPServer(BaseModel):
51
 
52
  class RemoteMCPServer(BaseModel):
53
  url: str
54
- transport: Literal["streamable-http", "sse", "http"] | None = None
55
  headers: dict[str, str] = Field(default_factory=dict)
 
56
 
57
  def to_transport(self) -> StreamableHttpTransport | SSETransport:
58
  from fastmcp.client.transports import SSETransport, StreamableHttpTransport
@@ -69,7 +70,7 @@ class RemoteMCPServer(BaseModel):
69
 
70
 
71
  class MCPConfig(BaseModel):
72
- mcpServers: dict[str, LocalMCPServer | RemoteMCPServer]
73
 
74
  @classmethod
75
  def from_dict(cls, config: dict[str, Any]) -> MCPConfig:
 
32
  return "streamable-http"
33
 
34
 
35
+ class StdioMCPServer(BaseModel):
36
  command: str
37
  args: list[str] = Field(default_factory=list)
38
  env: dict[str, Any] = Field(default_factory=dict)
39
  cwd: str | None = None
40
+ transport: Literal["stdio"] = "stdio"
41
 
42
  def to_transport(self) -> StdioTransport:
43
  from fastmcp.client.transports import StdioTransport
 
52
 
53
  class RemoteMCPServer(BaseModel):
54
  url: str
 
55
  headers: dict[str, str] = Field(default_factory=dict)
56
+ transport: Literal["streamable-http", "sse", "http"] | None = None
57
 
58
  def to_transport(self) -> StreamableHttpTransport | SSETransport:
59
  from fastmcp.client.transports import SSETransport, StreamableHttpTransport
 
70
 
71
 
72
  class MCPConfig(BaseModel):
73
+ mcpServers: dict[str, StdioMCPServer | RemoteMCPServer]
74
 
75
  @classmethod
76
  def from_dict(cls, config: dict[str, Any]) -> MCPConfig:
tests/client/test_client.py CHANGED
@@ -728,7 +728,19 @@ class TestInferTransport:
728
  assert transport.transport.command == "echo"
729
  assert transport.transport.args == ["hello"]
730
 
731
- def test_infer_composite_client(config):
 
 
 
 
 
 
 
 
 
 
 
 
732
  config = {
733
  "mcpServers": {
734
  "local": {
@@ -744,4 +756,17 @@ class TestInferTransport:
744
  transport = infer_transport(config)
745
  assert isinstance(transport, MCPConfigTransport)
746
  assert isinstance(transport.transport, FastMCPTransport)
747
- assert len(transport.transport.server._mounted_servers) == 2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
728
  assert transport.transport.command == "echo"
729
  assert transport.transport.args == ["hello"]
730
 
731
+ def test_config_with_no_servers(self):
732
+ """Test that an empty MCPConfig raises a ValueError."""
733
+ config = {"mcpServers": {}}
734
+ with pytest.raises(ValueError, match="No MCP servers defined in the config"):
735
+ infer_transport(config)
736
+
737
+ def test_mcpconfigtransport_with_no_servers(self):
738
+ """Test that MCPConfigTransport raises a ValueError when initialized with an empty config."""
739
+ config = {"mcpServers": {}}
740
+ with pytest.raises(ValueError, match="No MCP servers defined in the config"):
741
+ MCPConfigTransport(config=config)
742
+
743
+ def test_infer_composite_client(self):
744
  config = {
745
  "mcpServers": {
746
  "local": {
 
756
  transport = infer_transport(config)
757
  assert isinstance(transport, MCPConfigTransport)
758
  assert isinstance(transport.transport, FastMCPTransport)
759
+ assert len(cast(FastMCP, transport.transport.server)._mounted_servers) == 2
760
+
761
+ def test_infer_fastmcp_server(self, fastmcp_server):
762
+ """FastMCP server instances should infer to FastMCPTransport."""
763
+ transport = infer_transport(fastmcp_server)
764
+ assert isinstance(transport, FastMCPTransport)
765
+
766
+ def test_infer_fastmcp_v1_server(self):
767
+ """FastMCP 1.0 server instances should infer to FastMCPTransport."""
768
+ from mcp.server.fastmcp import FastMCP as FastMCP1
769
+
770
+ server = FastMCP1()
771
+ transport = infer_transport(server)
772
+ assert isinstance(transport, FastMCPTransport)
tests/deprecated/test_route_type_ignore.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the deprecated RouteType.IGNORE."""
2
+
3
+ import warnings
4
+
5
+ import httpx
6
+ import pytest
7
+
8
+ from fastmcp.server.openapi import (
9
+ FastMCPOpenAPI,
10
+ MCPType,
11
+ RouteMap,
12
+ RouteType,
13
+ )
14
+
15
+
16
+ def test_route_type_ignore_deprecation_warning():
17
+ """Test that using RouteType.IGNORE emits a deprecation warning."""
18
+ # Let's manually capture the warnings
19
+
20
+ # Record all warnings
21
+ with warnings.catch_warnings(record=True) as recorded:
22
+ # Make sure warnings are always triggered
23
+ warnings.simplefilter("always")
24
+
25
+ # Create a RouteMap with RouteType.IGNORE
26
+ route_map = RouteMap(
27
+ methods=["GET"], pattern=r"^/analytics$", route_type=RouteType.IGNORE
28
+ )
29
+
30
+ # Check for the expected warnings in the recorded warnings
31
+ route_type_warning = False
32
+ ignore_warning = False
33
+
34
+ for w in recorded:
35
+ if issubclass(w.category, DeprecationWarning):
36
+ message = str(w.message)
37
+ if "route_type' parameter is deprecated" in message:
38
+ route_type_warning = True
39
+ if "RouteType.IGNORE is deprecated" in message:
40
+ ignore_warning = True
41
+
42
+ # Make sure both warnings were triggered
43
+ assert route_type_warning, "Missing 'route_type' deprecation warning"
44
+ assert ignore_warning, "Missing 'RouteType.IGNORE' deprecation warning"
45
+
46
+ # Verify that RouteType.IGNORE was converted to MCPType.EXCLUDE
47
+ assert route_map.mcp_type == MCPType.EXCLUDE
48
+
49
+
50
+ class TestRouteTypeIgnoreDeprecation:
51
+ """Test class for the deprecated RouteType.IGNORE."""
52
+
53
+ @pytest.fixture
54
+ def basic_openapi_spec(self) -> dict:
55
+ """Create a simple OpenAPI spec for testing."""
56
+ return {
57
+ "openapi": "3.0.0",
58
+ "info": {"title": "Test API", "version": "1.0.0"},
59
+ "paths": {
60
+ "/items": {
61
+ "get": {
62
+ "operationId": "get_items",
63
+ "summary": "Get all items",
64
+ "responses": {"200": {"description": "Success"}},
65
+ }
66
+ },
67
+ "/analytics": {
68
+ "get": {
69
+ "operationId": "get_analytics",
70
+ "summary": "Get analytics data",
71
+ "responses": {"200": {"description": "Success"}},
72
+ }
73
+ },
74
+ },
75
+ }
76
+
77
+ @pytest.fixture
78
+ async def mock_client(self) -> httpx.AsyncClient:
79
+ """Create a mock client for testing."""
80
+
81
+ async def _responder(request):
82
+ return httpx.Response(200, json={"success": True})
83
+
84
+ return httpx.AsyncClient(transport=httpx.MockTransport(_responder))
85
+
86
+ async def test_route_type_ignore_conversion(self, basic_openapi_spec, mock_client):
87
+ """Test that routes with RouteType.IGNORE are properly excluded."""
88
+ # Capture the deprecation warning without checking the exact message
89
+ with pytest.warns(DeprecationWarning):
90
+ server = FastMCPOpenAPI(
91
+ openapi_spec=basic_openapi_spec,
92
+ client=mock_client,
93
+ route_maps=[
94
+ # Use the deprecated RouteType.IGNORE
95
+ RouteMap(
96
+ methods=["GET"],
97
+ pattern=r"^/analytics$",
98
+ route_type=RouteType.IGNORE,
99
+ ),
100
+ # Make everything else a resource
101
+ RouteMap(
102
+ methods=["GET"], pattern=r".*", route_type=RouteType.RESOURCE
103
+ ),
104
+ ],
105
+ )
106
+
107
+ # Check that the analytics route was excluded (converted from IGNORE to EXCLUDE)
108
+ resources = await server.get_resources()
109
+ resource_uris = [str(r.uri) for r in resources.values()]
110
+
111
+ # Analytics should be excluded
112
+ assert "resource://openapi/get_items" in resource_uris
113
+ assert "resource://openapi/get_analytics" not in resource_uris
tests/server/http/test_custom_routes.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from starlette.requests import Request
3
+ from starlette.responses import JSONResponse
4
+ from starlette.routing import Route
5
+
6
+ from fastmcp import FastMCP
7
+ from fastmcp.server.http import create_sse_app, create_streamable_http_app
8
+
9
+
10
+ class TestCustomRoutes:
11
+ @pytest.fixture
12
+ def server_with_custom_route(self):
13
+ """Create a FastMCP server with a custom route."""
14
+ server = FastMCP()
15
+
16
+ @server.custom_route("/custom-route", methods=["GET"])
17
+ async def custom_route(request: Request):
18
+ return JSONResponse({"message": "custom route"})
19
+
20
+ return server
21
+
22
+ def test_custom_routes_via_server_http_app(self, server_with_custom_route):
23
+ """Test that custom routes are included when using server.http_app()."""
24
+ # Get the app via server.http_app()
25
+ app = server_with_custom_route.http_app()
26
+
27
+ # Verify that the custom route is included
28
+ custom_route_found = False
29
+ for route in app.routes:
30
+ if isinstance(route, Route) and route.path == "/custom-route":
31
+ custom_route_found = True
32
+ break
33
+
34
+ assert custom_route_found, "Custom route was not found in app routes"
35
+
36
+ def test_custom_routes_via_streamable_http_app_direct(
37
+ self, server_with_custom_route
38
+ ):
39
+ """Test that custom routes are included when using create_streamable_http_app directly."""
40
+ # Create the app by calling the constructor function directly
41
+ app = create_streamable_http_app(
42
+ server=server_with_custom_route, streamable_http_path="/api"
43
+ )
44
+
45
+ # Verify that the custom route is included
46
+ custom_route_found = False
47
+ for route in app.routes:
48
+ if isinstance(route, Route) and route.path == "/custom-route":
49
+ custom_route_found = True
50
+ break
51
+
52
+ assert custom_route_found, "Custom route was not found in app routes"
53
+
54
+ def test_custom_routes_via_sse_app_direct(self, server_with_custom_route):
55
+ """Test that custom routes are included when using create_sse_app directly."""
56
+ # Create the app by calling the constructor function directly
57
+ app = create_sse_app(
58
+ server=server_with_custom_route, message_path="/message", sse_path="/sse"
59
+ )
60
+
61
+ # Verify that the custom route is included
62
+ custom_route_found = False
63
+ for route in app.routes:
64
+ if isinstance(route, Route) and route.path == "/custom-route":
65
+ custom_route_found = True
66
+ break
67
+
68
+ assert custom_route_found, "Custom route was not found in app routes"
69
+
70
+ def test_multiple_custom_routes(
71
+ self,
72
+ ):
73
+ """Test that multiple custom routes are included in both methods."""
74
+ server = FastMCP()
75
+
76
+ custom_paths = ["/route1", "/route2", "/route3"]
77
+
78
+ # Add multiple custom routes
79
+ for path in custom_paths:
80
+
81
+ @server.custom_route(path, methods=["GET"])
82
+ async def custom_route(request: Request):
83
+ return JSONResponse({"message": f"route {path}"})
84
+
85
+ # Test with server.http_app()
86
+ app1 = server.http_app()
87
+
88
+ # Test with direct constructor call
89
+ app2 = create_streamable_http_app(server=server, streamable_http_path="/api")
90
+
91
+ # Check all routes are in both apps
92
+ for path in custom_paths:
93
+ # Check in app1
94
+ route_in_app1 = any(
95
+ isinstance(route, Route) and route.path == path for route in app1.routes
96
+ )
97
+ assert route_in_app1, f"Route {path} not found in server.http_app()"
98
+
99
+ # Check in app2
100
+ route_in_app2 = any(
101
+ isinstance(route, Route) and route.path == path for route in app2.routes
102
+ )
103
+ assert route_in_app2, (
104
+ f"Route {path} not found in create_streamable_http_app()"
105
+ )
tests/server/{test_http_dependencies.py → http/test_http_dependencies.py} RENAMED
File without changes
tests/server/{test_http_middleware.py → http/test_http_middleware.py} RENAMED
File without changes
tests/server/{test_openapi.py → openapi/test_openapi.py} RENAMED
@@ -18,11 +18,11 @@ from fastmcp.client import Client
18
  from fastmcp.exceptions import ToolError
19
  from fastmcp.server.openapi import (
20
  FastMCPOpenAPI,
 
21
  OpenAPIResource,
22
  OpenAPIResourceTemplate,
23
  OpenAPITool,
24
  RouteMap,
25
- RouteType,
26
  )
27
 
28
 
@@ -304,7 +304,7 @@ class TestTools:
304
  openapi_spec=openapi_spec,
305
  client=api_client,
306
  route_maps=[
307
- RouteMap(methods=["GET"], pattern=r".*", route_type=RouteType.TOOL)
308
  ],
309
  )
310
  async with Client(mcp_server) as client:
@@ -956,9 +956,7 @@ async def test_empty_query_parameters_not_sent(
956
  mcp_server = FastMCPOpenAPI(
957
  openapi_spec=openapi_spec,
958
  client=api_client,
959
- route_maps=[
960
- RouteMap(methods=["GET"], pattern=r".*", route_type=RouteType.TOOL)
961
- ],
962
  )
963
 
964
  # Call the search tool with mixed parameter values
@@ -1499,17 +1497,15 @@ class TestFastAPIDescriptionPropagation:
1499
  # Create custom route mappings
1500
  route_maps = [
1501
  # Map GET /items to Resource
1502
- RouteMap(
1503
- methods=["GET"], pattern=r"^/items$", route_type=RouteType.RESOURCE
1504
- ),
1505
  # Map GET /items/{item_id} to ResourceTemplate
1506
  RouteMap(
1507
  methods=["GET"],
1508
  pattern=r"^/items/\{.*\}$",
1509
- route_type=RouteType.RESOURCE_TEMPLATE,
1510
  ),
1511
  # Map POST /items to Tool
1512
- RouteMap(methods=["POST"], pattern=r"^/items$", route_type=RouteType.TOOL),
1513
  ]
1514
 
1515
  # Create FastMCP server with the OpenAPI spec and custom route mappings
@@ -1918,7 +1914,7 @@ class TestRouteMapWildcard:
1918
  ):
1919
  """Test that a RouteMap with methods='*' matches all HTTP methods."""
1920
  # Create a single route map with wildcard method
1921
- route_maps = [RouteMap(methods="*", pattern=r".*", route_type=RouteType.TOOL)]
1922
 
1923
  mcp = FastMCPOpenAPI(
1924
  openapi_spec=basic_openapi_spec,
@@ -1930,225 +1926,223 @@ class TestRouteMapWildcard:
1930
  tools = mcp._tool_manager.list_tools()
1931
  tool_names = {tool.name for tool in tools}
1932
 
1933
- # Check that all operations were mapped as tools
1934
  expected_tools = {"getUsers", "createUser", "getPosts", "createPost"}
1935
  assert tool_names == expected_tools
1936
 
1937
- # No resources or templates should be created
1938
- resources = mcp._resource_manager.get_resources()
1939
- templates = mcp._resource_manager.get_templates()
1940
- assert len(resources) == 0
1941
- assert len(templates) == 0
1942
-
1943
- async def test_priority_specific_over_wildcard(
1944
- self, basic_openapi_spec, mock_basic_client
1945
- ):
1946
- """Test that specific method maps take priority over wildcard."""
1947
- # Create route maps with specific method first, then wildcard
1948
- route_maps = [
1949
- # GET operations should be mapped to resources
1950
- RouteMap(methods=["GET"], pattern=r".*", route_type=RouteType.RESOURCE),
1951
- # All other operations should be mapped to tools
1952
- RouteMap(methods="*", pattern=r".*", route_type=RouteType.TOOL),
1953
- ]
1954
-
1955
- mcp = FastMCPOpenAPI(
1956
- openapi_spec=basic_openapi_spec,
1957
- client=mock_basic_client,
1958
- route_maps=route_maps,
1959
- )
1960
-
1961
- # Check GET operations went to resources
1962
- resources = mcp._resource_manager.get_resources()
1963
- resource_names = {r.name for r in resources.values()}
1964
- assert "getUsers" in resource_names
1965
- assert "getPosts" in resource_names
1966
- assert len(resources) == 2
1967
-
1968
- # Check other operations went to tools
1969
- tools = mcp._tool_manager.list_tools()
1970
- tool_names = {tool.name for tool in tools}
1971
- assert "createUser" in tool_names
1972
- assert "createPost" in tool_names
1973
- assert len(tools) == 2
1974
-
1975
- async def test_priority_wildcard_first(self, basic_openapi_spec, mock_basic_client):
1976
- """Test that when wildcard is first, it matches everything."""
1977
- # Create route maps with wildcard first, then specific methods
1978
- route_maps = [
1979
- # Wildcard first matches everything
1980
- RouteMap(methods="*", pattern=r".*", route_type=RouteType.TOOL),
1981
- # This should never be reached
1982
- RouteMap(methods=["GET"], pattern=r".*", route_type=RouteType.RESOURCE),
1983
- ]
1984
-
1985
- mcp = FastMCPOpenAPI(
1986
- openapi_spec=basic_openapi_spec,
1987
- client=mock_basic_client,
1988
- route_maps=route_maps,
1989
- )
1990
-
1991
- # All operations should be tools
1992
- tools = mcp._tool_manager.list_tools()
1993
- assert len(tools) == 4
1994
-
1995
- # No resources should be created
1996
- resources = mcp._resource_manager.get_resources()
1997
- assert len(resources) == 0
1998
-
1999
- async def test_wildcard_with_specific_paths(
2000
- self, basic_openapi_spec, mock_basic_client
2001
- ):
2002
- """Test wildcard methods combined with specific path patterns."""
2003
- route_maps = [
2004
- # All methods on /users path -> Resources
2005
- RouteMap(methods="*", pattern=r".*/users$", route_type=RouteType.RESOURCE),
2006
- # All methods on /posts path -> Tools
2007
- RouteMap(methods="*", pattern=r".*/posts$", route_type=RouteType.TOOL),
2008
- ]
2009
-
2010
- mcp = FastMCPOpenAPI(
2011
- openapi_spec=basic_openapi_spec,
2012
- client=mock_basic_client,
2013
- route_maps=route_maps,
2014
- )
2015
 
2016
- # Check /users operations went to resources
2017
- resources = mcp._resource_manager.get_resources()
2018
- resource_names = {r.name for r in resources.values()}
2019
- assert "getUsers" in resource_names
2020
- assert "createUser" in resource_names
2021
- assert len(resources) == 2
2022
-
2023
- # Check /posts operations went to tools
2024
- tools = mcp._tool_manager.list_tools()
2025
- tool_names = {tool.name for tool in tools}
2026
- assert "getPosts" in tool_names
2027
- assert "createPost" in tool_names
2028
- assert len(tools) == 2
2029
-
2030
-
2031
- class TestAllRoutesAsTools:
2032
- """Tests for the all_routes_as_tools parameter in FastMCP class methods."""
2033
 
2034
  @pytest.fixture
2035
- def simple_api_spec(self) -> dict:
2036
- """A simple OpenAPI spec with both GET and POST methods."""
2037
  return {
2038
  "openapi": "3.1.0",
2039
- "info": {"title": "Test API", "version": "1.0.0"},
2040
  "paths": {
2041
- "/items": {
2042
  "get": {
2043
- "operationId": "getItems",
 
2044
  "responses": {"200": {"description": "Success"}},
2045
  },
2046
  "post": {
2047
- "operationId": "createItem",
 
2048
  "responses": {"201": {"description": "Created"}},
2049
  },
2050
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2051
  },
2052
  }
2053
 
2054
  @pytest.fixture
2055
  async def mock_client(self) -> httpx.AsyncClient:
2056
- """Simple mock client for testing."""
2057
 
2058
  async def _responder(request):
2059
- return httpx.Response(200, json={"result": "ok"})
2060
 
2061
  transport = httpx.MockTransport(_responder)
2062
  return httpx.AsyncClient(transport=transport, base_url="http://test")
2063
 
2064
- async def test_from_openapi_all_routes_as_tools(self, simple_api_spec, mock_client):
2065
- """Test FastMCP.from_openapi with all_routes_as_tools=True."""
2066
- # Create server with all routes as tools
2067
- server = FastMCP.from_openapi(
2068
- openapi_spec=simple_api_spec, client=mock_client, all_routes_as_tools=True
 
 
 
 
 
 
 
2069
  )
2070
 
2071
- # All operations (GET and POST) should be mapped to tools
2072
- tools = server._tool_manager.list_tools()
2073
- tool_names = {t.name for t in tools}
2074
 
2075
- assert "getItems" in tool_names
2076
- assert "createItem" in tool_names
2077
- assert len(tools) == 2
 
 
 
2078
 
2079
- # No resources or templates should be created
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2080
  resources = server._resource_manager.get_resources()
2081
- templates = server._resource_manager.get_templates()
2082
- assert len(resources) == 0
2083
- assert len(templates) == 0
2084
 
2085
- async def test_from_openapi_all_routes_as_tools_conflicting_args(
2086
- self, simple_api_spec, mock_client
2087
- ):
2088
- """Test FastMCP.from_openapi raises error when both route_maps and all_routes_as_tools are provided."""
2089
- # Try to create server with conflicting args
2090
- with pytest.raises(
2091
- ValueError, match="Cannot specify both all_routes_as_tools and route_maps"
2092
- ):
2093
- FastMCP.from_openapi(
2094
- openapi_spec=simple_api_spec,
2095
- client=mock_client,
2096
- all_routes_as_tools=True,
2097
- route_maps=[
2098
- RouteMap(
2099
- methods=["GET"], pattern=r".*", route_type=RouteType.RESOURCE
2100
- )
2101
- ],
2102
- )
2103
 
2104
- async def test_from_fastapi_all_routes_as_tools(self):
2105
- """Test FastMCP.from_fastapi with all_routes_as_tools=True."""
2106
- # Create a simple FastAPI app
2107
- app = FastAPI(title="Test FastAPI")
2108
 
2109
- @app.get("/items")
2110
- async def get_items():
2111
- return [{"id": 1, "name": "Item 1"}]
 
 
2112
 
2113
- @app.post("/items")
2114
- async def create_item(item: dict):
2115
- return {"id": 2, **item}
 
 
 
 
 
 
 
 
 
2116
 
2117
- # Create server with all routes as tools
2118
- server = FastMCP.from_fastapi(app=app, all_routes_as_tools=True)
 
 
 
2119
 
2120
- # Both GET and POST operations should be mapped to tools
2121
- tools = server._tool_manager.list_tools()
2122
 
2123
- # Get tool names from the generated operation IDs
2124
- tool_names = {t.name for t in tools}
2125
 
2126
- # Check that both routes were mapped to tools
2127
- # The exact names depend on FastAPI's operation ID generation
2128
- assert len(tools) == 2
2129
- assert any("get" in name.lower() for name in tool_names)
2130
- assert any("post" in name.lower() for name in tool_names)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2131
 
2132
- # No resources or templates should be created
2133
  resources = server._resource_manager.get_resources()
2134
- templates = server._resource_manager.get_templates()
2135
- assert len(resources) == 0
2136
- assert len(templates) == 0
2137
 
2138
- async def test_from_fastapi_all_routes_as_tools_conflicting_args(self):
2139
- """Test FastMCP.from_fastapi raises error when both route_maps and all_routes_as_tools are provided."""
2140
- app = FastAPI(title="Test FastAPI")
2141
 
2142
- # Try to create server with conflicting args
2143
- with pytest.raises(
2144
- ValueError, match="Cannot specify both all_routes_as_tools and route_maps"
2145
- ):
2146
- FastMCP.from_fastapi(
2147
- app=app,
2148
- all_routes_as_tools=True,
2149
- route_maps=[
2150
- RouteMap(
2151
- methods=["GET"], pattern=r".*", route_type=RouteType.RESOURCE
2152
- )
2153
- ],
2154
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  from fastmcp.exceptions import ToolError
19
  from fastmcp.server.openapi import (
20
  FastMCPOpenAPI,
21
+ MCPType,
22
  OpenAPIResource,
23
  OpenAPIResourceTemplate,
24
  OpenAPITool,
25
  RouteMap,
 
26
  )
27
 
28
 
 
304
  openapi_spec=openapi_spec,
305
  client=api_client,
306
  route_maps=[
307
+ RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.TOOL)
308
  ],
309
  )
310
  async with Client(mcp_server) as client:
 
956
  mcp_server = FastMCPOpenAPI(
957
  openapi_spec=openapi_spec,
958
  client=api_client,
959
+ route_maps=[RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.TOOL)],
 
 
960
  )
961
 
962
  # Call the search tool with mixed parameter values
 
1497
  # Create custom route mappings
1498
  route_maps = [
1499
  # Map GET /items to Resource
1500
+ RouteMap(methods=["GET"], pattern=r"^/items$", mcp_type=MCPType.RESOURCE),
 
 
1501
  # Map GET /items/{item_id} to ResourceTemplate
1502
  RouteMap(
1503
  methods=["GET"],
1504
  pattern=r"^/items/\{.*\}$",
1505
+ mcp_type=MCPType.RESOURCE_TEMPLATE,
1506
  ),
1507
  # Map POST /items to Tool
1508
+ RouteMap(methods=["POST"], pattern=r"^/items$", mcp_type=MCPType.TOOL),
1509
  ]
1510
 
1511
  # Create FastMCP server with the OpenAPI spec and custom route mappings
 
1914
  ):
1915
  """Test that a RouteMap with methods='*' matches all HTTP methods."""
1916
  # Create a single route map with wildcard method
1917
+ route_maps = [RouteMap(methods="*", pattern=r".*", mcp_type=MCPType.TOOL)]
1918
 
1919
  mcp = FastMCPOpenAPI(
1920
  openapi_spec=basic_openapi_spec,
 
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_openapi_path_parameters.py → openapi/test_openapi_path_parameters.py} RENAMED
@@ -6,7 +6,7 @@ import pytest
6
  from fastapi import FastAPI, Query
7
 
8
  from fastmcp import Client, FastMCP
9
- from fastmcp.server.openapi import OpenAPITool, RouteMap, RouteType
10
  from fastmcp.utilities.openapi import HTTPRoute, ParameterInfo
11
 
12
 
@@ -286,9 +286,7 @@ async def test_array_query_param_with_fastapi():
286
  # Create a FastMCP server from the FastAPI app
287
  mcp = FastMCP.from_fastapi(
288
  app,
289
- route_maps=[
290
- RouteMap(methods=["GET"], pattern=r".*", route_type=RouteType.TOOL)
291
- ],
292
  )
293
 
294
  # Test with the client
 
6
  from fastapi import FastAPI, Query
7
 
8
  from fastmcp import Client, FastMCP
9
+ from fastmcp.server.openapi import MCPType, OpenAPITool, RouteMap
10
  from fastmcp.utilities.openapi import HTTPRoute, ParameterInfo
11
 
12
 
 
286
  # Create a FastMCP server from the FastAPI app
287
  mcp = FastMCP.from_fastapi(
288
  app,
289
+ route_maps=[RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.TOOL)],
 
 
290
  )
291
 
292
  # Test with the client
tests/server/test_context.py CHANGED
@@ -2,9 +2,11 @@ import warnings
2
  from unittest.mock import MagicMock, patch
3
 
4
  import pytest
 
5
  from starlette.requests import Request
6
 
7
  from fastmcp.server.context import Context
 
8
 
9
 
10
  class TestContextDeprecations:
@@ -57,3 +59,30 @@ class TestContextDeprecations:
57
  assert "https://gofastmcp.com/patterns/http-requests" in str(
58
  warning.message
59
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  from unittest.mock import MagicMock, patch
3
 
4
  import pytest
5
+ from mcp.types import ModelPreferences
6
  from starlette.requests import Request
7
 
8
  from fastmcp.server.context import Context
9
+ from fastmcp.server.server import FastMCP
10
 
11
 
12
  class TestContextDeprecations:
 
59
  assert "https://gofastmcp.com/patterns/http-requests" in str(
60
  warning.message
61
  )
62
+
63
+
64
+ @pytest.fixture
65
+ def context():
66
+ return Context(fastmcp=FastMCP())
67
+
68
+
69
+ class TestParseModelPreferences:
70
+ def test_parse_model_preferences_string(self, context):
71
+ mp = context._parse_model_preferences("claude-3-sonnet")
72
+ assert isinstance(mp, ModelPreferences)
73
+ assert mp.hints is not None
74
+ assert mp.hints[0].name == "claude-3-sonnet"
75
+
76
+ def test_parse_model_preferences_list(self, context):
77
+ mp = context._parse_model_preferences(["claude-3-sonnet", "claude"])
78
+ assert isinstance(mp, ModelPreferences)
79
+ assert mp.hints is not None
80
+ assert [h.name for h in mp.hints] == ["claude-3-sonnet", "claude"]
81
+
82
+ def test_parse_model_preferences_object(self, context):
83
+ obj = ModelPreferences(hints=[])
84
+ assert context._parse_model_preferences(obj) is obj
85
+
86
+ def test_parse_model_preferences_invalid_type(self, context):
87
+ with pytest.raises(ValueError):
88
+ context._parse_model_preferences(123)
tests/utilities/test_mcp_config.py CHANGED
@@ -9,7 +9,7 @@ from fastmcp.client.transports import (
9
  StdioTransport,
10
  StreamableHttpTransport,
11
  )
12
- from fastmcp.utilities.mcp_config import LocalMCPServer, MCPConfig, RemoteMCPServer
13
 
14
 
15
  def test_parse_single_stdio_config():
@@ -89,7 +89,7 @@ def test_parse_multiple_servers():
89
  assert isinstance(mcp_config.mcpServers["test_server"], RemoteMCPServer)
90
  assert isinstance(mcp_config.mcpServers["test_server"].to_transport(), SSETransport)
91
 
92
- assert isinstance(mcp_config.mcpServers["test_server_2"], LocalMCPServer)
93
  assert isinstance(
94
  mcp_config.mcpServers["test_server_2"].to_transport(), StdioTransport
95
  )
 
9
  StdioTransport,
10
  StreamableHttpTransport,
11
  )
12
+ from fastmcp.utilities.mcp_config import MCPConfig, RemoteMCPServer, StdioMCPServer
13
 
14
 
15
  def test_parse_single_stdio_config():
 
89
  assert isinstance(mcp_config.mcpServers["test_server"], RemoteMCPServer)
90
  assert isinstance(mcp_config.mcpServers["test_server"].to_transport(), SSETransport)
91
 
92
+ assert isinstance(mcp_config.mcpServers["test_server_2"], StdioMCPServer)
93
  assert isinstance(
94
  mcp_config.mcpServers["test_server_2"].to_transport(), StdioTransport
95
  )