Spaces:
Running
Running
Merge pull request #861 from jlowin/mounting
Browse files- docs/servers/composition.mdx +81 -9
- src/fastmcp/server/server.py +196 -107
- test_revert_check.py +50 -0
- tests/deprecated/test_deprecated.py +0 -80
- tests/deprecated/test_mount_import_arg_order.py +275 -0
- tests/deprecated/test_mount_separators.py +71 -46
- tests/deprecated/test_resource_prefixes.py +6 -4
- tests/server/openapi/test_openapi.py +0 -22
- tests/server/test_import_server.py +233 -34
- tests/server/test_mount.py +379 -64
- tests/server/test_resource_prefix_formats.py +4 -4
- tests/server/test_server.py +14 -8
docs/servers/composition.mdx
CHANGED
|
@@ -26,9 +26,10 @@ The choice of importing or mounting depends on your use case and requirements.
|
|
| 26 |
|
| 27 |
| Feature | Importing | Mounting |
|
| 28 |
|---------|----------------|---------|
|
| 29 |
-
| **Method** | `FastMCP.import_server()` | `FastMCP.mount()` |
|
| 30 |
| **Composition Type** | One-time copy (static) | Live link (dynamic) |
|
| 31 |
| **Updates** | Changes to subserver NOT reflected | Changes to subserver immediately reflected |
|
|
|
|
| 32 |
| **Best For** | Bundling finalized components | Modular runtime composition |
|
| 33 |
|
| 34 |
### Proxy Servers
|
|
@@ -41,7 +42,7 @@ You can also create proxies from configuration dictionaries that follow the MCPC
|
|
| 41 |
|
| 42 |
## Importing (Static Composition)
|
| 43 |
|
| 44 |
-
The `import_server()` method copies all components (tools, resources, templates, prompts) from one `FastMCP` instance (the *subserver*) into another (the *main server*).
|
| 45 |
|
| 46 |
```python
|
| 47 |
from fastmcp import FastMCP
|
|
@@ -65,7 +66,7 @@ main_mcp = FastMCP(name="MainApp")
|
|
| 65 |
|
| 66 |
# Import subserver
|
| 67 |
async def setup():
|
| 68 |
-
await main_mcp.import_server("weather"
|
| 69 |
|
| 70 |
# Result: main_mcp now contains prefixed components:
|
| 71 |
# - Tool: "weather_get_forecast"
|
|
@@ -78,7 +79,7 @@ if __name__ == "__main__":
|
|
| 78 |
|
| 79 |
### How Importing Works
|
| 80 |
|
| 81 |
-
When you call `await main_mcp.import_server(
|
| 82 |
|
| 83 |
1. **Tools**: All tools from `subserver` are added to `main_mcp` with names prefixed using `{prefix}_`.
|
| 84 |
- `subserver.tool(name="my_tool")` becomes `main_mcp.tool(name="{prefix}_my_tool")`.
|
|
@@ -91,9 +92,63 @@ When you call `await main_mcp.import_server(prefix, subserver)`:
|
|
| 91 |
|
| 92 |
Note that `import_server` performs a **one-time copy** of components. Changes made to the `subserver` *after* importing **will not** be reflected in `main_mcp`. The `subserver`'s `lifespan` context is also **not** executed by the main server.
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
## Mounting (Live Linking)
|
| 95 |
|
| 96 |
-
The `mount()` method creates a **live link** between the `main_mcp` server and the `subserver`. Instead of copying components, requests for components matching the `prefix` are **delegated** to the `subserver` at runtime.
|
| 97 |
|
| 98 |
```python
|
| 99 |
import asyncio
|
|
@@ -109,7 +164,7 @@ def initial_tool():
|
|
| 109 |
|
| 110 |
# Mount subserver (synchronous operation)
|
| 111 |
main_mcp = FastMCP(name="MainAppLive")
|
| 112 |
-
main_mcp.mount("dynamic"
|
| 113 |
|
| 114 |
# Add a tool AFTER mounting - it will be accessible through main_mcp
|
| 115 |
@dynamic_mcp.tool
|
|
@@ -143,6 +198,20 @@ When mounting is configured:
|
|
| 143 |
|
| 144 |
The same prefixing rules apply as with `import_server` for naming tools, resources, templates, and prompts.
|
| 145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
### Direct vs. Proxy Mounting
|
| 147 |
|
| 148 |
<VersionBadge version="2.2.7" />
|
|
@@ -161,10 +230,13 @@ FastMCP supports two mounting modes:
|
|
| 161 |
|
| 162 |
```python
|
| 163 |
# Direct mounting (default when no custom lifespan)
|
| 164 |
-
main_mcp.mount("api"
|
| 165 |
|
| 166 |
# Proxy mounting (preserves full client lifecycle)
|
| 167 |
-
main_mcp.mount("api",
|
|
|
|
|
|
|
|
|
|
| 168 |
```
|
| 169 |
|
| 170 |
FastMCP automatically uses proxy mounting when the mounted server has a custom lifespan, but you can override this behavior with the `as_proxy` parameter.
|
|
@@ -178,7 +250,7 @@ When using `FastMCP.as_proxy()` to create a proxy server, mounting that server w
|
|
| 178 |
remote_proxy = FastMCP.as_proxy(Client("http://example.com/mcp"))
|
| 179 |
|
| 180 |
# Mount the proxy (always uses proxy mounting)
|
| 181 |
-
main_server.mount("remote"
|
| 182 |
```
|
| 183 |
|
| 184 |
|
|
|
|
| 26 |
|
| 27 |
| Feature | Importing | Mounting |
|
| 28 |
|---------|----------------|---------|
|
| 29 |
+
| **Method** | `FastMCP.import_server(server, prefix=None)` | `FastMCP.mount(server, prefix=None)` |
|
| 30 |
| **Composition Type** | One-time copy (static) | Live link (dynamic) |
|
| 31 |
| **Updates** | Changes to subserver NOT reflected | Changes to subserver immediately reflected |
|
| 32 |
+
| **Prefix** | Optional - omit for original names | Optional - omit for original names |
|
| 33 |
| **Best For** | Bundling finalized components | Modular runtime composition |
|
| 34 |
|
| 35 |
### Proxy Servers
|
|
|
|
| 42 |
|
| 43 |
## Importing (Static Composition)
|
| 44 |
|
| 45 |
+
The `import_server()` method copies all components (tools, resources, templates, prompts) from one `FastMCP` instance (the *subserver*) into another (the *main server*). An optional `prefix` can be provided to avoid naming conflicts. If no prefix is provided, components are imported without modification. When multiple servers are imported with the same prefix (or no prefix), the most recently imported server's components take precedence.
|
| 46 |
|
| 47 |
```python
|
| 48 |
from fastmcp import FastMCP
|
|
|
|
| 66 |
|
| 67 |
# Import subserver
|
| 68 |
async def setup():
|
| 69 |
+
await main_mcp.import_server(weather_mcp, prefix="weather")
|
| 70 |
|
| 71 |
# Result: main_mcp now contains prefixed components:
|
| 72 |
# - Tool: "weather_get_forecast"
|
|
|
|
| 79 |
|
| 80 |
### How Importing Works
|
| 81 |
|
| 82 |
+
When you call `await main_mcp.import_server(subserver, prefix={whatever})`:
|
| 83 |
|
| 84 |
1. **Tools**: All tools from `subserver` are added to `main_mcp` with names prefixed using `{prefix}_`.
|
| 85 |
- `subserver.tool(name="my_tool")` becomes `main_mcp.tool(name="{prefix}_my_tool")`.
|
|
|
|
| 92 |
|
| 93 |
Note that `import_server` performs a **one-time copy** of components. Changes made to the `subserver` *after* importing **will not** be reflected in `main_mcp`. The `subserver`'s `lifespan` context is also **not** executed by the main server.
|
| 94 |
|
| 95 |
+
<Tip>
|
| 96 |
+
The `prefix` parameter is optional. If omitted, components are imported without modification.
|
| 97 |
+
</Tip>
|
| 98 |
+
|
| 99 |
+
#### Importing Without Prefixes
|
| 100 |
+
|
| 101 |
+
<VersionBadge version="2.9.0" />
|
| 102 |
+
|
| 103 |
+
You can also import servers without specifying a prefix, which copies components using their original names:
|
| 104 |
+
|
| 105 |
+
```python
|
| 106 |
+
|
| 107 |
+
from fastmcp import FastMCP
|
| 108 |
+
import asyncio
|
| 109 |
+
|
| 110 |
+
# Define subservers
|
| 111 |
+
weather_mcp = FastMCP(name="WeatherService")
|
| 112 |
+
|
| 113 |
+
@weather_mcp.tool
|
| 114 |
+
def get_forecast(city: str) -> dict:
|
| 115 |
+
"""Get weather forecast."""
|
| 116 |
+
return {"city": city, "forecast": "Sunny"}
|
| 117 |
+
|
| 118 |
+
@weather_mcp.resource("data://cities/supported")
|
| 119 |
+
def list_supported_cities() -> list[str]:
|
| 120 |
+
"""List cities with weather support."""
|
| 121 |
+
return ["London", "Paris", "Tokyo"]
|
| 122 |
+
|
| 123 |
+
# Define main server
|
| 124 |
+
main_mcp = FastMCP(name="MainApp")
|
| 125 |
+
|
| 126 |
+
# Import subserver
|
| 127 |
+
async def setup():
|
| 128 |
+
# Import without prefix - components keep original names
|
| 129 |
+
await main_mcp.import_server(weather_mcp)
|
| 130 |
+
|
| 131 |
+
# Result: main_mcp now contains:
|
| 132 |
+
# - Tool: "get_forecast" (original name preserved)
|
| 133 |
+
# - Resource: "data://cities/supported" (original URI preserved)
|
| 134 |
+
|
| 135 |
+
if __name__ == "__main__":
|
| 136 |
+
asyncio.run(setup())
|
| 137 |
+
main_mcp.run()
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
#### Conflict Resolution
|
| 141 |
+
|
| 142 |
+
<VersionBadge version="2.9.0" />
|
| 143 |
+
|
| 144 |
+
When importing multiple servers with the same prefix, or no prefix, components from the **most recently imported** server take precedence.
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
|
| 149 |
## Mounting (Live Linking)
|
| 150 |
|
| 151 |
+
The `mount()` method creates a **live link** between the `main_mcp` server and the `subserver`. Instead of copying components, requests for components matching the optional `prefix` are **delegated** to the `subserver` at runtime. If no prefix is provided, the subserver's components are accessible without prefixing. When multiple servers are mounted with the same prefix (or no prefix), the most recently mounted server takes precedence for conflicting component names.
|
| 152 |
|
| 153 |
```python
|
| 154 |
import asyncio
|
|
|
|
| 164 |
|
| 165 |
# Mount subserver (synchronous operation)
|
| 166 |
main_mcp = FastMCP(name="MainAppLive")
|
| 167 |
+
main_mcp.mount(dynamic_mcp, prefix="dynamic")
|
| 168 |
|
| 169 |
# Add a tool AFTER mounting - it will be accessible through main_mcp
|
| 170 |
@dynamic_mcp.tool
|
|
|
|
| 198 |
|
| 199 |
The same prefixing rules apply as with `import_server` for naming tools, resources, templates, and prompts.
|
| 200 |
|
| 201 |
+
<Tip>
|
| 202 |
+
The `prefix` parameter is optional. If omitted, components are mounted without modification.
|
| 203 |
+
</Tip>
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
#### Mounting Without Prefixes
|
| 207 |
+
|
| 208 |
+
<VersionBadge version="2.9.0" />
|
| 209 |
+
|
| 210 |
+
You can also mount servers without specifying a prefix, which makes components accessible without prefixing. This works identically to [importing without prefixes](#importing-without-prefixes), including [conflict resolution](#conflict-resolution).
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
|
| 215 |
### Direct vs. Proxy Mounting
|
| 216 |
|
| 217 |
<VersionBadge version="2.2.7" />
|
|
|
|
| 230 |
|
| 231 |
```python
|
| 232 |
# Direct mounting (default when no custom lifespan)
|
| 233 |
+
main_mcp.mount(api_server, prefix="api")
|
| 234 |
|
| 235 |
# Proxy mounting (preserves full client lifecycle)
|
| 236 |
+
main_mcp.mount(api_server, prefix="api", as_proxy=True)
|
| 237 |
+
|
| 238 |
+
# Mounting without a prefix (components accessible without prefixing)
|
| 239 |
+
main_mcp.mount(api_server)
|
| 240 |
```
|
| 241 |
|
| 242 |
FastMCP automatically uses proxy mounting when the mounted server has a custom lifespan, but you can override this behavior with the `as_proxy` parameter.
|
|
|
|
| 250 |
remote_proxy = FastMCP.as_proxy(Client("http://example.com/mcp"))
|
| 251 |
|
| 252 |
# Mount the proxy (always uses proxy mounting)
|
| 253 |
+
main_server.mount(remote_proxy, prefix="remote")
|
| 254 |
```
|
| 255 |
|
| 256 |
|
src/fastmcp/server/server.py
CHANGED
|
@@ -12,9 +12,10 @@ from contextlib import (
|
|
| 12 |
AsyncExitStack,
|
| 13 |
asynccontextmanager,
|
| 14 |
)
|
|
|
|
| 15 |
from functools import partial
|
| 16 |
from pathlib import Path
|
| 17 |
-
from typing import TYPE_CHECKING, Any, Generic, Literal, overload
|
| 18 |
|
| 19 |
import anyio
|
| 20 |
import httpx
|
|
@@ -154,7 +155,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 154 |
self._cache = TimedCache(
|
| 155 |
expiration=datetime.timedelta(seconds=cache_expiration_seconds or 0)
|
| 156 |
)
|
| 157 |
-
self._mounted_servers:
|
| 158 |
self._additional_http_routes: list[BaseRoute] = []
|
| 159 |
self._tool_manager = ToolManager(
|
| 160 |
duplicate_behavior=on_duplicate_tools,
|
|
@@ -322,13 +323,21 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 322 |
"""Get all registered tools, indexed by registered key."""
|
| 323 |
if (tools := self._cache.get("tools")) is self._cache.NOT_FOUND:
|
| 324 |
tools: dict[str, Tool] = {}
|
| 325 |
-
|
|
|
|
|
|
|
| 326 |
try:
|
| 327 |
-
server_tools = await server.get_tools()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
tools.update(server_tools)
|
| 329 |
except Exception as e:
|
| 330 |
logger.warning(
|
| 331 |
-
f"Failed to get tools from mounted server '{prefix}': {e}"
|
| 332 |
)
|
| 333 |
continue
|
| 334 |
tools.update(self._tool_manager.get_tools())
|
|
@@ -345,13 +354,25 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 345 |
"""Get all registered resources, indexed by registered key."""
|
| 346 |
if (resources := self._cache.get("resources")) is self._cache.NOT_FOUND:
|
| 347 |
resources: dict[str, Resource] = {}
|
| 348 |
-
|
|
|
|
|
|
|
| 349 |
try:
|
| 350 |
-
server_resources = await server.get_resources()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
resources.update(server_resources)
|
| 352 |
except Exception as e:
|
| 353 |
logger.warning(
|
| 354 |
-
f"Failed to get resources from mounted server '{prefix}': {e}"
|
| 355 |
)
|
| 356 |
continue
|
| 357 |
resources.update(self._resource_manager.get_resources())
|
|
@@ -370,14 +391,28 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 370 |
templates := self._cache.get("resource_templates")
|
| 371 |
) is self._cache.NOT_FOUND:
|
| 372 |
templates: dict[str, ResourceTemplate] = {}
|
| 373 |
-
|
|
|
|
|
|
|
| 374 |
try:
|
| 375 |
-
server_templates =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
templates.update(server_templates)
|
| 377 |
except Exception as e:
|
| 378 |
logger.warning(
|
| 379 |
"Failed to get resource templates from mounted server "
|
| 380 |
-
f"'{prefix}': {e}"
|
| 381 |
)
|
| 382 |
continue
|
| 383 |
templates.update(self._resource_manager.get_templates())
|
|
@@ -396,13 +431,21 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 396 |
"""
|
| 397 |
if (prompts := self._cache.get("prompts")) is self._cache.NOT_FOUND:
|
| 398 |
prompts: dict[str, Prompt] = {}
|
| 399 |
-
|
|
|
|
|
|
|
| 400 |
try:
|
| 401 |
-
server_prompts = await server.get_prompts()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
prompts.update(server_prompts)
|
| 403 |
except Exception as e:
|
| 404 |
logger.warning(
|
| 405 |
-
f"Failed to get prompts from mounted server '{prefix}': {e}"
|
| 406 |
)
|
| 407 |
continue
|
| 408 |
prompts.update(self._prompt_manager.get_prompts())
|
|
@@ -562,10 +605,20 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 562 |
return await self._tool_manager.call_tool(key, arguments)
|
| 563 |
|
| 564 |
# Check mounted servers to see if they have the tool
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 569 |
|
| 570 |
raise NotFoundError(f"Unknown tool: {key!r}")
|
| 571 |
|
|
@@ -604,10 +657,28 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 604 |
)
|
| 605 |
]
|
| 606 |
else:
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 611 |
else:
|
| 612 |
raise NotFoundError(f"Unknown resource: {uri}")
|
| 613 |
|
|
@@ -653,10 +724,24 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 653 |
return await self._prompt_manager.render_prompt(name, arguments)
|
| 654 |
|
| 655 |
# Check mounted servers to see if they have the prompt
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 660 |
|
| 661 |
raise NotFoundError(f"Unknown prompt: {name}")
|
| 662 |
|
|
@@ -1380,15 +1465,15 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1380 |
|
| 1381 |
def mount(
|
| 1382 |
self,
|
| 1383 |
-
prefix: str,
|
| 1384 |
server: FastMCP[LifespanResultT],
|
|
|
|
| 1385 |
as_proxy: bool | None = None,
|
| 1386 |
*,
|
| 1387 |
tool_separator: str | None = None,
|
| 1388 |
resource_separator: str | None = None,
|
| 1389 |
prompt_separator: str | None = None,
|
| 1390 |
) -> None:
|
| 1391 |
-
"""Mount another FastMCP server on this server with
|
| 1392 |
|
| 1393 |
Unlike importing (with import_server), mounting establishes a dynamic connection
|
| 1394 |
between servers. When a client interacts with a mounted server's objects through
|
|
@@ -1396,7 +1481,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1396 |
This means changes to the mounted server are immediately reflected when accessed
|
| 1397 |
through the parent.
|
| 1398 |
|
| 1399 |
-
When a server is mounted:
|
| 1400 |
- Tools from the mounted server are accessible with prefixed names.
|
| 1401 |
Example: If server has a tool named "get_weather", it will be available as "prefix_get_weather".
|
| 1402 |
- Resources are accessible with prefixed URIs.
|
|
@@ -1409,6 +1494,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1409 |
Example: If server has a prompt named "weather_prompt", it will be available as
|
| 1410 |
"prefix_weather_prompt".
|
| 1411 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1412 |
There are two modes for mounting servers:
|
| 1413 |
1. Direct mounting (default when server has no custom lifespan): The parent server
|
| 1414 |
directly accesses the mounted server's objects in-memory for better performance.
|
|
@@ -1421,8 +1510,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1421 |
execution, but with slightly higher overhead.
|
| 1422 |
|
| 1423 |
Args:
|
| 1424 |
-
prefix: Prefix to use for the mounted server's objects.
|
| 1425 |
server: The FastMCP server to mount.
|
|
|
|
|
|
|
| 1426 |
as_proxy: Whether to treat the mounted server as a proxy. If None (default),
|
| 1427 |
automatically determined based on whether the server has a custom lifespan
|
| 1428 |
(True if it has a custom lifespan, False otherwise).
|
|
@@ -1434,6 +1524,20 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1434 |
from fastmcp.client.transports import FastMCPTransport
|
| 1435 |
from fastmcp.server.proxy import FastMCPProxy
|
| 1436 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1437 |
if tool_separator is not None:
|
| 1438 |
# Deprecated since 2.4.0
|
| 1439 |
if fastmcp.settings.deprecation_warnings:
|
|
@@ -1476,17 +1580,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1476 |
server=server,
|
| 1477 |
prefix=prefix,
|
| 1478 |
)
|
| 1479 |
-
self._mounted_servers
|
| 1480 |
-
self._cache.clear()
|
| 1481 |
-
|
| 1482 |
-
def unmount(self, prefix: str) -> None:
|
| 1483 |
-
self._mounted_servers.pop(prefix)
|
| 1484 |
self._cache.clear()
|
| 1485 |
|
| 1486 |
async def import_server(
|
| 1487 |
self,
|
| 1488 |
-
prefix: str,
|
| 1489 |
server: FastMCP[LifespanResultT],
|
|
|
|
| 1490 |
tool_separator: str | None = None,
|
| 1491 |
resource_separator: str | None = None,
|
| 1492 |
prompt_separator: str | None = None,
|
|
@@ -1500,7 +1600,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1500 |
future changes to the imported server will not be reflected in the
|
| 1501 |
importing server. Server-level configurations and lifespans are not imported.
|
| 1502 |
|
| 1503 |
-
When a server is imported:
|
| 1504 |
- The tools are imported with prefixed names
|
| 1505 |
Example: If server has a tool named "get_weather", it will be
|
| 1506 |
available as "prefix_get_weather"
|
|
@@ -1514,14 +1614,33 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1514 |
Example: If server has a prompt named "weather_prompt", it will be available as
|
| 1515 |
"prefix_weather_prompt"
|
| 1516 |
|
|
|
|
|
|
|
|
|
|
| 1517 |
Args:
|
| 1518 |
-
prefix: The prefix to use for the imported server
|
| 1519 |
server: The FastMCP server to import
|
|
|
|
|
|
|
| 1520 |
tool_separator: Deprecated. Separator for tool names.
|
| 1521 |
resource_separator: Deprecated and ignored. Prefix is now
|
| 1522 |
applied using the protocol://prefix/path format
|
| 1523 |
prompt_separator: Deprecated. Separator for prompt names.
|
| 1524 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1525 |
if tool_separator is not None:
|
| 1526 |
# Deprecated since 2.4.0
|
| 1527 |
if fastmcp.settings.deprecation_warnings:
|
|
@@ -1552,29 +1671,49 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1552 |
stacklevel=2,
|
| 1553 |
)
|
| 1554 |
|
| 1555 |
-
# Import tools from the
|
| 1556 |
-
tool_prefix = f"{prefix}_"
|
| 1557 |
for key, tool in (await server.get_tools()).items():
|
| 1558 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1559 |
|
| 1560 |
-
# Import resources and templates from the
|
| 1561 |
for key, resource in (await server.get_resources()).items():
|
| 1562 |
-
|
| 1563 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1564 |
|
| 1565 |
for key, template in (await server.get_resource_templates()).items():
|
| 1566 |
-
|
| 1567 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1568 |
|
| 1569 |
-
# Import prompts from the
|
| 1570 |
-
prompt_prefix = f"{prefix}_"
|
| 1571 |
for key, prompt in (await server.get_prompts()).items():
|
| 1572 |
-
|
| 1573 |
-
|
| 1574 |
-
|
| 1575 |
-
|
| 1576 |
-
|
| 1577 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1578 |
|
| 1579 |
self._cache.clear()
|
| 1580 |
|
|
@@ -1731,60 +1870,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1731 |
return True
|
| 1732 |
|
| 1733 |
|
|
|
|
| 1734 |
class MountedServer:
|
| 1735 |
-
|
| 1736 |
-
|
| 1737 |
-
prefix: str,
|
| 1738 |
-
server: FastMCP[LifespanResultT],
|
| 1739 |
-
):
|
| 1740 |
-
self.server = server
|
| 1741 |
-
self.prefix = prefix
|
| 1742 |
-
|
| 1743 |
-
async def get_tools(self) -> dict[str, Tool]:
|
| 1744 |
-
tools = await self.server.get_tools()
|
| 1745 |
-
return {f"{self.prefix}_{key}": tool for key, tool in tools.items()}
|
| 1746 |
-
|
| 1747 |
-
async def get_resources(self) -> dict[str, Resource]:
|
| 1748 |
-
resources = await self.server.get_resources()
|
| 1749 |
-
return {
|
| 1750 |
-
add_resource_prefix(
|
| 1751 |
-
key, self.prefix, self.server.resource_prefix_format
|
| 1752 |
-
): resource
|
| 1753 |
-
for key, resource in resources.items()
|
| 1754 |
-
}
|
| 1755 |
-
|
| 1756 |
-
async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
|
| 1757 |
-
templates = await self.server.get_resource_templates()
|
| 1758 |
-
return {
|
| 1759 |
-
add_resource_prefix(
|
| 1760 |
-
key, self.prefix, self.server.resource_prefix_format
|
| 1761 |
-
): template
|
| 1762 |
-
for key, template in templates.items()
|
| 1763 |
-
}
|
| 1764 |
-
|
| 1765 |
-
async def get_prompts(self) -> dict[str, Prompt]:
|
| 1766 |
-
prompts = await self.server.get_prompts()
|
| 1767 |
-
return {f"{self.prefix}_{key}": prompt for key, prompt in prompts.items()}
|
| 1768 |
-
|
| 1769 |
-
def match_tool(self, key: str) -> bool:
|
| 1770 |
-
return key.startswith(f"{self.prefix}_")
|
| 1771 |
-
|
| 1772 |
-
def strip_tool_prefix(self, key: str) -> str:
|
| 1773 |
-
return key.removeprefix(f"{self.prefix}_")
|
| 1774 |
-
|
| 1775 |
-
def match_resource(self, key: str) -> bool:
|
| 1776 |
-
return has_resource_prefix(key, self.prefix, self.server.resource_prefix_format)
|
| 1777 |
-
|
| 1778 |
-
def strip_resource_prefix(self, key: str) -> str:
|
| 1779 |
-
return remove_resource_prefix(
|
| 1780 |
-
key, self.prefix, self.server.resource_prefix_format
|
| 1781 |
-
)
|
| 1782 |
-
|
| 1783 |
-
def match_prompt(self, key: str) -> bool:
|
| 1784 |
-
return key.startswith(f"{self.prefix}_")
|
| 1785 |
-
|
| 1786 |
-
def strip_prompt_prefix(self, key: str) -> str:
|
| 1787 |
-
return key.removeprefix(f"{self.prefix}_")
|
| 1788 |
|
| 1789 |
|
| 1790 |
def add_resource_prefix(
|
|
|
|
| 12 |
AsyncExitStack,
|
| 13 |
asynccontextmanager,
|
| 14 |
)
|
| 15 |
+
from dataclasses import dataclass
|
| 16 |
from functools import partial
|
| 17 |
from pathlib import Path
|
| 18 |
+
from typing import TYPE_CHECKING, Any, Generic, Literal, cast, overload
|
| 19 |
|
| 20 |
import anyio
|
| 21 |
import httpx
|
|
|
|
| 155 |
self._cache = TimedCache(
|
| 156 |
expiration=datetime.timedelta(seconds=cache_expiration_seconds or 0)
|
| 157 |
)
|
| 158 |
+
self._mounted_servers: list[MountedServer] = []
|
| 159 |
self._additional_http_routes: list[BaseRoute] = []
|
| 160 |
self._tool_manager = ToolManager(
|
| 161 |
duplicate_behavior=on_duplicate_tools,
|
|
|
|
| 323 |
"""Get all registered tools, indexed by registered key."""
|
| 324 |
if (tools := self._cache.get("tools")) is self._cache.NOT_FOUND:
|
| 325 |
tools: dict[str, Tool] = {}
|
| 326 |
+
|
| 327 |
+
# iterate such that new mounts overwrite older ones
|
| 328 |
+
for mounted_server in self._mounted_servers:
|
| 329 |
try:
|
| 330 |
+
server_tools = await mounted_server.server.get_tools()
|
| 331 |
+
# Apply prefix to each tool key if prefix exists and is not empty
|
| 332 |
+
if mounted_server.prefix:
|
| 333 |
+
server_tools = {
|
| 334 |
+
f"{mounted_server.prefix}_{key}": tool
|
| 335 |
+
for key, tool in server_tools.items()
|
| 336 |
+
}
|
| 337 |
tools.update(server_tools)
|
| 338 |
except Exception as e:
|
| 339 |
logger.warning(
|
| 340 |
+
f"Failed to get tools from mounted server '{mounted_server.prefix}': {e}"
|
| 341 |
)
|
| 342 |
continue
|
| 343 |
tools.update(self._tool_manager.get_tools())
|
|
|
|
| 354 |
"""Get all registered resources, indexed by registered key."""
|
| 355 |
if (resources := self._cache.get("resources")) is self._cache.NOT_FOUND:
|
| 356 |
resources: dict[str, Resource] = {}
|
| 357 |
+
|
| 358 |
+
# iterate such that new mounts overwrite older ones
|
| 359 |
+
for mounted_server in self._mounted_servers:
|
| 360 |
try:
|
| 361 |
+
server_resources = await mounted_server.server.get_resources()
|
| 362 |
+
# Apply prefix to each resource key if prefix exists
|
| 363 |
+
if mounted_server.prefix:
|
| 364 |
+
server_resources = {
|
| 365 |
+
add_resource_prefix(
|
| 366 |
+
key,
|
| 367 |
+
mounted_server.prefix,
|
| 368 |
+
mounted_server.server.resource_prefix_format,
|
| 369 |
+
): resource
|
| 370 |
+
for key, resource in server_resources.items()
|
| 371 |
+
}
|
| 372 |
resources.update(server_resources)
|
| 373 |
except Exception as e:
|
| 374 |
logger.warning(
|
| 375 |
+
f"Failed to get resources from mounted server '{mounted_server.prefix}': {e}"
|
| 376 |
)
|
| 377 |
continue
|
| 378 |
resources.update(self._resource_manager.get_resources())
|
|
|
|
| 391 |
templates := self._cache.get("resource_templates")
|
| 392 |
) is self._cache.NOT_FOUND:
|
| 393 |
templates: dict[str, ResourceTemplate] = {}
|
| 394 |
+
|
| 395 |
+
# iterate such that new mounts overwrite older ones
|
| 396 |
+
for mounted_server in self._mounted_servers:
|
| 397 |
try:
|
| 398 |
+
server_templates = (
|
| 399 |
+
await mounted_server.server.get_resource_templates()
|
| 400 |
+
)
|
| 401 |
+
# Apply prefix to each template key if prefix exists
|
| 402 |
+
if mounted_server.prefix:
|
| 403 |
+
server_templates = {
|
| 404 |
+
add_resource_prefix(
|
| 405 |
+
key,
|
| 406 |
+
mounted_server.prefix,
|
| 407 |
+
mounted_server.server.resource_prefix_format,
|
| 408 |
+
): template
|
| 409 |
+
for key, template in server_templates.items()
|
| 410 |
+
}
|
| 411 |
templates.update(server_templates)
|
| 412 |
except Exception as e:
|
| 413 |
logger.warning(
|
| 414 |
"Failed to get resource templates from mounted server "
|
| 415 |
+
f"'{mounted_server.prefix}': {e}"
|
| 416 |
)
|
| 417 |
continue
|
| 418 |
templates.update(self._resource_manager.get_templates())
|
|
|
|
| 431 |
"""
|
| 432 |
if (prompts := self._cache.get("prompts")) is self._cache.NOT_FOUND:
|
| 433 |
prompts: dict[str, Prompt] = {}
|
| 434 |
+
|
| 435 |
+
# iterate such that new mounts overwrite older ones
|
| 436 |
+
for mounted_server in self._mounted_servers:
|
| 437 |
try:
|
| 438 |
+
server_prompts = await mounted_server.server.get_prompts()
|
| 439 |
+
# Apply prefix to each prompt key if prefix exists
|
| 440 |
+
if mounted_server.prefix:
|
| 441 |
+
server_prompts = {
|
| 442 |
+
f"{mounted_server.prefix}_{key}": prompt
|
| 443 |
+
for key, prompt in server_prompts.items()
|
| 444 |
+
}
|
| 445 |
prompts.update(server_prompts)
|
| 446 |
except Exception as e:
|
| 447 |
logger.warning(
|
| 448 |
+
f"Failed to get prompts from mounted server '{mounted_server.prefix}': {e}"
|
| 449 |
)
|
| 450 |
continue
|
| 451 |
prompts.update(self._prompt_manager.get_prompts())
|
|
|
|
| 605 |
return await self._tool_manager.call_tool(key, arguments)
|
| 606 |
|
| 607 |
# Check mounted servers to see if they have the tool
|
| 608 |
+
# iterate such that new mounts take precedence over older ones
|
| 609 |
+
for mounted_server in reversed(self._mounted_servers):
|
| 610 |
+
tool_key = key
|
| 611 |
+
try:
|
| 612 |
+
# If server has a prefix, check if key matches and strip prefix
|
| 613 |
+
if mounted_server.prefix:
|
| 614 |
+
if tool_key.startswith(f"{mounted_server.prefix}_"):
|
| 615 |
+
tool_key = tool_key.removeprefix(f"{mounted_server.prefix}_")
|
| 616 |
+
else:
|
| 617 |
+
continue
|
| 618 |
+
return await mounted_server.server._call_tool(tool_key, arguments)
|
| 619 |
+
except NotFoundError:
|
| 620 |
+
# Tool not found on this server, try the next one
|
| 621 |
+
continue
|
| 622 |
|
| 623 |
raise NotFoundError(f"Unknown tool: {key!r}")
|
| 624 |
|
|
|
|
| 657 |
)
|
| 658 |
]
|
| 659 |
else:
|
| 660 |
+
# iterate such that new mounts take precedence over older ones
|
| 661 |
+
for mounted_server in reversed(self._mounted_servers):
|
| 662 |
+
resource_uri = uri
|
| 663 |
+
try:
|
| 664 |
+
if mounted_server.prefix:
|
| 665 |
+
# If server has a prefix, check if URI matches and strip prefix
|
| 666 |
+
if has_resource_prefix(
|
| 667 |
+
str(resource_uri),
|
| 668 |
+
mounted_server.prefix,
|
| 669 |
+
mounted_server.server.resource_prefix_format,
|
| 670 |
+
):
|
| 671 |
+
resource_uri = remove_resource_prefix(
|
| 672 |
+
str(resource_uri),
|
| 673 |
+
mounted_server.prefix,
|
| 674 |
+
mounted_server.server.resource_prefix_format,
|
| 675 |
+
)
|
| 676 |
+
else:
|
| 677 |
+
continue
|
| 678 |
+
return await mounted_server.server._mcp_read_resource(resource_uri)
|
| 679 |
+
except NotFoundError:
|
| 680 |
+
# Resource not found on this server, try the next one
|
| 681 |
+
continue
|
| 682 |
else:
|
| 683 |
raise NotFoundError(f"Unknown resource: {uri}")
|
| 684 |
|
|
|
|
| 724 |
return await self._prompt_manager.render_prompt(name, arguments)
|
| 725 |
|
| 726 |
# Check mounted servers to see if they have the prompt
|
| 727 |
+
# iterate such that new mounts take precedence over older ones
|
| 728 |
+
for mounted_server in reversed(self._mounted_servers):
|
| 729 |
+
prompt_name = name
|
| 730 |
+
try:
|
| 731 |
+
if mounted_server.prefix:
|
| 732 |
+
# If server has a prefix, check if name matches and strip prefix
|
| 733 |
+
if prompt_name.startswith(f"{mounted_server.prefix}_"):
|
| 734 |
+
prompt_name = prompt_name.removeprefix(
|
| 735 |
+
f"{mounted_server.prefix}_"
|
| 736 |
+
)
|
| 737 |
+
else:
|
| 738 |
+
continue
|
| 739 |
+
return await mounted_server.server._mcp_get_prompt(
|
| 740 |
+
prompt_name, arguments
|
| 741 |
+
)
|
| 742 |
+
except NotFoundError:
|
| 743 |
+
# Prompt not found on this server, try the next one
|
| 744 |
+
continue
|
| 745 |
|
| 746 |
raise NotFoundError(f"Unknown prompt: {name}")
|
| 747 |
|
|
|
|
| 1465 |
|
| 1466 |
def mount(
|
| 1467 |
self,
|
|
|
|
| 1468 |
server: FastMCP[LifespanResultT],
|
| 1469 |
+
prefix: str | None = None,
|
| 1470 |
as_proxy: bool | None = None,
|
| 1471 |
*,
|
| 1472 |
tool_separator: str | None = None,
|
| 1473 |
resource_separator: str | None = None,
|
| 1474 |
prompt_separator: str | None = None,
|
| 1475 |
) -> None:
|
| 1476 |
+
"""Mount another FastMCP server on this server with an optional prefix.
|
| 1477 |
|
| 1478 |
Unlike importing (with import_server), mounting establishes a dynamic connection
|
| 1479 |
between servers. When a client interacts with a mounted server's objects through
|
|
|
|
| 1481 |
This means changes to the mounted server are immediately reflected when accessed
|
| 1482 |
through the parent.
|
| 1483 |
|
| 1484 |
+
When a server is mounted with a prefix:
|
| 1485 |
- Tools from the mounted server are accessible with prefixed names.
|
| 1486 |
Example: If server has a tool named "get_weather", it will be available as "prefix_get_weather".
|
| 1487 |
- Resources are accessible with prefixed URIs.
|
|
|
|
| 1494 |
Example: If server has a prompt named "weather_prompt", it will be available as
|
| 1495 |
"prefix_weather_prompt".
|
| 1496 |
|
| 1497 |
+
When a server is mounted without a prefix (prefix=None), its tools, resources, templates,
|
| 1498 |
+
and prompts are accessible with their original names. Multiple servers can be mounted
|
| 1499 |
+
without prefixes, and they will be tried in order until a match is found.
|
| 1500 |
+
|
| 1501 |
There are two modes for mounting servers:
|
| 1502 |
1. Direct mounting (default when server has no custom lifespan): The parent server
|
| 1503 |
directly accesses the mounted server's objects in-memory for better performance.
|
|
|
|
| 1510 |
execution, but with slightly higher overhead.
|
| 1511 |
|
| 1512 |
Args:
|
|
|
|
| 1513 |
server: The FastMCP server to mount.
|
| 1514 |
+
prefix: Optional prefix to use for the mounted server's objects. If None,
|
| 1515 |
+
the server's objects are accessible with their original names.
|
| 1516 |
as_proxy: Whether to treat the mounted server as a proxy. If None (default),
|
| 1517 |
automatically determined based on whether the server has a custom lifespan
|
| 1518 |
(True if it has a custom lifespan, False otherwise).
|
|
|
|
| 1524 |
from fastmcp.client.transports import FastMCPTransport
|
| 1525 |
from fastmcp.server.proxy import FastMCPProxy
|
| 1526 |
|
| 1527 |
+
# Deprecated since 2.9.0
|
| 1528 |
+
# Prior to 2.9.0, the first positional argument was the prefix and the
|
| 1529 |
+
# second was the server. Here we swap them if needed now that the prefix
|
| 1530 |
+
# is optional.
|
| 1531 |
+
if isinstance(server, str):
|
| 1532 |
+
if fastmcp.settings.deprecation_warnings:
|
| 1533 |
+
warnings.warn(
|
| 1534 |
+
"Mount prefixes are now optional and the first positional argument "
|
| 1535 |
+
"should be the server you want to mount.",
|
| 1536 |
+
DeprecationWarning,
|
| 1537 |
+
stacklevel=2,
|
| 1538 |
+
)
|
| 1539 |
+
server, prefix = cast(FastMCP[Any], prefix), server
|
| 1540 |
+
|
| 1541 |
if tool_separator is not None:
|
| 1542 |
# Deprecated since 2.4.0
|
| 1543 |
if fastmcp.settings.deprecation_warnings:
|
|
|
|
| 1580 |
server=server,
|
| 1581 |
prefix=prefix,
|
| 1582 |
)
|
| 1583 |
+
self._mounted_servers.append(mounted_server)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1584 |
self._cache.clear()
|
| 1585 |
|
| 1586 |
async def import_server(
|
| 1587 |
self,
|
|
|
|
| 1588 |
server: FastMCP[LifespanResultT],
|
| 1589 |
+
prefix: str | None = None,
|
| 1590 |
tool_separator: str | None = None,
|
| 1591 |
resource_separator: str | None = None,
|
| 1592 |
prompt_separator: str | None = None,
|
|
|
|
| 1600 |
future changes to the imported server will not be reflected in the
|
| 1601 |
importing server. Server-level configurations and lifespans are not imported.
|
| 1602 |
|
| 1603 |
+
When a server is imported with a prefix:
|
| 1604 |
- The tools are imported with prefixed names
|
| 1605 |
Example: If server has a tool named "get_weather", it will be
|
| 1606 |
available as "prefix_get_weather"
|
|
|
|
| 1614 |
Example: If server has a prompt named "weather_prompt", it will be available as
|
| 1615 |
"prefix_weather_prompt"
|
| 1616 |
|
| 1617 |
+
When a server is imported without a prefix (prefix=None), its tools, resources,
|
| 1618 |
+
templates, and prompts are imported with their original names.
|
| 1619 |
+
|
| 1620 |
Args:
|
|
|
|
| 1621 |
server: The FastMCP server to import
|
| 1622 |
+
prefix: Optional prefix to use for the imported server's objects. If None,
|
| 1623 |
+
objects are imported with their original names.
|
| 1624 |
tool_separator: Deprecated. Separator for tool names.
|
| 1625 |
resource_separator: Deprecated and ignored. Prefix is now
|
| 1626 |
applied using the protocol://prefix/path format
|
| 1627 |
prompt_separator: Deprecated. Separator for prompt names.
|
| 1628 |
"""
|
| 1629 |
+
|
| 1630 |
+
# Deprecated since 2.9.0
|
| 1631 |
+
# Prior to 2.9.0, the first positional argument was the prefix and the
|
| 1632 |
+
# second was the server. Here we swap them if needed now that the prefix
|
| 1633 |
+
# is optional.
|
| 1634 |
+
if isinstance(server, str):
|
| 1635 |
+
if fastmcp.settings.deprecation_warnings:
|
| 1636 |
+
warnings.warn(
|
| 1637 |
+
"Import prefixes are now optional and the first positional argument "
|
| 1638 |
+
"should be the server you want to import.",
|
| 1639 |
+
DeprecationWarning,
|
| 1640 |
+
stacklevel=2,
|
| 1641 |
+
)
|
| 1642 |
+
server, prefix = cast(FastMCP[Any], prefix), server
|
| 1643 |
+
|
| 1644 |
if tool_separator is not None:
|
| 1645 |
# Deprecated since 2.4.0
|
| 1646 |
if fastmcp.settings.deprecation_warnings:
|
|
|
|
| 1671 |
stacklevel=2,
|
| 1672 |
)
|
| 1673 |
|
| 1674 |
+
# Import tools from the server
|
|
|
|
| 1675 |
for key, tool in (await server.get_tools()).items():
|
| 1676 |
+
if prefix:
|
| 1677 |
+
tool_key = f"{prefix}_{key}"
|
| 1678 |
+
else:
|
| 1679 |
+
tool_key = key
|
| 1680 |
+
self._tool_manager.add_tool(tool, key=tool_key)
|
| 1681 |
|
| 1682 |
+
# Import resources and templates from the server
|
| 1683 |
for key, resource in (await server.get_resources()).items():
|
| 1684 |
+
if prefix:
|
| 1685 |
+
resource_key = add_resource_prefix(
|
| 1686 |
+
key, prefix, self.resource_prefix_format
|
| 1687 |
+
)
|
| 1688 |
+
else:
|
| 1689 |
+
resource_key = key
|
| 1690 |
+
self._resource_manager.add_resource(resource, key=resource_key)
|
| 1691 |
|
| 1692 |
for key, template in (await server.get_resource_templates()).items():
|
| 1693 |
+
if prefix:
|
| 1694 |
+
template_key = add_resource_prefix(
|
| 1695 |
+
key, prefix, self.resource_prefix_format
|
| 1696 |
+
)
|
| 1697 |
+
else:
|
| 1698 |
+
template_key = key
|
| 1699 |
+
self._resource_manager.add_template(template, key=template_key)
|
| 1700 |
|
| 1701 |
+
# Import prompts from the server
|
|
|
|
| 1702 |
for key, prompt in (await server.get_prompts()).items():
|
| 1703 |
+
if prefix:
|
| 1704 |
+
prompt_key = f"{prefix}_{key}"
|
| 1705 |
+
else:
|
| 1706 |
+
prompt_key = key
|
| 1707 |
+
self._prompt_manager.add_prompt(prompt, key=prompt_key)
|
| 1708 |
+
|
| 1709 |
+
if prefix:
|
| 1710 |
+
logger.info(f"Imported server {server.name} with prefix '{prefix}'")
|
| 1711 |
+
logger.debug(f"Imported tools with prefix '{prefix}_'")
|
| 1712 |
+
logger.debug(f"Imported resources and templates with prefix '{prefix}/'")
|
| 1713 |
+
logger.debug(f"Imported prompts with prefix '{prefix}_'")
|
| 1714 |
+
else:
|
| 1715 |
+
logger.info(f"Imported server {server.name}")
|
| 1716 |
+
logger.debug("Imported tools, resources, templates, and prompts")
|
| 1717 |
|
| 1718 |
self._cache.clear()
|
| 1719 |
|
|
|
|
| 1870 |
return True
|
| 1871 |
|
| 1872 |
|
| 1873 |
+
@dataclass
|
| 1874 |
class MountedServer:
|
| 1875 |
+
prefix: str | None
|
| 1876 |
+
server: FastMCP[Any]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1877 |
|
| 1878 |
|
| 1879 |
def add_resource_prefix(
|
test_revert_check.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Quick test to verify the revert worked correctly."""
|
| 3 |
+
|
| 4 |
+
import asyncio
|
| 5 |
+
|
| 6 |
+
from fastmcp import FastMCP
|
| 7 |
+
from fastmcp.client import Client
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
async def test_empty_prefix_behavior():
|
| 11 |
+
"""Test that empty prefix correctly adds underscore."""
|
| 12 |
+
|
| 13 |
+
main_app = FastMCP("MainApp")
|
| 14 |
+
sub_app = FastMCP("SubApp")
|
| 15 |
+
|
| 16 |
+
@sub_app.tool
|
| 17 |
+
def sub_tool() -> str:
|
| 18 |
+
return "This is from the sub app"
|
| 19 |
+
|
| 20 |
+
@sub_app.resource("data://test")
|
| 21 |
+
def sub_resource():
|
| 22 |
+
return "Resource data"
|
| 23 |
+
|
| 24 |
+
# Mount with empty prefix
|
| 25 |
+
main_app.mount("", sub_app)
|
| 26 |
+
|
| 27 |
+
# Check that tools have underscore prefix
|
| 28 |
+
tools = await main_app.get_tools()
|
| 29 |
+
print(f"Tools: {list(tools.keys())}")
|
| 30 |
+
assert "_sub_tool" in tools, f"Expected '_sub_tool' in {list(tools.keys())}"
|
| 31 |
+
|
| 32 |
+
# Check that resources work correctly
|
| 33 |
+
resources = await main_app.get_resources()
|
| 34 |
+
print(f"Resources: {list(resources.keys())}")
|
| 35 |
+
# Empty prefix for resources should result in no prefix change
|
| 36 |
+
assert "data://test" in resources, (
|
| 37 |
+
f"Expected 'data://test' in {list(resources.keys())}"
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# Test calling the tool
|
| 41 |
+
async with Client(main_app) as client:
|
| 42 |
+
result = await client.call_tool("_sub_tool", {})
|
| 43 |
+
print(f"Tool result: {result[0].text}")
|
| 44 |
+
assert "This is from the sub app" in result[0].text
|
| 45 |
+
|
| 46 |
+
print("✅ Empty prefix correctly adds underscore for tools!")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
if __name__ == "__main__":
|
| 50 |
+
asyncio.run(test_empty_prefix_behavior())
|
tests/deprecated/test_deprecated.py
CHANGED
|
@@ -109,83 +109,3 @@ def test_from_client_deprecation_warning():
|
|
| 109 |
server = FastMCP("TestServer")
|
| 110 |
with pytest.warns(DeprecationWarning, match="from_client"):
|
| 111 |
FastMCP.from_client(Client(server))
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
def test_mount_tool_separator_deprecation_warning():
|
| 115 |
-
"""Test that using tool_separator in mount() raises a deprecation warning."""
|
| 116 |
-
main_app = FastMCP("MainApp")
|
| 117 |
-
sub_app = FastMCP("SubApp")
|
| 118 |
-
|
| 119 |
-
with pytest.warns(
|
| 120 |
-
DeprecationWarning,
|
| 121 |
-
match="The tool_separator parameter is deprecated and will be removed in a future version",
|
| 122 |
-
):
|
| 123 |
-
main_app.mount("sub", sub_app, tool_separator="-")
|
| 124 |
-
|
| 125 |
-
# Verify the separator is ignored and the default is used
|
| 126 |
-
@sub_app.tool
|
| 127 |
-
def test_tool():
|
| 128 |
-
return "test"
|
| 129 |
-
|
| 130 |
-
mounted_server = main_app._mounted_servers["sub"]
|
| 131 |
-
assert mounted_server.match_tool("sub_test_tool")
|
| 132 |
-
assert not mounted_server.match_tool("sub-test_tool")
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
def test_mount_resource_separator_deprecation_warning():
|
| 136 |
-
"""Test that using resource_separator in mount() raises a deprecation warning."""
|
| 137 |
-
main_app = FastMCP("MainApp")
|
| 138 |
-
sub_app = FastMCP("SubApp")
|
| 139 |
-
|
| 140 |
-
with pytest.warns(
|
| 141 |
-
DeprecationWarning,
|
| 142 |
-
match="The resource_separator parameter is deprecated and ignored",
|
| 143 |
-
):
|
| 144 |
-
main_app.mount("sub", sub_app, resource_separator="+")
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
def test_mount_prompt_separator_deprecation_warning():
|
| 148 |
-
"""Test that using prompt_separator in mount() raises a deprecation warning."""
|
| 149 |
-
main_app = FastMCP("MainApp")
|
| 150 |
-
sub_app = FastMCP("SubApp")
|
| 151 |
-
|
| 152 |
-
with pytest.warns(
|
| 153 |
-
DeprecationWarning,
|
| 154 |
-
match="The prompt_separator parameter is deprecated and will be removed in a future version",
|
| 155 |
-
):
|
| 156 |
-
main_app.mount("sub", sub_app, prompt_separator="-")
|
| 157 |
-
|
| 158 |
-
# Verify the separator is ignored and the default is used
|
| 159 |
-
@sub_app.prompt
|
| 160 |
-
def test_prompt():
|
| 161 |
-
return "test"
|
| 162 |
-
|
| 163 |
-
mounted_server = main_app._mounted_servers["sub"]
|
| 164 |
-
assert mounted_server.match_prompt("sub_test_prompt")
|
| 165 |
-
assert not mounted_server.match_prompt("sub-test_prompt")
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
async def test_import_server_separator_deprecation_warnings():
|
| 169 |
-
"""Test that using separators in import_server() raises deprecation warnings."""
|
| 170 |
-
main_app = FastMCP("MainApp")
|
| 171 |
-
sub_app = FastMCP("SubApp")
|
| 172 |
-
|
| 173 |
-
with pytest.warns(
|
| 174 |
-
DeprecationWarning,
|
| 175 |
-
match="The tool_separator parameter is deprecated and will be removed in a future version",
|
| 176 |
-
):
|
| 177 |
-
await main_app.import_server("sub", sub_app, tool_separator="-")
|
| 178 |
-
|
| 179 |
-
main_app = FastMCP("MainApp")
|
| 180 |
-
with pytest.warns(
|
| 181 |
-
DeprecationWarning,
|
| 182 |
-
match="The resource_separator parameter is deprecated and ignored",
|
| 183 |
-
):
|
| 184 |
-
await main_app.import_server("sub", sub_app, resource_separator="+")
|
| 185 |
-
|
| 186 |
-
main_app = FastMCP("MainApp")
|
| 187 |
-
with pytest.warns(
|
| 188 |
-
DeprecationWarning,
|
| 189 |
-
match="The prompt_separator parameter is deprecated and will be removed in a future version",
|
| 190 |
-
):
|
| 191 |
-
await main_app.import_server("sub", sub_app, prompt_separator="-")
|
|
|
|
| 109 |
server = FastMCP("TestServer")
|
| 110 |
with pytest.warns(DeprecationWarning, match="from_client"):
|
| 111 |
FastMCP.from_client(Client(server))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/deprecated/test_mount_import_arg_order.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import warnings
|
| 2 |
+
|
| 3 |
+
from fastmcp import FastMCP
|
| 4 |
+
from fastmcp.client import Client
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class TestDeprecatedMountArgOrder:
|
| 8 |
+
"""Test deprecated positional argument order for mount() method."""
|
| 9 |
+
|
| 10 |
+
async def test_mount_deprecated_arg_order_with_warning(self):
|
| 11 |
+
"""Test that mount(prefix, server) still works but raises deprecation warning."""
|
| 12 |
+
main_app = FastMCP("MainApp")
|
| 13 |
+
sub_app = FastMCP("SubApp")
|
| 14 |
+
|
| 15 |
+
@sub_app.tool
|
| 16 |
+
def sub_tool() -> str:
|
| 17 |
+
return "Sub tool result"
|
| 18 |
+
|
| 19 |
+
# Test the deprecated argument order: mount(prefix, server)
|
| 20 |
+
with warnings.catch_warnings(record=True) as w:
|
| 21 |
+
warnings.simplefilter("always")
|
| 22 |
+
main_app.mount("sub", sub_app) # type: ignore[arg-type] # Old order: prefix first, server second
|
| 23 |
+
|
| 24 |
+
# Check that a deprecation warning was raised
|
| 25 |
+
assert len(w) == 1
|
| 26 |
+
assert issubclass(w[0].category, DeprecationWarning)
|
| 27 |
+
assert (
|
| 28 |
+
"Mount prefixes are now optional and the first positional argument should be the server"
|
| 29 |
+
in str(w[0].message)
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
# Verify the mount worked correctly despite deprecated order
|
| 33 |
+
tools = await main_app.get_tools()
|
| 34 |
+
assert "sub_sub_tool" in tools
|
| 35 |
+
|
| 36 |
+
# Test functionality
|
| 37 |
+
async with Client(main_app) as client:
|
| 38 |
+
result = await client.call_tool("sub_sub_tool", {})
|
| 39 |
+
assert result[0].text == "Sub tool result" # type: ignore[attr-defined]
|
| 40 |
+
|
| 41 |
+
async def test_mount_new_arg_order_no_warning(self):
|
| 42 |
+
"""Test that mount(server, prefix) works without deprecation warning."""
|
| 43 |
+
main_app = FastMCP("MainApp")
|
| 44 |
+
sub_app = FastMCP("SubApp")
|
| 45 |
+
|
| 46 |
+
@sub_app.tool
|
| 47 |
+
def sub_tool() -> str:
|
| 48 |
+
return "Sub tool result"
|
| 49 |
+
|
| 50 |
+
# Test the new argument order: mount(server, prefix)
|
| 51 |
+
with warnings.catch_warnings(record=True) as w:
|
| 52 |
+
warnings.simplefilter("always")
|
| 53 |
+
main_app.mount(sub_app, "sub") # New order: server first, prefix second
|
| 54 |
+
|
| 55 |
+
# Check that no deprecation warning was raised for argument order
|
| 56 |
+
mount_warnings = [
|
| 57 |
+
warning
|
| 58 |
+
for warning in w
|
| 59 |
+
if "Mount prefixes are now optional" in str(warning.message)
|
| 60 |
+
]
|
| 61 |
+
assert len(mount_warnings) == 0
|
| 62 |
+
|
| 63 |
+
# Verify the mount worked correctly
|
| 64 |
+
tools = await main_app.get_tools()
|
| 65 |
+
assert "sub_sub_tool" in tools
|
| 66 |
+
|
| 67 |
+
async def test_mount_deprecated_order_no_prefix(self):
|
| 68 |
+
"""Test deprecated order detection when first arg is empty string."""
|
| 69 |
+
main_app = FastMCP("MainApp")
|
| 70 |
+
sub_app = FastMCP("SubApp")
|
| 71 |
+
|
| 72 |
+
@sub_app.tool
|
| 73 |
+
def sub_tool() -> str:
|
| 74 |
+
return "Sub tool result"
|
| 75 |
+
|
| 76 |
+
# Test with empty string as first argument (old style for no prefix)
|
| 77 |
+
with warnings.catch_warnings(record=True) as w:
|
| 78 |
+
warnings.simplefilter("always")
|
| 79 |
+
main_app.mount("", sub_app) # type: ignore[arg-type] # Old order: empty prefix first, server second
|
| 80 |
+
|
| 81 |
+
# Check that a deprecation warning was raised
|
| 82 |
+
assert len(w) == 1
|
| 83 |
+
assert issubclass(w[0].category, DeprecationWarning)
|
| 84 |
+
assert (
|
| 85 |
+
"Mount prefixes are now optional and the first positional argument should be the server"
|
| 86 |
+
in str(w[0].message)
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
# Verify the mount worked correctly (no prefix)
|
| 90 |
+
tools = await main_app.get_tools()
|
| 91 |
+
assert "sub_tool" in tools # No prefix applied
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class TestDeprecatedImportArgOrder:
|
| 95 |
+
"""Test deprecated positional argument order for import_server() method."""
|
| 96 |
+
|
| 97 |
+
async def test_import_deprecated_arg_order_with_warning(self):
|
| 98 |
+
"""Test that import_server(prefix, server) still works but raises deprecation warning."""
|
| 99 |
+
main_app = FastMCP("MainApp")
|
| 100 |
+
sub_app = FastMCP("SubApp")
|
| 101 |
+
|
| 102 |
+
@sub_app.tool
|
| 103 |
+
def sub_tool() -> str:
|
| 104 |
+
return "Sub tool result"
|
| 105 |
+
|
| 106 |
+
# Test the deprecated argument order: import_server(prefix, server)
|
| 107 |
+
with warnings.catch_warnings(record=True) as w:
|
| 108 |
+
warnings.simplefilter("always")
|
| 109 |
+
await main_app.import_server("sub", sub_app) # type: ignore[arg-type] # Old order: prefix first, server second
|
| 110 |
+
|
| 111 |
+
# Check that a deprecation warning was raised
|
| 112 |
+
assert len(w) == 1
|
| 113 |
+
assert issubclass(w[0].category, DeprecationWarning)
|
| 114 |
+
assert (
|
| 115 |
+
"Import prefixes are now optional and the first positional argument should be the server"
|
| 116 |
+
in str(w[0].message)
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
# Verify the import worked correctly despite deprecated order
|
| 120 |
+
assert "sub_sub_tool" in main_app._tool_manager._tools
|
| 121 |
+
|
| 122 |
+
# Test functionality
|
| 123 |
+
async with Client(main_app) as client:
|
| 124 |
+
result = await client.call_tool("sub_sub_tool", {})
|
| 125 |
+
assert result[0].text == "Sub tool result" # type: ignore[attr-defined]
|
| 126 |
+
|
| 127 |
+
async def test_import_new_arg_order_no_warning(self):
|
| 128 |
+
"""Test that import_server(server, prefix) works without deprecation warning."""
|
| 129 |
+
main_app = FastMCP("MainApp")
|
| 130 |
+
sub_app = FastMCP("SubApp")
|
| 131 |
+
|
| 132 |
+
@sub_app.tool
|
| 133 |
+
def sub_tool() -> str:
|
| 134 |
+
return "Sub tool result"
|
| 135 |
+
|
| 136 |
+
# Test the new argument order: import_server(server, prefix)
|
| 137 |
+
with warnings.catch_warnings(record=True) as w:
|
| 138 |
+
warnings.simplefilter("always")
|
| 139 |
+
await main_app.import_server(
|
| 140 |
+
sub_app, "sub"
|
| 141 |
+
) # New order: server first, prefix second
|
| 142 |
+
|
| 143 |
+
# Check that no deprecation warning was raised for argument order
|
| 144 |
+
import_warnings = [
|
| 145 |
+
warning
|
| 146 |
+
for warning in w
|
| 147 |
+
if "Import prefixes are now optional" in str(warning.message)
|
| 148 |
+
]
|
| 149 |
+
assert len(import_warnings) == 0
|
| 150 |
+
|
| 151 |
+
# Verify the import worked correctly
|
| 152 |
+
assert "sub_sub_tool" in main_app._tool_manager._tools
|
| 153 |
+
|
| 154 |
+
async def test_import_deprecated_order_no_prefix(self):
|
| 155 |
+
"""Test deprecated order detection when first arg is empty string."""
|
| 156 |
+
main_app = FastMCP("MainApp")
|
| 157 |
+
sub_app = FastMCP("SubApp")
|
| 158 |
+
|
| 159 |
+
@sub_app.tool
|
| 160 |
+
def sub_tool() -> str:
|
| 161 |
+
return "Sub tool result"
|
| 162 |
+
|
| 163 |
+
# Test with empty string as first argument (old style for no prefix)
|
| 164 |
+
with warnings.catch_warnings(record=True) as w:
|
| 165 |
+
warnings.simplefilter("always")
|
| 166 |
+
await main_app.import_server("", sub_app) # type: ignore[arg-type] # Old order: empty prefix first, server second
|
| 167 |
+
|
| 168 |
+
# Check that a deprecation warning was raised
|
| 169 |
+
assert len(w) == 1
|
| 170 |
+
assert issubclass(w[0].category, DeprecationWarning)
|
| 171 |
+
assert (
|
| 172 |
+
"Import prefixes are now optional and the first positional argument should be the server"
|
| 173 |
+
in str(w[0].message)
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
# Verify the import worked correctly (no prefix)
|
| 177 |
+
assert "sub_tool" in main_app._tool_manager._tools # No prefix applied
|
| 178 |
+
|
| 179 |
+
async def test_import_deprecated_order_with_resources_and_prompts(self):
|
| 180 |
+
"""Test deprecated order works with all component types."""
|
| 181 |
+
main_app = FastMCP("MainApp")
|
| 182 |
+
sub_app = FastMCP("SubApp")
|
| 183 |
+
|
| 184 |
+
@sub_app.tool
|
| 185 |
+
def sub_tool() -> str:
|
| 186 |
+
return "Sub tool result"
|
| 187 |
+
|
| 188 |
+
@sub_app.resource(uri="data://config")
|
| 189 |
+
def sub_resource():
|
| 190 |
+
return "Sub resource data"
|
| 191 |
+
|
| 192 |
+
@sub_app.resource(uri="users://{user_id}/info")
|
| 193 |
+
def sub_template(user_id: str):
|
| 194 |
+
return f"Sub template for user {user_id}"
|
| 195 |
+
|
| 196 |
+
@sub_app.prompt
|
| 197 |
+
def sub_prompt() -> str:
|
| 198 |
+
return "Sub prompt content"
|
| 199 |
+
|
| 200 |
+
# Test the deprecated argument order with all component types
|
| 201 |
+
with warnings.catch_warnings(record=True) as w:
|
| 202 |
+
warnings.simplefilter("always")
|
| 203 |
+
await main_app.import_server("api", sub_app) # type: ignore[arg-type] # Old order: prefix first, server second
|
| 204 |
+
|
| 205 |
+
# Check that a deprecation warning was raised
|
| 206 |
+
assert len(w) == 1
|
| 207 |
+
assert issubclass(w[0].category, DeprecationWarning)
|
| 208 |
+
|
| 209 |
+
# Verify all component types were imported correctly with prefix
|
| 210 |
+
assert "api_sub_tool" in main_app._tool_manager._tools
|
| 211 |
+
assert "data://api/config" in main_app._resource_manager._resources
|
| 212 |
+
assert "users://api/{user_id}/info" in main_app._resource_manager._templates
|
| 213 |
+
assert "api_sub_prompt" in main_app._prompt_manager._prompts
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
class TestArgOrderDetection:
|
| 217 |
+
"""Test that argument order detection works correctly."""
|
| 218 |
+
|
| 219 |
+
async def test_mount_correctly_identifies_server_vs_string(self):
|
| 220 |
+
"""Test that mount correctly identifies FastMCP instances vs strings."""
|
| 221 |
+
main_app = FastMCP("MainApp")
|
| 222 |
+
sub_app = FastMCP("SubApp")
|
| 223 |
+
|
| 224 |
+
# This should NOT trigger deprecation warning (server first, prefix second)
|
| 225 |
+
with warnings.catch_warnings(record=True) as w:
|
| 226 |
+
warnings.simplefilter("always")
|
| 227 |
+
main_app.mount(sub_app, "prefix")
|
| 228 |
+
|
| 229 |
+
mount_warnings = [
|
| 230 |
+
warning
|
| 231 |
+
for warning in w
|
| 232 |
+
if "Mount prefixes are now optional" in str(warning.message)
|
| 233 |
+
]
|
| 234 |
+
assert len(mount_warnings) == 0
|
| 235 |
+
|
| 236 |
+
# This SHOULD trigger deprecation warning (string first, server second)
|
| 237 |
+
with warnings.catch_warnings(record=True) as w:
|
| 238 |
+
warnings.simplefilter("always")
|
| 239 |
+
main_app.mount("prefix2", sub_app) # type: ignore[arg-type]
|
| 240 |
+
|
| 241 |
+
mount_warnings = [
|
| 242 |
+
warning
|
| 243 |
+
for warning in w
|
| 244 |
+
if "Mount prefixes are now optional" in str(warning.message)
|
| 245 |
+
]
|
| 246 |
+
assert len(mount_warnings) == 1
|
| 247 |
+
|
| 248 |
+
async def test_import_correctly_identifies_server_vs_string(self):
|
| 249 |
+
"""Test that import_server correctly identifies FastMCP instances vs strings."""
|
| 250 |
+
main_app = FastMCP("MainApp")
|
| 251 |
+
sub_app = FastMCP("SubApp")
|
| 252 |
+
|
| 253 |
+
# This should NOT trigger deprecation warning (server first, prefix second)
|
| 254 |
+
with warnings.catch_warnings(record=True) as w:
|
| 255 |
+
warnings.simplefilter("always")
|
| 256 |
+
await main_app.import_server(sub_app, "prefix")
|
| 257 |
+
|
| 258 |
+
import_warnings = [
|
| 259 |
+
warning
|
| 260 |
+
for warning in w
|
| 261 |
+
if "Import prefixes are now optional" in str(warning.message)
|
| 262 |
+
]
|
| 263 |
+
assert len(import_warnings) == 0
|
| 264 |
+
|
| 265 |
+
# This SHOULD trigger deprecation warning (string first, server second)
|
| 266 |
+
with warnings.catch_warnings(record=True) as w:
|
| 267 |
+
warnings.simplefilter("always")
|
| 268 |
+
await main_app.import_server("prefix2", sub_app) # type: ignore[arg-type]
|
| 269 |
+
|
| 270 |
+
import_warnings = [
|
| 271 |
+
warning
|
| 272 |
+
for warning in w
|
| 273 |
+
if "Import prefixes are now optional" in str(warning.message)
|
| 274 |
+
]
|
| 275 |
+
assert len(import_warnings) == 1
|
tests/deprecated/test_mount_separators.py
CHANGED
|
@@ -1,65 +1,80 @@
|
|
| 1 |
"""Tests for the deprecated separator parameters in mount() and import_server() methods."""
|
| 2 |
|
| 3 |
import pytest
|
|
|
|
| 4 |
|
| 5 |
-
from fastmcp import FastMCP
|
| 6 |
|
| 7 |
# reset deprecation warnings for this module
|
| 8 |
pytestmark = pytest.mark.filterwarnings("default::DeprecationWarning")
|
| 9 |
|
| 10 |
|
| 11 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
"""Test that using tool_separator in mount() raises a deprecation warning."""
|
| 13 |
main_app = FastMCP("MainApp")
|
| 14 |
sub_app = FastMCP("SubApp")
|
| 15 |
|
| 16 |
-
with pytest.warns(
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
# Verify the separator is ignored and the default is used
|
| 23 |
@sub_app.tool
|
| 24 |
def test_tool():
|
| 25 |
return "test"
|
| 26 |
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
|
| 31 |
|
| 32 |
-
def
|
| 33 |
-
"""Test that using resource_separator in mount() raises a deprecation warning."""
|
| 34 |
-
main_app = FastMCP("MainApp")
|
| 35 |
-
sub_app = FastMCP("SubApp")
|
| 36 |
-
|
| 37 |
-
with pytest.warns(
|
| 38 |
-
DeprecationWarning,
|
| 39 |
-
match="The resource_separator parameter is deprecated and ignored",
|
| 40 |
-
):
|
| 41 |
-
main_app.mount("sub", sub_app, resource_separator="+")
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
def test_mount_prompt_separator_deprecation_warning():
|
| 45 |
"""Test that using prompt_separator in mount() raises a deprecation warning."""
|
| 46 |
main_app = FastMCP("MainApp")
|
| 47 |
sub_app = FastMCP("SubApp")
|
| 48 |
|
| 49 |
-
with pytest.warns(
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
# Verify the separator is ignored and the default is used
|
| 56 |
@sub_app.prompt
|
| 57 |
def test_prompt():
|
| 58 |
return "test"
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
| 63 |
|
| 64 |
|
| 65 |
async def test_import_server_separator_deprecation_warnings():
|
|
@@ -67,22 +82,32 @@ async def test_import_server_separator_deprecation_warnings():
|
|
| 67 |
main_app = FastMCP("MainApp")
|
| 68 |
sub_app = FastMCP("SubApp")
|
| 69 |
|
| 70 |
-
with pytest.warns(
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
main_app = FastMCP("MainApp")
|
| 77 |
-
with pytest.warns(
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
)
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
main_app = FastMCP("MainApp")
|
| 84 |
-
with pytest.warns(
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
)
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""Tests for the deprecated separator parameters in mount() and import_server() methods."""
|
| 2 |
|
| 3 |
import pytest
|
| 4 |
+
from mcp import McpError
|
| 5 |
|
| 6 |
+
from fastmcp import Client, FastMCP
|
| 7 |
|
| 8 |
# reset deprecation warnings for this module
|
| 9 |
pytestmark = pytest.mark.filterwarnings("default::DeprecationWarning")
|
| 10 |
|
| 11 |
|
| 12 |
+
def test_mount_resource_separator_deprecation_warning():
|
| 13 |
+
"""Test that using resource_separator in mount() raises a deprecation warning."""
|
| 14 |
+
main_app = FastMCP("MainApp")
|
| 15 |
+
sub_app = FastMCP("SubApp")
|
| 16 |
+
|
| 17 |
+
with pytest.warns(DeprecationWarning) as warnings:
|
| 18 |
+
main_app.mount("sub", sub_app, resource_separator="+") # type: ignore[arg-type]
|
| 19 |
+
|
| 20 |
+
# Check that we get both the argument order warning and the resource_separator warning
|
| 21 |
+
warning_messages = [str(w.message) for w in warnings]
|
| 22 |
+
assert any(
|
| 23 |
+
"resource_separator parameter is deprecated and ignored" in msg
|
| 24 |
+
for msg in warning_messages
|
| 25 |
+
)
|
| 26 |
+
assert any("Mount prefixes are now optional" in msg for msg in warning_messages)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
async def test_mount_tool_separator_deprecation_warning():
|
| 30 |
"""Test that using tool_separator in mount() raises a deprecation warning."""
|
| 31 |
main_app = FastMCP("MainApp")
|
| 32 |
sub_app = FastMCP("SubApp")
|
| 33 |
|
| 34 |
+
with pytest.warns(DeprecationWarning) as warnings:
|
| 35 |
+
main_app.mount("sub", sub_app, tool_separator="-") # type: ignore[arg-type]
|
| 36 |
+
|
| 37 |
+
# Check that we get both the argument order warning and the tool_separator warning
|
| 38 |
+
warning_messages = [str(w.message) for w in warnings]
|
| 39 |
+
assert any(
|
| 40 |
+
"tool_separator parameter is deprecated" in msg for msg in warning_messages
|
| 41 |
+
)
|
| 42 |
+
assert any("Mount prefixes are now optional" in msg for msg in warning_messages)
|
| 43 |
|
| 44 |
# Verify the separator is ignored and the default is used
|
| 45 |
@sub_app.tool
|
| 46 |
def test_tool():
|
| 47 |
return "test"
|
| 48 |
|
| 49 |
+
async with Client(main_app) as client:
|
| 50 |
+
assert "sub_test_tool" in {t.name for t in await client.list_tools()}
|
| 51 |
+
assert "sub-test_tool" not in {t.name for t in await client.list_tools()}
|
| 52 |
|
| 53 |
|
| 54 |
+
async def test_mount_prompt_separator_deprecation_warning():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
"""Test that using prompt_separator in mount() raises a deprecation warning."""
|
| 56 |
main_app = FastMCP("MainApp")
|
| 57 |
sub_app = FastMCP("SubApp")
|
| 58 |
|
| 59 |
+
with pytest.warns(DeprecationWarning) as warnings:
|
| 60 |
+
main_app.mount("sub", sub_app, prompt_separator="-") # type: ignore[arg-type]
|
| 61 |
+
|
| 62 |
+
# Check that we get both the argument order warning and the prompt_separator warning
|
| 63 |
+
warning_messages = [str(w.message) for w in warnings]
|
| 64 |
+
assert any(
|
| 65 |
+
"prompt_separator parameter is deprecated" in msg for msg in warning_messages
|
| 66 |
+
)
|
| 67 |
+
assert any("Mount prefixes are now optional" in msg for msg in warning_messages)
|
| 68 |
|
| 69 |
# Verify the separator is ignored and the default is used
|
| 70 |
@sub_app.prompt
|
| 71 |
def test_prompt():
|
| 72 |
return "test"
|
| 73 |
|
| 74 |
+
async with Client(main_app) as client:
|
| 75 |
+
assert await client.get_prompt("sub_test_prompt")
|
| 76 |
+
with pytest.raises(McpError, match="Unknown prompt"):
|
| 77 |
+
await client.get_prompt("sub-test_prompt")
|
| 78 |
|
| 79 |
|
| 80 |
async def test_import_server_separator_deprecation_warnings():
|
|
|
|
| 82 |
main_app = FastMCP("MainApp")
|
| 83 |
sub_app = FastMCP("SubApp")
|
| 84 |
|
| 85 |
+
with pytest.warns(DeprecationWarning) as warnings:
|
| 86 |
+
await main_app.import_server("sub", sub_app, tool_separator="-") # type: ignore[arg-type]
|
| 87 |
+
|
| 88 |
+
# Check that we get both warnings
|
| 89 |
+
warning_messages = [str(w.message) for w in warnings]
|
| 90 |
+
assert any(
|
| 91 |
+
"tool_separator parameter is deprecated" in msg for msg in warning_messages
|
| 92 |
+
)
|
| 93 |
+
assert any("Import prefixes are now optional" in msg for msg in warning_messages)
|
| 94 |
|
| 95 |
main_app = FastMCP("MainApp")
|
| 96 |
+
with pytest.warns(DeprecationWarning) as warnings:
|
| 97 |
+
await main_app.import_server("sub", sub_app, resource_separator="+") # type: ignore[arg-type]
|
| 98 |
+
|
| 99 |
+
warning_messages = [str(w.message) for w in warnings]
|
| 100 |
+
assert any(
|
| 101 |
+
"resource_separator parameter is deprecated" in msg for msg in warning_messages
|
| 102 |
+
)
|
| 103 |
+
assert any("Import prefixes are now optional" in msg for msg in warning_messages)
|
| 104 |
|
| 105 |
main_app = FastMCP("MainApp")
|
| 106 |
+
with pytest.warns(DeprecationWarning) as warnings:
|
| 107 |
+
await main_app.import_server("sub", sub_app, prompt_separator="-") # type: ignore[arg-type]
|
| 108 |
+
|
| 109 |
+
warning_messages = [str(w.message) for w in warnings]
|
| 110 |
+
assert any(
|
| 111 |
+
"prompt_separator parameter is deprecated" in msg for msg in warning_messages
|
| 112 |
+
)
|
| 113 |
+
assert any("Import prefixes are now optional" in msg for msg in warning_messages)
|
tests/deprecated/test_resource_prefixes.py
CHANGED
|
@@ -67,8 +67,9 @@ async def test_mount_with_legacy_prefixes():
|
|
| 67 |
def get_test():
|
| 68 |
return "test content"
|
| 69 |
|
| 70 |
-
# Mount the server with a prefix
|
| 71 |
-
|
|
|
|
| 72 |
|
| 73 |
# Check that the resource is prefixed using the legacy format
|
| 74 |
resources = await main_server.get_resources()
|
|
@@ -93,8 +94,9 @@ async def test_import_server_with_legacy_prefixes():
|
|
| 93 |
def get_test():
|
| 94 |
return "test content"
|
| 95 |
|
| 96 |
-
# Import the server with a prefix
|
| 97 |
-
|
|
|
|
| 98 |
|
| 99 |
# Check that the resource is prefixed using the legacy format
|
| 100 |
resources = main_server._resource_manager.get_resources()
|
|
|
|
| 67 |
def get_test():
|
| 68 |
return "test content"
|
| 69 |
|
| 70 |
+
# Mount the server with a prefix (using old argument order for this legacy test)
|
| 71 |
+
with pytest.warns(DeprecationWarning, match="Mount prefixes are now optional"):
|
| 72 |
+
main_server.mount("sub", sub_server) # type: ignore[arg-type]
|
| 73 |
|
| 74 |
# Check that the resource is prefixed using the legacy format
|
| 75 |
resources = await main_server.get_resources()
|
|
|
|
| 94 |
def get_test():
|
| 95 |
return "test content"
|
| 96 |
|
| 97 |
+
# Import the server with a prefix (using old argument order for this legacy test)
|
| 98 |
+
with pytest.warns(DeprecationWarning, match="Import prefixes are now optional"):
|
| 99 |
+
await main_server.import_server("sub", sub_server) # type: ignore[arg-type]
|
| 100 |
|
| 101 |
# Check that the resource is prefixed using the legacy format
|
| 102 |
resources = main_server._resource_manager.get_resources()
|
tests/server/openapi/test_openapi.py
CHANGED
|
@@ -911,28 +911,6 @@ class TestOpenAPI31Compatibility:
|
|
| 911 |
assert order["items"] == ["item4", "item5"]
|
| 912 |
|
| 913 |
|
| 914 |
-
class TestMountFastMCP:
|
| 915 |
-
"""Tests for mounting FastMCP servers."""
|
| 916 |
-
|
| 917 |
-
async def test_mount_fastmcp(
|
| 918 |
-
self, fastmcp_openapi_server_with_all_types: FastMCPOpenAPI
|
| 919 |
-
):
|
| 920 |
-
"""Test mounting an OpenAPI server."""
|
| 921 |
-
mcp = FastMCP("MainApp")
|
| 922 |
-
|
| 923 |
-
await mcp.import_server("fastapi", fastmcp_openapi_server_with_all_types)
|
| 924 |
-
|
| 925 |
-
# Check that resources are available with prefixed URIs
|
| 926 |
-
async with Client(mcp) as client:
|
| 927 |
-
resources = await client.list_resources()
|
| 928 |
-
assert len(resources) == 4 # Updated to account for new search endpoint
|
| 929 |
-
# We're checking the key used by mcp to store the resource
|
| 930 |
-
# The prefixed URI is used as the key, but the resource's original uri is preserved
|
| 931 |
-
prefixed_uri = "resource://fastapi/get_users_users_get"
|
| 932 |
-
resource = mcp._resource_manager.get_resources().get(prefixed_uri)
|
| 933 |
-
assert resource is not None
|
| 934 |
-
|
| 935 |
-
|
| 936 |
async def test_empty_query_parameters_not_sent(
|
| 937 |
fastapi_app: FastAPI, api_client: httpx.AsyncClient
|
| 938 |
):
|
|
|
|
| 911 |
assert order["items"] == ["item4", "item5"]
|
| 912 |
|
| 913 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 914 |
async def test_empty_query_parameters_not_sent(
|
| 915 |
fastapi_app: FastAPI, api_client: httpx.AsyncClient
|
| 916 |
):
|
tests/server/test_import_server.py
CHANGED
|
@@ -18,7 +18,7 @@ async def test_import_basic_functionality():
|
|
| 18 |
return "This is from the sub app"
|
| 19 |
|
| 20 |
# Import the sub-app to the main app
|
| 21 |
-
await main_app.import_server("sub"
|
| 22 |
|
| 23 |
# Verify the tool was imported with the prefix
|
| 24 |
assert "sub_sub_tool" in main_app._tool_manager._tools
|
|
@@ -49,8 +49,8 @@ async def test_import_multiple_apps():
|
|
| 49 |
return "News headlines"
|
| 50 |
|
| 51 |
# Import both sub-apps to the main app
|
| 52 |
-
await main_app.import_server("weather"
|
| 53 |
-
await main_app.import_server("news"
|
| 54 |
|
| 55 |
# Verify tools were imported with the correct prefixes
|
| 56 |
assert "weather_get_forecast" in main_app._tool_manager._tools
|
|
@@ -74,11 +74,11 @@ async def test_import_combines_tools():
|
|
| 74 |
return "Second app tool"
|
| 75 |
|
| 76 |
# Import first app
|
| 77 |
-
await main_app.import_server("api"
|
| 78 |
assert "api_first_tool" in main_app._tool_manager._tools
|
| 79 |
|
| 80 |
# Import second app to same prefix
|
| 81 |
-
await main_app.import_server("api"
|
| 82 |
|
| 83 |
# Verify second tool is there
|
| 84 |
assert "api_second_tool" in main_app._tool_manager._tools
|
|
@@ -99,7 +99,7 @@ async def test_import_with_resources():
|
|
| 99 |
return ["user1", "user2"]
|
| 100 |
|
| 101 |
# Import the data app
|
| 102 |
-
await main_app.import_server("data"
|
| 103 |
|
| 104 |
# Verify the resource was imported with the prefix
|
| 105 |
assert "data://data/users" in main_app._resource_manager._resources
|
|
@@ -117,7 +117,7 @@ async def test_import_with_resource_templates():
|
|
| 117 |
return {"id": user_id, "name": f"User {user_id}"}
|
| 118 |
|
| 119 |
# Import the user app
|
| 120 |
-
await main_app.import_server("api"
|
| 121 |
|
| 122 |
# Verify the template was imported with the prefix
|
| 123 |
assert "users://api/{user_id}/profile" in main_app._resource_manager._templates
|
|
@@ -135,7 +135,7 @@ async def test_import_with_prompts():
|
|
| 135 |
return f"Hello, {name}!"
|
| 136 |
|
| 137 |
# Import the assistant app
|
| 138 |
-
await main_app.import_server("assistant"
|
| 139 |
|
| 140 |
# Verify the prompt was imported with the prefix
|
| 141 |
assert "assistant_greeting" in main_app._prompt_manager._prompts
|
|
@@ -158,8 +158,8 @@ async def test_import_multiple_resource_templates():
|
|
| 158 |
return f"News for {category}"
|
| 159 |
|
| 160 |
# Import both apps
|
| 161 |
-
await main_app.import_server("data"
|
| 162 |
-
await main_app.import_server("content"
|
| 163 |
|
| 164 |
# Verify templates were imported with correct prefixes
|
| 165 |
assert "weather://data/{city}" in main_app._resource_manager._templates
|
|
@@ -183,8 +183,8 @@ async def test_import_multiple_prompts():
|
|
| 183 |
return f"Explaining SQL query:\n{query}"
|
| 184 |
|
| 185 |
# Import both apps
|
| 186 |
-
await main_app.import_server("python"
|
| 187 |
-
await main_app.import_server("sql"
|
| 188 |
|
| 189 |
# Verify prompts were imported with correct prefixes
|
| 190 |
assert "python_review_python" in main_app._prompt_manager._prompts
|
|
@@ -200,7 +200,7 @@ async def test_tool_custom_name_preserved_when_imported():
|
|
| 200 |
return f"Data for query: {query}"
|
| 201 |
|
| 202 |
api_app.add_tool(Tool.from_function(fetch_data, name="get_data"))
|
| 203 |
-
await main_app.import_server("api"
|
| 204 |
|
| 205 |
# Check that the tool is accessible by its prefixed name
|
| 206 |
tool = main_app._tool_manager.get_tool("api_get_data")
|
|
@@ -220,7 +220,7 @@ async def test_call_imported_custom_named_tool():
|
|
| 220 |
return f"Data for query: {query}"
|
| 221 |
|
| 222 |
api_app.add_tool(Tool.from_function(fetch_data, name="get_data"))
|
| 223 |
-
await main_app.import_server("api"
|
| 224 |
|
| 225 |
async with Client(main_app) as client:
|
| 226 |
result = await client.call_tool("api_get_data", {"query": "test"})
|
|
@@ -236,7 +236,7 @@ async def test_first_level_importing_with_custom_name():
|
|
| 236 |
return input * 2
|
| 237 |
|
| 238 |
provider_app.add_tool(Tool.from_function(calculate_value, name="compute"))
|
| 239 |
-
await service_app.import_server("provider"
|
| 240 |
|
| 241 |
# Tool is accessible in the service app with the first prefix
|
| 242 |
tool = service_app._tool_manager.get_tool("provider_compute")
|
|
@@ -255,8 +255,8 @@ async def test_nested_importing_preserves_prefixes():
|
|
| 255 |
return input * 2
|
| 256 |
|
| 257 |
provider_app.add_tool(Tool.from_function(calculate_value, name="compute"))
|
| 258 |
-
await service_app.import_server("provider"
|
| 259 |
-
await main_app.import_server("service"
|
| 260 |
|
| 261 |
# Tool is accessible in the main app with both prefixes
|
| 262 |
tool = main_app._tool_manager.get_tool("service_provider_compute")
|
|
@@ -273,13 +273,12 @@ async def test_call_nested_imported_tool():
|
|
| 273 |
return input * 2
|
| 274 |
|
| 275 |
provider_app.add_tool(Tool.from_function(calculate_value, name="compute"))
|
| 276 |
-
await service_app.import_server("provider"
|
| 277 |
-
await main_app.import_server("service"
|
| 278 |
|
| 279 |
-
|
| 280 |
-
"service_provider_compute", {"input": 21}
|
| 281 |
-
|
| 282 |
-
assert result[0].text == "42" # type: ignore[attr-defined]
|
| 283 |
|
| 284 |
|
| 285 |
async def test_import_with_proxy_tools():
|
|
@@ -299,10 +298,11 @@ async def test_import_with_proxy_tools():
|
|
| 299 |
return f"Data for query: {query}"
|
| 300 |
|
| 301 |
proxy_app = FastMCP.as_proxy(Client(api_app))
|
| 302 |
-
await main_app.import_server("api"
|
| 303 |
|
| 304 |
-
|
| 305 |
-
|
|
|
|
| 306 |
|
| 307 |
|
| 308 |
async def test_import_with_proxy_prompts():
|
|
@@ -322,11 +322,12 @@ async def test_import_with_proxy_prompts():
|
|
| 322 |
return f"Hello, {name} from API!"
|
| 323 |
|
| 324 |
proxy_app = FastMCP.as_proxy(Client(api_app))
|
| 325 |
-
await main_app.import_server("api"
|
| 326 |
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
|
|
|
| 330 |
|
| 331 |
|
| 332 |
async def test_import_with_proxy_resources():
|
|
@@ -349,7 +350,7 @@ async def test_import_with_proxy_resources():
|
|
| 349 |
}
|
| 350 |
|
| 351 |
proxy_app = FastMCP.as_proxy(Client(api_app))
|
| 352 |
-
await main_app.import_server("api"
|
| 353 |
|
| 354 |
# Access the resource through the main app with the prefixed key
|
| 355 |
async with Client(main_app) as client:
|
|
@@ -376,7 +377,7 @@ async def test_import_with_proxy_resource_templates():
|
|
| 376 |
return {"name": name, "email": email}
|
| 377 |
|
| 378 |
proxy_app = FastMCP.as_proxy(Client(api_app))
|
| 379 |
-
await main_app.import_server("api"
|
| 380 |
|
| 381 |
# Instantiate the template through the main app with the prefixed key
|
| 382 |
|
|
@@ -396,7 +397,7 @@ async def test_import_invalid_resource_prefix():
|
|
| 396 |
# This test doesn't apply anymore with the new prefix format since we're not validating
|
| 397 |
# the protocol://prefix/path format
|
| 398 |
# Just import the server to maintain test coverage without deprecated parameters
|
| 399 |
-
await main_app.import_server(
|
| 400 |
|
| 401 |
|
| 402 |
async def test_import_invalid_resource_separator():
|
|
@@ -405,4 +406,202 @@ async def test_import_invalid_resource_separator():
|
|
| 405 |
|
| 406 |
# This test is for maintaining coverage for importing with prefixes
|
| 407 |
# We no longer pass the deprecated resource_separator parameter
|
| 408 |
-
await main_app.import_server("api"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
return "This is from the sub app"
|
| 19 |
|
| 20 |
# Import the sub-app to the main app
|
| 21 |
+
await main_app.import_server(sub_app, "sub")
|
| 22 |
|
| 23 |
# Verify the tool was imported with the prefix
|
| 24 |
assert "sub_sub_tool" in main_app._tool_manager._tools
|
|
|
|
| 49 |
return "News headlines"
|
| 50 |
|
| 51 |
# Import both sub-apps to the main app
|
| 52 |
+
await main_app.import_server(weather_app, "weather")
|
| 53 |
+
await main_app.import_server(news_app, "news")
|
| 54 |
|
| 55 |
# Verify tools were imported with the correct prefixes
|
| 56 |
assert "weather_get_forecast" in main_app._tool_manager._tools
|
|
|
|
| 74 |
return "Second app tool"
|
| 75 |
|
| 76 |
# Import first app
|
| 77 |
+
await main_app.import_server(first_app, "api")
|
| 78 |
assert "api_first_tool" in main_app._tool_manager._tools
|
| 79 |
|
| 80 |
# Import second app to same prefix
|
| 81 |
+
await main_app.import_server(second_app, "api")
|
| 82 |
|
| 83 |
# Verify second tool is there
|
| 84 |
assert "api_second_tool" in main_app._tool_manager._tools
|
|
|
|
| 99 |
return ["user1", "user2"]
|
| 100 |
|
| 101 |
# Import the data app
|
| 102 |
+
await main_app.import_server(data_app, "data")
|
| 103 |
|
| 104 |
# Verify the resource was imported with the prefix
|
| 105 |
assert "data://data/users" in main_app._resource_manager._resources
|
|
|
|
| 117 |
return {"id": user_id, "name": f"User {user_id}"}
|
| 118 |
|
| 119 |
# Import the user app
|
| 120 |
+
await main_app.import_server(user_app, "api")
|
| 121 |
|
| 122 |
# Verify the template was imported with the prefix
|
| 123 |
assert "users://api/{user_id}/profile" in main_app._resource_manager._templates
|
|
|
|
| 135 |
return f"Hello, {name}!"
|
| 136 |
|
| 137 |
# Import the assistant app
|
| 138 |
+
await main_app.import_server(assistant_app, "assistant")
|
| 139 |
|
| 140 |
# Verify the prompt was imported with the prefix
|
| 141 |
assert "assistant_greeting" in main_app._prompt_manager._prompts
|
|
|
|
| 158 |
return f"News for {category}"
|
| 159 |
|
| 160 |
# Import both apps
|
| 161 |
+
await main_app.import_server(weather_app, "data")
|
| 162 |
+
await main_app.import_server(news_app, "content")
|
| 163 |
|
| 164 |
# Verify templates were imported with correct prefixes
|
| 165 |
assert "weather://data/{city}" in main_app._resource_manager._templates
|
|
|
|
| 183 |
return f"Explaining SQL query:\n{query}"
|
| 184 |
|
| 185 |
# Import both apps
|
| 186 |
+
await main_app.import_server(python_app, "python")
|
| 187 |
+
await main_app.import_server(sql_app, "sql")
|
| 188 |
|
| 189 |
# Verify prompts were imported with correct prefixes
|
| 190 |
assert "python_review_python" in main_app._prompt_manager._prompts
|
|
|
|
| 200 |
return f"Data for query: {query}"
|
| 201 |
|
| 202 |
api_app.add_tool(Tool.from_function(fetch_data, name="get_data"))
|
| 203 |
+
await main_app.import_server(api_app, "api")
|
| 204 |
|
| 205 |
# Check that the tool is accessible by its prefixed name
|
| 206 |
tool = main_app._tool_manager.get_tool("api_get_data")
|
|
|
|
| 220 |
return f"Data for query: {query}"
|
| 221 |
|
| 222 |
api_app.add_tool(Tool.from_function(fetch_data, name="get_data"))
|
| 223 |
+
await main_app.import_server(api_app, "api")
|
| 224 |
|
| 225 |
async with Client(main_app) as client:
|
| 226 |
result = await client.call_tool("api_get_data", {"query": "test"})
|
|
|
|
| 236 |
return input * 2
|
| 237 |
|
| 238 |
provider_app.add_tool(Tool.from_function(calculate_value, name="compute"))
|
| 239 |
+
await service_app.import_server(provider_app, "provider")
|
| 240 |
|
| 241 |
# Tool is accessible in the service app with the first prefix
|
| 242 |
tool = service_app._tool_manager.get_tool("provider_compute")
|
|
|
|
| 255 |
return input * 2
|
| 256 |
|
| 257 |
provider_app.add_tool(Tool.from_function(calculate_value, name="compute"))
|
| 258 |
+
await service_app.import_server(provider_app, "provider")
|
| 259 |
+
await main_app.import_server(service_app, "service")
|
| 260 |
|
| 261 |
# Tool is accessible in the main app with both prefixes
|
| 262 |
tool = main_app._tool_manager.get_tool("service_provider_compute")
|
|
|
|
| 273 |
return input * 2
|
| 274 |
|
| 275 |
provider_app.add_tool(Tool.from_function(calculate_value, name="compute"))
|
| 276 |
+
await service_app.import_server(provider_app, "provider")
|
| 277 |
+
await main_app.import_server(service_app, "service")
|
| 278 |
|
| 279 |
+
async with Client(main_app) as client:
|
| 280 |
+
result = await client.call_tool("service_provider_compute", {"input": 21})
|
| 281 |
+
assert result[0].text == "42" # type: ignore[attr-defined]
|
|
|
|
| 282 |
|
| 283 |
|
| 284 |
async def test_import_with_proxy_tools():
|
|
|
|
| 298 |
return f"Data for query: {query}"
|
| 299 |
|
| 300 |
proxy_app = FastMCP.as_proxy(Client(api_app))
|
| 301 |
+
await main_app.import_server(proxy_app, "api")
|
| 302 |
|
| 303 |
+
async with Client(main_app) as client:
|
| 304 |
+
result = await client.call_tool("api_get_data", {"query": "test"})
|
| 305 |
+
assert result[0].text == "Data for query: test" # type: ignore[attr-defined]
|
| 306 |
|
| 307 |
|
| 308 |
async def test_import_with_proxy_prompts():
|
|
|
|
| 322 |
return f"Hello, {name} from API!"
|
| 323 |
|
| 324 |
proxy_app = FastMCP.as_proxy(Client(api_app))
|
| 325 |
+
await main_app.import_server(proxy_app, "api")
|
| 326 |
|
| 327 |
+
async with Client(main_app) as client:
|
| 328 |
+
result = await client.get_prompt("api_greeting", {"name": "World"})
|
| 329 |
+
assert result.messages[0].content.text == "Hello, World from API!" # type: ignore[attr-defined]
|
| 330 |
+
assert result.description == "Example greeting prompt."
|
| 331 |
|
| 332 |
|
| 333 |
async def test_import_with_proxy_resources():
|
|
|
|
| 350 |
}
|
| 351 |
|
| 352 |
proxy_app = FastMCP.as_proxy(Client(api_app))
|
| 353 |
+
await main_app.import_server(proxy_app, "api")
|
| 354 |
|
| 355 |
# Access the resource through the main app with the prefixed key
|
| 356 |
async with Client(main_app) as client:
|
|
|
|
| 377 |
return {"name": name, "email": email}
|
| 378 |
|
| 379 |
proxy_app = FastMCP.as_proxy(Client(api_app))
|
| 380 |
+
await main_app.import_server(proxy_app, "api")
|
| 381 |
|
| 382 |
# Instantiate the template through the main app with the prefixed key
|
| 383 |
|
|
|
|
| 397 |
# This test doesn't apply anymore with the new prefix format since we're not validating
|
| 398 |
# the protocol://prefix/path format
|
| 399 |
# Just import the server to maintain test coverage without deprecated parameters
|
| 400 |
+
await main_app.import_server(api_app, "api")
|
| 401 |
|
| 402 |
|
| 403 |
async def test_import_invalid_resource_separator():
|
|
|
|
| 406 |
|
| 407 |
# This test is for maintaining coverage for importing with prefixes
|
| 408 |
# We no longer pass the deprecated resource_separator parameter
|
| 409 |
+
await main_app.import_server(api_app, "api")
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
async def test_import_with_no_prefix():
|
| 413 |
+
"""Test importing a server without providing a prefix."""
|
| 414 |
+
main_app = FastMCP("MainApp")
|
| 415 |
+
sub_app = FastMCP("SubApp")
|
| 416 |
+
|
| 417 |
+
@sub_app.tool
|
| 418 |
+
def sub_tool() -> str:
|
| 419 |
+
return "Sub tool result"
|
| 420 |
+
|
| 421 |
+
@sub_app.resource(uri="data://config")
|
| 422 |
+
def sub_resource():
|
| 423 |
+
return "Sub resource data"
|
| 424 |
+
|
| 425 |
+
@sub_app.resource(uri="users://{user_id}/info")
|
| 426 |
+
def sub_template(user_id: str):
|
| 427 |
+
return f"Sub template for user {user_id}"
|
| 428 |
+
|
| 429 |
+
@sub_app.prompt
|
| 430 |
+
def sub_prompt() -> str:
|
| 431 |
+
return "Sub prompt content"
|
| 432 |
+
|
| 433 |
+
# Import without prefix
|
| 434 |
+
await main_app.import_server(sub_app)
|
| 435 |
+
|
| 436 |
+
# Verify all component types are accessible with original names
|
| 437 |
+
assert "sub_tool" in main_app._tool_manager._tools
|
| 438 |
+
assert "data://config" in main_app._resource_manager._resources
|
| 439 |
+
assert "users://{user_id}/info" in main_app._resource_manager._templates
|
| 440 |
+
assert "sub_prompt" in main_app._prompt_manager._prompts
|
| 441 |
+
|
| 442 |
+
# Test actual functionality through Client
|
| 443 |
+
async with Client(main_app) as client:
|
| 444 |
+
# Test tool
|
| 445 |
+
tool_result = await client.call_tool("sub_tool", {})
|
| 446 |
+
assert tool_result[0].text == "Sub tool result" # type: ignore[attr-defined]
|
| 447 |
+
|
| 448 |
+
# Test resource
|
| 449 |
+
resource_result = await client.read_resource("data://config")
|
| 450 |
+
assert resource_result[0].text == "Sub resource data" # type: ignore[attr-defined]
|
| 451 |
+
|
| 452 |
+
# Test template
|
| 453 |
+
template_result = await client.read_resource("users://123/info")
|
| 454 |
+
assert template_result[0].text == "Sub template for user 123" # type: ignore[attr-defined]
|
| 455 |
+
|
| 456 |
+
# Test prompt
|
| 457 |
+
prompt_result = await client.get_prompt("sub_prompt", {})
|
| 458 |
+
assert prompt_result.messages is not None
|
| 459 |
+
assert prompt_result.messages[0].content.text == "Sub prompt content" # type: ignore[attr-defined]
|
| 460 |
+
|
| 461 |
+
|
| 462 |
+
async def test_import_conflict_resolution_tools():
|
| 463 |
+
"""Test that later imported tools overwrite earlier ones when names conflict."""
|
| 464 |
+
main_app = FastMCP("MainApp")
|
| 465 |
+
first_app = FastMCP("FirstApp")
|
| 466 |
+
second_app = FastMCP("SecondApp")
|
| 467 |
+
|
| 468 |
+
@first_app.tool(name="shared_tool")
|
| 469 |
+
def first_shared_tool() -> str:
|
| 470 |
+
return "First app tool"
|
| 471 |
+
|
| 472 |
+
@second_app.tool(name="shared_tool")
|
| 473 |
+
def second_shared_tool() -> str:
|
| 474 |
+
return "Second app tool"
|
| 475 |
+
|
| 476 |
+
# Import both apps without prefix
|
| 477 |
+
await main_app.import_server(first_app)
|
| 478 |
+
await main_app.import_server(second_app)
|
| 479 |
+
|
| 480 |
+
async with Client(main_app) as client:
|
| 481 |
+
# The later imported server should win
|
| 482 |
+
tools = await client.list_tools()
|
| 483 |
+
tool_names = [t.name for t in tools]
|
| 484 |
+
assert "shared_tool" in tool_names
|
| 485 |
+
assert tool_names.count("shared_tool") == 1 # Should only appear once
|
| 486 |
+
|
| 487 |
+
result = await client.call_tool("shared_tool", {})
|
| 488 |
+
assert result[0].text == "Second app tool" # type: ignore[attr-defined]
|
| 489 |
+
|
| 490 |
+
|
| 491 |
+
async def test_import_conflict_resolution_resources():
|
| 492 |
+
"""Test that later imported resources overwrite earlier ones when URIs conflict."""
|
| 493 |
+
main_app = FastMCP("MainApp")
|
| 494 |
+
first_app = FastMCP("FirstApp")
|
| 495 |
+
second_app = FastMCP("SecondApp")
|
| 496 |
+
|
| 497 |
+
@first_app.resource(uri="shared://data")
|
| 498 |
+
def first_resource():
|
| 499 |
+
return "First app data"
|
| 500 |
+
|
| 501 |
+
@second_app.resource(uri="shared://data")
|
| 502 |
+
def second_resource():
|
| 503 |
+
return "Second app data"
|
| 504 |
+
|
| 505 |
+
# Import both apps without prefix
|
| 506 |
+
await main_app.import_server(first_app)
|
| 507 |
+
await main_app.import_server(second_app)
|
| 508 |
+
|
| 509 |
+
async with Client(main_app) as client:
|
| 510 |
+
# The later imported server should win
|
| 511 |
+
resources = await client.list_resources()
|
| 512 |
+
resource_uris = [str(r.uri) for r in resources]
|
| 513 |
+
assert "shared://data" in resource_uris
|
| 514 |
+
assert resource_uris.count("shared://data") == 1 # Should only appear once
|
| 515 |
+
|
| 516 |
+
result = await client.read_resource("shared://data")
|
| 517 |
+
assert result[0].text == "Second app data" # type: ignore[attr-defined]
|
| 518 |
+
|
| 519 |
+
|
| 520 |
+
async def test_import_conflict_resolution_templates():
|
| 521 |
+
"""Test that later imported templates overwrite earlier ones when URI templates conflict."""
|
| 522 |
+
main_app = FastMCP("MainApp")
|
| 523 |
+
first_app = FastMCP("FirstApp")
|
| 524 |
+
second_app = FastMCP("SecondApp")
|
| 525 |
+
|
| 526 |
+
@first_app.resource(uri="users://{user_id}/profile")
|
| 527 |
+
def first_template(user_id: str):
|
| 528 |
+
return f"First app user {user_id}"
|
| 529 |
+
|
| 530 |
+
@second_app.resource(uri="users://{user_id}/profile")
|
| 531 |
+
def second_template(user_id: str):
|
| 532 |
+
return f"Second app user {user_id}"
|
| 533 |
+
|
| 534 |
+
# Import both apps without prefix
|
| 535 |
+
await main_app.import_server(first_app)
|
| 536 |
+
await main_app.import_server(second_app)
|
| 537 |
+
|
| 538 |
+
async with Client(main_app) as client:
|
| 539 |
+
# The later imported server should win
|
| 540 |
+
templates = await client.list_resource_templates()
|
| 541 |
+
template_uris = [t.uriTemplate for t in templates]
|
| 542 |
+
assert "users://{user_id}/profile" in template_uris
|
| 543 |
+
assert (
|
| 544 |
+
template_uris.count("users://{user_id}/profile") == 1
|
| 545 |
+
) # Should only appear once
|
| 546 |
+
|
| 547 |
+
result = await client.read_resource("users://123/profile")
|
| 548 |
+
assert result[0].text == "Second app user 123" # type: ignore[attr-defined]
|
| 549 |
+
|
| 550 |
+
|
| 551 |
+
async def test_import_conflict_resolution_prompts():
|
| 552 |
+
"""Test that later imported prompts overwrite earlier ones when names conflict."""
|
| 553 |
+
main_app = FastMCP("MainApp")
|
| 554 |
+
first_app = FastMCP("FirstApp")
|
| 555 |
+
second_app = FastMCP("SecondApp")
|
| 556 |
+
|
| 557 |
+
@first_app.prompt(name="shared_prompt")
|
| 558 |
+
def first_shared_prompt() -> str:
|
| 559 |
+
return "First app prompt"
|
| 560 |
+
|
| 561 |
+
@second_app.prompt(name="shared_prompt")
|
| 562 |
+
def second_shared_prompt() -> str:
|
| 563 |
+
return "Second app prompt"
|
| 564 |
+
|
| 565 |
+
# Import both apps without prefix
|
| 566 |
+
await main_app.import_server(first_app)
|
| 567 |
+
await main_app.import_server(second_app)
|
| 568 |
+
|
| 569 |
+
async with Client(main_app) as client:
|
| 570 |
+
# The later imported server should win
|
| 571 |
+
prompts = await client.list_prompts()
|
| 572 |
+
prompt_names = [p.name for p in prompts]
|
| 573 |
+
assert "shared_prompt" in prompt_names
|
| 574 |
+
assert prompt_names.count("shared_prompt") == 1 # Should only appear once
|
| 575 |
+
|
| 576 |
+
result = await client.get_prompt("shared_prompt", {})
|
| 577 |
+
assert result.messages is not None
|
| 578 |
+
assert result.messages[0].content.text == "Second app prompt" # type: ignore[attr-defined]
|
| 579 |
+
|
| 580 |
+
|
| 581 |
+
async def test_import_conflict_resolution_with_prefix():
|
| 582 |
+
"""Test that later imported components overwrite earlier ones when prefixed names conflict."""
|
| 583 |
+
main_app = FastMCP("MainApp")
|
| 584 |
+
first_app = FastMCP("FirstApp")
|
| 585 |
+
second_app = FastMCP("SecondApp")
|
| 586 |
+
|
| 587 |
+
@first_app.tool(name="shared_tool")
|
| 588 |
+
def first_shared_tool() -> str:
|
| 589 |
+
return "First app tool"
|
| 590 |
+
|
| 591 |
+
@second_app.tool(name="shared_tool")
|
| 592 |
+
def second_shared_tool() -> str:
|
| 593 |
+
return "Second app tool"
|
| 594 |
+
|
| 595 |
+
# Import both apps with same prefix
|
| 596 |
+
await main_app.import_server(first_app, "api")
|
| 597 |
+
await main_app.import_server(second_app, "api")
|
| 598 |
+
|
| 599 |
+
async with Client(main_app) as client:
|
| 600 |
+
# The later imported server should win
|
| 601 |
+
tools = await client.list_tools()
|
| 602 |
+
tool_names = [t.name for t in tools]
|
| 603 |
+
assert "api_shared_tool" in tool_names
|
| 604 |
+
assert tool_names.count("api_shared_tool") == 1 # Should only appear once
|
| 605 |
+
|
| 606 |
+
result = await client.call_tool("api_shared_tool", {})
|
| 607 |
+
assert result[0].text == "Second app tool" # type: ignore[attr-defined]
|
tests/server/test_mount.py
CHANGED
|
@@ -7,7 +7,6 @@ import pytest
|
|
| 7 |
from fastmcp import FastMCP
|
| 8 |
from fastmcp.client import Client
|
| 9 |
from fastmcp.client.transports import FastMCPTransport, SSETransport
|
| 10 |
-
from fastmcp.exceptions import NotFoundError
|
| 11 |
from fastmcp.server.proxy import FastMCPProxy
|
| 12 |
|
| 13 |
|
|
@@ -26,7 +25,7 @@ class TestBasicMount:
|
|
| 26 |
return "This is from the sub app"
|
| 27 |
|
| 28 |
# Mount the sub-app to the main app
|
| 29 |
-
main_app.mount("sub"
|
| 30 |
|
| 31 |
# Get tools from main app, should include sub_app's tools
|
| 32 |
tools = await main_app.get_tools()
|
|
@@ -46,7 +45,7 @@ class TestBasicMount:
|
|
| 46 |
return f"Hello, {name}!"
|
| 47 |
|
| 48 |
# Mount without custom separator - custom separators are deprecated
|
| 49 |
-
main_app.mount("sub"
|
| 50 |
|
| 51 |
# Tool should be accessible with the default separator
|
| 52 |
tools = await main_app.get_tools()
|
|
@@ -62,7 +61,7 @@ class TestBasicMount:
|
|
| 62 |
|
| 63 |
# This test doesn't apply anymore with the new prefix format
|
| 64 |
# just mount the server to maintain test coverage
|
| 65 |
-
main_app.mount("api:sub"
|
| 66 |
|
| 67 |
async def test_mount_invalid_resource_separator(self):
|
| 68 |
main_app = FastMCP("MainApp")
|
|
@@ -70,10 +69,9 @@ class TestBasicMount:
|
|
| 70 |
|
| 71 |
# This test doesn't apply anymore with the new prefix format
|
| 72 |
# Mount without deprecated parameters
|
| 73 |
-
main_app.mount("api"
|
| 74 |
|
| 75 |
-
async def
|
| 76 |
-
"""Test unmounting a server removes access to its tools."""
|
| 77 |
main_app = FastMCP("MainApp")
|
| 78 |
sub_app = FastMCP("SubApp")
|
| 79 |
|
|
@@ -81,38 +79,112 @@ class TestBasicMount:
|
|
| 81 |
def sub_tool() -> str:
|
| 82 |
return "This is from the sub app"
|
| 83 |
|
| 84 |
-
# Mount
|
| 85 |
-
main_app.mount(
|
| 86 |
|
| 87 |
-
# Verify it was mounted
|
| 88 |
tools = await main_app.get_tools()
|
| 89 |
-
|
|
|
|
| 90 |
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
-
# Verify it was unmounted
|
| 95 |
tools = await main_app.get_tools()
|
| 96 |
-
|
|
|
|
| 97 |
|
| 98 |
-
#
|
| 99 |
-
|
| 100 |
-
|
| 101 |
|
| 102 |
-
async def
|
|
|
|
| 103 |
main_app = FastMCP("MainApp")
|
| 104 |
sub_app = FastMCP("SubApp")
|
| 105 |
|
| 106 |
@sub_app.tool
|
| 107 |
def sub_tool() -> str:
|
| 108 |
-
return "
|
| 109 |
|
| 110 |
-
# Mount
|
| 111 |
-
main_app.mount(
|
| 112 |
|
|
|
|
| 113 |
tools = await main_app.get_tools()
|
| 114 |
-
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
|
| 118 |
class TestMultipleServerMount:
|
|
@@ -133,8 +205,8 @@ class TestMultipleServerMount:
|
|
| 133 |
return "News headlines"
|
| 134 |
|
| 135 |
# Mount both apps
|
| 136 |
-
main_app.mount("weather"
|
| 137 |
-
main_app.mount("news"
|
| 138 |
|
| 139 |
# Check both are accessible
|
| 140 |
tools = await main_app.get_tools()
|
|
@@ -163,18 +235,16 @@ class TestMultipleServerMount:
|
|
| 163 |
return "Second app tool"
|
| 164 |
|
| 165 |
# Mount first app
|
| 166 |
-
main_app.mount("api"
|
| 167 |
tools = await main_app.get_tools()
|
| 168 |
assert "api_first_tool" in tools
|
| 169 |
|
| 170 |
# Mount second app with same prefix
|
| 171 |
-
main_app.mount("api"
|
| 172 |
tools = await main_app.get_tools()
|
| 173 |
|
| 174 |
-
#
|
| 175 |
-
assert "api_first_tool"
|
| 176 |
-
|
| 177 |
-
# Second app's tool should be accessible
|
| 178 |
assert "api_second_tool" in tools
|
| 179 |
|
| 180 |
@pytest.mark.skipif(
|
|
@@ -199,7 +269,7 @@ class TestMultipleServerMount:
|
|
| 199 |
return "Working prompt"
|
| 200 |
|
| 201 |
# Mount the working server
|
| 202 |
-
main_app.mount("working"
|
| 203 |
|
| 204 |
# Use an unreachable port
|
| 205 |
unreachable_client = Client(
|
|
@@ -210,7 +280,7 @@ class TestMultipleServerMount:
|
|
| 210 |
unreachable_proxy = FastMCP.as_proxy(unreachable_client)
|
| 211 |
|
| 212 |
# Mount the unreachable proxy
|
| 213 |
-
main_app.mount("unreachable"
|
| 214 |
|
| 215 |
# All object types should work from working server despite unreachable proxy
|
| 216 |
async with Client(main_app) as client:
|
|
@@ -251,6 +321,252 @@ class TestMultipleServerMount:
|
|
| 251 |
)
|
| 252 |
|
| 253 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
class TestDynamicChanges:
|
| 255 |
"""Test that changes to mounted servers are reflected dynamically."""
|
| 256 |
|
|
@@ -260,7 +576,7 @@ class TestDynamicChanges:
|
|
| 260 |
sub_app = FastMCP("SubApp")
|
| 261 |
|
| 262 |
# Mount the sub-app before adding any tools
|
| 263 |
-
main_app.mount("sub"
|
| 264 |
|
| 265 |
# Initially, there should be no tools from sub_app
|
| 266 |
tools = await main_app.get_tools()
|
|
@@ -289,7 +605,7 @@ class TestDynamicChanges:
|
|
| 289 |
return "Temporary tool"
|
| 290 |
|
| 291 |
# Mount the sub-app
|
| 292 |
-
main_app.mount("sub"
|
| 293 |
|
| 294 |
# Initially, the tool should be accessible
|
| 295 |
tools = await main_app.get_tools()
|
|
@@ -331,7 +647,7 @@ class TestResourcesAndTemplates:
|
|
| 331 |
return ["user1", "user2"]
|
| 332 |
|
| 333 |
# Mount the data app
|
| 334 |
-
main_app.mount("data"
|
| 335 |
|
| 336 |
# Resource should be accessible through main app
|
| 337 |
resources = await main_app.get_resources()
|
|
@@ -352,7 +668,7 @@ class TestResourcesAndTemplates:
|
|
| 352 |
return {"id": user_id, "name": f"User {user_id}"}
|
| 353 |
|
| 354 |
# Mount the user app
|
| 355 |
-
main_app.mount("api"
|
| 356 |
|
| 357 |
# Template should be accessible through main app
|
| 358 |
templates = await main_app.get_resource_templates()
|
|
@@ -371,7 +687,7 @@ class TestResourcesAndTemplates:
|
|
| 371 |
data_app = FastMCP("DataApp")
|
| 372 |
|
| 373 |
# Mount the data app before adding resources
|
| 374 |
-
main_app.mount("data"
|
| 375 |
|
| 376 |
# Add a resource after mounting
|
| 377 |
@data_app.resource(uri="data://config")
|
|
@@ -402,7 +718,7 @@ class TestPrompts:
|
|
| 402 |
return f"Hello, {name}!"
|
| 403 |
|
| 404 |
# Mount the assistant app
|
| 405 |
-
main_app.mount("assistant"
|
| 406 |
|
| 407 |
# Prompt should be accessible through main app
|
| 408 |
prompts = await main_app.get_prompts()
|
|
@@ -419,7 +735,7 @@ class TestPrompts:
|
|
| 419 |
assistant_app = FastMCP("AssistantApp")
|
| 420 |
|
| 421 |
# Mount the assistant app before adding prompts
|
| 422 |
-
main_app.mount("assistant"
|
| 423 |
|
| 424 |
# Add a prompt after mounting
|
| 425 |
@assistant_app.prompt
|
|
@@ -455,7 +771,7 @@ class TestProxyServer:
|
|
| 455 |
|
| 456 |
# Mount proxy server
|
| 457 |
main_app = FastMCP("MainApp")
|
| 458 |
-
main_app.mount("proxy"
|
| 459 |
|
| 460 |
# Tool should be accessible through main app
|
| 461 |
tools = await main_app.get_tools()
|
|
@@ -477,7 +793,7 @@ class TestProxyServer:
|
|
| 477 |
|
| 478 |
# Mount proxy server
|
| 479 |
main_app = FastMCP("MainApp")
|
| 480 |
-
main_app.mount("proxy"
|
| 481 |
|
| 482 |
# Add a tool to the original server
|
| 483 |
@original_server.tool
|
|
@@ -508,7 +824,7 @@ class TestProxyServer:
|
|
| 508 |
|
| 509 |
# Mount proxy server
|
| 510 |
main_app = FastMCP("MainApp")
|
| 511 |
-
main_app.mount("proxy"
|
| 512 |
|
| 513 |
# Resource should be accessible through main app
|
| 514 |
result = await main_app._mcp_read_resource("config://proxy/settings")
|
|
@@ -531,7 +847,7 @@ class TestProxyServer:
|
|
| 531 |
|
| 532 |
# Mount proxy server
|
| 533 |
main_app = FastMCP("MainApp")
|
| 534 |
-
main_app.mount("proxy"
|
| 535 |
|
| 536 |
# Prompt should be accessible through main app
|
| 537 |
result = await main_app._mcp_get_prompt("proxy_welcome", {"name": "World"})
|
|
@@ -546,26 +862,25 @@ class TestAsProxyKwarg:
|
|
| 546 |
mcp = FastMCP("Main")
|
| 547 |
sub = FastMCP("Sub")
|
| 548 |
|
| 549 |
-
mcp.mount(
|
| 550 |
-
|
| 551 |
-
assert mcp._mounted_servers["sub"].server is sub
|
| 552 |
|
| 553 |
async def test_as_proxy_false(self):
|
| 554 |
mcp = FastMCP("Main")
|
| 555 |
sub = FastMCP("Sub")
|
| 556 |
|
| 557 |
-
mcp.mount(
|
| 558 |
|
| 559 |
-
assert mcp._mounted_servers[
|
| 560 |
|
| 561 |
async def test_as_proxy_true(self):
|
| 562 |
mcp = FastMCP("Main")
|
| 563 |
sub = FastMCP("Sub")
|
| 564 |
|
| 565 |
-
mcp.mount(
|
| 566 |
|
| 567 |
-
assert mcp._mounted_servers[
|
| 568 |
-
assert isinstance(mcp._mounted_servers[
|
| 569 |
|
| 570 |
async def test_as_proxy_defaults_true_if_lifespan(self):
|
| 571 |
@asynccontextmanager
|
|
@@ -575,43 +890,43 @@ class TestAsProxyKwarg:
|
|
| 575 |
mcp = FastMCP("Main")
|
| 576 |
sub = FastMCP("Sub", lifespan=lifespan)
|
| 577 |
|
| 578 |
-
mcp.mount(
|
| 579 |
|
| 580 |
-
assert mcp._mounted_servers[
|
| 581 |
-
assert isinstance(mcp._mounted_servers[
|
| 582 |
|
| 583 |
async def test_as_proxy_ignored_for_proxy_mounts_default(self):
|
| 584 |
mcp = FastMCP("Main")
|
| 585 |
sub = FastMCP("Sub")
|
| 586 |
sub_proxy = FastMCP.as_proxy(Client(transport=FastMCPTransport(sub)))
|
| 587 |
|
| 588 |
-
mcp.mount("sub"
|
| 589 |
|
| 590 |
-
assert mcp._mounted_servers[
|
| 591 |
|
| 592 |
async def test_as_proxy_ignored_for_proxy_mounts_false(self):
|
| 593 |
mcp = FastMCP("Main")
|
| 594 |
sub = FastMCP("Sub")
|
| 595 |
sub_proxy = FastMCP.as_proxy(Client(transport=FastMCPTransport(sub)))
|
| 596 |
|
| 597 |
-
mcp.mount("sub",
|
| 598 |
|
| 599 |
-
assert mcp._mounted_servers[
|
| 600 |
|
| 601 |
async def test_as_proxy_ignored_for_proxy_mounts_true(self):
|
| 602 |
mcp = FastMCP("Main")
|
| 603 |
sub = FastMCP("Sub")
|
| 604 |
sub_proxy = FastMCP.as_proxy(Client(transport=FastMCPTransport(sub)))
|
| 605 |
|
| 606 |
-
mcp.mount("sub",
|
| 607 |
|
| 608 |
-
assert mcp._mounted_servers[
|
| 609 |
|
| 610 |
async def test_as_proxy_mounts_still_have_live_link(self):
|
| 611 |
mcp = FastMCP("Main")
|
| 612 |
sub = FastMCP("Sub")
|
| 613 |
|
| 614 |
-
mcp.mount(
|
| 615 |
|
| 616 |
assert len(await mcp.get_tools()) == 0
|
| 617 |
|
|
@@ -636,7 +951,7 @@ class TestAsProxyKwarg:
|
|
| 636 |
def hello():
|
| 637 |
return "hi"
|
| 638 |
|
| 639 |
-
mcp.mount(
|
| 640 |
|
| 641 |
assert lifespan_check == []
|
| 642 |
|
|
|
|
| 7 |
from fastmcp import FastMCP
|
| 8 |
from fastmcp.client import Client
|
| 9 |
from fastmcp.client.transports import FastMCPTransport, SSETransport
|
|
|
|
| 10 |
from fastmcp.server.proxy import FastMCPProxy
|
| 11 |
|
| 12 |
|
|
|
|
| 25 |
return "This is from the sub app"
|
| 26 |
|
| 27 |
# Mount the sub-app to the main app
|
| 28 |
+
main_app.mount(sub_app, "sub")
|
| 29 |
|
| 30 |
# Get tools from main app, should include sub_app's tools
|
| 31 |
tools = await main_app.get_tools()
|
|
|
|
| 45 |
return f"Hello, {name}!"
|
| 46 |
|
| 47 |
# Mount without custom separator - custom separators are deprecated
|
| 48 |
+
main_app.mount(sub_app, "sub")
|
| 49 |
|
| 50 |
# Tool should be accessible with the default separator
|
| 51 |
tools = await main_app.get_tools()
|
|
|
|
| 61 |
|
| 62 |
# This test doesn't apply anymore with the new prefix format
|
| 63 |
# just mount the server to maintain test coverage
|
| 64 |
+
main_app.mount(api_app, "api:sub")
|
| 65 |
|
| 66 |
async def test_mount_invalid_resource_separator(self):
|
| 67 |
main_app = FastMCP("MainApp")
|
|
|
|
| 69 |
|
| 70 |
# This test doesn't apply anymore with the new prefix format
|
| 71 |
# Mount without deprecated parameters
|
| 72 |
+
main_app.mount(api_app, "api")
|
| 73 |
|
| 74 |
+
async def test_mount_with_no_prefix(self):
|
|
|
|
| 75 |
main_app = FastMCP("MainApp")
|
| 76 |
sub_app = FastMCP("SubApp")
|
| 77 |
|
|
|
|
| 79 |
def sub_tool() -> str:
|
| 80 |
return "This is from the sub app"
|
| 81 |
|
| 82 |
+
# Mount with empty prefix but without deprecated separators
|
| 83 |
+
main_app.mount(sub_app, prefix="")
|
| 84 |
|
|
|
|
| 85 |
tools = await main_app.get_tools()
|
| 86 |
+
# With empty prefix, the tool should keep its original name
|
| 87 |
+
assert "sub_tool" in tools
|
| 88 |
|
| 89 |
+
async def test_mount_with_no_prefix_provided(self):
|
| 90 |
+
"""Test mounting without providing a prefix at all."""
|
| 91 |
+
main_app = FastMCP("MainApp")
|
| 92 |
+
sub_app = FastMCP("SubApp")
|
| 93 |
+
|
| 94 |
+
@sub_app.tool
|
| 95 |
+
def sub_tool() -> str:
|
| 96 |
+
return "This is from the sub app"
|
| 97 |
+
|
| 98 |
+
# Mount without providing a prefix (should be None)
|
| 99 |
+
main_app.mount(sub_app)
|
| 100 |
|
|
|
|
| 101 |
tools = await main_app.get_tools()
|
| 102 |
+
# Without prefix, the tool should keep its original name
|
| 103 |
+
assert "sub_tool" in tools
|
| 104 |
|
| 105 |
+
# Call the tool to verify it works
|
| 106 |
+
result = await main_app._mcp_call_tool("sub_tool", {})
|
| 107 |
+
assert result[0].text == "This is from the sub app" # type: ignore[attr-defined]
|
| 108 |
|
| 109 |
+
async def test_mount_tools_no_prefix(self):
|
| 110 |
+
"""Test mounting a server with tools without prefix."""
|
| 111 |
main_app = FastMCP("MainApp")
|
| 112 |
sub_app = FastMCP("SubApp")
|
| 113 |
|
| 114 |
@sub_app.tool
|
| 115 |
def sub_tool() -> str:
|
| 116 |
+
return "Sub tool result"
|
| 117 |
|
| 118 |
+
# Mount without prefix
|
| 119 |
+
main_app.mount(sub_app)
|
| 120 |
|
| 121 |
+
# Verify tool is accessible with original name
|
| 122 |
tools = await main_app.get_tools()
|
| 123 |
+
assert "sub_tool" in tools
|
| 124 |
+
|
| 125 |
+
# Test actual functionality
|
| 126 |
+
tool_result = await main_app._mcp_call_tool("sub_tool", {})
|
| 127 |
+
assert tool_result[0].text == "Sub tool result" # type: ignore[attr-defined]
|
| 128 |
+
|
| 129 |
+
async def test_mount_resources_no_prefix(self):
|
| 130 |
+
"""Test mounting a server with resources without prefix."""
|
| 131 |
+
main_app = FastMCP("MainApp")
|
| 132 |
+
sub_app = FastMCP("SubApp")
|
| 133 |
+
|
| 134 |
+
@sub_app.resource(uri="data://config")
|
| 135 |
+
def sub_resource():
|
| 136 |
+
return "Sub resource data"
|
| 137 |
+
|
| 138 |
+
# Mount without prefix
|
| 139 |
+
main_app.mount(sub_app)
|
| 140 |
+
|
| 141 |
+
# Verify resource is accessible with original URI
|
| 142 |
+
resources = await main_app.get_resources()
|
| 143 |
+
assert "data://config" in resources
|
| 144 |
+
|
| 145 |
+
# Test actual functionality
|
| 146 |
+
resource_result = await main_app._mcp_read_resource("data://config")
|
| 147 |
+
assert resource_result[0].content == "Sub resource data" # type: ignore[attr-defined]
|
| 148 |
+
|
| 149 |
+
async def test_mount_resource_templates_no_prefix(self):
|
| 150 |
+
"""Test mounting a server with resource templates without prefix."""
|
| 151 |
+
main_app = FastMCP("MainApp")
|
| 152 |
+
sub_app = FastMCP("SubApp")
|
| 153 |
+
|
| 154 |
+
@sub_app.resource(uri="users://{user_id}/info")
|
| 155 |
+
def sub_template(user_id: str):
|
| 156 |
+
return f"Sub template for user {user_id}"
|
| 157 |
+
|
| 158 |
+
# Mount without prefix
|
| 159 |
+
main_app.mount(sub_app)
|
| 160 |
+
|
| 161 |
+
# Verify template is accessible with original URI template
|
| 162 |
+
templates = await main_app.get_resource_templates()
|
| 163 |
+
assert "users://{user_id}/info" in templates
|
| 164 |
+
|
| 165 |
+
# Test actual functionality
|
| 166 |
+
template_result = await main_app._mcp_read_resource("users://123/info")
|
| 167 |
+
assert template_result[0].content == "Sub template for user 123" # type: ignore[attr-defined]
|
| 168 |
+
|
| 169 |
+
async def test_mount_prompts_no_prefix(self):
|
| 170 |
+
"""Test mounting a server with prompts without prefix."""
|
| 171 |
+
main_app = FastMCP("MainApp")
|
| 172 |
+
sub_app = FastMCP("SubApp")
|
| 173 |
+
|
| 174 |
+
@sub_app.prompt
|
| 175 |
+
def sub_prompt() -> str:
|
| 176 |
+
return "Sub prompt content"
|
| 177 |
+
|
| 178 |
+
# Mount without prefix
|
| 179 |
+
main_app.mount(sub_app)
|
| 180 |
+
|
| 181 |
+
# Verify prompt is accessible with original name
|
| 182 |
+
prompts = await main_app.get_prompts()
|
| 183 |
+
assert "sub_prompt" in prompts
|
| 184 |
+
|
| 185 |
+
# Test actual functionality
|
| 186 |
+
prompt_result = await main_app._mcp_get_prompt("sub_prompt", {})
|
| 187 |
+
assert prompt_result.messages is not None
|
| 188 |
|
| 189 |
|
| 190 |
class TestMultipleServerMount:
|
|
|
|
| 205 |
return "News headlines"
|
| 206 |
|
| 207 |
# Mount both apps
|
| 208 |
+
main_app.mount(weather_app, "weather")
|
| 209 |
+
main_app.mount(news_app, "news")
|
| 210 |
|
| 211 |
# Check both are accessible
|
| 212 |
tools = await main_app.get_tools()
|
|
|
|
| 235 |
return "Second app tool"
|
| 236 |
|
| 237 |
# Mount first app
|
| 238 |
+
main_app.mount(first_app, "api")
|
| 239 |
tools = await main_app.get_tools()
|
| 240 |
assert "api_first_tool" in tools
|
| 241 |
|
| 242 |
# Mount second app with same prefix
|
| 243 |
+
main_app.mount(second_app, "api")
|
| 244 |
tools = await main_app.get_tools()
|
| 245 |
|
| 246 |
+
# Both apps' tools should be accessible (new behavior)
|
| 247 |
+
assert "api_first_tool" in tools
|
|
|
|
|
|
|
| 248 |
assert "api_second_tool" in tools
|
| 249 |
|
| 250 |
@pytest.mark.skipif(
|
|
|
|
| 269 |
return "Working prompt"
|
| 270 |
|
| 271 |
# Mount the working server
|
| 272 |
+
main_app.mount(working_app, "working")
|
| 273 |
|
| 274 |
# Use an unreachable port
|
| 275 |
unreachable_client = Client(
|
|
|
|
| 280 |
unreachable_proxy = FastMCP.as_proxy(unreachable_client)
|
| 281 |
|
| 282 |
# Mount the unreachable proxy
|
| 283 |
+
main_app.mount(unreachable_proxy, "unreachable")
|
| 284 |
|
| 285 |
# All object types should work from working server despite unreachable proxy
|
| 286 |
async with Client(main_app) as client:
|
|
|
|
| 321 |
)
|
| 322 |
|
| 323 |
|
| 324 |
+
class TestPrefixConflictResolution:
|
| 325 |
+
"""Test that later mounted servers win when there are conflicts."""
|
| 326 |
+
|
| 327 |
+
async def test_later_server_wins_tools_no_prefix(self):
|
| 328 |
+
"""Test that later mounted server wins for tools when no prefix is used."""
|
| 329 |
+
main_app = FastMCP("MainApp")
|
| 330 |
+
first_app = FastMCP("FirstApp")
|
| 331 |
+
second_app = FastMCP("SecondApp")
|
| 332 |
+
|
| 333 |
+
@first_app.tool(name="shared_tool")
|
| 334 |
+
def first_shared_tool() -> str:
|
| 335 |
+
return "First app tool"
|
| 336 |
+
|
| 337 |
+
@second_app.tool(name="shared_tool")
|
| 338 |
+
def second_shared_tool() -> str:
|
| 339 |
+
return "Second app tool"
|
| 340 |
+
|
| 341 |
+
# Mount both apps without prefix
|
| 342 |
+
main_app.mount(first_app)
|
| 343 |
+
main_app.mount(second_app)
|
| 344 |
+
|
| 345 |
+
async with Client(main_app) as client:
|
| 346 |
+
# Test that list_tools shows the tool from later server
|
| 347 |
+
tools = await client.list_tools()
|
| 348 |
+
tool_names = [t.name for t in tools]
|
| 349 |
+
assert "shared_tool" in tool_names
|
| 350 |
+
assert tool_names.count("shared_tool") == 1 # Should only appear once
|
| 351 |
+
|
| 352 |
+
# Test that calling the tool uses the later server's implementation
|
| 353 |
+
result = await client.call_tool("shared_tool", {})
|
| 354 |
+
assert result[0].text == "Second app tool" # type: ignore[attr-defined]
|
| 355 |
+
|
| 356 |
+
async def test_later_server_wins_tools_same_prefix(self):
|
| 357 |
+
"""Test that later mounted server wins for tools when same prefix is used."""
|
| 358 |
+
main_app = FastMCP("MainApp")
|
| 359 |
+
first_app = FastMCP("FirstApp")
|
| 360 |
+
second_app = FastMCP("SecondApp")
|
| 361 |
+
|
| 362 |
+
@first_app.tool(name="shared_tool")
|
| 363 |
+
def first_shared_tool() -> str:
|
| 364 |
+
return "First app tool"
|
| 365 |
+
|
| 366 |
+
@second_app.tool(name="shared_tool")
|
| 367 |
+
def second_shared_tool() -> str:
|
| 368 |
+
return "Second app tool"
|
| 369 |
+
|
| 370 |
+
# Mount both apps with same prefix
|
| 371 |
+
main_app.mount(first_app, "api")
|
| 372 |
+
main_app.mount(second_app, "api")
|
| 373 |
+
|
| 374 |
+
async with Client(main_app) as client:
|
| 375 |
+
# Test that list_tools shows the tool from later server
|
| 376 |
+
tools = await client.list_tools()
|
| 377 |
+
tool_names = [t.name for t in tools]
|
| 378 |
+
assert "api_shared_tool" in tool_names
|
| 379 |
+
assert tool_names.count("api_shared_tool") == 1 # Should only appear once
|
| 380 |
+
|
| 381 |
+
# Test that calling the tool uses the later server's implementation
|
| 382 |
+
result = await client.call_tool("api_shared_tool", {})
|
| 383 |
+
assert result[0].text == "Second app tool" # type: ignore[attr-defined]
|
| 384 |
+
|
| 385 |
+
async def test_later_server_wins_resources_no_prefix(self):
|
| 386 |
+
"""Test that later mounted server wins for resources when no prefix is used."""
|
| 387 |
+
main_app = FastMCP("MainApp")
|
| 388 |
+
first_app = FastMCP("FirstApp")
|
| 389 |
+
second_app = FastMCP("SecondApp")
|
| 390 |
+
|
| 391 |
+
@first_app.resource(uri="shared://data")
|
| 392 |
+
def first_resource():
|
| 393 |
+
return "First app data"
|
| 394 |
+
|
| 395 |
+
@second_app.resource(uri="shared://data")
|
| 396 |
+
def second_resource():
|
| 397 |
+
return "Second app data"
|
| 398 |
+
|
| 399 |
+
# Mount both apps without prefix
|
| 400 |
+
main_app.mount(first_app)
|
| 401 |
+
main_app.mount(second_app)
|
| 402 |
+
|
| 403 |
+
async with Client(main_app) as client:
|
| 404 |
+
# Test that list_resources shows the resource from later server
|
| 405 |
+
resources = await client.list_resources()
|
| 406 |
+
resource_uris = [str(r.uri) for r in resources]
|
| 407 |
+
assert "shared://data" in resource_uris
|
| 408 |
+
assert resource_uris.count("shared://data") == 1 # Should only appear once
|
| 409 |
+
|
| 410 |
+
# Test that reading the resource uses the later server's implementation
|
| 411 |
+
result = await client.read_resource("shared://data")
|
| 412 |
+
assert result[0].text == "Second app data" # type: ignore[attr-defined]
|
| 413 |
+
|
| 414 |
+
async def test_later_server_wins_resources_same_prefix(self):
|
| 415 |
+
"""Test that later mounted server wins for resources when same prefix is used."""
|
| 416 |
+
main_app = FastMCP("MainApp")
|
| 417 |
+
first_app = FastMCP("FirstApp")
|
| 418 |
+
second_app = FastMCP("SecondApp")
|
| 419 |
+
|
| 420 |
+
@first_app.resource(uri="shared://data")
|
| 421 |
+
def first_resource():
|
| 422 |
+
return "First app data"
|
| 423 |
+
|
| 424 |
+
@second_app.resource(uri="shared://data")
|
| 425 |
+
def second_resource():
|
| 426 |
+
return "Second app data"
|
| 427 |
+
|
| 428 |
+
# Mount both apps with same prefix
|
| 429 |
+
main_app.mount(first_app, "api")
|
| 430 |
+
main_app.mount(second_app, "api")
|
| 431 |
+
|
| 432 |
+
async with Client(main_app) as client:
|
| 433 |
+
# Test that list_resources shows the resource from later server
|
| 434 |
+
resources = await client.list_resources()
|
| 435 |
+
resource_uris = [str(r.uri) for r in resources]
|
| 436 |
+
assert "shared://api/data" in resource_uris
|
| 437 |
+
assert (
|
| 438 |
+
resource_uris.count("shared://api/data") == 1
|
| 439 |
+
) # Should only appear once
|
| 440 |
+
|
| 441 |
+
# Test that reading the resource uses the later server's implementation
|
| 442 |
+
result = await client.read_resource("shared://api/data")
|
| 443 |
+
assert result[0].text == "Second app data" # type: ignore[attr-defined]
|
| 444 |
+
|
| 445 |
+
async def test_later_server_wins_resource_templates_no_prefix(self):
|
| 446 |
+
"""Test that later mounted server wins for resource templates when no prefix is used."""
|
| 447 |
+
main_app = FastMCP("MainApp")
|
| 448 |
+
first_app = FastMCP("FirstApp")
|
| 449 |
+
second_app = FastMCP("SecondApp")
|
| 450 |
+
|
| 451 |
+
@first_app.resource(uri="users://{user_id}/profile")
|
| 452 |
+
def first_template(user_id: str):
|
| 453 |
+
return f"First app user {user_id}"
|
| 454 |
+
|
| 455 |
+
@second_app.resource(uri="users://{user_id}/profile")
|
| 456 |
+
def second_template(user_id: str):
|
| 457 |
+
return f"Second app user {user_id}"
|
| 458 |
+
|
| 459 |
+
# Mount both apps without prefix
|
| 460 |
+
main_app.mount(first_app)
|
| 461 |
+
main_app.mount(second_app)
|
| 462 |
+
|
| 463 |
+
async with Client(main_app) as client:
|
| 464 |
+
# Test that list_resource_templates shows the template from later server
|
| 465 |
+
templates = await client.list_resource_templates()
|
| 466 |
+
template_uris = [t.uriTemplate for t in templates]
|
| 467 |
+
assert "users://{user_id}/profile" in template_uris
|
| 468 |
+
assert (
|
| 469 |
+
template_uris.count("users://{user_id}/profile") == 1
|
| 470 |
+
) # Should only appear once
|
| 471 |
+
|
| 472 |
+
# Test that reading the resource uses the later server's implementation
|
| 473 |
+
result = await client.read_resource("users://123/profile")
|
| 474 |
+
assert result[0].text == "Second app user 123" # type: ignore[attr-defined]
|
| 475 |
+
|
| 476 |
+
async def test_later_server_wins_resource_templates_same_prefix(self):
|
| 477 |
+
"""Test that later mounted server wins for resource templates when same prefix is used."""
|
| 478 |
+
main_app = FastMCP("MainApp")
|
| 479 |
+
first_app = FastMCP("FirstApp")
|
| 480 |
+
second_app = FastMCP("SecondApp")
|
| 481 |
+
|
| 482 |
+
@first_app.resource(uri="users://{user_id}/profile")
|
| 483 |
+
def first_template(user_id: str):
|
| 484 |
+
return f"First app user {user_id}"
|
| 485 |
+
|
| 486 |
+
@second_app.resource(uri="users://{user_id}/profile")
|
| 487 |
+
def second_template(user_id: str):
|
| 488 |
+
return f"Second app user {user_id}"
|
| 489 |
+
|
| 490 |
+
# Mount both apps with same prefix
|
| 491 |
+
main_app.mount(first_app, "api")
|
| 492 |
+
main_app.mount(second_app, "api")
|
| 493 |
+
|
| 494 |
+
async with Client(main_app) as client:
|
| 495 |
+
# Test that list_resource_templates shows the template from later server
|
| 496 |
+
templates = await client.list_resource_templates()
|
| 497 |
+
template_uris = [t.uriTemplate for t in templates]
|
| 498 |
+
assert "users://api/{user_id}/profile" in template_uris
|
| 499 |
+
assert (
|
| 500 |
+
template_uris.count("users://api/{user_id}/profile") == 1
|
| 501 |
+
) # Should only appear once
|
| 502 |
+
|
| 503 |
+
# Test that reading the resource uses the later server's implementation
|
| 504 |
+
result = await client.read_resource("users://api/123/profile")
|
| 505 |
+
assert result[0].text == "Second app user 123" # type: ignore[attr-defined]
|
| 506 |
+
|
| 507 |
+
async def test_later_server_wins_prompts_no_prefix(self):
|
| 508 |
+
"""Test that later mounted server wins for prompts when no prefix is used."""
|
| 509 |
+
main_app = FastMCP("MainApp")
|
| 510 |
+
first_app = FastMCP("FirstApp")
|
| 511 |
+
second_app = FastMCP("SecondApp")
|
| 512 |
+
|
| 513 |
+
@first_app.prompt(name="shared_prompt")
|
| 514 |
+
def first_shared_prompt() -> str:
|
| 515 |
+
return "First app prompt"
|
| 516 |
+
|
| 517 |
+
@second_app.prompt(name="shared_prompt")
|
| 518 |
+
def second_shared_prompt() -> str:
|
| 519 |
+
return "Second app prompt"
|
| 520 |
+
|
| 521 |
+
# Mount both apps without prefix
|
| 522 |
+
main_app.mount(first_app)
|
| 523 |
+
main_app.mount(second_app)
|
| 524 |
+
|
| 525 |
+
async with Client(main_app) as client:
|
| 526 |
+
# Test that list_prompts shows the prompt from later server
|
| 527 |
+
prompts = await client.list_prompts()
|
| 528 |
+
prompt_names = [p.name for p in prompts]
|
| 529 |
+
assert "shared_prompt" in prompt_names
|
| 530 |
+
assert prompt_names.count("shared_prompt") == 1 # Should only appear once
|
| 531 |
+
|
| 532 |
+
# Test that getting the prompt uses the later server's implementation
|
| 533 |
+
result = await client.get_prompt("shared_prompt", {})
|
| 534 |
+
assert result.messages is not None
|
| 535 |
+
assert result.messages[0].content.text == "Second app prompt" # type: ignore[attr-defined]
|
| 536 |
+
|
| 537 |
+
async def test_later_server_wins_prompts_same_prefix(self):
|
| 538 |
+
"""Test that later mounted server wins for prompts when same prefix is used."""
|
| 539 |
+
main_app = FastMCP("MainApp")
|
| 540 |
+
first_app = FastMCP("FirstApp")
|
| 541 |
+
second_app = FastMCP("SecondApp")
|
| 542 |
+
|
| 543 |
+
@first_app.prompt(name="shared_prompt")
|
| 544 |
+
def first_shared_prompt() -> str:
|
| 545 |
+
return "First app prompt"
|
| 546 |
+
|
| 547 |
+
@second_app.prompt(name="shared_prompt")
|
| 548 |
+
def second_shared_prompt() -> str:
|
| 549 |
+
return "Second app prompt"
|
| 550 |
+
|
| 551 |
+
# Mount both apps with same prefix
|
| 552 |
+
main_app.mount(first_app, "api")
|
| 553 |
+
main_app.mount(second_app, "api")
|
| 554 |
+
|
| 555 |
+
async with Client(main_app) as client:
|
| 556 |
+
# Test that list_prompts shows the prompt from later server
|
| 557 |
+
prompts = await client.list_prompts()
|
| 558 |
+
prompt_names = [p.name for p in prompts]
|
| 559 |
+
assert "api_shared_prompt" in prompt_names
|
| 560 |
+
assert (
|
| 561 |
+
prompt_names.count("api_shared_prompt") == 1
|
| 562 |
+
) # Should only appear once
|
| 563 |
+
|
| 564 |
+
# Test that getting the prompt uses the later server's implementation
|
| 565 |
+
result = await client.get_prompt("api_shared_prompt", {})
|
| 566 |
+
assert result.messages is not None
|
| 567 |
+
assert result.messages[0].content.text == "Second app prompt" # type: ignore[attr-defined]
|
| 568 |
+
|
| 569 |
+
|
| 570 |
class TestDynamicChanges:
|
| 571 |
"""Test that changes to mounted servers are reflected dynamically."""
|
| 572 |
|
|
|
|
| 576 |
sub_app = FastMCP("SubApp")
|
| 577 |
|
| 578 |
# Mount the sub-app before adding any tools
|
| 579 |
+
main_app.mount(sub_app, "sub")
|
| 580 |
|
| 581 |
# Initially, there should be no tools from sub_app
|
| 582 |
tools = await main_app.get_tools()
|
|
|
|
| 605 |
return "Temporary tool"
|
| 606 |
|
| 607 |
# Mount the sub-app
|
| 608 |
+
main_app.mount(sub_app, "sub")
|
| 609 |
|
| 610 |
# Initially, the tool should be accessible
|
| 611 |
tools = await main_app.get_tools()
|
|
|
|
| 647 |
return ["user1", "user2"]
|
| 648 |
|
| 649 |
# Mount the data app
|
| 650 |
+
main_app.mount(data_app, "data")
|
| 651 |
|
| 652 |
# Resource should be accessible through main app
|
| 653 |
resources = await main_app.get_resources()
|
|
|
|
| 668 |
return {"id": user_id, "name": f"User {user_id}"}
|
| 669 |
|
| 670 |
# Mount the user app
|
| 671 |
+
main_app.mount(user_app, "api")
|
| 672 |
|
| 673 |
# Template should be accessible through main app
|
| 674 |
templates = await main_app.get_resource_templates()
|
|
|
|
| 687 |
data_app = FastMCP("DataApp")
|
| 688 |
|
| 689 |
# Mount the data app before adding resources
|
| 690 |
+
main_app.mount(data_app, "data")
|
| 691 |
|
| 692 |
# Add a resource after mounting
|
| 693 |
@data_app.resource(uri="data://config")
|
|
|
|
| 718 |
return f"Hello, {name}!"
|
| 719 |
|
| 720 |
# Mount the assistant app
|
| 721 |
+
main_app.mount(assistant_app, "assistant")
|
| 722 |
|
| 723 |
# Prompt should be accessible through main app
|
| 724 |
prompts = await main_app.get_prompts()
|
|
|
|
| 735 |
assistant_app = FastMCP("AssistantApp")
|
| 736 |
|
| 737 |
# Mount the assistant app before adding prompts
|
| 738 |
+
main_app.mount(assistant_app, "assistant")
|
| 739 |
|
| 740 |
# Add a prompt after mounting
|
| 741 |
@assistant_app.prompt
|
|
|
|
| 771 |
|
| 772 |
# Mount proxy server
|
| 773 |
main_app = FastMCP("MainApp")
|
| 774 |
+
main_app.mount(proxy_server, "proxy")
|
| 775 |
|
| 776 |
# Tool should be accessible through main app
|
| 777 |
tools = await main_app.get_tools()
|
|
|
|
| 793 |
|
| 794 |
# Mount proxy server
|
| 795 |
main_app = FastMCP("MainApp")
|
| 796 |
+
main_app.mount(proxy_server, "proxy")
|
| 797 |
|
| 798 |
# Add a tool to the original server
|
| 799 |
@original_server.tool
|
|
|
|
| 824 |
|
| 825 |
# Mount proxy server
|
| 826 |
main_app = FastMCP("MainApp")
|
| 827 |
+
main_app.mount(proxy_server, "proxy")
|
| 828 |
|
| 829 |
# Resource should be accessible through main app
|
| 830 |
result = await main_app._mcp_read_resource("config://proxy/settings")
|
|
|
|
| 847 |
|
| 848 |
# Mount proxy server
|
| 849 |
main_app = FastMCP("MainApp")
|
| 850 |
+
main_app.mount(proxy_server, "proxy")
|
| 851 |
|
| 852 |
# Prompt should be accessible through main app
|
| 853 |
result = await main_app._mcp_get_prompt("proxy_welcome", {"name": "World"})
|
|
|
|
| 862 |
mcp = FastMCP("Main")
|
| 863 |
sub = FastMCP("Sub")
|
| 864 |
|
| 865 |
+
mcp.mount(sub, "sub")
|
| 866 |
+
assert mcp._mounted_servers[0].server is sub
|
|
|
|
| 867 |
|
| 868 |
async def test_as_proxy_false(self):
|
| 869 |
mcp = FastMCP("Main")
|
| 870 |
sub = FastMCP("Sub")
|
| 871 |
|
| 872 |
+
mcp.mount(sub, "sub", as_proxy=False)
|
| 873 |
|
| 874 |
+
assert mcp._mounted_servers[0].server is sub
|
| 875 |
|
| 876 |
async def test_as_proxy_true(self):
|
| 877 |
mcp = FastMCP("Main")
|
| 878 |
sub = FastMCP("Sub")
|
| 879 |
|
| 880 |
+
mcp.mount(sub, "sub", as_proxy=True)
|
| 881 |
|
| 882 |
+
assert mcp._mounted_servers[0].server is not sub
|
| 883 |
+
assert isinstance(mcp._mounted_servers[0].server, FastMCPProxy)
|
| 884 |
|
| 885 |
async def test_as_proxy_defaults_true_if_lifespan(self):
|
| 886 |
@asynccontextmanager
|
|
|
|
| 890 |
mcp = FastMCP("Main")
|
| 891 |
sub = FastMCP("Sub", lifespan=lifespan)
|
| 892 |
|
| 893 |
+
mcp.mount(sub, "sub")
|
| 894 |
|
| 895 |
+
assert mcp._mounted_servers[0].server is not sub
|
| 896 |
+
assert isinstance(mcp._mounted_servers[0].server, FastMCPProxy)
|
| 897 |
|
| 898 |
async def test_as_proxy_ignored_for_proxy_mounts_default(self):
|
| 899 |
mcp = FastMCP("Main")
|
| 900 |
sub = FastMCP("Sub")
|
| 901 |
sub_proxy = FastMCP.as_proxy(Client(transport=FastMCPTransport(sub)))
|
| 902 |
|
| 903 |
+
mcp.mount(sub_proxy, "sub")
|
| 904 |
|
| 905 |
+
assert mcp._mounted_servers[0].server is sub_proxy
|
| 906 |
|
| 907 |
async def test_as_proxy_ignored_for_proxy_mounts_false(self):
|
| 908 |
mcp = FastMCP("Main")
|
| 909 |
sub = FastMCP("Sub")
|
| 910 |
sub_proxy = FastMCP.as_proxy(Client(transport=FastMCPTransport(sub)))
|
| 911 |
|
| 912 |
+
mcp.mount(sub_proxy, "sub", as_proxy=False)
|
| 913 |
|
| 914 |
+
assert mcp._mounted_servers[0].server is sub_proxy
|
| 915 |
|
| 916 |
async def test_as_proxy_ignored_for_proxy_mounts_true(self):
|
| 917 |
mcp = FastMCP("Main")
|
| 918 |
sub = FastMCP("Sub")
|
| 919 |
sub_proxy = FastMCP.as_proxy(Client(transport=FastMCPTransport(sub)))
|
| 920 |
|
| 921 |
+
mcp.mount(sub_proxy, "sub", as_proxy=True)
|
| 922 |
|
| 923 |
+
assert mcp._mounted_servers[0].server is sub_proxy
|
| 924 |
|
| 925 |
async def test_as_proxy_mounts_still_have_live_link(self):
|
| 926 |
mcp = FastMCP("Main")
|
| 927 |
sub = FastMCP("Sub")
|
| 928 |
|
| 929 |
+
mcp.mount(sub, "sub", as_proxy=True)
|
| 930 |
|
| 931 |
assert len(await mcp.get_tools()) == 0
|
| 932 |
|
|
|
|
| 951 |
def hello():
|
| 952 |
return "hi"
|
| 953 |
|
| 954 |
+
mcp.mount(sub, "sub", as_proxy=True)
|
| 955 |
|
| 956 |
assert lifespan_check == []
|
| 957 |
|
tests/server/test_resource_prefix_formats.py
CHANGED
|
@@ -26,8 +26,8 @@ async def test_resource_prefix_format_in_constructor():
|
|
| 26 |
main_server_protocol = FastMCP("MainProtocol", resource_prefix_format="protocol")
|
| 27 |
|
| 28 |
# Mount the servers
|
| 29 |
-
main_server_path.mount("sub"
|
| 30 |
-
main_server_protocol.mount("sub"
|
| 31 |
|
| 32 |
# Check that the resources are prefixed correctly
|
| 33 |
path_resources = await main_server_path.get_resources()
|
|
@@ -49,11 +49,11 @@ async def test_resource_prefix_format_in_import_server():
|
|
| 49 |
|
| 50 |
# Import with path format
|
| 51 |
main_server_path = FastMCP("MainPath", resource_prefix_format="path")
|
| 52 |
-
await main_server_path.import_server("sub"
|
| 53 |
|
| 54 |
# Import with protocol format
|
| 55 |
main_server_protocol = FastMCP("MainProtocol", resource_prefix_format="protocol")
|
| 56 |
-
await main_server_protocol.import_server("sub"
|
| 57 |
|
| 58 |
# Check that the resources are prefixed correctly
|
| 59 |
path_resources = main_server_path._resource_manager.get_resources()
|
|
|
|
| 26 |
main_server_protocol = FastMCP("MainProtocol", resource_prefix_format="protocol")
|
| 27 |
|
| 28 |
# Mount the servers
|
| 29 |
+
main_server_path.mount(server_path, "sub")
|
| 30 |
+
main_server_protocol.mount(server_protocol, "sub")
|
| 31 |
|
| 32 |
# Check that the resources are prefixed correctly
|
| 33 |
path_resources = await main_server_path.get_resources()
|
|
|
|
| 49 |
|
| 50 |
# Import with path format
|
| 51 |
main_server_path = FastMCP("MainPath", resource_prefix_format="path")
|
| 52 |
+
await main_server_path.import_server(server, "sub")
|
| 53 |
|
| 54 |
# Import with protocol format
|
| 55 |
main_server_protocol = FastMCP("MainProtocol", resource_prefix_format="protocol")
|
| 56 |
+
await main_server_protocol.import_server(server, "sub")
|
| 57 |
|
| 58 |
# Check that the resources are prefixed correctly
|
| 59 |
path_resources = main_server_path._resource_manager.get_resources()
|
tests/server/test_server.py
CHANGED
|
@@ -9,7 +9,6 @@ from fastmcp.exceptions import NotFoundError
|
|
| 9 |
from fastmcp.prompts.prompt import FunctionPrompt, Prompt
|
| 10 |
from fastmcp.resources import Resource, ResourceTemplate
|
| 11 |
from fastmcp.server.server import (
|
| 12 |
-
MountedServer,
|
| 13 |
add_resource_prefix,
|
| 14 |
has_resource_prefix,
|
| 15 |
remove_resource_prefix,
|
|
@@ -1126,7 +1125,7 @@ class TestResourcePrefixMounting:
|
|
| 1126 |
|
| 1127 |
# Create a main server and mount the resource server
|
| 1128 |
main_server = FastMCP(name="MainServer")
|
| 1129 |
-
main_server.mount("prefix"
|
| 1130 |
|
| 1131 |
# Check that the resources are mounted with the correct prefixes
|
| 1132 |
resources = await main_server.get_resources()
|
|
@@ -1183,16 +1182,23 @@ class TestResourcePrefixMounting:
|
|
| 1183 |
async def test_mounted_server_matching_and_stripping(
|
| 1184 |
self, uri, prefix, expected_match, expected_strip
|
| 1185 |
):
|
| 1186 |
-
"""Test that
|
| 1187 |
-
|
|
|
|
|
|
|
| 1188 |
server = FastMCP()
|
| 1189 |
-
mounted = MountedServer(prefix=prefix, server=server)
|
| 1190 |
|
| 1191 |
# Test matching
|
| 1192 |
-
assert
|
|
|
|
|
|
|
|
|
|
| 1193 |
|
| 1194 |
# Test stripping
|
| 1195 |
-
assert
|
|
|
|
|
|
|
|
|
|
| 1196 |
|
| 1197 |
async def test_import_server_with_new_prefix_format(self):
|
| 1198 |
"""Test that import_server correctly uses the new prefix format."""
|
|
@@ -1213,7 +1219,7 @@ class TestResourcePrefixMounting:
|
|
| 1213 |
|
| 1214 |
# Create target server and import the source server
|
| 1215 |
target_server = FastMCP(name="TargetServer")
|
| 1216 |
-
await target_server.import_server("imported"
|
| 1217 |
|
| 1218 |
# Check that the resources were imported with the correct prefixes
|
| 1219 |
resources = await target_server.get_resources()
|
|
|
|
| 9 |
from fastmcp.prompts.prompt import FunctionPrompt, Prompt
|
| 10 |
from fastmcp.resources import Resource, ResourceTemplate
|
| 11 |
from fastmcp.server.server import (
|
|
|
|
| 12 |
add_resource_prefix,
|
| 13 |
has_resource_prefix,
|
| 14 |
remove_resource_prefix,
|
|
|
|
| 1125 |
|
| 1126 |
# Create a main server and mount the resource server
|
| 1127 |
main_server = FastMCP(name="MainServer")
|
| 1128 |
+
main_server.mount(server, "prefix")
|
| 1129 |
|
| 1130 |
# Check that the resources are mounted with the correct prefixes
|
| 1131 |
resources = await main_server.get_resources()
|
|
|
|
| 1182 |
async def test_mounted_server_matching_and_stripping(
|
| 1183 |
self, uri, prefix, expected_match, expected_strip
|
| 1184 |
):
|
| 1185 |
+
"""Test that resource prefix utility functions correctly match and strip resource prefixes."""
|
| 1186 |
+
from fastmcp.server.server import has_resource_prefix, remove_resource_prefix
|
| 1187 |
+
|
| 1188 |
+
# Create a basic server to get the default resource prefix format
|
| 1189 |
server = FastMCP()
|
|
|
|
| 1190 |
|
| 1191 |
# Test matching
|
| 1192 |
+
assert (
|
| 1193 |
+
has_resource_prefix(uri, prefix, server.resource_prefix_format)
|
| 1194 |
+
== expected_match
|
| 1195 |
+
)
|
| 1196 |
|
| 1197 |
# Test stripping
|
| 1198 |
+
assert (
|
| 1199 |
+
remove_resource_prefix(uri, prefix, server.resource_prefix_format)
|
| 1200 |
+
== expected_strip
|
| 1201 |
+
)
|
| 1202 |
|
| 1203 |
async def test_import_server_with_new_prefix_format(self):
|
| 1204 |
"""Test that import_server correctly uses the new prefix format."""
|
|
|
|
| 1219 |
|
| 1220 |
# Create target server and import the source server
|
| 1221 |
target_server = FastMCP(name="TargetServer")
|
| 1222 |
+
await target_server.import_server(source_server, "imported")
|
| 1223 |
|
| 1224 |
# Check that the resources were imported with the correct prefixes
|
| 1225 |
resources = await target_server.get_resources()
|