欢迎光临
我们一直在努力

MCP客户端使用指南:从单Server到多Server管理,我踩过的坑都在这里了

MCP客户端使用指南:从单Server到多Server管理,我踩过的坑都在这里了

写在前面

上一篇文章把MCP的核心概念讲清楚了,有读者说:“概念我懂了,但具体到代码里,MCP客户端到底怎么用?”

这篇文章就来解决这个问题。我会从最基础的单Server连接开始,一步步讲到多Server管理、错误处理、高级用法,中间穿插我自己在实际使用中踩过的坑。

说实话,MCP客户端的API看起来简单,但真用起来细节不少。我第一次写多Server客户端的时候,光是工具名冲突的问题就调试了一下午。希望这篇文章能帮你少走点弯路。

一、基础用法:四步跑通一个MCP客户端

我们先从最简单的情况开始:连接一个MCP Server,列出它的工具,调用一个工具。

第一步:创建客户端

MCP官方SDK提供了Client类,创建方式很简单:

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

# 定义Server参数:用什么命令启动Server
server_params = StdioServerParameters(
command="python",
args=["weather_server.py"],
env=None # 可以传环境变量
)

# 创建stdio传输通道
async with stdio_client(server_params) as (read, write):
# 创建客户端会话
async with ClientSession(read, write) as session:
# 初始化连接
await session.initialize()
print("连接成功!")

这里有几个细节要注意:

  • StdioServerParameters里的command是启动Server的命令,args是参数。如果你的Server是一个Python脚本,command就是"python",args就是脚本路径。
  • stdio_client是一个异步上下文管理器,它会帮你启动Server进程并建立通信通道。
  • ClientSession也是异步上下文管理器,initialize()方法必须调用,这是MCP协议的握手步骤,不调用的话后续操作都会报错。

我第一次用的时候忘了调用initialize(),结果调list_tools()一直报"未初始化"的错,查了半天才发现是少了这一步。

第二步:列出工具

连接成功后,就可以获取Server提供的工具列表了:

tools_result = await session.list_tools()
for tool in tools_result.tools:
print(f"工具名:{tool.name}")
print(f"描述:{tool.description}")
print(f"参数:{tool.inputSchema}")
print("—")

list_tools()返回的是一个ListToolsResult对象,里面的tools字段是工具列表。每个工具包含name、description、inputSchema三个核心字段。

inputSchema是一个JSON Schema对象,描述了工具需要什么参数。比如天气查询工具的inputSchema可能长这样:

{
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"}
},
"required": ["city"]
}

这个Schema很重要,大模型就是靠它来判断该传什么参数的。你在写MCP Server的时候,一定要把Schema写清楚,否则大模型很容易传错参数。

第三步:调用工具

拿到工具列表之后,就可以调用具体的工具了:

result = await session.call_tool(
name="get_weather",
arguments={"city": "北京"}
)

# 解析返回结果
for content in result.content:
if content.type == "text":
print(f"天气结果:{content.text}")

call_tool()的参数很直观:name是工具名,arguments是参数字典。返回结果是一个CallToolResult对象,里面的content是一个列表,每个元素是一段内容。

为什么content是列表而不是单个字符串?因为MCP支持多模态返回——工具可以同时返回文本、图片、结构化数据等多种内容。目前大部分工具只返回文本,但设计上是支持多模态的。

另外,CallToolResult还有一个isError字段,如果工具执行出错,这个字段会是True,content里会包含错误信息。

第四步:关闭连接

因为用了async with上下文管理器,连接会在退出时自动关闭,不需要手动调用。但如果你不是用上下文管理器的方式,记得调用session.close()来释放资源。

二、多Server管理:真正的挑战在这

单Server的用法很简单,十分钟就能上手。但实际项目中,你的Agent可能需要同时连接好几个MCP Server——天气Server、数据库Server、GitHub Server、文件系统Server……这时候问题就来了。

问题1:怎么同时管理多个连接?

我一开始的做法是写一个列表,把所有session存进去,需要的时候遍历。但这样管理起来很麻烦,尤其是连接断开重连的时候。

后来我封装了一个简单的MCPClientManager类,专门负责多Server的连接管理:

class MCPClientManager:
def __init__(self):
self._sessions = {} # server_name -> ClientSession
self._transports = {} # server_name -> transport

async def connect_server(self, name: str, command: str, args: list):
"""连接一个MCP Server"""
server_params = StdioServerParameters(command=command, args=args)
transport = stdio_client(server_params)
read, write = await transport.__aenter__()
session = ClientSession(read, write)
await session.__aenter__()
await session.initialize()

self._sessions[name] = session
self._transports[name] = transport
print(f"已连接Server:{name}")

