Contents

MCP协议:智能体的标准化工具接口

1. 引言:MCP解决了什么问题

在前面几章中,我们反复使用了一个关键能力——Function Calling(函数调用)。无论是查数据库、调API,还是操作文件系统,智能体都需要通过"函数"这一抽象来触达外部世界。然而,当工具数量膨胀、应用场景复杂化时,一个令人头疼的问题浮现出来:

工具集成碎片化(Tool Integration Fragmentation)

具体表现为:

  • 协议不统一:每家大模型厂商都定义了自己的Function Calling格式。OpenAI、Anthropic、Google、阿里通义各有各的工具描述规范,同一份工具定义要写好几遍。
  • 上下文割裂:工具本身没有"资源"概念。Agent要读取一份文件、一段数据库Schema,都得靠开发者手动拼接到Prompt里,缺乏统一的资源访问接口。
  • 生态重复造轮子:搜数据库、查天气、读GitHub,每个Agent项目都在重复封装相同的工具。A项目写的工具,B项目无法直接复用。
  • 运维耦合:工具进程和Agent进程常常绑死在一起,工具升级、权限管理、多工具编排都很难解耦。

这就好比每个Agent都自带一个"私有的工具箱",彼此不通,每次都要从零打造。2024年11月,Anthropic发布了Model Context Protocol(MCP,模型上下文协议),给出了一个令人眼前一亮的解法:

把工具、资源、提示模板从Agent中解耦出来,变成可独立部署、可被任意支持MCP的客户端发现的"Server",Agent则作为"Client"按需连接,即插即用。

用一个生活比喻来理解:MCP之于AI工具生态,就像USB-C接口之于数码设备。在USB-C统一之前,键盘是PS/2口、打印机是并口、老手机是Micro-USB、相机是Mini-USB,出门得带一整包转接头;USB-C一统天下后,一根线就能给笔记本、手机、平板充电传数据,即插即用、正反都能插。MCP正是希望成为"AI Agent的USB-C接口"——你写一个MCP工具,任何支持MCP的Agent都能直接用,不用改一行代码。

💡 小贴士:什么是MCP? MCP(Model Context Protocol,模型上下文协议)是Anthropic开源的一套通信标准,规定了"AI模型如何发现并调用外部工具"的统一格式。它不绑定任何一家模型厂商,谁都可以免费使用。你可以把它理解成"工具界的HTTP"——HTTP统一了网页传输,MCP统一了AI调工具。

本章将从协议原理讲到Server/Client开发,最后用一个天气查询Agent串起完整链路。

2. MCP协议概述

2.1 协议的诞生与定位

MCP(Model Context Protocol)由Anthropic于2024年11月开源发布,是一个开放、基于JSON-RPC 2.0的应用层协议。其设计目标有四:

  1. 标准化:任何MCP Server都能被任何兼容MCP的Host(如Claude Desktop、Cursor、各类Agent框架)发现并调用。
  2. 解耦:Server与Client通过协议而非代码耦合,可独立部署、独立升级。
  3. 可组合:一个Host可同时连接多个Server,工具数量线性扩展。
  4. 安全:Server运行在本地或受控环境,Host控制权限边界。

💡 小贴士:什么是JSON-RPC 2.0? JSON-RPC是一种用JSON格式进行"远程过程调用"的轻量协议。简单说,就是"我用JSON给你发一条消息,告诉你调用哪个方法、传什么参数,你也用JSON回我结果"。2.0版本规定了jsonrpcidmethodparams这几个字段。MCP所有通信都套这个格式,所以任何语言都能轻松实现。

2.2 三大组件:Host / Client / Server

MCP架构由三个核心角色构成。再用USB-C的比喻串一下:

  • Host(宿主应用):最终用户使用的应用,比如Claude Desktop、Cursor IDE,或我们自己开发的Agent程序。Host就像你的笔记本电脑——它有多个USB-C口,负责管理外设、汇总能力、向用户暴露交互入口。
  • Client(协议客户端):Host内部的一个连接器,每个Client与一个Server保持1:1的会话。Client就像笔记本里那个"USB-C控制器芯片",负责协议握手、能力协商、消息路由。
  • Server(服务提供方):独立运行的进程,向Client暴露三类能力:Tools(工具)、Resources(资源)、Prompts(提示模板)。Server就像你插上的U盘、键盘、显示器——各自提供不同功能。

