跳转至

会话组

机器翻译

本页由英文文档自动翻译而来,以英文页面为准。如果有读起来不对的地方,翻译页面说明了如何反馈。

一个 Client 连接一个服务器。实际应用往往需要好几个(一个搜索服务器、一个数据库服务器、一个内部 API),结果要为每个服务器各管一条连接和一份工具列表。

ClientSessionGroup 是一个对象,它持有多条连接,并把它们公开的所有内容合并成一个统一视图。

两个服务器

先看两个普通的服务器。它们彼此毫无关联,所以很自然地都把自己的工具命名为 search

library_server.py
from mcp.server import MCPServer

mcp = MCPServer("Library")


@mcp.tool()
def search(query: str) -> str:
    """Search the library catalog."""
    return f"3 books match {query!r}."


@mcp.resource("library://hours")
def hours() -> str:
    """When the library is open."""
    return "Mon-Fri 09:00-17:00"
web_server.py
from mcp.server import MCPServer

mcp = MCPServer("Web")


@mcp.tool()
def search(query: str) -> str:
    """Search the web."""
    return f"12 pages match {query!r}."

一个组

创建一个 ClientSessionGroup,对每个服务器调用一次 connect_to_server

client.py
import asyncio

from mcp import ClientSessionGroup, StdioServerParameters


async def main() -> None:
    library = StdioServerParameters(command="uv", args=["run", "mcp", "run", "library_server.py"])
    web = StdioServerParameters(command="uv", args=["run", "mcp", "run", "web_server.py"])

    async with ClientSessionGroup() as group:
        await group.connect_to_server(library)
        await group.connect_to_server(web)

        result = await group.call_tool("search", {"query": "model context protocol"})
        print(result.structured_content)


if __name__ == "__main__":
    asyncio.run(main())
  • connect_to_server 接受的是传输参数,而不是服务器对象:用 StdioServerParameters(来自 mcp)启动子进程,或用 StreamableHttpParameters / SseServerParameters(来自 mcp.client.session_group)连接已在某个 URL 上监听的服务器。
  • group.tools 是一个 dict[str, Tool],包含所有已连接服务器的工具。group.resourcesgroup.prompts 形式相同。
  • group.call_tool(name, arguments) 查找名称,找到拥有它的会话,然后转发调用。不需要指明是哪个服务器。

Check

client.py 放在两个服务器旁边运行。第二次 connect_to_server 会被拒绝:

mcp.shared.exceptions.MCPError: {'search'} already exist in group tools.

这是一个 MCPError,在第二个服务器的任何内容注册之前就抛出了。名称必须在整个组内唯一,而两个不受你控制的服务器迟早会冲突。

component_name_hook

这个问题在组这一层解决,而不是在服务器上。传入一个接受 (name, server_info) 的函数,组会对它注册的每个名称都运行这个函数:

client.py
import asyncio

from mcp import ClientSessionGroup, StdioServerParameters
from mcp.types import Implementation


def by_server(name: str, server_info: Implementation) -> str:
    return f"{server_info.name}.{name}"


async def main() -> None:
    library = StdioServerParameters(command="uv", args=["run", "mcp", "run", "library_server.py"])
    web = StdioServerParameters(command="uv", args=["run", "mcp", "run", "web_server.py"])

    async with ClientSessionGroup(component_name_hook=by_server) as group:
        await group.connect_to_server(library)
        await group.connect_to_server(web)

        print(sorted(group.tools))
        result = await group.call_tool("Web.search", {"query": "model context protocol"})
        print(result.structured_content)


if __name__ == "__main__":
    asyncio.run(main())

再运行一次。print(sorted(group.tools)) 现在两个都显示了:

['Library.search', 'Web.search']
  • 由你决定。by_serverserver_info.name 构造它,也就是每个 MCPServer(...) 构造时传入的名称。
  • 里面的 Tool 原封不动:group.tools["Web.search"].name 仍然是 "search",这也是 call_tool 发到线路上的名称。前缀永远不会离开你的进程。
  • 不只是工具。library 的 hours 资源注册为 Library.hours

Tip

这个 hook 对每个服务器的每个名称都会运行,而不仅限于冲突的名称:没有"仅在冲突时加前缀"的模式。选定一种方案,让它处处生效。

添加和移除服务器

connect_to_server 返回它打开的 ClientSession。如果以后想移除这个服务器,就保留它:await group.disconnect_from_server(session) 会把它的工具、资源和提示词从组中移除。

如果手上已经有一个已连接的 ClientSessionClient.session 就是一个),把它交给 await group.connect_with_session(server_info, session),而不用打开新的传输。聚合方式相同。组永远不会关闭不是它自己打开的会话。server_info 为组件前缀提供服务器名称;在 2026 年代的连接上,client.server_info 可能是 None(身份是可选的),这种情况下传入你自己的 Implementation(name=..., version=...)

经典握手

ClientSessionGroup 构建在 ClientSession 之上,而不是 Client。每次 connect_to_server 都运行经典的 initialize 握手。它从不发送 协议版本 中描述的 server/discover 探测。每个 MCP 服务器都理解这种握手,所以这不会损失任何兼容性;它只意味着,面对一个本可以做得更好的服务器,组走的是较旧、较慢的路径。

回顾

  • ClientSessionGroup 持有多条服务器连接,并把它们的工具、资源和提示词各自合并成一个 dict
  • 每个服务器调用一次 connect_to_server(params)。它接受传输参数,从不接受 Client 所接受的服务器对象或 URL。
  • group.call_tool(name, arguments) 替你路由到拥有该工具的服务器。
  • 名称必须在整个组内唯一;两个都有 search 工具的服务器无法直接共存。
  • component_name_hook= 改写每个注册的名称。改变的是 dict 的键,线路上的名称不变。
  • connect_with_session 添加一个你已持有的会话;disconnect_from_server 移除一个。

组所用的握手(以及 Client 更倾向的那种更快的握手)详见 协议版本