TodayAI

GuidesMCP

构建可以连接 MCP Server 的 Client

用 Python 做一个能连接 MCP server 的聊天客户端:发现工具、执行调用、把结果回传模型。

基于 Model Context Protocol Docs 整理 · 官方资料 ↗

写完 Server 之后,还缺一环:谁去连接它、发现工具、在模型要求时真正发起 call_tool?这就是 MCP Client。

官方 Python 教程(MCP SDK 2.0.0+)用 uv、mcp、anthropic、python-dotenv 做一个命令行聊天客户端。建议先完成 构建你的第一个 MCP Server,再用同一个 weather server 验证端到端。

Client 在 MCP 里到底负责什么

Host 是用户看到的应用壳;Server 暴露 Tool;Client 夹在中间,按 MCP 协议说话。

在官方示例里,Client 的职责很具体:启动并连接 Server 子进程、list_tools、把工具 schema 交给 Claude、在模型返回 tool_use 时 call_tool、把结果写回对话,再让模型生成最终自然语言回答。

没有可用的 Server 时,Client 教程无法单独验证工具环。准备好 weather.py 或任意 .py / .js Server 脚本再继续。

连接一个 Server

先按官方要求初始化项目。需要 Mac 或 Windows、最新 Python、已安装 uv,以及 Python MCP SDK 2.0.0+;对话侧使用 Anthropic API Key。

官方 macOS / Linux 初始化

bash
uv init mcp-client
cd mcp-client
uv venv
source .venv/bin/activate
uv add mcp anthropic python-dotenv
rm main.py
touch client.py

StdioServerParameters 只是配置,不是连接本身。它描述「用什么命令启动哪个 Server 脚本」。

python
from mcp import Client, StdioServerParameters
from mcp.client.stdio import stdio_client

def server_params(server_script_path: str) -> StdioServerParameters:
    if server_script_path.endswith(".py"):
        command = "python"
    elif server_script_path.endswith(".js"):
        command = "node"
    else:
        raise ValueError("Server script must be a .py or .js file")

    return StdioServerParameters(command=command, args=[server_script_path])

建立 Session

stdio_client() 把参数变成 stdio 传输;Client 在进入 async with 时打开传输、拉起 Server,并完成协议版本协商。

官方导入与入口形态

python
import asyncio
import sys

from mcp import Client, StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp_types import TextContent

from anthropic import Anthropic
from dotenv import load_dotenv

load_dotenv()
MODEL = "claude-opus-5"
anthropic = Anthropic()

async def main() -> None:
    if len(sys.argv) < 2:
        print("Usage: python client.py <path_to_server_script>")
        sys.exit(1)

    async with Client(stdio_client(server_params(sys.argv[1]))) as client:
        tool_list = await client.list_tools()
        tool_names = [tool.name for tool in tool_list.tools]
        print("\nConnected to server with tools:", tool_names)
        # ... chat_loop(client)

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

官方写明:async with 就是整个连接生命周期。进入时连接,离开时断开并关掉子进程,没有单独的 connect/close 要对。

列出 Server 暴露的 Tools

连接成功后,第一件事通常是 await client.list_tools()。把 name、description、input_schema 转成 Anthropic tools 参数,模型才能决定是否调用。

python
tool_list = await client.list_tools()
available_tools = [{
    "name": tool.name,
    "description": tool.description,
    "input_schema": tool.input_schema,
} for tool in tool_list.tools]

调用一个 Tool

用户提问后,用 MODEL = "claude-opus-5" 调用 anthropic.messages.create,并传入 available_tools。若响应里出现 type 为 tool_use 的内容块,就从中取出 tool_name 与 tool_args,再执行 await client.call_tool(tool_name, tool_args)。

python
response = anthropic.messages.create(
    model=MODEL,
    max_tokens=1000,
    messages=messages,
    tools=available_tools,
)

for content in response.content:
    if content.type == "tool_use":
        tool_name = content.name
        tool_args = content.input
        result = await client.call_tool(tool_name, tool_args)

读取结果

call_tool 返回 CallToolResult。content 是一组内容块;官方示例只抽取 TextContent 的 .text。工具失败通常不会在这里抛异常,而是 result.is_error 为真——把该标志一并回传,方便模型换策略。

python
tool_results = [{
    "type": "tool_result",
    "tool_use_id": content.id,
    "content": "\n".join(
        block.text
        for block in result.content
        if isinstance(block, TextContent)
    ),
    "is_error": result.is_error,
}]

messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})

response = anthropic.messages.create(
    model=MODEL,
    max_tokens=1000,
    messages=messages,
    tools=available_tools,
)

正确关闭连接

不要手写一对 connect/close。让 async with Client(...) 拥有连接:离开代码块时会断开并关闭 Server 子进程。

聊天循环里输入 quit,或遇到 EOF,应干净退出 main;单次查询失败应打印错误并继续会话,而不是默默结束整个 Client。

对接 weather server 运行

bash
uv run client.py path/to/weather.py

Server / Client / Host 最容易混淆在哪里

Server 只暴露能力,不负责对话 UI,也不直接调用 Claude。Client 负责协议与工具调度。Host 是承载体验的应用;在这篇教程里,你的命令行程序既是 Host,也内嵌了 Client。

Claude for Desktop 是另一个 Host:它内置 Client,去连你写的 Server。你在这篇写的 Client,则是自己充当 Host,去连同一个 Server。两边连的是同一类 Server,角色不要对调理解。

若还没有 Server,先回到 构建你的第一个 MCP Server

最容易出错的地方

连不上 Server

检查命令行传入的脚本路径(相对或绝对),确认扩展名是 .py 或 .js;Windows 注意斜杠写法。

Anthropic 鉴权失败

在 .env 中设置 ANTHROPIC_API_KEY,并用 python-dotenv 的 load_dotenv() 加载;不要把 Key 提交进仓库。

工具调用参数不对

打印模型返回的 tool input,对照 Server 工具的 docstring 与类型提示;list_tools 里的 schema 才是模型看到的真相。

工具失败却当成异常崩溃

按官方说明检查 result.is_error,把错误内容回传给模型,而不是假设 call_tool 一定会抛异常。

官方资料

Model Context Protocol Docs

Build an MCP client