它们的关系如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
┌─────────────────────────────┐
│           Host (Agent)      │
│  ┌────────┐  ┌────────┐     │
│  │Client A│  │Client B│ ... │
│  └───┬────┘  └───┬────┘     │
└──────┼───────────┼──────────┘
       │           │
   stdio/SSE   stdio/SSE
       │           │
   ┌───▼────┐  ┌───▼────┐
   │Server A│  │Server B│
   │ 文件系统 │  │ 数据库  │
   └────────┘  └────────┘

💡 小贴士:Server和Client到底谁是谁? 记住一条线:提供能力的是Server,使用能力的是Client。文件系统Server提供"读文件"能力,Agent端的Client来调用它。Host里可以同时挂很多个Client,每个Client连一个Server,于是Agent就能用上所有Server的工具。

2.3 与传统Function Calling的区别

维度 Function Calling MCP
工具定义位置 写死在Agent代码或Prompt中 独立Server,运行时动态发现
复用性 每个项目重写 一次开发,多方复用
上下文资源 无统一抽象,手动拼接 内置Resources概念,按URI访问
提示模板 内置Prompts,可被Host列出并调用
多模型兼容 各厂商格式不同 协议中立,与具体模型解耦
部署形态 与Agent同进程 可独立进程,本地或远程

一个直观的差异:在Function Calling模式下,你换一个模型厂商,工具描述格式可能就要改一遍;而在MCP下,Server完全不用动,Host换底层模型即可。这就像USB-C口不变,你换什么牌子的电脑都能用同一个U盘。

2.4 2026年生态状态

截至2026年中,MCP已被主流Agent框架广泛采纳:

  • 官方/社区MCP Server数量已突破5000+,覆盖文件系统、Git、数据库(PostgreSQL/MySQL/SQLite)、Slack、GitHub、Notion、Linear、Sentry、Brave Search、Puppeteer等场景。
  • 主流Host支持:Claude Desktop、Cursor、Windsurf、Zed、Cline等均原生支持MCP;LangChain、LlamaIndex、AutoGen也提供了MCP适配层。
  • 多语言SDK:官方提供了TypeScript、Python SDK,社区贡献了Go、Rust、Java、C#等实现。
  • 远程MCP:基于HTTP+SSE的远程Server部署模式逐渐成熟,出现了MCP Gateway、MCP Hub等托管平台。

可以说,MCP已经从"Anthropic的提案"成长为事实标准。

3. MCP架构详解

3.1 Host:宿主应用

Host是用户视角的"Agent应用"。它的职责:

  1. 创建并管理多个Client实例(一个Server对应一个Client)。
  2. 汇总所有Server暴露的Tools/Resources/Prompts,统一呈现给LLM或用户。
  3. 在LLM决定调用某个工具时,路由请求到对应Client,再把结果回传给LLM。
  4. 控制权限边界:是否允许某个Server读取某资源、是否允许某工具执行,由Host把关。

Claude Desktop就是典型Host:用户可以在配置文件中声明若干MCP Server,应用启动时拉起这些Server进程,并把工具接入对话。

3.2 Client:协议客户端

Client是Host内部维护的"会话管理器"。每个Client对应一个Server,负责:

  • 初始化握手:发送initialize请求,交换协议版本与能力声明。
  • 能力协商:Server声明自己支持哪些Tools/Resources/Prompts,Client声明自己支持哪些回调(如采样sampling、根目录roots)。
  • 消息收发:所有调用基于JSON-RPC 2.0,Client既是请求方,也接收Server发起的通知与反向请求。
  • 生命周期管理:维持会话、处理断连、优雅关闭。

3.3 Server:服务提供方

Server是能力提供者,可暴露三类原语(primitive):

原语 语义 谁主导 典型例子
Tools 可执行的函数,有副作用 LLM主动调用 发邮件、查数据库、运行Shell
Resources 可读取的数据,无副作用 应用/用户主动拉取 文件内容、DB Schema、日志
Prompts 预制的提示模板 用户主动选择 “代码审查"模板、“周报"模板

注意三者的控制权差异:Tools是模型自主决定调用;Resources和Prompts更多是应用层主动获取后注入上下文。这种分工避免了LLM滥用只读资源,也让权限管理更清晰。