async def list_all_tools(self) –> dict:
"""获取所有Server的工具列表"""
all_tools = {}
for server_name, session in self._sessions.items():
result = await session.list_tools()
for tool in result.tools:
# 用 server_name.tool_name 的方式避免重名
full_name = f"{server_name}.{tool.name}"
all_tools[full_name] = {
"server": server_name,
"tool": tool
}
return all_tools

async def call_tool(self, full_name: str, arguments: dict):
"""调用工具,full_name格式为 server_name.tool_name"""
server_name, tool_name = full_name.split(".", 1)
session = self._sessions[server_name]
return await session.call_tool(name=tool_name, arguments=arguments)

这个Manager类做了三件事:

  • 统一管理所有Server的连接和session
  • 收集所有Server的工具列表,用server_name.tool_name的方式命名
  • 提供统一的工具调用接口
  • 问题2:工具名冲突怎么办?

    这是我踩的第一个大坑。我连接了两个Server,一个叫"github",一个叫"gitlab",它们都提供了一个叫get_repository的工具。

    如果直接用工具名调用,大模型根本不知道该调哪个。我的解决方案是上面代码里的做法:给工具名加上Server前缀,变成github.get_repository和gitlab.get_repository。

    但这样又有一个问题:大模型生成工具调用的时候,它不知道要加前缀。你需要在给大模型的工具描述里,把前缀加上去。比如:

    # 整理给大模型的工具列表
    def format_tools_for_llm(all_tools: dict) –> list:
    tools = []
    for full_name, info in all_tools.items():
    tool = info["tool"]
    tools.append({
    "type": "function",
    "function": {
    "name": full_name, # 带前缀的完整名称
    "description": tool.description,
    "parameters": tool.inputSchema
    }
    })
    return tools

    这样大模型看到的工具名就是带前缀的,它生成调用请求的时候也会带上前缀,你再根据前缀路由到对应的Server。

    问题3:某个Server挂了怎么办?

    多Server环境下,某个Server进程崩溃是常有的事。如果不做处理,整个Agent都会因为一个Server挂掉而瘫痪。

    我的做法是在call_tool里加异常捕获和自动重连:

    async def call_tool(self, full_name: str, arguments: dict, max_retries: int = 2):
    server_name, tool_name = full_name.split(".", 1)

    for attempt in range(max_retries + 1):
    try:
    session = self._sessions.get(server_name)
    if session is None:
    raise ConnectionError(f"Server {server_name} 未连接")
    return await session.call_tool(name=tool_name, arguments=arguments)
    except Exception as e:
    if attempt < max_retries:
    print(f"调用失败(第{attempt+1}次),正在重连:{e}")
    await self._reconnect(server_name)
    else:
    raise RuntimeError(f"工具调用失败,已重试{max_retries}次:{e}")

    async def _reconnect(self, server_name: str):
    """重连指定Server"""
    # 先关闭旧连接
    old_session = self._sessions.get(server_name)
    if old_session:
    await old_session.close()
    # 重新连接(需要保存每个Server的启动参数)
    config = self._server_configs[server_name]
    await self.connect_server(server_name, config["command"], config["args"])

    这样即使某个Server临时挂了,客户端也会自动重连,不会影响其他Server的正常工作。

    三、错误处理:这些错误你一定会遇到

    用MCP客户端的过程中,我遇到过各种各样的错误。整理一下最常见的几种:

    错误1:连接失败

    ConnectionRefusedError: [Errno 111] Connection refused

    这种错误通常是因为Server没有正常启动。检查一下:

    • command和args是否正确
    • Server脚本是否有语法错误
    • Server依赖是否都安装了

    调试技巧:先在终端里手动运行Server命令,看看能不能正常启动。如果手动运行都报错,那MCP客户端肯定连不上。

    错误2:工具不存在

    Method not found: tools/call

    或者调用时返回isError=True,错误信息说工具不存在。

    这种情况通常是因为:

    • 工具名拼写错误
    • 你连的Server不对(多Server时路由错了)
    • Server端没有正确注册这个工具

    调试方法:先调用list_tools()看看Server到底提供了哪些工具,确认工具名是否正确。

    错误3:参数校验失败

    Invalid params: 'city' is a required property

    这是因为你传的参数不符合工具的JSON Schema。检查一下:

    • 必填参数是否都传了
    • 参数类型是否正确(比如该传数字的传了字符串)
    • 参数名是否拼写正确

    我经常犯的错误是把city写成City,大小写不一致导致校验失败。JSON Schema是区分大小写的,一定要注意。

    错误4:超时

    MCP客户端默认没有超时限制,如果Server端卡住了,客户端会一直等。建议自己加超时:

    import asyncio

    async def call_with_timeout(session, name, arguments, timeout=30):
    try:
    result = await asyncio.wait_for(
    session.call_tool(name=name, arguments=arguments),
    timeout=timeout
    )
    return result
    except asyncio.TimeoutError:
    raise TimeoutError(f"工具 {name} 调用超时({timeout}秒)")

    超时时间根据工具的实际情况来定,查天气可能5秒就够了,跑代码分析可能需要60秒。

    四、高级用法:不只是调用工具

    MCP客户端不只能调用工具,还有一些高级功能值得了解。

    资源读取

    如果Server提供了Resource,客户端可以读取资源内容:

    # 列出所有资源
    resources = await session.list_resources()

    # 读取指定资源
    resource_content = await session.read_resource(
    uri="file:///home/user/doc.txt"
    )

    Resource跟Tool的区别是:Tool是"执行一个操作",Resource是"读取一份数据"。比如文件系统Server,read_file是Tool,而文件本身是Resource。

    进度通知

    有些工具执行时间比较长,Server可以在执行过程中发送进度通知。客户端可以通过on_progress回调来接收:

    async def on_progress(progress):
    print(f"进度:{progress.progress} / {progress.total} – {progress.message}")

    result = await session.call_tool(
    name="long_running_task",
    arguments={},
    on_progress=on_progress
    )

    这个功能在处理长时间任务的时候很有用,比如大文件处理、批量数据分析等。

    通知(Notification)

    MCP还支持Server主动向客户端发送通知,不需要客户端请求。比如Server可以在有新数据的时候主动通知客户端:

    # 客户端注册通知处理器
    @session.notification("custom/new_data")
    async def handle_new_data(params):
    print(f"收到新数据:{params}")

    这个功能目前用得还不多,但在事件驱动的场景下很有价值。

    五、一个完整的实战例子

    最后,我把上面讲的内容整合起来,写一个完整的多Server MCP客户端例子。这个例子同时连接天气Server和文件Server,让大模型可以根据用户问题自动选择工具:

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

    class MultiServerMCPClient:
    def __init__(self):
    self.sessions = {}
    self.server_configs = {}

    async def connect(self, name, command, args):
    params = StdioServerParameters(command=command, args=args)
    transport = stdio_client(params)
    read, write = await transport.__aenter__()
    session = ClientSession(read, write)
    await session.__aenter__()
    await session.initialize()
    self.sessions[name] = session
    self.server_configs[name] = {"command": command, "args": args}

    async def get_all_tools(self):
    tools = []
    for server_name, session in self.sessions.items():
    result = await session.list_tools()
    for tool in result.tools:
    tools.append({
    "name": f"{server_name}.{tool.name}",
    "description": tool.description,
    "parameters": tool.inputSchema
    })
    return tools

    async def call(self, full_name, arguments, timeout=30):
    server_name, tool_name = full_name.split(".", 1)
    session = self.sessions[server_name]
    return await asyncio.wait_for(
    session.call_tool(name=tool_name, arguments=arguments),
    timeout=timeout
    )

    async def main():
    client = MultiServerMCPClient()

    # 连接两个Server
    await client.connect("weather", "python", ["weather_server.py"])
    await client.connect("filesystem", "python", ["fs_server.py"])

    # 获取所有工具
    tools = await client.get_all_tools()
    print(f"已加载 {len(tools)} 个工具:")
    for t in tools:
    print(f" – {t['name']}: {t['description']}")

    # 调用天气工具
    result = await client.call("weather.get_weather", {"city": "上海"})
    print(f"\\n上海天气:{result.content[0].text}")

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

    这个例子虽然简单,但已经包含了多Server管理、工具收集、统一调用的核心逻辑。你可以在这个基础上加上错误处理、自动重连、大模型集成等功能,变成一个生产级的MCP客户端。

    写在最后

    MCP客户端的使用,入门很简单,但要做好并不容易。单Server连接四步搞定,多Server管理就需要考虑工具冲突、连接管理、错误处理、超时控制等一堆问题。

    我的建议是:不要一上来就自己封装复杂的客户端,先用官方SDK的基础API跑通流程,遇到问题再逐步封装。我自己就是从最简单的单Server开始,慢慢加功能,最后才形成了上面那个Manager类。

    另外,MCP协议还在快速演进中,客户端API可能会有变化。建议定期关注官方仓库的更新,及时适配新版本。

    你们在使用MCP客户端的时候遇到过什么问题?或者有什么好用的封装技巧?欢迎在评论区分享。


    下一篇预告:10.2.3 MCP传输方式详解。MCP不只有stdio一种传输方式,还有HTTP、WebSocket等。我们会深入对比各种传输方式的优缺点,以及在什么场景下该用哪种。

    赞(0)
    未经允许不得转载:171主机测评 » MCP客户端使用指南:从单Server到多Server管理,我踩过的坑都在这里了
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址