一、服务组合
使用挂载和导入功能,将多个 FastMCP 服务器合并成一个更大的应用程序。
随着您的 MCP 应用程序不断发展,您可能希望将工具、资源和提示组织到逻辑模块中,或者重用现有的服务器组件。FastMCP 通过两种方法支持组合:
import_server: 用于带有前缀的组件的一次性复制(静态组合)。
mount: 用于创建一个实时链接,其中主服务器将请求委托给子服务器(动态组合)。
二、为什么要组合服务?
Modularity 将大型应用程序拆分为更小、专注于特定功能的服务器(例如,天气服务器、数据库服务器、日历服务器)。
Reusability 创建通用的实用服务器(例如,一个文本处理服务器),并在任何需要的地方挂载它们。
Teamwork 不同的团队可以在各自独立的 FastMCP 服务器上工作,这些服务器之后会被合并。
Organization 将相关功能按逻辑分组在一起。
三、导入和挂载的对比
选择导入还是挂载取决于您的使用场景和需求。
| 特性 | 导入 | 挂载 |
|---|---|---|
| 方法 | FastMCP.import_server(server, prefix=None) | FastMCP.mount(server, prefix=None) |
| 组成类型 | 一次性复制(静态) | 实时链接(动态) |
| 更新 | 子服务器的更改未显示出来 | 对子服务器的更改会立即生效 |
| 性能 | 快速 ------ 无运行时委托 | 更慢 ------ 受最慢的已挂载服务器影响 |
| 前缀 | 可选 - 对于原名可省略 | 可选 - 原名可省略 |
| 最适合 | 捆绑最终确定的组件、性能关键型设置 | 模块化运行时组合 |
四、代理服务器
FastMCP 支持 MCP 代理功能,这使你能够在本地的 FastMCP 实例中镜像本地或远程服务器。代理与导入和挂载功能都完全兼容。
4.1 导入(静态组合)
import_server () (注意,这个方法是异步的 ) 方法会将所有组件(工具、资源、模板、提示词)从一个 FastMCP 实例(子服务器)复制到另一个实例(主服务器)中。可以提供一个可选前缀以避免命名冲突。如果未提供前缀,组件将不经修改地被导入。当多个服务器使用相同前缀(或无前缀)导入时,最近导入的服务器的组件将具有优先权。
服务端
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_mcp, prefix="weather")
# Result: main_mcp now contains prefixed components:
# - Tool: "weather_get_forecast"
# - Resource: "data://weather/cities/supported"
if __name__ == "__main__":
asyncio.run(setup())
main_mcp.run(
transport="http", # 使用HTTP传输
host="127.0.0.1", # 绑定本地地址,外部访问可改为 0.0.0.0
port=8000, # 服务端口
)
客户端
python
import asyncio
from fastmcp import Client
client = Client("http://localhost:8000/mcp")
async def call():
async with client:
result = await client.call_tool("weather_get_forecast", {"city":"西安"})
print(result)
result = await client.read_resource("data://weather/cities/supported")
print(result)
asyncio.run(call())
导入是如何工作的?
当你调用 await main_mcp.import_server(subserver, prefix={whatever}):
-
Tools 子服务器中的所有工具都被添加到
main_mcp中,其名称前缀为{prefix}_。subserver.tool(name="my_tool")becomesmain_mcp.tool(name="{prefix}_my_tool")
-
Resources: 所有资源在添加时,其统一资源标识符(URI)和名称都带有前缀。
- URI:
subserver.resource(uri="data://info")变成了main_mcp.resource(uri="data://{prefix}/info") - Name :
resource.namebecomes"{prefix}_{resource.name}"
- URI:
-
Resource Templates: 模板的前缀与资源的前缀类似
- URI
subserver.resource(uri="data://{id}")变成了main_mcp.resource(uri="data://{prefix}/{id}"). - Name :
template.namebecomes"{prefix}_{template.name}".
- URI
-
Prompts : 所有提示词都添加了以 {prefix}_为前缀的名称。
subserver.prompt(name="my_prompt")变成了main_mcp.prompt(name="{prefix}_my_prompt")
请注意,import_server 会执行组件的一次性复制。导入后对子服务器所做的更改不会反映在 main_mcp 中。主服务器也不会执行子服务器的生命周期上下文。
prefix 参数是可选的。如果省略,组件将未经修改地导入。
服务端-无前缀导入
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():
# Import without prefix - components keep original names
await main_mcp.import_server(weather_mcp)
# Result: main_mcp now contains:
# - Tool: "get_forecast" (original name preserved)
# - Resource: "data://cities/supported" (original URI preserved)
if __name__ == "__main__":
asyncio.run(setup())
main_mcp.run()
无前缀-客户端
python
import asyncio
from fastmcp import Client
client = Client("http://localhost:8000/mcp")
async def call():
async with client:
result = await client.call_tool("get_forecast", {"city":"西安"})
print(result)
result = await client.read_resource("data://cities/supported")
print(result)
asyncio.run(call())
冲突解决
导入具有相同前缀或无前缀的多个服务器时,最近导入的服务器中的组件具有优先级。
五、挂载(实时链接)
mount () 方法会在 main_mcp 服务器和子服务器之间创建一个实时链接。它不会复制组件,而是在运行时将与可选prefix匹配的组件请求委托给subserver。如果未提供前缀,子服务器的组件可以无需前缀直接访问。当多个服务器以相同前缀(或无前缀)挂载时,对于存在冲突的组件名称,最近挂载的服务器具有优先级。
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_mcp, prefix="dynamic")
# 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.content[0].text)
# Shows: "Tool Added Dynamically!"
if __name__ == "__main__":
asyncio.run(test_dynamic_mount())
执行结果
bash
Available tools: ['dynamic_initial_tool', 'dynamic_added_later']
Result: Tool Added Dynamically!
挂载如何工作的?
- 实时链接:父服务器与挂载的服务器建立连接。
- 动态更新:当通过父服务器访问时,挂载服务器的更改会立即体现出来。
- 前缀访问:父服务器使用前缀将请求路由到挂载的服务器。
- 委派:在运行时,与前缀匹配的组件请求会被委派给挂载的服务器。
给工具、资源、模板还有提示词命名的时候,用的前缀规则和import_server是一模一样的。
这就意味着,不管是统一资源标识符 / 密钥,还是资源、模板的名称,都得加上前缀 ------ 这样在多服务器的配置环境里,才能更方便地识别它们。
prefix 参数是可选的。如果省略该参数,组件将在不经过修改的情况下被挂载。
在挂载服务器时,使用 @server.custom_route () 定义的自定义 HTTP 路由也会转发到父服务器,从而可以通过父服务器的 HTTP 应用程序访问它们。
性能考量
由于 "实时链接" 的存在,父服务器上诸如 list_tools () 之类的操作会受到最慢挂载服务器速度的影响。特别是,基于 HTTP 的挂载服务器可能会引入显著的延迟(300-400 毫秒,而本地工具仅为 1-2 毫秒),并且这种减速会影响整个服务器,而不仅仅是与 HTTP 代理工具的交互。如果性能很重要,通过 import_server () 导入工具可能是一个更合适的解决方案,因为它在启动时一次性复制组件,而不是在运行时委托请求。
不带前缀的挂载
你也可以在不指定前缀的情况下挂载服务器,这样组件就可以在不添加前缀的情况下被访问。这与不带前缀的导入方式效果完全相同,包括冲突解决机制。
直接挂载和代理挂载的比较
FastMCP 支持两种安装模式:
- 直接挂载(默认):父服务器直接访问已挂载服务器在内存中的对象。
- 已挂载服务器上不会发生任何客户端生命周期事件
- 已挂载服务器的生命周期上下文不会执行
- 通信通过直接方法调用进行处理
- 代理挂载:父服务器将被挂载的服务器视为一个独立实体,并通过客户端接口与其通信。
- 完整的客户端生命周期事件在被挂载的服务器上发生
- 当客户端连接时,被挂载服务器的生命周期开始执行
- 通信通过内存中的客户端传输进行
python
# Direct mounting (default when no custom lifespan)
main_mcp.mount(api_server, prefix="api")
# Proxy mounting (preserves full client lifecycle)
main_mcp.mount(api_server, prefix="api", as_proxy=True)
# Mounting without a prefix (components accessible without prefixing)
main_mcp.mount(api_server)
当挂载的服务器具有自定义生命周期时,FastMCP 会自动使用代理挂载,但您可以通过 as_proxy 参数覆盖此行为。
在FastMCP中,Direct Mounting(直接挂载)和Proxy Mounting(代理挂载)是两种核心挂载模式,核心差异体现在通信方式、生命周期处理、适用场景上,具体区别如下:
| 对比维度 | Direct Mounting(默认) | Proxy Mounting(代理模式) |
|---|---|---|
| 通信方式 | 父服务器直接访问子服务器的内存对象,通过直接方法调用通信 | 父服务器将子服务器视为独立实体,通过客户端接口(in-memory Client transport)通信 |
| 客户端生命周期事件 | 子服务器不会触发任何客户端生命周期事件 | 子服务器会触发完整的客户端生命周期事件(如连接、断开等) |
| 子服务器生命周期执行 | 子服务器的lifespan上下文(初始化、清理等逻辑)不会被执行 |
当客户端连接时,子服务器的lifespan上下文会被执行 |
| 启用方式 | 默认模式(无需额外参数),除非子服务器有自定义lifespan或手动指定as_proxy=True |
1. 子服务器有自定义lifespan时自动启用;2. 手动指定as_proxy=True参数;3. 挂载通过FastMCP.as_proxy()创建的代理服务器时强制启用 |
| 适用场景 | 简单本地挂载、追求低延迟、无需子服务器生命周期管理的场景 | 需要子服务器完整生命周期(如初始化资源、连接数据库)、需模拟客户端-服务器交互、挂载远程代理服务器的场景 |
| 代码示例 | main_mcp.mount(api_server, prefix="api") |
main_mcp.mount(api_server, prefix="api", as_proxy=True) |
补充说明
- 自动切换逻辑 :FastMCP会在子服务器定义了自定义
lifespan时,自动将挂载模式切换为Proxy Mounting,确保子服务器的生命周期逻辑能正常执行;若需强制使用Direct Mounting,需手动指定as_proxy=False(需确认子服务器无依赖lifespan的逻辑)。 - 性能差异:Direct Mounting因直接操作内存对象,通信延迟更低;Proxy Mounting因多了客户端接口转发和生命周期处理,性能略逊,但功能更完整。
- 与代理服务器的兼容性 :若通过
FastMCP.as_proxy()创建远程服务器代理,挂载该代理时强制使用Proxy Mounting ,无论是否指定as_proxy参数。
与代理服务器的交互
使用 FastMCP.as_proxy () 创建代理服务器时,挂载该服务器将始终使用代理挂载:
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_proxy, prefix="remote")
六、带有组合标签的筛选
在父服务器上使用 include_tags 或 exclude_tags 时,这些过滤器会递归地应用于所有组件,包括来自已挂载或导入的服务器的组件。这使您能够控制在父级暴露哪些组件,无论您的应用程序是如何组成的。
python
import asyncio
from fastmcp import FastMCP, Client
# Create a subserver with tools tagged for different environments
api_server = FastMCP(name="APIServer")
@api_server.tool(tags={"production"})
def prod_endpoint() -> str:
"""Production-ready endpoint."""
return "Production data"
@api_server.tool(tags={"development"})
def dev_endpoint() -> str:
"""Development-only endpoint."""
return "Debug data"
# Mount the subserver with production tag filtering at parent level
prod_app = FastMCP(name="ProductionApp", include_tags={"production"})
prod_app.mount(api_server, prefix="api")
# Test the filtering
async def test_filtering():
async with Client(prod_app) as client:
tools = await client.list_tools()
print("Available tools:", [t.name for t in tools])
# Shows: ['api_prod_endpoint']
# The 'api_dev_endpoint' is filtered out
# Calling the filtered tool raises an error
try:
await client.call_tool("api_dev_endpoint")
except Exception as e:
print(f"Filtered tool not accessible: {e}")
if __name__ == "__main__":
asyncio.run(test_filtering())
执行结果
python
Available tools: ['api_prod_endpoint']
Filtered tool not accessible: Unknown tool: api_dev_endpoint
Tool 'api_dev_endpoint' not listed, no validation will be performed
递归过滤的工作原理
- 标签过滤器按以下顺序应用:
- 子服务器过滤器:每个挂载 / 导入的服务器首先对其组件应用自身的包含标签 / 排除标签。
- 父服务器过滤器:然后,父服务器对所有组件(包括来自子服务器的组件)应用自身的包含标签 / 排除标签。
这确保了父服务器标签策略充当父服务器所公开的所有内容的全局策略,无论你的应用程序是如何组成的。
这种过滤既适用于列表操作(例如,list_tools ()),也适用于执行操作(例如,call_tool ())。被过滤的组件既无法通过父服务器看到,也无法通过父服务器执行。