💡 小贴士:为什么要分Tools和Resources? Tools会"动手”(可能发邮件、删文件),必须由LLM慎重决策后调用;Resources只"读不动”(看一份文档、查一段Schema),由应用层按需取用即可。把"读"和"做"分开,权限审批就能区别对待——读操作可以放行,写操作必须人工确认。

3.4 通信协议:JSON-RPC over stdio/SSE

MCP传输层支持两种主流模式:

  • stdio(标准输入输出):本地进程间通信。Host以子进程方式拉起Server,通过stdin/stdout交换JSON-RPC消息。延迟低、部署简单,是本地集成的主流方式。
  • HTTP + SSE(Server-Sent Events):远程通信。Server作为HTTP服务暴露,Client通过POST发请求、SSE接收推送。适合跨机器、云托管场景。新版本也支持Streamable HTTP,简化了SSE的双向通道。

💡 小贴士:stdio和SSE各是啥?

  • stdio 就是程序的标准输入输出流。你在终端敲python xxx.py,键盘输入走stdin,屏幕打印走stdout。MCP本地模式下,Host和Server就是靠这两个流互发JSON消息,省去了开网络端口的麻烦。
  • SSE(Server-Sent Events) 是一种服务器单向推消息给浏览器的HTTP技术。MCP远程模式下,Client发请求用普通POST,Server推结果用SSE,省去轮询。

消息格式示例(Client调用工具):

1
2
3
4
5
6
7
8
9
{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "tools/call",
  "params": {
    "name": "read_file",
    "arguments": {"path": "/tmp/note.md"}
  }
}

Server响应:

1
2
3
4
5
6
7
8
{
  "jsonrpc": "2.0",
  "id": 42,
  "result": {
    "content": [{"type": "text", "text": "笔记内容..."}],
    "isError": false
  }
}

可以看到,每条消息都有一个id用于配对请求与响应,method指明调用哪个方法,params携带参数。这就是JSON-RPC的全部精髓。

3.5 生命周期:初始化→能力交换→工具调用→关闭

一次完整的Client-Server会话:

  1. Initialize:Client发送initialize,携带本端支持的协议版本和能力。Server回应当前版本及自身能力。
  2. Initialized 通知:Client发送notifications/initialized,握手完成。
  3. 能力列举:Client调用tools/listresources/listprompts/list获取清单。
  4. 运行期调用:Client按需调用tools/callresources/readprompts/get;Server也可反向发起sampling/createMessage让Host代为调用LLM。
  5. 关闭:Client发送关闭信号或直接结束stdio通道,Server清理资源退出。

理解这一生命周期对调试MCP连接问题非常重要——多数"工具不出现"的故障都源于握手或能力交换阶段出错。

4. 开发MCP Server

4.1 安装Python SDK

1
pip install mcp

mcp是官方维护的Python SDK,同时支持stdio与SSE两种传输方式。它提供两层API:

  • 高层FastMCP:声明式,用类型注解和装饰器自动生成Schema,代码极简,推荐日常使用
  • 低层Server:手动实现每个JSON-RPC方法的handler,控制力强,适合需要精细定制的场景。

💡 小贴士:本节用哪层API? 本章代码采用高层FastMCP。它会把你的函数签名自动转成tools/listtools/call这些JSON-RPC消息,底层细节全被封装。理解原理看第3节,动手写代码看FastMCP即可。

下面我们用它开发一个文件系统MCP Server,暴露读取文件、列出目录两个工具,并演示Resources和Prompts。

4.2 三类原语速览

在写代码前,先记住三类原语的FastMCP写法:

  • Tool(工具):用@mcp.tool()装饰一个函数,函数名即工具名,类型注解自动生成参数Schema,docstring自动变成给LLM看的描述。
  • Resource(资源):用@mcp.resource("uri模板")装饰一个函数,按URI暴露只读数据。
  • Prompt(提示模板):用@mcp.prompt()装饰一个函数,返回一段预制提示词。

4.3 完整示例:文件系统MCP Server

下面是完整可运行的Server代码。注意看注释——它解释了每一块对应MCP的哪个概念:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# filesystem_mcp_server.py
# 文件系统 MCP Server:提供「读取文件」「列出目录」两个工具,
# 另外演示 Resources(资源)和 Prompts(提示模板)两类原语。
# 运行:python filesystem_mcp_server.py
# 依赖:pip install mcp

