你有没有想过一个问题------为什么每次换一个 AI 模型,所有工具集成都要重写一遍?
一、先讲个故事
假设你是一个 AI 应用开发者。
第一个需求很简单:让 AI 能查天气。你调了一下 OpenAI 的 function calling,传了个 JSON schema,搞定了。Nice。
第二个需求:让 AI 能查 GitHub repo。你又写了一套 schema。还行。
第三个需求:让 AI 能读数据库、发 Slack 消息、操作 Jira、搜公司知识库......
现在你看着代码陷入了沉思:每接一个「工具」,就要写一遍 JSON schema、配一次认证、处理一遍不同厂商的格式差异。而且如果哪天想从 GPT 切到 Claude,好家伙------所有工具定义又得改一遍。
这就是 AI 集成的「NxM 问题」: N 个 AI 模型 × M 个工具,每个组合都要写一套集成代码。
有没有一种标准,写一次工具,任何 AI 都能用?
有。它叫 MCP------Model Context Protocol。
二、MCP 是什么?一句话讲明白
MCP(Model Context Protocol) 是 Anthropic 在 2024 年 11 月开源的一个开放协议,专门用来标准化 AI 模型和外部工具/数据源之间的通信。
你可以把它想象成 AI 世界的 USB-C 接口:
- 以前:每个设备(AI 模型)都有自己的充电线(工具集成方式),换个设备线就不能用。
- 现在:有了统一的标准接口,一个 MCP 服务器插到任何 MCP 客户端上都能用。
这个思路其实有前辈------LSP(Language Server Protocol)。LSP 标准化了编辑器(VS Code、Neovim、JetBrains)和语言服务器(Python、TypeScript、Rust)之间的通信协议。MCP 做的事情几乎是镜像级的:把「编程语言」换成「AI 工具」,把「编辑器」换成「AI 应用」。
三、MCP 到底解决了什么核心问题?
3.1 集成爆炸问题
没有 MCP 的时候,每个 AI 应用都要为每个工具写自定义集成代码:
css
AI 应用 A ──→ 自定义代码 ──→ 天气 API
AI 应用 A ──→ 自定义代码 ──→ GitHub API
AI 应用 A ──→ 自定义代码 ──→ Slack API
AI 应用 B ──→ 自定义代码 ──→ 天气 API
AI 应用 B ──→ 自定义代码 ──→ GitHub API
...
有了 MCP:
css
AI 应用 A ──→ MCP 客户端 ──→ MCP 协议 ──→ 天气 MCP 服务器
AI 应用 B ──→ MCP 客户端 ──→ MCP 协议 ──→ 天气 MCP 服务器 (同一个!)
GitHub MCP 服务器
Slack MCP 服务器
工具开发者只需要写一个 MCP 服务器,所有兼容 MCP 的客户端都能用。 这就是「写一次,到处运行」。
3.2 安全隔离问题
在传统 function calling 里,API 密钥通常跟应用代码放在一起。如果应用被攻破,所有凭证都暴露了。
MCP 把每个工具的凭证隔离在各自的服务器里,客户端从来不直接访问后端系统。自然地实现了 最小权限原则。
3.3 跨模型可移植性
Function calling 的 schema 是各家自定的------OpenAI 的格式和 Anthropic 的格式就不一样。但 MCP 是标准协议,任何模型只要实现了 MCP 客户端就能互通。
四、核心架构:四层结构
MCP 的架构分成四个角色:
scss
┌─────────────────────────────────────┐
│ MCP Host │ ← AI 应用(Claude Desktop、VS Code、你的应用)
│ ┌───────────────────────────────┐ │
│ │ MCP Client │ │ ← 维护到服务器的连接
│ └──────────┬────────────────────┘ │
└─────────────┼────────────────────────┘
│ JSON-RPC 2.0
│ (stdio / Streamable HTTP)
▼
┌─────────────────────────────────────┐
│ MCP Server │ ← 提供工具/资源/提示词
│ ┌──────────┬──────────┬─────────┐ │
│ │ Tools │ Resources │ Prompts │ │
│ │ (能做啥) │ (能读啥) │ (怎么说) │ │
│ └──────────┴──────────┴─────────┘ │
└─────────────────────────────────────┘
各角色说明
| 角色 | 谁扮演 | 干什么 |
|---|---|---|
| Host | 你的 AI 应用(Claude Desktop、VS Code、你的 Python 程序) | 启动和管理多个 MCP 客户端 |
| Client | 应用内部组件 | 跟一个 MCP 服务器建立连接、协商能力、收发消息 |
| Server | 工具/数据提供方 | 暴露工具、资源和提示词,处理请求 |
| Transport | 底层通信方式 | 主要负责把 JSON-RPC 消息从 A 传到 B |
传输层的两种方式
1️⃣ stdio 传输(本地)
MCP 服务器作为子进程启动,通过标准输入输出(stdin/stdout)通信。这是最常用、性能最好的方式。
c
MCP 客户端 MCP 服务器(子进程)
│ │
│ ── 写 JSON-RPC 到 stdin ──→ │ (服务器读 stdin)
│ │
│ ←─ 读 JSON-RPC 从 stdout ── │ (服务器写 stdout)
│ │
│ stderr → 客户端日志 │ (日志通道,被客户端忽略或收集)
它是怎么实现的?
其实就是**子进程 + 管道(pipe)**的经典操作系统模式。服务端和客户端两端的代码都非常简单:
服务端:
python
# FastMCP 内部做的事情:
# 1. 从 sys.stdin 读取 JSON-RPC 消息
# 2. 处理请求(调用注册的 tool/resource/prompt)
# 3. 把结果写回 sys.stdout
mcp.run(transport='stdio')
客户端:
python
# MCP SDK 内部做的事情:
# 1. subprocess.Popen(["python", "server.py"],
# stdin=PIPE, stdout=PIPE, stderr=PIPE)
# 2. 把 JSON-RPC 消息写进子进程的 stdin
# 3. 从子进程的 stdout 读取返回的 JSON-RPC 响应
server_params = StdioServerParameters(
command="python",
args=["server.py"],
)
其实它借鉴了 LSP(Language Server Protocol) 的做法------语言服务器和编辑器之间也用 stdio 通信。
为什么用 stdio 而不是直接 HTTP?
| 优势 | 说明 |
|---|---|
| ⚡ 零网络开销 | 不走 HTTP、不走 TCP,直接读写管道,性能最佳 |
| 🔌 不需要端口 | 不存在端口冲突问题,也不怕防火墙 |
| 🔄 自动生命周期 | 客户端退出 → 子进程自动终止,不用手动清理 |
| 🔒 安全性好 | 子进程只跟父进程通信,不暴露网络端口 |
| 🌍 跨平台 | Linux、macOS、Windows 都有 stdin/stdout |
用一句话来类比:stdio 就像把 U 盘直接插在电脑上(本地直连),而 HTTP 就像通过网络访问远程打印机。
2️⃣ Streamable HTTP 传输(远程)
取代了早期版本的 SSE 传输。服务器独立运行,可服务多个客户端。支持 POST + 可选的 Server-Sent Events 流式响应。
bash
POST /mcp → 发送请求
GET /mcp → 打开 SSE 流,收听服务器推送的消息
什么时候用什么?
| 场景 | 推荐传输 |
|---|---|
| 本地开发、桌面应用(如 Claude Desktop) | ✅ stdio |
| 远程服务器、多客户端共享 | ✅ Streamable HTTP |
| 需要不同机器访问 | ✅ HTTP |
| 追求最低延迟 | ✅ stdio |
五、三大原语:Tools vs Resources vs Prompts
这是 MCP 最核心也最容易混淆的概念。我用一句话先给你定调:
Tools = 能做啥(动作),Resources = 能看啥(数据),Prompts = 怎么说(指令)
5.1 Tools(工具)------ AI 的「手」
Tools 是 AI 模型可以调用的函数/服务,用来执行真实世界的操作。
类比:公司里的执行岗------送外卖的、写代码的、付钱的。你告诉它们做什么,它们就去干。
特点:
- 有副作用(调用 API、改数据库、发邮件)
- AI 主动调用(模型觉得该用它的时候自己选)
- 定义包含:名称、描述、输入 schema(JSON Schema)
实际通信流程(JSON-RPC):
客户端请求列出工具:
json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}
服务器返回:
json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "get_weather",
"description": "获取某个地点的当前天气",
"inputSchema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市名或邮编"
}
},
"required": ["location"]
}
}
]
}
}
客户端调用工具:
json
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": { "location": "Beijing" }
}
}
服务器返回结果(支持多种内容类型:文本、图片、音频、结构化数据):
json
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{ "type": "text", "text": "北京当前天气:22°C,晴朗" }
],
"isError": false
}
}
5.2 Resources(资源)------ AI 的「眼睛」
Resources 是只读的数据源,AI 模型可以读取它们来获取上下文。
类比:公司里的图书馆------你可以查资料,但不能在上面乱写乱画。
特点:
- 只读(没有副作用)
- 由 URI 唯一标识(
file://、https://、git://或自定义 scheme) - 支持订阅更新(服务器通知客户端「这个资源变了哦」)
- 支持模板化 URI(比如
file:///logs/{date}.txt)
常见场景:
- 读取文件内容
- 查询数据库 schema
- 获取 API 响应数据
- 提供项目文档
资源模板示例:
json
{
"uri": "file:///logs/{date}.txt",
"name": "每日日志",
"mimeType": "text/plain"
}
然后客户端可以用 resources/read 传入参数化的 URI 来读取特定日期的文件。
5.3 Prompts(提示词)------ AI 的「脑子怎么转」
Prompts 是预定义的模板,告诉 AI 应该以什么方式处理某个任务。
类比:给新员工的 SOP(标准操作流程)------「如果客户投诉,先道歉,再查订单,然后提出补偿方案」。
特点:
- 由用户主动触发(通常是 UI 里的斜杠命令之类的)
- 可以带参数(比如
{code}、{language}、{ticket_id}) - 返回的是一组对话消息(可以是 user 角色,也可以是 assistant 角色)
示例:一个代码审查 prompt
json
{
"name": "code_review",
"description": "让 AI 分析代码质量并给出改进建议",
"arguments": [
{ "name": "code", "description": "要审查的代码", "required": true }
]
}
获取时的结果:
json
{
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "请审查以下 Python 代码,指出潜在问题并给出改进建议:\ndef hello():\n print('world')"
}
}
]
}
可视化对比
| 维度 | Tools | Resources | Prompts |
|---|---|---|---|
| 本质 | 动作(Action) | 数据(Data) | 指令(Instruction) |
| 谁触发 | AI 模型自动选择 | AI 模型读取 | 用户手动触发 |
| 有无副作用 | ✅ 有 | ❌ 无(只读) | ❌ 无 |
| 内容类型 | JSON Schema 定义参数 | URI + MIME Type | 消息模板 |
| 调用方式 | tools/call |
resources/read |
prompts/get |
六、MCP vs Function Calling:对比与取舍
这是很多人会问的问题:Function Calling 也能让 AI 调用工具啊,为什么还要 MCP?
我的看法是:它们不是替代关系,而是不同层次的解决方案。
| 维度 | Function Calling | MCP |
|---|---|---|
| 诞生背景 | LLM 的参数(JSON schema 嵌入请求) | 系统级的通信协议 |
| 耦合度 | 紧耦合------工具定义在 LLM 请求里 | 松耦合------独立的客户端-服务器架构 |
| 可复用性 | 低------换模型就得重写 | 高------一个服务器,任何 MCP 客户端都能用 |
| 安全隔离 | 凭证和应用在一起 | 凭证在各自服务器,天然隔离 |
| 协议标准 | 各家定义不一样 | 统一标准 |
| 适用场景 | 简单、少量工具(2-3 个),快速原型 | 复杂集成、多工具、企业级 |
简单来说:
- 如果你只是做个 demo,给 AI 加两个小功能------Function Calling 就够用,几分钟的事。
- 如果你在构建一个 AI agent 系统,需要接入多个工具/数据源,考虑可维护性和安全性------MCP 是更正确的选择。
甚至它们可以共存:Function Calling 负责「把自然语言变成函数调用指令」,MCP 负责「执行这个指令」。 这俩完全可以搭着用。
七、生命周期:一次 MCP 连接从开始到结束
整个流程分成三个阶段:
阶段 1:初始化(能力协商)
scss
客户端 服务器
│ │
├── initialize(protocolVersion, │
│ capabilities, clientInfo) ──────→ │
│ │
│ ←── InitializeResult(protocolVersion, │
│ capabilities, serverInfo) ──────┤
│ │
├── notifications/initialized ────────→ │
│ │
双方各自声明自己支持什么功能:服务器说「我支持 tools 和 resources」,客户端说「我可以 sampling 和 elicitation」。然后只使用双方都支持的子集。
阶段 2:正常运行(工具调用、资源读取、提示获取)
双向 JSON-RPC 通信。客户端可以发请求,服务器也可以主动发通知(比如「工具列表变了哦」)。
阶段 3:终止
任意一方断开连接。
八、实战:用 Python 从零写一个 MCP Server + Client
理论说了这么多,来动手写个真实的东西吧!
我们要做什么?
一个能搜索网页的 MCP 服务器 + 一个能连上它并用 OpenAI 模型问问题的客户端。
8.1 MCP Server:搜索工具
python
# server.py
from mcp.server.fastmcp import FastMCP
from googlesearch import search as google_search
# 创建 MCP 服务器
mcp = FastMCP("web-search-server")
@mcp.tool()
def search_web(query: str) -> str:
"""
搜索网络并返回结果摘要。
Args:
query: 搜索关键词
"""
results = google_search(query, num_results=5, lang="zh", advanced=True)
snippets = []
for r in results:
snippets.append(f"- {r.title}\n {r.url}\n {r.description}")
return "\n\n".join(snippets) if snippets else "没有找到结果。"
@mcp.resource("config://search-limits")
def get_search_limits() -> str:
"""返回搜索配置信息"""
return "每次搜索最多返回 5 条结果,默认语言为中文"
@mcp.prompt()
def search_assistant() -> str:
"""搜索助手提示词模板"""
return """你是一个搜索助手。当用户想了解某个话题时,请使用 search_web 工具
获取最新信息,然后基于搜索结果回答用户的问题。
注意在回答中引用来源。"""
if __name__ == "__main__":
mcp.run(transport='stdio')
这个不到 30 行的代码就完成了一个完整的 MCP 服务器!它暴露了:
- 1 个 Tool :
search_web------AI 可以自动调用它来搜索 - 1 个 Resource :
config://search-limits------提供配置信息 - 1 个 Prompt :
search_assistant------用户手动使用的提示词模板
用
FastMCP写 server 是真的爽------装饰器一挂,自动帮你处理 JSON-RPC 协议、schema 生成、类型推导。
8.2 测试服务器
bash
# 安装依赖
pip install "mcp[cli]" googlesearch-python
# 用 MCP Inspector 测试(图形界面)
mcp dev server.py
它会打开一个浏览器页面,可以可视化地查看 tools/resources/prompts 并交互测试。
8.3 MCP Client:连上服务器,让 AI 用工具
python
# client.py
import asyncio
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from openai import OpenAI
class MCPClient:
def __init__(self):
self.session = None
self.exit_stack = AsyncExitStack()
self.openai = OpenAI()
async def connect_to_server(self, server_script: str):
"""连接 MCP 服务器"""
server_params = StdioServerParameters(
command="python",
args=[server_script],
)
stdio_transport = await self.exit_stack.enter_async_context(
stdio_client(server_params)
)
stdio, write = stdio_transport
self.session = await self.exit_stack.enter_async_context(
ClientSession(stdio, write)
)
await self.session.initialize()
# 列出所有可用工具
response = await self.session.list_tools()
print("已连接服务器,可用工具:", [t.name for t in response.tools])
async def process_query(self, query: str) -> str:
"""处理用户查询:调用 LLM + 自动使用工具"""
messages = [{"role": "user", "content": query}]
# 获取所有工具定义(自动转换成 OpenAI 兼容的格式)
tools_response = await self.session.list_tools()
tools = []
for tool in tools_response.tools:
tools.append({
"type": "function",
"function": {
"name": tool.name,
"description": tool.description or "",
"parameters": tool.inputSchema,
}
})
# 循环:LLM 可能多次调用工具
while True:
response = self.openai.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools,
)
msg = response.choices[0].message
if not msg.tool_calls:
# LLM 不再调用工具,返回最终回答
return msg.content
# 处理工具调用
messages.append(msg)
for tc in msg.tool_calls:
tool_name = tc.function.name
tool_args = tc.function.arguments
# 通过 MCP 调用工具
result = await self.session.call_tool(tool_name, tool_args)
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": str(result.content[0].text),
})
async def cleanup(self):
await self.exit_stack.aclose()
async def main():
client = MCPClient()
try:
await client.connect_to_server("server.py")
result = await client.process_query("最近 AI agent 领域有什么新进展?")
print(f"\n回答:{result}")
finally:
await client.cleanup()
if __name__ == "__main__":
asyncio.run(main())
这段代码的核心逻辑很清晰:
- 连接 MCP 服务器
- 通过
tools/list获取所有工具定义 - 转换成 OpenAI 能理解的格式
- 让 LLM 决定要不要调用工具
- 如果要调用,通过
tools/call在 MCP 那边执行 - 把结果喂回 LLM,让它生成最终回答
这就是 MCP 的核心工作流------通过标准协议解耦工具定义和模型调用。
九、安全:MCP 是怎么考虑安全的?
MCP 规范里面明确列出了几个安全原则。不过我想先从一个具体的对比场景讲起,这样更容易理解它的设计意图。
传统 Function Calling 的安全问题
假设你的 AI 助手需要调用三个工具:
python
# 你的应用代码 ------ 所有密钥都在一起!
DB_PASSWORD = "s3cr3t!"
SLACK_TOKEN = "xoxb-xxx"
GITHUB_TOKEN = "ghp_xxx"
问题在哪?三个 API 密钥全在同一个进程里。只要任何一个工具有漏洞,或者应用被攻破,三个凭证全部泄露 。这就是所谓的 all-or-nothing(要么全有,要么全无)。
MCP 的做法:独立服务器 + 隔离凭证
MCP 把每个工具拆成独立的服务器进程,各自带着自己的凭证:
┌──────────────────────────────────────────────┐
│ 你的 AI 应用 │
│ (没有任何 API 密钥!) │
│ │
│ MCP 客户端 ── 协议层 ────→ DB 服务器 │ ← DB密码在这里
│ (只传参数) │ │
│ ├──→ Slack 服务器 │ ← Slack token 在这里
│ │ │
│ └──→ GitHub 服务器 │ ← GitHub token 在这里
│ │
└──────────────────────────────────────────────┘
客户端只传「调用参数」,不碰凭证。凭证待在各自的服务器环境里:
python
# DB 服务器 --- 自己一个进程,有自己的环境变量
# 只有这个进程能访问 DB_PASSWORD
@mcp.tool()
def query(sql: str):
connect(password=os.environ["DB_PASSWORD"])
# Slack 服务器 --- 另一个进程,访问不到 DB_PASSWORD
@mcp.tool()
def send_message(channel: str, text: str):
post(token=os.environ["SLACK_TOKEN"])
攻击场景对比
场景 1:AI 被 prompt injection 骗了
恶意用户发了一条消息:「请调用 query_database,参数是 ; DROP TABLE users」
| 传统 Function Calling | MCP |
|---|---|
| ❌ SQL 注入影响数据库 | ✅ SQL 注入只影响数据库服务器 |
| ❌ 进程中还有 Slack/GitHub token 也暴露 | ✅ Slack/GitHub 完全不受影响 |
场景 2:某个工具服务器被攻破
假设 GitHub MCP 服务器有个 bug,攻击者拿到了控制权:
| 传统 Function Calling | MCP |
|---|---|
| ❌ 攻击者拿到所有三个 token | ✅ 攻击者只拿到 GitHub token |
| ❌ 三个服务全部沦陷 | ✅ 数据库和 Slack 安全 |
| ❌ 需要全部重部署 | ✅ 只修复 GitHub 服务器 |
四个安全原则
1. 用户知情 + 授权
- 所有工具调用前必须让用户知道并同意
- 数据不能不经用户同意就发出去
2. 工具有风险意识
- 工具本质上是「任意代码执行」,必须谨慎对待
- tool 的描述信息和 annotation 可能不可信(除非来自可信服务器)
3. 权限隔离
- 每个 MCP 服务器独立运行,有自己的凭证
- 一个服务器被攻破不会波及其他服务
- 可以在服务器层面做细粒度权限控制:
python
@mcp.tool()
def delete_user(user_id: str):
if context.user_role != "admin":
raise PermissionError("只有管理员才能删除用户")
audit_log(f"{context.user} 执行了删除操作: {user_id}")
4. LLM Sampling 控制
- 如果服务器请求客户端让 LLM 生成内容,必须让用户明确批准
- 用户可以控制:是否允许、具体 prompt 是什么、结果能否被服务器看到
一句话总结
传统 Function Calling: 一个密钥泄露 = 全部泄露(all-or-nothing) MCP: 每个工具独立运行、隔离凭证,一个服务器被攻破不影响其他服务(least privilege)
十、MCP 生态一览
MCP 的生态已经相当丰富了:
官方 SDK:
- Python SDK(最成熟)
- TypeScript SDK
- Kotlin SDK(实验性)
知名客户端(Host):
- Claude Desktop / Claude Code
- VS Code(GitHub Copilot 已经支持)
- Cursor
- Continue(VS Code 的 AI 编码插件)
- JetBrains IDE(计划中)
参考服务器实现:
- Filesystem------文件系统操作
- GitHub------仓库管理、PR、Issue
- Git------版本控制操作
- PostgreSQL / SQLite------数据库查询
- Puppeteer / Playwright------浏览器自动化
- Brave Search------网络搜索
- Slack------消息和频道管理
- Cloudflare------Workers 和 KV 存储
- Docker------容器管理
十一、总结与思考
写到这里,我想说一些自己的感受。
MCP 最打动我的不是它「能做很多事情」,而是它的设计哲学:
- 避免重新发明轮子------借鉴了 LSP 的成功经验,证明这种「协议级标准化」在 AI 领域同样适用
- 松耦合------服务器和客户端通过协议而非代码耦合,各自独立演进
- 渐进式采用------可以从一个简单的工具开始,逐步扩展成完整的工具网络
- 面向生态------不绑定任何模型或厂商,任何人都可以参与
当然,MCP 也不是银弹:
- 对于「只有两个小工具」的场景,引入 MCP 确实是过度工程
- HTTP 传输的性能还在优化中,高并发场景需要谨慎
- 生态还在快速变化,2024-11-05 版本的 SSE 传输已经被 2025-06-18 的 Streamable HTTP 取代了------说明协议还在演进
不过我觉得,MCP 代表的趋势是不可逆的:AI 不再是孤立的聊天机器人,而是要接入真实世界的工具和数据。而「协议标准化」是让这件事大规模发生的前提。
就像 USB-C 接口一样,有了统一标准,整个生态才能繁荣起来。
本文是 MCP 协议学习的笔记整理,希望能帮到正在了解 MCP 的你~如果有什么写得不准确的地方,欢迎讨论!
参考来源: