网上 90% 的 MCP 文章都在讲「怎么写一个 MCP Server」。但真到你自己做 Agent 时,更常见的问题是反过来的:怎么在自家的 Python 程序里,把别人已经写好的那些 MCP Server(文件系统、网页抓取、数据库、GitHub……)的远程工具,直接变成我 Agent 能调的函数?这篇就解决这件事——自写 MCP Client。
mcp SDK 连上一个 stdio MCP Server;② 列出它提供的工具并真去调用;③ 把工具喂给大模型,跑通「模型决策 → Client 真调 → 结果回灌」的 Agent 循环;④ 换上社区里现成的真实 Server。先搞懂:Server 和 Client 各干啥?
一句话:Server 提供能力(工具),Client 把能力接进你的程序。MCP 走的是「客户端-服务器」那一套,最常见的传输方式是 stdio——你的 Client 用子进程把 Server 拉起来,双方通过标准输入输出(stdin/stdout)发 JSON-RPC 消息通信。
| 角色 | 负责 | 本文落在哪 |
|---|---|---|
| MCP Server | 暴露工具(如 add、web_search) | Step 2 先写一个最小 Server 练手 |
| MCP Client | 连 Server、列工具、调工具 | Step 3 核心 |
| 你的 Agent | 用 LLM 决定「调哪个、传什么」 | Step 5 串成闭环 |
为什么不直接用 HTTP 版 Server?stdio 版的 Server 不需要你起服务、不用管端口,Client 一行命令拉起就能用,最适合把「别人的能力」快速接进本地 Agent。本文全程用 stdio。
Step 1:准备环境(Python 3.10+)
mkdir mcp-client-demo && cd mcp-client-demo
python -m venv .venv
# Windows 激活: .venv\Scripts\activate
# macOS/Linux 激活:source .venv/bin/activate
pip install "mcp>=1.2.0" openai
mcp 官方 SDK。大模型那一步用 OpenAI 兼容接口,没有 API Key 也能先把 Step 2–4 跑通(那几步不联网、不调模型)。需要模型时在 Step 5 再配 OPENAI_API_KEY。Step 2:写一个最小 MCP Server(FastMCP)
先有一个能被调用的 Server。用官方提供的 FastMCP,加个装饰器就是一个工具:
# server.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("demo")
@mcp.tool()
def add(a: int, b: int) -> int:
"""把两个数相加。"""
return a + b
@mcp.tool()
def web_search(q: str) -> str:
"""返回一段示例搜索结果(演示用,不联网)。"""
return f"关于「{q}」的示例结果:MCP 让 Agent 调用外部工具。"
if __name__ == "__main__":
mcp.run() # 默认 stdio 传输
关键坑:这个 Server 不要自己手动 python server.py 当常驻进程跑。stdio 模式下,它必须被 Client 以子进程方式拉起,靠 stdin/stdout 通信。你手动跑起来,Client 是连不上的。
Step 3:用 mcp SDK 写 Client 连上 Server
这才是本文的主角。stdio_client 负责拉起 Server,ClientSession 负责协议握手和调用:
# client.py
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
params = StdioServerParameters(command="python", args=["server.py"])
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize() # 必须先握手
tools = await session.list_tools()
print("可用工具:", [t.name for t in tools.tools])
res = await session.call_tool("add", {"a": 2, "b": 3})
print("add(2,3) =", res.content[0].text)
asyncio.run(main())
运行:
python client.py
# 输出:
# 可用工具: ['add', 'web_search']
# add(2,3) = 5
session.list_tools() 拿到的工具 schema(名字、参数、描述)正是下一步喂给大模型的东西。注意 initialize() 必须在任何调用之前,漏掉会直接报错。Step 4:把工具描述喂给大模型
把 Server 的工具列表转成 OpenAI 风格的 tools schema,模型就能决定「该调哪个」:
import json, openai
client = openai.OpenAI() # 需要 OPENAI_API_KEY
tools_schema = [
{
"type": "function",
"function": {
"name": t.name,
"description": t.description or "",
"parameters": t.inputSchema, # MCP 返回的 JSON Schema
},
}
for t in tools.tools
]
没有 OpenAI Key?换成任意 OpenAI 兼容端点都行:openai.OpenAI(base_url="https://你的端点", api_key="..."),例如 DeepSeek、通义、本地 Ollama(需开 function calling)。本步只是构造 schema,不强制某家模型。
Step 5:Agent 循环——模型调度、Client 真调、结果回灌
真正的 Agent 是「循环」:模型说「我要调 add」→ Client 真去 Server 上算 → 把结果以 role=tool 回灌 → 模型继续推理,直到给出最终答案。
# agent.py 核心骨架(与 Step 3 的 session 同一上下文)
messages = [{"role": "user", "content": "帮我算 12+30,并搜一下 MCP 是什么"}]
while True:
r = client.chat.completions.create(
model="gpt-4o-mini", messages=messages, tools=tools_schema)
msg = r.choices[0].message
if not msg.tool_calls: # 模型认为不需要再调工具
print(msg.content); break
messages.append(msg) # 把模型的「调工具意图」记下来
for call in msg.tool_calls:
result = await session.call_tool(
call.function.name, json.loads(call.function.arguments))
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": result.content[0].text,
})
tool_call_id 以 role=tool 回灌,模型才能把这次结果和刚才那次「调用意图」对上号,继续往下推理。漏了这一步,模型会「忘了」自己刚调了什么。Step 6:接一个真实 MCP Server(文件系统 / 抓取)
社区已经有大量现成 Server。把 StdioServerParameters 的启动命令换成它们即可,Client 代码一行都不用改:
# 例:用官方文件系统 Server,把当前目录授权给它
params = StdioServerParameters(
command="npx",
args=["-y", "@modelcontextprotocol/server-filesystem", "."])
# 例:网页抓取 Server
params = StdioServerParameters(
command="npx",
args=["-y", "@modelcontextprotocol/server-fetch"])
权限即风险。文件系统类 Server 一旦连上,你的 Agent 就能读写你授权出去的目录。千万别把 ~、桌面、含密钥的目录暴露给它;给最小必要目录就好。npx 运行时会先联网下载包,离线环境请提前装好。
Step 7:收尾与进阶
- 多 Server 聚合:起多个
stdio_client,把各自的list_tools()合并成一张工具表,你的 Agent 就同时拥有「算数 + 读文件 + 抓网页」等一揽子能力。 - 可视化调试:用官方 Inspector 看工具列表和每次调用——
npx @modelcontextprotocol/inspector,浏览器打开它给的地址即可。 - 安全收敛:给工具做白名单(只允许模型调某几个)、校验参数类型、对写操作加人工确认,别让 Agent 拿到「任意工具任意参数」的完全权限。
常见问题速查
| 现象 | 原因 & 解决 |
|---|---|
| Client 启动就退出 / 连不上 | 多半是 Server 没用 stdio 方式被拉起;检查 command/args 路径与 Python 解释器 |
| call_tool 报参数错误 | MCP 参数是 JSON,用 json.loads 解析模型给的 arguments 再传 |
| 模型不调工具 | 工具 description 写得太含糊;把「做什么、返回什么」写具体 |
| npx 卡在下载 | 网络受限,先在本机 npm i -g 对应包再指本地路径 |