import os
from mcp.server.fastmcp import FastMCP

# 创建 Server 实例,名称会在握手阶段告诉 Client「我是谁」
mcp = FastMCP("filesystem-mcp")


# ---------- Tools:工具(LLM 主动调用)----------

@mcp.tool()
async def read_file(path: str) -> str:
    """读取指定路径的文本文件内容。路径必须是绝对路径。"""
    try:
        with open(path, "r", encoding="utf-8") as f:
            return f.read()
    except FileNotFoundError:
        # 用「返回文本」承载错误,而不是抛异常——
        # 这样 LLM 能看到错误描述,自行决定是否重试或换工具
        return f"错误:文件不存在 {path}"
    except Exception as e:
        return f"错误:{e}"


@mcp.tool()
async def list_directory(path: str) -> str:
    """列出指定目录下的文件与子目录名称。"""
    try:
        entries = os.listdir(path)
        lines = []
        for entry in entries:
            full = os.path.join(path, entry)
            kind = "目录" if os.path.isdir(full) else "文件"
            lines.append(f"{entry} ({kind})")
        return "\n".join(lines)
    except Exception as e:
        return f"错误:{e}"


# ---------- Resources:资源(应用层按 URI 主动读取)----------

@mcp.resource("file://{path}")
async def read_resource(path: str) -> str:
    """按 URI 读取文件内容,作为只读资源暴露给 Client。
    Client 读取 file:///tmp/note.md 时,path 会被解析为 /tmp/note.md。"""
    with open(path, "r", encoding="utf-8") as f:
        return f.read()


# ---------- Prompts:提示模板(用户选择后由 Host 渲染)----------

@mcp.prompt()
def summarize_file(path: str) -> str:
    """生成一段提示词,让模型总结指定文件内容。"""
    return f"请阅读路径 {path} 的文件内容,并用三句话总结要点。"


# ---------- 启动 ----------

if __name__ == "__main__":
    # mcp.run() 默认以 stdio 模式启动,等待 Host 拉起并通信
    mcp.run()

几个值得注意的细节:

  • 类型注解即Schemapath: str会被FastMCP自动转成{"type": "string"}的JSON Schema,再也不用手写inputSchema字典了。
  • docstring即描述:函数下方的三引号文档字符串,会原样成为工具的description,直接给LLM看。所以写好docstring等于写好工具说明书
  • 错误处理风格:MCP推荐"返回文本错误"而非抛异常,这样LLM能看到错误描述并自行决定是否重试或换工具。
  • Resource的URI:必须符合协议://路径形式,file://是最常用的,也可以自定义如db://users/42
  • mcp.run()默认stdio:本地集成时Host以子进程拉起本脚本,通过stdin/stdout收发消息,无需开端口。

💡 小贴士:FastMCP背后发生了什么? 当Client发来tools/list时,FastMCP会扫描所有@mcp.tool()装饰的函数,自动生成工具清单返回;当Client发来tools/call并指定name="read_file",FastMCP会调用对应函数、把返回值包成TextContent回传。你写的只是普通Python函数,协议细节全自动。

5. 开发MCP Client

5.1 Client的职责

一个完整的MCP Client需要做四件事:

  1. 连接:通过stdio或SSE与Server建立会话。
  2. 初始化:发送initialize、接收能力声明、发送initialized通知。
  3. 发现:调用tools/list等列举能力。
  4. 调用:按需调用tools/call,解析返回的content数组。

5.2 完整示例:MCP Client集成到Agent

下面这段代码启动上一节的文件系统Server,并把它的工具接入一个简易Agent循环。代码分六步,每步都有注释说明在做什么:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# mcp_client_agent.py
# 通过 MCP Client 连接文件系统 Server,并把工具接入一个简易 Agent。
# 依赖:pip install mcp openai
# 环境变量:OPENAI_API_KEY、OPENAI_BASE_URL(可选)、MODEL_NAME(可选)

import os
import sys
import asyncio
import json

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

