文章目录
-
- [1. 引言](#1. 引言)
- [2. 什么是 MCP 协议](#2. 什么是 MCP 协议)
-
- [2.1 一句话理解](#2.1 一句话理解)
- [2.2 核心概念](#2.2 核心概念)
- [2.3 三种关键能力](#2.3 三种关键能力)
- [3. MCP 架构与传输方式](#3. MCP 架构与传输方式)
-
- [3.1 Client-Server 架构](#3.1 Client-Server 架构)
- [3.2 传输方式](#3.2 传输方式)
- [4. 环境准备](#4. 环境准备)
-
- [4.1 创建虚拟环境](#4.1 创建虚拟环境)
- [4.2 安装依赖](#4.2 安装依赖)
- [5. 从零构建第一个 MCP Server](#5. 从零构建第一个 MCP Server)
-
- [5.1 最小示例](#5.1 最小示例)
- [5.2 添加更多工具](#5.2 添加更多工具)
- [6. MCP Client 开发:连接与调用](#6. MCP Client 开发:连接与调用)
- [7. 实战:搭建 AI Agent 工具链](#7. 实战:搭建 AI Agent 工具链)
-
- [7.1 Agent 主循环](#7.1 Agent 主循环)
- [7.2 关键设计点](#7.2 关键设计点)
- [8. 使用 MCP Inspector 调试](#8. 使用 MCP Inspector 调试)
- [9. 进阶:拆分为模块化工具链](#9. 进阶:拆分为模块化工具链)
- [10. 最佳实践与注意事项](#10. 最佳实践与注意事项)
-
- [10.1 工具设计原则](#10.1 工具设计原则)
- [10.2 安全注意事项](#10.2 安全注意事项)
- [10.3 常见陷阱](#10.3 常见陷阱)
- [11. 总结](#11. 总结)
1. 引言
在 AI Agent 快速发展的今天,一个核心痛点越来越突出:大语言模型(LLM)能力再强,也无法直接访问本地文件、数据库、浏览器或企业内部 API。传统做法是为每个工具写一套定制的调用逻辑,导致代码高度耦合、难以复用,每接入一个新模型或新工具都要重写胶水代码。
MCP(Model Context Protocol,模型上下文协议) 正是为解决这一问题而生。它由 Anthropic 在 2024 年底开源,提供了一套标准化的「模型---工具」通信协议。只要工具方按 MCP 规范暴露能力,任何支持 MCP 的 Agent 都能即插即用地调用这些工具,彻底解耦模型与工具。
本文将带你从零开始,理解 MCP 的架构原理,并用 Python 亲手实现一个 MCP Server 与 Client,最终搭建出一条可运行的 AI Agent 工具链。
2. 什么是 MCP 协议
2.1 一句话理解
MCP 之于 AI Agent,类似于 LSP(Language Server Protocol)之于代码编辑器:编辑器通过 LSP 获得任意语言的提示能力,Agent 通过 MCP 获得任意工具的执行能力。一次开发,处处可用。
2.2 核心概念
MCP 定义了三个核心角色:
| 角色 | 职责 | 典型示例 |
|---|---|---|
| Host | 承载 Agent 的宿主应用 | Claude Desktop、自研 Chat 应用 |
| Client | 协议客户端,负责与 Server 建立连接 | Agent 内部的连接器 |
| Server | 工具提供方,暴露「能力」 | 文件系统服务、数据库服务、Git 服务 |
一次典型的调用流程:
#mermaid-svg-X391LRoBNr4EJt87{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-X391LRoBNr4EJt87 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-X391LRoBNr4EJt87 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-X391LRoBNr4EJt87 .error-icon{fill:#552222;}#mermaid-svg-X391LRoBNr4EJt87 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-X391LRoBNr4EJt87 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-X391LRoBNr4EJt87 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-X391LRoBNr4EJt87 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-X391LRoBNr4EJt87 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-X391LRoBNr4EJt87 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-X391LRoBNr4EJt87 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-X391LRoBNr4EJt87 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-X391LRoBNr4EJt87 .marker.cross{stroke:#333333;}#mermaid-svg-X391LRoBNr4EJt87 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-X391LRoBNr4EJt87 p{margin:0;}#mermaid-svg-X391LRoBNr4EJt87 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-X391LRoBNr4EJt87 .cluster-label text{fill:#333;}#mermaid-svg-X391LRoBNr4EJt87 .cluster-label span{color:#333;}#mermaid-svg-X391LRoBNr4EJt87 .cluster-label span p{background-color:transparent;}#mermaid-svg-X391LRoBNr4EJt87 .label text,#mermaid-svg-X391LRoBNr4EJt87 span{fill:#333;color:#333;}#mermaid-svg-X391LRoBNr4EJt87 .node rect,#mermaid-svg-X391LRoBNr4EJt87 .node circle,#mermaid-svg-X391LRoBNr4EJt87 .node ellipse,#mermaid-svg-X391LRoBNr4EJt87 .node polygon,#mermaid-svg-X391LRoBNr4EJt87 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-X391LRoBNr4EJt87 .rough-node .label text,#mermaid-svg-X391LRoBNr4EJt87 .node .label text,#mermaid-svg-X391LRoBNr4EJt87 .image-shape .label,#mermaid-svg-X391LRoBNr4EJt87 .icon-shape .label{text-anchor:middle;}#mermaid-svg-X391LRoBNr4EJt87 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-X391LRoBNr4EJt87 .rough-node .label,#mermaid-svg-X391LRoBNr4EJt87 .node .label,#mermaid-svg-X391LRoBNr4EJt87 .image-shape .label,#mermaid-svg-X391LRoBNr4EJt87 .icon-shape .label{text-align:center;}#mermaid-svg-X391LRoBNr4EJt87 .node.clickable{cursor:pointer;}#mermaid-svg-X391LRoBNr4EJt87 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-X391LRoBNr4EJt87 .arrowheadPath{fill:#333333;}#mermaid-svg-X391LRoBNr4EJt87 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-X391LRoBNr4EJt87 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-X391LRoBNr4EJt87 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-X391LRoBNr4EJt87 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-X391LRoBNr4EJt87 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-X391LRoBNr4EJt87 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-X391LRoBNr4EJt87 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-X391LRoBNr4EJt87 .cluster text{fill:#333;}#mermaid-svg-X391LRoBNr4EJt87 .cluster span{color:#333;}#mermaid-svg-X391LRoBNr4EJt87 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-X391LRoBNr4EJt87 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-X391LRoBNr4EJt87 rect.text{fill:none;stroke-width:0;}#mermaid-svg-X391LRoBNr4EJt87 .icon-shape,#mermaid-svg-X391LRoBNr4EJt87 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-X391LRoBNr4EJt87 .icon-shape p,#mermaid-svg-X391LRoBNr4EJt87 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-X391LRoBNr4EJt87 .icon-shape .label rect,#mermaid-svg-X391LRoBNr4EJt87 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-X391LRoBNr4EJt87 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-X391LRoBNr4EJt87 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-X391LRoBNr4EJt87 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} AI Agent
(Host)
MCP Client
MCP Server
文件系统 / 数据库 / API
2.3 三种关键能力
MCP Server 可以对外提供三类能力:
- Tools(工具):可被模型调用的具体操作,比如「查询用户表」「读取文件」。模型根据工具的 schema 决定何时调用、传什么参数。
- Resources(资源):可被读取的数据,比如「数据库表结构」。资源与工具的区别在于:工具是「做动作」,资源是「读数据」。
- Prompts(提示模板):预设好的提示词片段,便于复用。
其中 Tools 是 Agent 工具链中最常用、最先要掌握的部分,本文将重点围绕它展开。
3. MCP 架构与传输方式
3.1 Client-Server 架构
MCP 采用严格的 C/S 架构,Client 与 Server 之间通过 JSON-RPC 2.0 交换消息。一次工具调用在协议层表现为几个标准方法:
tools/list:Client 向 Server 请求可用工具列表及参数 schema;tools/call:Client 发起一次工具调用,携带参数;initialize:建立连接时的握手;notifications:取消、进度通知等。
3.2 传输方式
MCP 官方支持两种传输:
- stdio(标准输入输出):最常见、最适合本地工具。Server 作为子进程启动,Client 通过 stdin/stdout 与其通信。
- HTTP + SSE(Server-Sent Events):适合远程服务,Server 以 HTTP 服务形式运行,Client 通过网络连接。
本地开发优先推荐 stdio,部署简单、调试方便;需要多用户共享或跨机器调用时再切换到 HTTP 模式。
4. 环境准备
本文使用 Python 3.10+ 作为开发语言。MCP 官方提供了 Python SDK,README 后续示例均基于 Python 3.10 验证。
4.1 创建虚拟环境
bash
mkdir mcp-demo && cd mcp-demo
python3 -m venv .venv
source .venv/bin/activate # Windows 使用 .venv\Scripts\activate
4.2 安装依赖
bash
pip install "mcp[cli]"
pip install httpx python-dotenv
安装完成后验证版本:
bash
mcp version
输出 MCP 的 CLI 版本号即表示安装成功。mcp[cli] 同时提供了 mcp dev、mcp run 等调试命令,后续会用到。
5. 从零构建第一个 MCP Server
5.1 最小示例
新建 server.py:
python
from mcp.server.fastmcp import FastMCP
# 创建一个 MCP Server 实例
mcp = FastMCP("Demo Server")
@mcp.tool()
def add(a: int, b: int) -> int:
"""计算两个整数之和"""
return a + b
if __name__ == "__main__":
mcp.run()
这里有几个关键点:
FastMCP是官方提供的高层封装,可以大幅减少样板代码;@mcp.tool()装饰器把一个普通 Python 函数注册为 MCP Tool;- 函数的 docstring 会自动成为工具的 description,类型注解则自动生成参数 schema,供模型理解何时调用、如何传参。
运行 Server:
bash
python server.py
提示:stdout 是 MCP 协议的通信通道,不要在 Server 代码里用
5.2 添加更多工具
下面扩展一个简单的「待办事项」服务,让 Server 具备真正的业务能力:
python
from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field
mcp = FastMCP("Todo Server")
_todos: dict[int, str] = {}
_next_id = 1
class TodoResult(BaseModel):
"""待办操作结果"""
id: int = Field(description="待办 ID")
content: str = Field(description="待办内容")
@mcp.tool()
def add_todo(content: str) -> TodoResult:
"""添加一条待办事项
Args:
content: 待办内容,比如「下午 3 点开会」
"""
global _next_id
todo_id = _next_id
_next_id += 1
_todos[todo_id] = content
return TodoResult(id=todo_id, content=content)
@mcp.tool()
def list_todos() -> list[TodoResult]:
"""列出所有待办事项"""
return [TodoResult(id=k, content=v) for k, v in _todos.items()]
@mcp.tool()
def remove_todo(todo_id: int) -> str:
"""删除指定待办事项
Args:
todo_id: 要删除的待办 ID
"""
if todo_id not in _todos:
return f"待办 {todo_id} 不存在"
content = _todos.pop(todo_id)
return f"已删除待办:{content}"
当函数返回 Pydantic 模型时,MCP 会自动将其序列化为结构化输出,并附带字段说明,方便模型理解返回内容。
6. MCP Client 开发:连接与调用
Server 就绪后,我们编写 Client 来连接它。编写 client.py:
python
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
server_params = StdioServerParameters(
command="python",
args=["server.py"],
)
async with stdio_client(server_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])
# 调用 add_todo 工具
result = await session.call_tool(
"add_todo",
arguments={"content": "阅读 MCP 官方文档"},
)
print("添加结果:", result.content)
# 调用 list_todos 工具
result = await session.call_tool("list_todos", arguments={})
print("待办列表:", result.content)
asyncio.run(main())
运行结果示例:
text
可用工具: ['add_todo', 'list_todos', 'remove_todo']
添加结果: [TextContent(text='{"id": 1, "content": "阅读 MCP 官方文档"}', type='text')]
待办列表: [TextContent(text='[{"id": 1, "content": "阅读 MCP 官方文档"}]', type='text')]
注意 call_tool 返回的 content 是一个列表,其中每个元素是 TextContent 对象,实际文本在 .text 属性里。实践中通常将其拼接后交给大模型继续处理。
7. 实战:搭建 AI Agent 工具链
有了 Server 与 Client,最后一步是把它们接入大模型,形成一个完整的 Agent 闭环:模型理解用户意图 → 决定调用工具 → 执行工具 → 根据结果生成回答。
7.1 Agent 主循环
我们使用 OpenAI 兼容接口作为模型层(可根据需要替换为其他兼容服务)。先安装依赖:
bash
pip install openai
编写 agent.py:
python
import json
import asyncio
from openai import AsyncOpenAI
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
SYSTEM_PROMPT = """你是一个待办管理助手。
当用户需要添加、查询或删除待办时,调用对应工具完成操作。
不要在未经调用工具的情况下编造待办数据。"""
async def run_agent(user_input: str):
# 1. 启动 MCP 连接
server_params = StdioServerParameters(command="python", args=["server.py"])
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# 2. 把 MCP 工具转换成 OpenAI function 格式
mcp_tools = await session.list_tools()
functions = [
{
"type": "function",
"function": {
"name": t.name,
"description": t.description or "",
"parameters": t.inputSchema,
},
}
for t in mcp_tools.tools
]
# 创建 DeepSeek 兼容客户端(可按需更换)。
client = AsyncOpenAI(
api_key="your-api-key",
base_url="https://api.deepseek.com",
)
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_input},
]
# 3. 模型决策:是否需要调用工具
response = await client.chat.completions.create(
model="deepseek-chat",
messages=messages,
tools=functions,
)
msg = response.choices[0].message
# 4. 如果模型请求调用工具,则执行并回填结果
while msg.tool_calls:
messages.append(msg) # 把 assistant 的工具调用请求加入上下文
for tool_call in msg.tool_calls:
tool_name = tool_call.function.name
tool_args = json.loads(tool_call.function.arguments)
result = await session.call_tool(tool_name, arguments=tool_args)
result_text = result.content[0].text if result.content else ""
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result_text,
})
print(f"[工具执行] {tool_name}({tool_args}) -> {result_text}")
# 带着工具结果再次请求模型
response = await client.chat.completions.create(
model="deepseek-chat",
messages=messages,
tools=functions,
)
msg = response.choices[0].message
# 5. 输出最终回答
print(f"[Agent 回答] {msg.content}")
if __name__ == "__main__":
asyncio.run(run_agent("帮我添加一条待办:明天上午 10 点参加评审"))
运行后可以看到完整链路:
text
[工具执行] add_todo({'content': '明天上午 10 点参加评审'}) -> {"id": 3, "content": "明天上午 10 点参加评审"}
[Agent 回答] 已为你添加待办:明天上午 10 点参加评审。
7.2 关键设计点
- 工具描述质量决定调用准确率:模型依靠 description 和参数 schema 判断是否调用工具,描述应包含「何时用、怎么用、参数含义、返回什么」。
- 工具执行结果必须回填 :很多 Agent 失败的根因是遗漏了
role: tool的回填步骤,导致模型收不到工具结果。 - 循环上限 :生产环境应给
while循环加上最大迭代次数(如 5 次),防止模型反复调用工具造成死循环或高额成本。 - 错误处理:工具内部应捕获异常并返回可读的错误信息,而不是把堆栈抛给模型。
8. 使用 MCP Inspector 调试
MCP 官方提供了图形化调试工具 MCP Inspector,通过 mcp CLI 启动:
bash
mcp dev server.py
启动后终端会显示一个本地调试地址,浏览器打开后即可:
- 查看 Server 暴露的所有 Tools / Resources / Prompts;
- 手动填写参数并调用工具,实时查看请求与响应;
- 不需要编写 Client 代码即可验证 Server 逻辑。
这是开发阶段排查「工具调用失败」「返回格式异常」问题最有效的手段。
9. 进阶:拆分为模块化工具链
当工具越来越多时,建议采用模块化组织:
text
mcp-agent/
├── agent.py # Agent 主循环
├── client.py # MCP Client 封装
└── servers/ # 多个 MCP Server
├── todo/
│ └── server.py
├── filesystem/
│ └── server.py
└── database/
└── server.py
每个 Server 独立实现、独立运行,Agent 侧同时连接多个 Server 并汇总所有工具。这样的好处是:
- 工具按职责拆分,单个 Server 保持轻量;
- 不同 Server 可由不同团队维护,互不影响;
- Agent 可以按需启用或禁用某个工具集。
同时连接多个 Server 的典型写法:
python
async def connect_servers():
servers = [
StdioServerParameters(command="python", args=["servers/todo/server.py"]),
StdioServerParameters(command="python", args=["servers/filesystem/server.py"]),
]
sessions = []
for params in servers:
read, write = await stdio_client(params).__aenter__()
session = await ClientSession(read, write).__aenter__()
await session.initialize()
sessions.append(session)
return sessions
10. 最佳实践与注意事项
10.1 工具设计原则
- 单一职责:一个工具只做一件事,参数清晰、返回明确;
- 描述先行:把 description 当作给模型的「使用说明书」来写;
- 幂等优先:能做成幂等的操作尽量幂等,方便模型重试;
- 返回结构化数据:优先使用 Pydantic 模型定义返回类型,方便后续程序化处理。
10.2 安全注意事项
- 输入校验:对工具参数做严格校验,防止模型幻觉参数导致越权或异常;
- 权限隔离:敏感操作(删除、写入、执行命令)需要额外确认或白名单机制;
- 不暴露密钥:工具内部使用的 API key 等敏感信息要放在环境变量中,不写入 schema 和返回结果;
- 协议日志外移:stdio 模式下不要向 stdout 写日志。
10.3 常见陷阱
- 在 stdio Server 中使用
print会破坏协议通信,导致 Client 报「协议解析失败」; - Windows 下 stdio 子进程的编码问题可能导致中文乱码,建议显式设置 UTF-8;
call_tool返回的是CallToolResult,需要从.content列表里取文本,直接打印对象会得到不可读的内容;- 模型可能返回不完整的 JSON 参数,需要在解析
tool_call.function.arguments时做异常兜底。
11. 总结
本文从 MCP 的概念出发,完整走通了「Server 开发 → Client 连接 → Agent 集成 → 调试调试 → 模块化拆分」的全过程。通过 MCP,工具与模型彻底解耦,你的 AI Agent 可以像搭积木一样组合任意工具能力,而不必为每次集成重写胶水代码。
下一步可以尝试:
- 接入官方提供的 filesystem、git、sqlite 等参考 Server;
- 将自研 Server 部署为远程 HTTP 服务,支持多客户端共享;
- 在 Agent 中加入多轮工具调用与审批流,构建更复杂的业务 Agent。
掌握 MCP,就等于掌握了 AI Agent 工具链的通用语言。现在就动手,把你手头的第一个内部工具接入 MCP 吧。