mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-17 02:59:11 +02:00
212 lines
8.4 KiB
Text
212 lines
8.4 KiB
Text
---
|
|
title: Server Composition
|
|
sidebarTitle: Composition
|
|
description: Combine multiple FastMCP servers into a single, larger application using mounting and importing.
|
|
icon: puzzle-piece
|
|
---
|
|
import { VersionBadge } from '/snippets/version-badge.mdx'
|
|
|
|
<VersionBadge version="2.2.0" />
|
|
|
|
As your MCP applications grow, you might want to organize your tools, resources, and prompts into logical modules or reuse existing server components. FastMCP supports composition through two methods:
|
|
|
|
- **`import_server`**: For a one-time copy of components with prefixing (static composition).
|
|
- **`mount`**: For creating a live link where the main server delegates requests to the subserver (dynamic composition).
|
|
|
|
## Why Compose Servers?
|
|
|
|
- **Modularity**: Break down large applications into smaller, focused servers (e.g., a `WeatherServer`, a `DatabaseServer`, a `CalendarServer`).
|
|
- **Reusability**: Create common utility servers (e.g., a `TextProcessingServer`) and mount them wherever needed.
|
|
- **Teamwork**: Different teams can work on separate FastMCP servers that are later combined.
|
|
- **Organization**: Keep related functionality grouped together logically.
|
|
|
|
### Importing vs Mounting
|
|
|
|
The choice of importing or mounting depends on your use case and requirements.
|
|
|
|
| Feature | Importing | Mounting |
|
|
|---------|----------------|---------|
|
|
| **Method** | `FastMCP.import_server()` | `FastMCP.mount()` |
|
|
| **Composition Type** | One-time copy (static) | Live link (dynamic) |
|
|
| **Updates** | Changes to subserver NOT reflected | Changes to subserver immediately reflected |
|
|
| **Best For** | Bundling finalized components | Modular runtime composition |
|
|
|
|
### Proxy Servers
|
|
|
|
FastMCP supports [MCP proxying](/patterns/proxy), which allows you to mirror a local or remote server in a local FastMCP instance. Proxies are fully compatible with both importing and mounting.
|
|
|
|
## Importing (Static Composition)
|
|
|
|
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.
|
|
|
|
```python
|
|
from fastmcp import FastMCP
|
|
import asyncio
|
|
|
|
# Define subservers
|
|
weather_mcp = FastMCP(name="WeatherService")
|
|
|
|
@weather_mcp.tool()
|
|
def get_forecast(city: str) -> dict:
|
|
"""Get weather forecast."""
|
|
return {"city": city, "forecast": "Sunny"}
|
|
|
|
@weather_mcp.resource("data://cities/supported")
|
|
def list_supported_cities() -> list[str]:
|
|
"""List cities with weather support."""
|
|
return ["London", "Paris", "Tokyo"]
|
|
|
|
# Define main server
|
|
main_mcp = FastMCP(name="MainApp")
|
|
|
|
# Import subserver
|
|
async def setup():
|
|
await main_mcp.import_server("weather", weather_mcp)
|
|
|
|
# Result: main_mcp now contains prefixed components:
|
|
# - Tool: "weather_get_forecast"
|
|
# - Resource: "weather+data://cities/supported"
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(setup())
|
|
main_mcp.run()
|
|
```
|
|
|
|
### How Importing Works
|
|
|
|
When you call `await main_mcp.import_server(prefix, subserver)`:
|
|
|
|
1. **Tools**: All tools from `subserver` are added to `main_mcp` with names prefixed using `{prefix}_`.
|
|
- `subserver.tool(name="my_tool")` becomes `main_mcp.tool(name="{prefix}_my_tool")`.
|
|
2. **Resources**: All resources are added with URIs prefixed using `{prefix}+`.
|
|
- `subserver.resource(uri="data://info")` becomes `main_mcp.resource(uri="{prefix}+data://info")`.
|
|
3. **Resource Templates**: Templates are prefixed similarly to resources.
|
|
- `subserver.resource(uri="data://{id}")` becomes `main_mcp.resource(uri="{prefix}+data://{id}")`.
|
|
4. **Prompts**: All prompts are added with names prefixed like tools.
|
|
- `subserver.prompt(name="my_prompt")` becomes `main_mcp.prompt(name="{prefix}_my_prompt")`.
|
|
|
|
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.
|
|
|
|
## Mounting (Live Linking)
|
|
|
|
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.
|
|
|
|
```python
|
|
import asyncio
|
|
from fastmcp import FastMCP, Client
|
|
|
|
# Define subserver
|
|
dynamic_mcp = FastMCP(name="DynamicService")
|
|
|
|
@dynamic_mcp.tool()
|
|
def initial_tool():
|
|
"""Initial tool demonstration."""
|
|
return "Initial Tool Exists"
|
|
|
|
# Mount subserver (synchronous operation)
|
|
main_mcp = FastMCP(name="MainAppLive")
|
|
main_mcp.mount("dynamic", dynamic_mcp)
|
|
|
|
# Add a tool AFTER mounting - it will be accessible through main_mcp
|
|
@dynamic_mcp.tool()
|
|
def added_later():
|
|
"""Tool added after mounting."""
|
|
return "Tool Added Dynamically!"
|
|
|
|
# Testing access to mounted tools
|
|
async def test_dynamic_mount():
|
|
tools = await main_mcp.get_tools()
|
|
print("Available tools:", list(tools.keys()))
|
|
# Shows: ['dynamic_initial_tool', 'dynamic_added_later']
|
|
|
|
async with Client(main_mcp) as client:
|
|
result = await client.call_tool("dynamic_added_later")
|
|
print("Result:", result[0].text)
|
|
# Shows: "Tool Added Dynamically!"
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(test_dynamic_mount())
|
|
```
|
|
|
|
### How Mounting Works
|
|
|
|
When mounting is configured:
|
|
|
|
1. **Live Link**: The parent server establishes a connection to the mounted server.
|
|
2. **Dynamic Updates**: Changes to the mounted server are immediately reflected when accessed through the parent.
|
|
3. **Prefixed Access**: The parent server uses prefixes to route requests to the mounted server.
|
|
4. **Delegation**: Requests for components matching the prefix are delegated to the mounted server at runtime.
|
|
|
|
The same prefixing rules apply as with `import_server` for naming tools, resources, templates, and prompts.
|
|
|
|
### Direct vs. Proxy Mounting
|
|
|
|
<VersionBadge version="2.2.7" />
|
|
|
|
FastMCP supports two mounting modes:
|
|
|
|
1. **Direct Mounting** (default): The parent server directly accesses the mounted server's objects in memory.
|
|
- No client lifecycle events occur on the mounted server
|
|
- The mounted server's lifespan context is not executed
|
|
- Communication is handled through direct method calls
|
|
|
|
2. **Proxy Mounting**: The parent server treats the mounted server as a separate entity and communicates with it through a client interface.
|
|
- Full client lifecycle events occur on the mounted server
|
|
- The mounted server's lifespan is executed when a client connects
|
|
- Communication happens via an in-memory Client transport
|
|
|
|
```python
|
|
# Direct mounting (default when no custom lifespan)
|
|
main_mcp.mount("api", api_server)
|
|
|
|
# Proxy mounting (preserves full client lifecycle)
|
|
main_mcp.mount("api", api_server, as_proxy=True)
|
|
```
|
|
|
|
FastMCP automatically uses proxy mounting when the mounted server has a custom lifespan, but you can override this behavior with the `as_proxy` parameter.
|
|
|
|
#### Interaction with Proxy Servers
|
|
|
|
When using `FastMCP.as_proxy()` to create a proxy server, mounting that server will always use proxy mounting:
|
|
|
|
```python
|
|
# Create a proxy for a remote server
|
|
remote_proxy = FastMCP.as_proxy(Client("http://example.com/mcp"))
|
|
|
|
# Mount the proxy (always uses proxy mounting)
|
|
main_server.mount("remote", remote_proxy)
|
|
```
|
|
|
|
## Customizing Separators
|
|
|
|
Both `import_server()` and `mount()` allow you to customize the separators used for prefixing components. The defaults are `_` for tools and prompts, and `+` for resources.
|
|
|
|
<CodeGroup>
|
|
|
|
```python import_server
|
|
await main_mcp.import_server(
|
|
prefix="api",
|
|
app=some_subserver,
|
|
tool_separator="_", # Tool name becomes: "api_sub_tool_name"
|
|
resource_separator="+", # Resource URI becomes: "api+data://sub_resource"
|
|
prompt_separator="_" # Prompt name becomes: "api_sub_prompt_name"
|
|
)
|
|
```
|
|
|
|
```python mount
|
|
main_mcp.mount(
|
|
prefix="api",
|
|
app=some_subserver,
|
|
tool_separator="_", # Tool name becomes: "api_sub_tool_name"
|
|
resource_separator="+", # Resource URI becomes: "api+data://sub_resource"
|
|
prompt_separator="_" # Prompt name becomes: "api_sub_prompt_name"
|
|
)
|
|
```
|
|
</CodeGroup>
|
|
<Warning>
|
|
Be cautious when choosing separators. Some MCP clients (like Claude Desktop) might have restrictions on characters allowed in tool names (e.g., `/` might not be supported). The defaults (`_` for names, `+` for URIs) are generally safe.
|
|
</Warning>
|
|
|
|
<Tip>
|
|
To "cleanly" import or mount a server, set the prefix and all separators to `""` (empty string). This is generally unecessary but could save a couple tokens at the risk of a name collision!
|
|
</Tip>
|