# LLM 客户端:可替换为任何兼容 OpenAI 接口的模型服务
llm = OpenAI(
    api_key=os.environ.get("OPENAI_API_KEY", "sk-xxx"),
    base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
)
MODEL = os.getenv("MODEL_NAME", "gpt-5.4")


async def run_agent(user_query: str):
    """启动 MCP Server 子进程,建立会话,运行一轮 Agent 对话"""

    # 第 1 步:配置 stdio 连接参数,以子进程方式拉起 Server
    server_params = StdioServerParameters(
        command=sys.executable,            # 用当前 Python 解释器
        args=["filesystem_mcp_server.py"], # Server 脚本路径
        env=os.environ.copy()             # 透传环境变量
    )

    # 第 2 步:建立 stdio 连接,再创建 Client 会话
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            # 第 3 步:初始化握手,交换协议版本与能力声明
            await session.initialize()

            # 第 4 步:发现工具,调用 tools/list 拿到清单
            tools_resp = await session.list_tools()
            mcp_tools = tools_resp.tools
            print(f"[Client] 发现 {len(mcp_tools)} 个工具:",
                  [t.name for t in mcp_tools])

            # 第 5 步:把 MCP 工具转换为 OpenAI Function Calling 格式
            oa_tools = []
            for t in mcp_tools:
                oa_tools.append({
                    "type": "function",
                    "function": {
                        "name": t.name,
                        "description": t.description,
                        "parameters": t.inputSchema
                    }
                })

            # 第 6 步:Agent 主循环:对话 → 可能调用工具 → 回填结果 → 继续对话
            messages = [
                {"role": "system", "content": "你是一个文件助手,可以读写本地文件。"},
                {"role": "user", "content": user_query}
            ]

            for _ in range(5):  # 最多 5 轮,防止死循环
                resp = llm.chat.completions.create(
                    model=MODEL,
                    messages=messages,
                    tools=oa_tools
                )
                msg = resp.choices[0].message
                messages.append(msg.model_dump(exclude_none=True))

                # 没有工具调用,说明模型已经给出最终回答
                if not msg.tool_calls:
                    print(f"[Agent] 回复:{msg.content}")
                    break

                # 执行模型要求的每一个工具调用
                for call in msg.tool_calls:
                    fn_name = call.function.name
                    fn_args = json.loads(call.function.arguments or "{}")
                    print(f"[Agent] 调用工具 {fn_name},参数:{fn_args}")

                    # 通过 MCP Client 调用 Server 上的工具
                    result = await session.call_tool(fn_name, fn_args)
                    # 把工具结果回填为 tool 角色消息
                    tool_text = "\n".join(
                        c.text for c in result.content if hasattr(c, "text")
                    )
                    messages.append({
                        "role": "tool",
                        "tool_call_id": call.id,
                        "content": tool_text
                    })
                    print(f"[Tool] 结果:{tool_text[:200]}")


if __name__ == "__main__":
    # 示例:让 Agent 列出 /tmp 目录并读取某个 .md 文件
    asyncio.run(run_agent("请列出 /tmp 目录下有哪些文件,并读取其中任意一个 .md 文件的内容。"))

注意我们做了协议桥接:MCP工具的inputSchema正好就是OpenAI Function Calling所需的JSON Schema,所以转换几乎零成本——直接把t.inputSchema塞进parameters字段即可。这也是MCP设计中"协议中立"红利的体现:你可以用任意LLM厂商的API,工具层完全复用。

💡 小贴士:为什么说MCP是"协议中立"的? MCP Server只懂JSON-RPC,根本不知道调用它的是GPT、Claude还是通义。Client这边把MCP工具格式转成各家API需要的格式(OpenAI用function字段、Anthropic用tools字段),转换层很薄。换模型只动Client,Server一行不改。

6. MCP实战:构建天气查询Agent

理论清楚了,下面用一个更贴近真实场景的例子把全流程串起来。我们将:

  1. 开发一个天气MCP Server,封装开放天气API。
  2. 用MCP Client把它接入Agent,让用户用自然语言问天气。

6.1 天气MCP Server

这个Server只暴露一个get_weather工具,背后调用免费的wttr.in服务(无需API Key,便于演示)。同样用FastMCP,几十行搞定:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# weather_mcp_server.py
# 天气查询 MCP Server:暴露 get_weather 工具
# 依赖:pip install mcp httpx
# 运行:python weather_mcp_server.py

