跳到主内容

一个客户端接多个 MCP Server:编排方式、命名冲突与成本控制

100%
一个客户端接多个 MCP Server:编排方式、命名冲突与成本控制

一个 MCP 客户端同时接多个 Server 是常态,但协议只保证「工具名在单个 Server 内唯一」,跨 Server 的唯一性要你自己解决。所以多 Server 编排的核心不是连上,而是三件事:合并工具列表时做命名空间、维护「工具名 → 所属会话」的路由表、以及控制暴露给模型的工具总量。

连接层:一个 Server 一条会话

标准的客户端做法是每个 Server 建一条独立的 ClientSession,各自 initialize(),然后遍历所有会话调 list_tools(),把结果合并成一张表交给模型,同时保留「工具名 → (server, tool)」的映射用于回程路由。

import asyncio
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

SERVERS = {
    "fs": StdioServerParameters(
        command="npx",
        args=["-y", "@modelcontextprotocol/server-filesystem", "/data"],
    ),
    "db": StdioServerParameters(command="python", args=["db_server.py"]),
}

async def connect_all(servers):
    stack = AsyncExitStack()          # 任一 Server 启动失败也能干净收尾
    sessions, tools = {}, []
    for name, params in servers.items():
        read, write = await stack.enter_async_context(stdio_client(params))
        session = await stack.enter_async_context(ClientSession(read, write))
        await session.initialize()
        sessions[name] = session
        for t in (await session.list_tools()).tools:
            tools.append({
                "exposed": f"{name}__{t.name}",   # 命名空间前缀,防重名
                "route": (name, t.name),          # 回程路由
                "schema": t.inputSchema,
                "desc": f"[{name}] {t.description}",
            })
    return sessions, tools, stack

分隔符用双下划线或冒号都行——关键是选一个真实工具名里不会出现的字符,这样回程时按分隔符切开就能定位到具体会话。

不做命名空间前缀是最常见的坑:两个 Server 都注册了 search,客户端要么随机挑一个,要么直接拒绝重复名,结果是模型以为自己调了 A,实际请求永远到不了 A 。

三种编排策略

策略做法工具总量适用规模
全量平铺所有 Server 的所有工具一次性给模型不限≤ 3 个 Server 、≤ 40 个工具
命名空间 + 全量加前缀后全量暴露不限3–8 个 Server,需要消歧
分组按需按任务阶段只挂载相关 Server 组每组 ≤ 208 个以上 Server 或工具上百

判断标准很直接:工具描述占用的上下文,是不是已经挤掉了模型用来推理的空间。工具数量一多,选型准确率会明显下降,这时该做的不是优化描述文案,而是减少暴露。

配置层:Server 放哪一级

多 Server 的第一现场是配置文件。常见客户端的形态是在 mcpServers 下给每个 Server 一个唯一 key,stdio 与远程 HTTP 可以混写:

{
  "mcpServers": {
    "github": {
      "type": "http",
      "url": "https://api.example.com/mcp/",
      "headers": { "Authorization": "Bearer ${GITHUB_TOKEN}" }
    },
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"]
    },
    "internal": {
      "command": "python",
      "args": ["internal_server.py"],
      "env": { "INTERNAL_API_KEY": "${KEY}" }
    }
  }
}

作用域上分三档:用户级(个人通用工具)、项目级(随仓库提交,团队共享)、本地级(含密钥,不进版本库)。原则是「能项目级就别用户级,含密钥一律本地级」。

远程 Server 的会话与恢复

远程走 Streamable HTTP 时,MCP 规范(2025-11-25 版)对会话与断线恢复有明确要求,多 Server 场景下尤其要留意:

  • 会话 ID 。服务端在初始化响应里通过 MCP-Session-Id 下发,客户端此后每个请求都必须带上;服务端可随时终止会话,之后该 ID 的请求会返回 404,客户端必须重新初始化。
  • 协议版本头。HTTP 场景下所有后续请求都要带 MCP-Protocol-Version,服务端据此决定响应行为。
  • 多连接。允许客户端同时保持多条 SSE 流,但服务端不得把同一条消息广播到多条流上——这意味着你的路由表必须精确到「哪条流属于哪个会话」。
  • 断线续传。服务端可为事件附加 ID,客户端重连时用 Last-Event-ID 请求重放;服务端只能重放同一条流上的消息,不能跨流补发。
  • 主动关闭。不再需要某会话时应发 HTTP DELETE 显式终止,避免服务端堆积僵尸会话。
多 Server 故障隔离的四条做法
一、连接阶段用 AsyncExitStack 统一管理,单个 Server 启动失败不影响其余。二、给每个会话设独立超时,避免一个慢 Server 拖垮整轮对话。三、工具调用失败时区分「协议错误」和「工具执行错误」,前者重试意义不大,后者可以退避重试。四、对关键 Server 做健康检查,不可用时从工具清单里摘掉并向模型说明,而不是让它反复调一个永远失败的工具。

成本控制:别把所有工具都塞进上下文

  1. 先统计每个 Server 的工具数与描述总长度,按占用排序。
  2. 把低频 Server 拆成独立配置组,按任务阶段挂载。
  3. 描述里写清楚「什么时候不该用这个工具」,比堆参数说明更有效。
  4. 定期清理从不被调用的工具——日志里零命中的工具,就是在白烧上下文。

相关话题本站已经分开写过:配置放在哪一层见 MCP 配置作用域与团队共享;协议错误与执行错误的区分见 MCP 错误处理的两种路径;本地文件类 Server 的接法见 Filesystem MCP Server 实战;选哪些 Server 值得接可参考 常用 MCP 服务器推荐清单。


MCP 规范会帮我校验跨 Server 的工具重名吗?
不会。规范只保证工具名在单个 Server 内唯一,跨 Server 唯一性由客户端实现方负责,这也是命名空间前缀必须自己做的原因。

一个客户端最多能接多少个 Server?
协议没有上限,实际瓶颈在上下文。工具总数超过几十个之后,模型的选型准确率会明显下降,此时应该按任务阶段分组挂载,而不是继续往上堆。

stdio 和 Streamable HTTP 能混着接吗?
能,而且很常见:本地工具用 stdio(进程随客户端启停),远程服务用 HTTP 。混接时注意 stdio Server 的生命周期绑定在客户端进程上,客户端重启会全部重连。

某个 Server 一直连不上,会影响其他 Server 吗?
取决于实现。用 AsyncExitStack 或等价的连接管理器就能做到隔离:失败的那个被跳过并记日志,其余正常提供服务。串行连接且不做异常捕获的实现则会整轮挂掉。

延伸阅读:MCP 配置作用域与团队共享
这篇有帮助吗?
云上的幻象
云上的幻象查看主页

七彩云博客,分享 WordPress 建站实战与 AI 工具测评,覆盖服务器运维、站长工具、软件资源与电商运营干货,专注原创实用的主题插件、网站加速与安全优化教程。

942文章4评论

相关文章

评论 (0)

欢迎你,新朋友,感谢参与互动!文明发言,理性交流 · 首次评论将在审核后展示