实战 📋 7 个步骤 第 451 / 455 篇

用 MCP Client 接入任意工具服务:在自写 Agent 里调用 MCP Server(Python 实战)

已有大量现成的 MCP Server(文件系统、网页抓取、数据库……),但多数教程只教怎么写 Server。本文反过来自写 MCP Client:用官方 mcp SDK 连上 stdio MCP Server,把远程工具变成你 Agent 能直接调用的函数,并配一个最小可运行的 Agent 串联整条链路。

2026.09.19· 22 分钟阅读· 约 1721 字· 🔌 MCP / 🐍 Python

网上 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+)

1 建虚拟环境并装依赖
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)

2 用 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

3 列工具 + 真调一次

这才是本文的主角。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
🧩 到这里你已经完成「自写 Client 调通 Server」的核心闭环。session.list_tools() 拿到的工具 schema(名字、参数、描述)正是下一步喂给大模型的东西。注意 initialize() 必须在任何调用之前,漏掉会直接报错。

Step 4:把工具描述喂给大模型

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 真调、结果回灌

5 把工具结果接回对话

真正的 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,
        })
🔑 一句话记住 Agent 循环:工具结果必须带 tool_call_idrole=tool 回灌,模型才能把这次结果和刚才那次「调用意图」对上号,继续往下推理。漏了这一步,模型会「忘了」自己刚调了什么。

Step 6:接一个真实 MCP Server(文件系统 / 抓取)

6 换个官方 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:收尾与进阶

7 多 Server 聚合 + 调试与安全
  • 多 Server 聚合:起多个 stdio_client,把各自的 list_tools() 合并成一张工具表,你的 Agent 就同时拥有「算数 + 读文件 + 抓网页」等一揽子能力。
  • 可视化调试:用官方 Inspector 看工具列表和每次调用——npx @modelcontextprotocol/inspector,浏览器打开它给的地址即可。
  • 安全收敛:给工具做白名单(只允许模型调某几个)、校验参数类型、对写操作加人工确认,别让 Agent 拿到「任意工具任意参数」的完全权限。
🎉 到这一步,你已经拥有一条可复用的「MCP Client 接入管线」:以后看到任何 stdio MCP Server,改一行启动命令就能把它接进你的 Agent。本中心《MCP 协议实战》《从零写一个 MCP Server》可配合食用。

常见问题速查

现象原因 & 解决
Client 启动就退出 / 连不上多半是 Server 没用 stdio 方式被拉起;检查 command/args 路径与 Python 解释器
call_tool 报参数错误MCP 参数是 JSON,用 json.loads 解析模型给的 arguments 再传
模型不调工具工具 description 写得太含糊;把「做什么、返回什么」写具体
npx 卡在下载网络受限,先在本机 npm i -g 对应包再指本地路径
← 返回教程中心