import httpx
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("weather-mcp")

# wttr.in 是免费天气服务,?format=j1 返回 JSON
WEATHER_URL = "https://wttr.in/{city}?format=j1"


@mcp.tool()
async def get_weather(city: str) -> str:
    """查询指定城市的当前天气,返回温度、天气状况、湿度、风速等。
    城市名用英文或拼音,例如 Beijing、shanghai。"""
    try:
        async with httpx.AsyncClient(timeout=10) as client:
            resp = await client.get(WEATHER_URL.format(city=city))
            resp.raise_for_status()
            data = resp.json()

        # 解析当前天气字段
        current = data["current_condition"][0]
        temp_c = current["temp_C"]
        desc = current["weatherDesc"][0]["value"]
        humidity = current["humidity"]
        wind = current["windspeedKmph"]

        return (
            f"城市:{city}\n"
            f"温度:{temp_c}°C\n"
            f"天气:{desc}\n"
            f"湿度:{humidity}%\n"
            f"风速:{wind}km/h"
        )
    except Exception as e:
        return f"查询失败:{e}"


if __name__ == "__main__":
    mcp.run()

💡 小贴士:为什么用httpx.AsyncClient而不是requests MCP的Python SDK基于asyncio异步框架。如果工具内部用同步的requests发请求,会阻塞整个事件循环,导致Server卡住无法响应其他调用。httpx的异步客户端与asyncio天然兼容,不会阻塞。同理,访问数据库应该用aiosqliteasyncpg等异步驱动。

6.2 Agent通过MCP调用天气工具

我们复用上一节的Client骨架,只换Server脚本与System Prompt。注意StdioServerParametersargs改成了weather_mcp_server.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# weather_agent.py
# 天气查询 Agent:通过 MCP 调用 weather_mcp_server
# 依赖:pip install mcp openai

import os
import sys
import asyncio
import json

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

llm = OpenAI(
    api_key=os.environ.get("OPENAI_API_KEY", "sk-xxx"),
    base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
)
MODEL = os.getenv("MODEL_NAME", "gpt-5.4")


async def run_weather_agent(user_query: str):
    # 以子进程拉起天气 Server
    server_params = StdioServerParameters(
        command=sys.executable,
        args=["weather_mcp_server.py"],
        env=os.environ.copy()
    )

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()  # 握手

            # 发现工具并转成 OpenAI 格式
            tools_resp = await session.list_tools()
            oa_tools = [{
                "type": "function",
                "function": {
                    "name": t.name,
                    "description": t.description,
                    "parameters": t.inputSchema
                }
            } for t in tools_resp.tools]

            messages = [
                {"role": "system", "content": "你是天气助手,可以查询全球城市天气。"},
                {"role": "user", "content": user_query}
            ]

            for _ in range(4):  # 最多 4 轮
                resp = llm.chat.completions.create(
                    model=MODEL, messages=messages, tools=oa_tools
                )
                msg = resp.choices[0].message
                messages.append(msg.model_dump(exclude_none=True))

                # 没有工具调用,输出最终回答并结束
                if not msg.tool_calls:
                    print(f"天气助手:{msg.content}")
                    return

                # 执行每个工具调用
                for call in msg.tool_calls:
                    fn_name = call.function.name
                    fn_args = json.loads(call.function.arguments or "{}")
                    print(f"[调用] {fn_name}({fn_args})")

                    result = await session.call_tool(fn_name, fn_args)
                    tool_text = "\n".join(
                        c.text for c in result.content if hasattr(c, "text")
                    )
                    messages.append({
                        "role": "tool",
                        "tool_call_id": call.id,
                        "content": tool_text
                    })


if __name__ == "__main__":
    asyncio.run(run_weather_agent("北京和上海今天分别多少度?需要带伞吗?"))

运行效果(示意):

1
2
3
4
[调用] get_weather({'city': 'Beijing'})
[调用] get_weather({'city': 'Shanghai'})
天气助手:北京今天约28°C,晴,湿度40%;上海26°C,小雨,湿度85%。
上海有雨,建议带伞;北京晴朗,无需带伞。

可以看到,LLM自主决定调用两次工具(一次问北京、一次问上海),再把结果综合成自然语言回答。整个过程Server完全不知道用的是哪个模型,模型也不关心Server用什么语言写——这就是MCP带来的解耦价值,也是它"USB-C即插即用"的真正含义。

