Jeremiah Lowin commited on
Commit
7c08231
·
unverified ·
2 Parent(s): 7343f797682ff5

Merge pull request #861 from jlowin/mounting

Browse files
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*). A `prefix` is added to avoid naming conflicts.
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", weather_mcp)
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(prefix, subserver)`:
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", dynamic_mcp)
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", api_server)
165
 
166
  # Proxy mounting (preserves full client lifecycle)
167
- main_mcp.mount("api", api_server, as_proxy=True)
 
 
 
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", remote_proxy)
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: dict[str, MountedServer] = {}
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
- for prefix, server in self._mounted_servers.items():
 
 
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
- for prefix, server in self._mounted_servers.items():
 
 
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
- for prefix, server in self._mounted_servers.items():
 
 
374
  try:
375
- server_templates = await server.get_resource_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
- for prefix, server in self._mounted_servers.items():
 
 
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
- for server in self._mounted_servers.values():
566
- if server.match_tool(key):
567
- tool_key = server.strip_tool_prefix(key)
568
- return await server.server._call_tool(tool_key, arguments)
 
 
 
 
 
 
 
 
 
 
569
 
570
  raise NotFoundError(f"Unknown tool: {key!r}")
571
 
@@ -604,10 +657,28 @@ class FastMCP(Generic[LifespanResultT]):
604
  )
605
  ]
606
  else:
607
- for server in self._mounted_servers.values():
608
- if server.match_resource(str(uri)):
609
- new_uri = server.strip_resource_prefix(str(uri))
610
- return await server.server._mcp_read_resource(new_uri)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- for server in self._mounted_servers.values():
657
- if server.match_prompt(name):
658
- prompt_name = server.strip_prompt_prefix(name)
659
- return await server.server._mcp_get_prompt(prompt_name, arguments)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 the given prefix.
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[prefix] = mounted_server
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 mounted server
1556
- tool_prefix = f"{prefix}_"
1557
  for key, tool in (await server.get_tools()).items():
1558
- self._tool_manager.add_tool(tool, key=f"{tool_prefix}{key}")
 
 
 
 
1559
 
1560
- # Import resources and templates from the mounted server
1561
  for key, resource in (await server.get_resources()).items():
1562
- prefixed_key = add_resource_prefix(key, prefix, self.resource_prefix_format)
1563
- self._resource_manager.add_resource(resource, key=prefixed_key)
 
 
 
 
 
1564
 
1565
  for key, template in (await server.get_resource_templates()).items():
1566
- prefixed_key = add_resource_prefix(key, prefix, self.resource_prefix_format)
1567
- self._resource_manager.add_template(template, key=prefixed_key)
 
 
 
 
 
1568
 
1569
- # Import prompts from the mounted server
1570
- prompt_prefix = f"{prefix}_"
1571
  for key, prompt in (await server.get_prompts()).items():
1572
- self._prompt_manager.add_prompt(prompt, key=f"{prompt_prefix}{key}")
1573
-
1574
- logger.info(f"Imported server {server.name} with prefix '{prefix}'")
1575
- logger.debug(f"Imported tools with prefix '{tool_prefix}'")
1576
- logger.debug(f"Imported resources and templates with prefix '{prefix}/'")
1577
- logger.debug(f"Imported prompts with prefix '{prompt_prefix}'")
 
 
 
 
 
 
 
 
1578
 
1579
  self._cache.clear()
1580
 
@@ -1731,60 +1870,10 @@ class FastMCP(Generic[LifespanResultT]):
1731
  return True
1732
 
1733
 
 
1734
  class MountedServer:
1735
- def __init__(
1736
- self,
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 test_mount_tool_separator_deprecation_warning():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- DeprecationWarning,
18
- match="The tool_separator parameter is deprecated and will be removed in a future version",
19
- ):
20
- main_app.mount("sub", sub_app, tool_separator="-")
 
 
 
 
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
- mounted_server = main_app._mounted_servers["sub"]
28
- assert mounted_server.match_tool("sub_test_tool")
29
- assert not mounted_server.match_tool("sub-test_tool")
30
 
31
 
32
- def test_mount_resource_separator_deprecation_warning():
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
- DeprecationWarning,
51
- match="The prompt_separator parameter is deprecated and will be removed in a future version",
52
- ):
53
- main_app.mount("sub", sub_app, prompt_separator="-")
 
 
 
 
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
- mounted_server = main_app._mounted_servers["sub"]
61
- assert mounted_server.match_prompt("sub_test_prompt")
62
- assert not mounted_server.match_prompt("sub-test_prompt")
 
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
- DeprecationWarning,
72
- match="The tool_separator parameter is deprecated and will be removed in a future version",
73
- ):
74
- await main_app.import_server("sub", sub_app, tool_separator="-")
 
 
 
 
75
 
76
  main_app = FastMCP("MainApp")
77
- with pytest.warns(
78
- DeprecationWarning,
79
- match="The resource_separator parameter is deprecated and ignored",
80
- ):
81
- await main_app.import_server("sub", sub_app, resource_separator="+")
 
 
 
82
 
83
  main_app = FastMCP("MainApp")
84
- with pytest.warns(
85
- DeprecationWarning,
86
- match="The prompt_separator parameter is deprecated and will be removed in a future version",
87
- ):
88
- await main_app.import_server("sub", sub_app, prompt_separator="-")
 
 
 
 
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
- main_server.mount("sub", sub_server)
 
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
- await main_server.import_server("sub", sub_server)
 
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", sub_app)
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", weather_app)
53
- await main_app.import_server("news", news_app)
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", first_app)
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", second_app)
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", data_app)
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", user_app)
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", assistant_app)
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", weather_app)
162
- await main_app.import_server("content", news_app)
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", python_app)
187
- await main_app.import_server("sql", sql_app)
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", api_app)
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", api_app)
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", provider_app)
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", provider_app)
259
- await main_app.import_server("service", service_app)
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", provider_app)
277
- await main_app.import_server("service", service_app)
278
 
279
- result = await main_app._tool_manager.call_tool(
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", proxy_app)
303
 
304
- result = await main_app._mcp_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,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", proxy_app)
326
 
327
- result = await main_app._mcp_get_prompt("api_greeting", {"name": "World"})
328
- assert result.messages[0].content.text == "Hello, World from API!" # type: ignore[attr-defined]
329
- assert result.description == "Example greeting prompt."
 
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", proxy_app)
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", proxy_app)
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("api_sub", api_app)
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", api_app)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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", sub_app)
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", sub_app)
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", api_app)
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", api_app)
74
 
75
- async def test_unmount_server(self):
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 the sub-app
85
- main_app.mount("sub", sub_app)
86
 
87
- # Verify it was mounted
88
  tools = await main_app.get_tools()
89
- assert "sub_sub_tool" in tools
 
90
 
91
- # Unmount the sub-app
92
- main_app.unmount("sub")
 
 
 
 
 
 
 
 
 
93
 
94
- # Verify it was unmounted
95
  tools = await main_app.get_tools()
96
- assert "sub_sub_tool" not in tools
 
97
 
98
- # Calling the tool should fail
99
- with pytest.raises(NotFoundError, match="Unknown tool: sub_sub_tool"):
100
- await main_app._mcp_call_tool("sub_sub_tool", {})
101
 
102
- async def test_mount_with_no_prefix(self):
 
103
  main_app = FastMCP("MainApp")
104
  sub_app = FastMCP("SubApp")
105
 
106
  @sub_app.tool
107
  def sub_tool() -> str:
108
- return "This is from the sub app"
109
 
110
- # Mount with empty prefix but without deprecated separators
111
- main_app.mount(prefix="", server=sub_app)
112
 
 
113
  tools = await main_app.get_tools()
114
- # With empty prefix, the format is now "_sub_tool" instead of "sub_tool"
115
- assert "_sub_tool" in tools
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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", weather_app)
137
- main_app.mount("news", news_app)
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", first_app)
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", second_app)
172
  tools = await main_app.get_tools()
173
 
174
- # First app's tool should no longer be accessible
175
- assert "api_first_tool" not in tools
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", working_app)
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", unreachable_proxy)
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", sub_app)
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", sub_app)
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", data_app)
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", user_app)
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", data_app)
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", assistant_app)
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", assistant_app)
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", proxy_server)
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", proxy_server)
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", proxy_server)
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", proxy_server)
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("sub", sub)
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("sub", sub, as_proxy=False)
558
 
559
- assert mcp._mounted_servers["sub"].server is sub
560
 
561
  async def test_as_proxy_true(self):
562
  mcp = FastMCP("Main")
563
  sub = FastMCP("Sub")
564
 
565
- mcp.mount("sub", sub, as_proxy=True)
566
 
567
- assert mcp._mounted_servers["sub"].server is not sub
568
- assert isinstance(mcp._mounted_servers["sub"].server, FastMCPProxy)
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("sub", sub)
579
 
580
- assert mcp._mounted_servers["sub"].server is not sub
581
- assert isinstance(mcp._mounted_servers["sub"].server, FastMCPProxy)
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", sub_proxy)
589
 
590
- assert mcp._mounted_servers["sub"].server is sub_proxy
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", sub_proxy, as_proxy=False)
598
 
599
- assert mcp._mounted_servers["sub"].server is sub_proxy
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", sub_proxy, as_proxy=True)
607
 
608
- assert mcp._mounted_servers["sub"].server is sub_proxy
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("sub", sub, as_proxy=True)
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("sub", sub, as_proxy=True)
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", server_path)
30
- main_server_protocol.mount("sub", server_protocol)
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", server)
53
 
54
  # Import with protocol format
55
  main_server_protocol = FastMCP("MainProtocol", resource_prefix_format="protocol")
56
- await main_server_protocol.import_server("sub", server)
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", server)
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 MountedServer correctly matches and strips resource prefixes."""
1187
- # Create a basic server to mount
 
 
1188
  server = FastMCP()
1189
- mounted = MountedServer(prefix=prefix, server=server)
1190
 
1191
  # Test matching
1192
- assert mounted.match_resource(uri) == expected_match
 
 
 
1193
 
1194
  # Test stripping
1195
- assert mounted.strip_resource_prefix(uri) == expected_strip
 
 
 
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", source_server)
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()