7. MCP最佳实践

7.1 Server设计原则

  • 工具粒度适中:太细(每个SQL一个工具)会让LLM选择困难;太粗(一个"do_anything"工具)又失去可组合性。建议按"一个完整用户意图"为粒度。
  • 描述写给LLM看:docstring不仅是文档,更是模型决策依据。写清楚"何时用、不用、输入输出含义",比堆砌功能列表更有效。
  • Schema严格:尽量用具体的类型注解(int而非str装数字),必要时配合Literal、枚举约束取值,能显著降低模型传错参数的概率。
  • 幂等优先:读类工具尽量幂等;写类工具应在描述中明确副作用,便于Host做权限审批。

7.2 安全与权限

  • 最小权限:Server只暴露必要能力。文件系统Server应限制在指定根目录,避免暴露/etc/passwd
  • 路径校验:对路径参数做规范化与越权检查,防止../穿越。
  • Host审批:生产环境Host应实现"工具调用确认"机制,敏感操作需用户点击确认后再下发。
  • 远程Server鉴权:基于HTTP的远程MCP应使用Token、mTLS等手段鉴权,避免裸暴露。
  • 审计日志:记录每一次tools/call的入参出参,便于事后追溯。

7.3 错误处理

  • 返回而非抛出:工具失败时返回文本描述错误,让LLM有机会重试或换策略;只在协议层错误(如序列化失败)才抛异常。
  • 结构化错误码:可在文本中嵌入[ERR_TYPE]前缀,便于Host做策略化处理(如配额耗尽自动切换Server)。
  • 超时与重试:网络类工具设置合理超时;Client层可对瞬时错误做有限次重试。

7.4 性能优化

  • 异步IO:Python SDK基于asyncio,工具内部应使用httpxaiosqlite等异步库,避免阻塞事件循环。
  • 批量接口:若工具常被连续调用多次(如批量查天气),可设计get_weather_batch减少往返。
  • 缓存:对慢且少变的资源(如DB Schema)在Server端缓存,减少resources/read延迟。
  • 连接复用:远程MCP Client应复用HTTP连接,避免每次调用都重建TLS。
  • 冷启动:stdio模式下Server是子进程,启动慢会拖慢Host首屏;可在Server启动时预加载模型/连接池。

8. 小结

本章我们从"工具集成碎片化"这一痛点出发,完整走过了MCP协议的理论与实践:

  • MCP是什么:Anthropic提出的开放协议,用Host/Client/Server三段式架构,把工具、资源、提示模板从Agent中解耦。
  • 为什么重要:它让工具像USB-C外设一样即插即用,2026年已有5000+ Server、主流框架原生支持,正在成为AI工具生态的事实标准。
  • 怎么开发:用Python mcp SDK的FastMCP高层API,函数加个@mcp.tool()装饰器、配好类型注解和docstring,就是一个完整工具;Client侧通过stdio连接,几行代码就能把工具接入任意LLM的Function Calling循环。
  • 怎么用对:注意工具粒度、Schema严格性、安全权限与异步性能,才能让MCP真正成为"可生产"的集成层。

一句话总结:MCP把"Agent怎么用工具"这件事从代码层升级到了协议层。掌握MCP,你写的工具就不再属于某一个项目,而属于整个生态。

9. 预告:第11章 评估与可观测性

当Agent接入了越来越多的工具(无论通过Function Calling还是MCP),一个新的问题随之浮现——怎么知道它干得好不好? 一个跑偏的工具调用、一次幻觉的回答、一段静默失败的任务,都可能让整个Agent失去可信度。

第11章我们将聚焦评估与可观测性

  • 评估(Evaluation):如何用基准任务集、LLM-as-a-Judge、人工评分等手段量化Agent能力,避免"看着像对的"幻觉。
  • 可观测性(Observability):如何用Trace、Span、指标体系把Agent的一次完整运行"切片"展现,定位到底是Prompt、工具还是模型的问题。
  • 工具链:介绍LangSmith、Langfuse、Phoenix、OpenTelemetry等在Agent场景的应用。

从"能跑"到"可信",评估与可观测性是Agent走向生产不可绕过的一关。我们下一章见。