模型上下文协议 MCP
模型上下文协议 MCP
面向后端工程师讨论 MCP(Model Context Protocol),重点不在于引入一个新名词,而在于回答 3 个核心问题:
- 为什么在 Function Calling 已经存在的情况下,还需要 MCP?
- MCP 到底标准化了什么?
- 在真实工程里,MCP 的调用链是怎么跑起来的?
目录
-
- 背景与定位:为什么需要 MCP
-
- MCP vs Function Calling
-
- MCP 的核心原理
- 3.1 核心架构(Host / Client / Server)
- 3.2 核心交互流程
- 3.3 关键技术规范
-
- MCP 应用实例(智能数据分析助手)
- 4.1 先看完整调用链
- 4.2 Step 1:MCP Server 定义工具
- 4.3 Step 2:实现 MCP Server
- 4.4 Step 3:大模型如何“使用” MCP
-
- 工程实践与注意要点
1. 背景与定位:为什么需要 MCP
MCP(Model Context Protocol,模型上下文协议)是一套面向大模型与外部系统交互的开放协议。它的核心目标,不是替代模型 API,也不是替代 Prompt 工程,而是把“模型如何安全、结构化、可维护地调用外部能力”这件事标准化。
这里的外部能力包括:
- 工具
- 数据源
- 文件系统
- 业务服务
- 应用端上下文
1.1 没有 MCP 时,问题出在哪里
在 MCP 出现之前,模型和外部系统的集成通常会遇到 5 类典型问题:
1. 交互格式碎片化
不同模型、不同 SDK、不同工具往往有各自的字段命名和请求格式,例如:
- 有的系统用
history表示历史消息 - 有的系统用
context - 有的系统把上下文直接拼进 Prompt
工具调用的参数结构、返回结构也经常各不相同,导致每接一个新模型或新工具,都要重新写一层适配逻辑。
2. 上下文管理混乱
复杂任务不是一次请求就能完成的,通常会涉及:
- 历史对话
- 用户身份
- 当前任务状态
- 上一步工具返回结果
如果这些上下文没有统一的生命周期管理机制,多轮交互时就容易出现“失忆”“串上下文”“工具结果接不上”的问题。
3. 工具接入缺少统一抽象
传统做法中,工具调用往往散落在业务代码里:
- 这里手写一个 HTTP 请求
- 那里直接查一次数据库
- 另一处再拼一个本地脚本调用
这种方式短期能跑,长期很难治理。因为模型侧看不到统一的工具描述,服务侧也没有统一的校验、鉴权和错误处理。
4. 错误处理与观测割裂
调用失败时,常见问题包括:
- 参数不合法
- 权限不足
- 工具不存在
- 目标服务超时
- 工具本身执行异常
如果没有统一的消息结构和错误码约定,排查问题时就会非常痛苦,尤其是在多组件协作的链路里。
5. 工程可维护性差
对于后端工程师来说,真正麻烦的不是“第一次接上”,而是后续的:
- 新增工具
- 升级模型
- 替换客户端
- 做权限控制
- 加审计日志
没有统一协议时,这些改动都会演变成到处修改、重复适配。
1.2 MCP 的定位
MCP 的定位可以概括为一句话:
它是模型与外部系统之间的“统一交互层”。
它主要标准化 4 件事:
- 上下文如何传
- 工具如何描述
- 请求和响应如何封装
- 错误和生命周期如何管理
因此,MCP 不是某个厂商的私有能力,而更接近一层“连接规范”。它解决的重点,不是让模型“更聪明”,而是让模型更稳定地接入外部世界。
1.3 MCP 不是什么
为了避免混淆,需要先明确边界:
- MCP 不是模型 API 本身。API 解决“怎么访问模型”,MCP 解决“模型怎么访问外部能力”。
- MCP 不是 Prompt 工程。Prompt 工程优化的是“怎么提问”,MCP 优化的是“怎么接系统”。
- MCP 不是向量数据库。向量数据库负责“存和检索信息”,MCP 负责“把这些信息和工具能力规范地送进调用链”。
2. MCP vs Function Calling
很多工程师第一次看到 MCP 时都会问:Function Calling 已经能让模型调用工具了,为什么还要 MCP?
短答案是:
Function Calling 解决的是“单个模型如何调用某个工具”;MCP 解决的是“不同模型 / 客户端 / 工具服务如何通过统一协议协作”。
2.1 对比表
| 对比维度 | Function Calling | MCP |
|---|---|---|
| 关注点 | 模型输出一个工具调用意图 | 用统一协议组织模型、客户端、工具服务之间的协作 |
| 抽象层级 | 更偏模型能力层 | 更偏系统集成层 |
| 工具描述 | 往往绑定在某个模型 API 或 SDK 上 | 通过协议统一描述,可被不同 Host / Client / Server 理解 |
| 上下文管理 | 通常由业务方自己处理 | 协议显式关注上下文的传递、更新与生命周期 |
| 错误处理 | 取决于具体 SDK / 业务实现 | 可以统一消息结构、错误码与回传方式 |
| 跨厂商互操作 | 较弱,常常跟具体模型供应商绑定 | 更强,目标就是解耦模型、客户端和工具服务 |
| 工程治理 | 适合快速接一个工具 | 更适合长期维护、扩展和治理整套工具体系 |
2.2 一句话理解增量价值
二者可以概括为:
- Function Calling:让模型“知道该调用哪个工具”
- MCP:让整个系统“知道这次工具调用该怎么被组织、传输、校验、执行、回传和追踪”
所以,MCP 的增量价值主要在 3 点:
- 标准化:把工具调用从“各写各的”变成统一协议。
- 互操作性:降低模型、客户端、工具服务之间的耦合。
- 生命周期管理:不仅关注“调一下工具”,还关注前后的上下文、错误、状态和治理。
可以归纳为如下判断:
如果你只是想让某个模型快速调一个工具,Function Calling 可能已经够了。
如果你在做的是一个长期维护的 Agent 系统,需要跨组件、跨工具、跨客户端协作,那么 MCP 更值得引入。
2.3 什么情况下不必急着上 MCP
并不是所有项目都值得一开始就引入 MCP。
如果你的场景同时满足下面几个条件,那么直接使用 Function Calling 或一层轻量工具封装,通常已经够用:
- 只有 1 到 2 个工具
- 工具调用链很短,没有复杂的上下文生命周期
- 不需要跨多个 Host / Client / Server 组件协作
- 不需要统一治理权限、审计、错误码和可观测性
- 项目本身规模小,更关注“先跑通”而不是“长期平台化”
换句话说:
MCP 更适合“系统化建设”,不一定适合“所有小项目的第一天”。
如果问题规模还没到协议层,先用简单方案往往更划算。
3. MCP 的核心原理
MCP 的核心可以概括为一句话:
用统一的消息结构和组件分工,把“模型 ↔ 工具”的临时集成,变成“Host ↔ Client ↔ Server”的可维护系统。
3.1 核心架构(Host / Client / Server)
MCP 通常可以拆成 3 个核心组件。
MCP Host(宿主应用)
Host 是整个 MCP 调用链的入口和中枢。它通常就是你真正的 AI 应用,例如:
- Claude Desktop
- 编辑器插件
- 企业级 AI 助手后端
- 自己实现的 Agent 服务
它的核心职责包括:
- 接收用户请求
- 管理上下文
- 决定要不要调用工具
- 把请求分发给对应的 MCP Client
- 汇总结果后返回给用户
从职责划分看,Host 对应业务调度层。
MCP Client(客户端)
Client 是协议适配层,负责把 Host 的意图转成 MCP 标准请求,再把 Server 的结果转回 Host 能理解的内容。
它的核心职责包括:
- 与 MCP Server 建立连接
- 封装 MCP 请求
- 接收 MCP 响应
- 处理超时、重试、连接状态
从职责划分看,Client 对应协议适配层。
MCP Server(工具服务端)
Server 部署在真正提供能力的一侧,也就是:
- 数据库代理
- 文件系统工具
- 搜索服务
- 邮件服务
- 内部业务接口
它的核心职责包括:
- 暴露工具能力
- 描述这些工具能做什么
- 校验参数是否合法
- 执行真实操作
- 返回标准化结果或错误
从职责划分看,Server 对应受控能力层。
3.2 核心交互流程
以一次完整的工具调用为例,MCP 的核心交互流程可以概括为 6 步:
-
Host 收集上下文
Host 收集用户请求、历史消息、当前任务状态等信息。 -
Client 封装标准请求
Client 把 Host 给出的请求和上下文,封装成 MCP 标准消息。 -
Server 解析请求
Server 解析消息,识别调用的是哪个工具、参数是否合法、当前调用是否有权限。 -
Server 执行工具
如果校验通过,Server 调用本地能力,例如数据库查询、文件读取、HTTP 接口请求。 -
Client 接收并解析响应
Client 把 Server 的结果或错误转换为 Host 更容易消费的形式。 -
Host 更新上下文并继续推理
Host 把工具结果放回当前上下文,让 LLM 继续总结、推理或发起下一次调用。
这个流程的重点在于:工具调用不是孤立的一跳,而是嵌在上下文生命周期里的。
3.3 关键技术规范
MCP 的工程价值,最终落在 3 类规范上:
- 消息结构
- 错误处理
- 传输方式
标准消息结构
MCP 消息通常采用 JSON 格式,核心字段类似这样:
{
"message_id": "uuid-123",
"sender": "agent-billing",
"receiver": "tool-stripe",
"timestamp": "2025-07-30T12:00:00Z",
"performative": "request",
"content": "{\"action\":\"charge\",\"amount\":99.99}",
"metadata": {
"trace_id": "span-456",
"priority": "high"
}
}
这些字段的意义不是“多写几个 JSON 字段”,而是把一次调用变成可追踪、可审计、可治理的标准事件。
标准化错误处理
MCP 很适合采用类似 JSON-RPC 2.0 的错误码体系,把常见错误统一起来:
| 错误码 | 错误含义 | 后端处理建议 |
|---|---|---|
-32700 |
解析错误 | 重点检查请求 JSON 是否损坏、字段是否缺失 |
-32600 |
无效请求 | 检查请求结构是否符合 MCP 约定 |
-32601 |
方法不存在 | 确认工具名拼写正确,且 Server 已注册该工具 |
-32602 |
参数错误 | 对照工具 schema 校验参数完整性与类型 |
-32603 |
服务器错误 | 查看 Server 日志,排查工具执行异常或依赖故障 |
灵活的传输方式
MCP 可以根据部署方式选择不同传输层:
| 应用场景 | 传输方式 | 核心特点 |
|---|---|---|
| 本地工具调用 | Stdio |
简单直接,适合本地进程通信 |
| 远程服务集成 | HTTP+SSE |
更容易跨网络部署,兼容性较好 |
| 实时双向交互 | WebSocket |
适合持续推送和低延迟场景 |
这里最重要的不是背协议名字,而是理解:MCP 把“传什么”和“怎么传”分开了。消息结构由协议约束,底层传输方式按部署场景选择。
4. MCP 应用实例(智能数据分析助手)
下面用一个“智能数据分析助手”的例子,说明 MCP 在工程里到底怎么跑。
4.1 先看完整调用链
在进入代码之前,先看完整调用链:
User
↓
Host(接收请求、维护上下文)
↓
LLM(判断需要调用哪个工具)
↓
MCP Client(封装协议请求)
↓
MCP Server(校验并执行工具)
↓
DB / API / File
↑
MCP Server 返回结果
↑
MCP Client 解析结果
↑
Host 更新上下文
↑
LLM(总结 & 推理)
↑
User
这张图里最关键的点是:
- LLM 不直接访问数据库
- LLM 不直接执行代码
- LLM 只是表达“我要什么能力”
- 真正执行能力的是 MCP Server
有了这个全局视角,再看下面的步骤会清晰很多。
4.2 Step 1:MCP Server 定义工具
先定义你允许模型使用什么能力。
这个例子里,我们只开放两类能力:
- 查看有哪些表
- 对指定表做只读查询
// tools.ts
export const tools = [
{
name: "list_tables",
description: "列出当前数据库的表",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "query_table",
description: "对指定表执行只读 SQL 查询",
inputSchema: {
type: "object",
properties: {
table: { type: "string" },
columns: {
type: "array",
items: { type: "string" }
},
limit: { type: "number", default: 100 }
},
required: ["table", "columns"]
}
}
]
这里的关键不是代码量,而是设计思想:
- 不是让模型自由生成 SQL
- 而是由你先把“允许的能力”定义成结构化接口
- 模型只能在你开放的边界内调用
这构成了“受控能力”的核心。
4.3 Step 2:实现 MCP Server
有了工具描述之后,就要把这些工具真正挂到 Server 上。
// server.ts
import { Server } from "@modelcontextprotocol/sdk/server"
import { tools } from "./tools"
import { db } from "./db"
const server = new Server({ name: "db-mcp-server" })
server.setTools(tools)
server.onToolCall(async (tool, args) => {
if (tool === "list_tables") {
return await db.listTables()
}
if (tool === "query_table") {
return await db.select(
args.table,
args.columns,
args.limit
)
}
})
server.start()
这段代码中,最需要解释的是两个 API:
server.setTools(tools) 做了什么
它的作用是:把你定义好的工具 schema 注册给 MCP Server。
注册之后,Server 才知道:
- 当前有哪些工具
- 每个工具叫什么
- 每个工具接收什么参数
- 参数是否合法
它可以视为“能力清单注册”。
更精确地说,这一步完成的是 capability discovery 的基础准备。
它使 Client / Host 一侧能够获知:这个 Server 暴露了哪些能力、这些能力各自的 schema 是什么。这个“能力发现”机制,也是 MCP 与普通 RPC 风格接口的重要区别之一。普通 RPC 往往假设双方已经预先强绑定接口,而 MCP 更强调通过协议显式暴露能力,再由调用方按协议理解和消费这些能力。
server.onToolCall(...) 做了什么
它的作用是:定义当某个工具真的被调用时,服务端该怎么执行。
也就是说:
setTools负责“声明能力”onToolCall负责“执行能力”
前者更接近接口定义,后者更接近路由分发与真实业务执行。
到这里为止,已经形成了一个“AI 可调用的数据库服务”,但它仍然处在受控边界内:
- 模型不能随便执行
DROP - 模型不能绕过你定义好的 schema
- 模型也不能跳过服务端校验直接碰数据库
4.4 Step 3:大模型如何“使用” MCP
假设用户输入:
分析一下最近 30 天用户增长趋势
此时实际发生的并不是“模型直接去查数据库”,而是如下链路:
- Host 收到用户请求,并把当前对话上下文交给 LLM。
- LLM 判断:这个问题需要先获取结构化数据。
- LLM 生成一个“工具调用意图”。
- Host / Client 把这个意图封装成 MCP 请求,发送给 MCP Server。
- MCP Server 根据工具名和参数执行真实查询。
- 返回结果后,Host 再把结果交给 LLM 做总结。
从“模型意图”的角度看,LLM 内部更接近于表达如下结构:
{
"tool": "query_table",
"arguments": {
"table": "user_daily_stats",
"columns": ["date", "new_users"],
"limit": 30
}
}
这段 JSON 本身不是重点,重点是它在链路里的位置:
- 它不是数据库查询结果
- 它不是 Server 代码
- 它是模型表达出来的“我要调用哪个工具、带什么参数”
而真正把这段意图变成一次受控调用的,是 Host → Client → Server 这一整套机制。
MCP Server 可能返回这样的数据:
[
{ "date": "2026-01-01", "new_users": 120 },
{ "date": "2026-01-02", "new_users": 135 }
]
然后,LLM 再基于这个结果生成自然语言结论,例如:
近 30 天用户数整体呈现上升趋势,周末增长略有放缓,但工作日增长更明显。
这个例子要说明的核心点是:
MCP 不是让模型“自己直接操作数据库”,而是让模型通过标准化、受控的协议链路去调用数据库能力。
5. 工程实践与注意要点
这一节对后端工程师最重要,因为系统真正上线后,大多数问题都会集中暴露在这里。
5.1 安全规范
1. 传输安全不能后补
MCP 链路里传输的不只是普通请求,还可能包含:
- 用户上下文
- 工具参数
- 工具返回结果
- 凭证或资源定位信息
所以应默认使用加密传输,例如 HTTPS / TLS。
如果在开发阶段为了缩短调试路径而直接使用明文 HTTP,本地可运行并不意味着线上可接受。
开发阶段临时使用明文链路并不少见,但一旦系统被复用到更长的调用链中,这类“临时方案”通常会最先演化为风险点。
2. 身份认证必须落到工具层
很多人以为“用户已经登录了”就够了,但 MCP 链路里还要继续问一个问题:
当前这个工具调用,是否真的有权限执行?
例如:
- 查询报表可以开放给分析角色
- 删除文件不应该默认开放
- 修改生产配置必须更严格
如果没有把权限校验放到 Server 或工具层,模型就可能在“意图正确”的情况下,执行“权限错误”的操作。
3. 凭证管理不能偷懒
不要把:
- API Key
- 数据库密码
- OAuth Token
直接硬编码在工具实现里,也不要明文落盘。
正确做法是:
- 使用密钥管理设施
- 做最小权限分配
- 支持轮换与吊销
4. 高风险操作要有二次确认和审计
像下面这类能力,不应该和普通查询放在同一级别对待:
- 删除
- 修改
- 批量导出
- 调用高敏感内部接口
如果你的 MCP Server 对这些操作没有审计日志、调用记录和额外确认机制,那么后续追责和排查会非常被动。
5.2 性能优化技巧
1. 不要把所有上下文一次性塞满
上下文越长,传输和推理成本越高。
在多轮工具调用场景中,更合理的做法是:
- 保留当前任务真正需要的信息
- 把历史结果做摘要
- 对超长内容做分片传输
如果每轮请求都把完整历史、完整工具返回和完整附件重新塞给模型,延迟会非常容易失控。
2. 连接复用比频繁重建更重要
如果每次调用工具都重新建连,再立刻断开,那么在高频调用场景下,性能损耗会非常明显。
更稳妥的做法是:
- 复用 Client 与 Server 的连接
- 明确超时与重试策略
- 在工具执行层做并发控制
3. 只在必要时调用工具
工具调用不是越多越好。
这会带来:
- 更多延迟
- 更多错误面
- 更高资源开销
所以,Host 需要有基本的调用判断逻辑,避免无意义的“碰碰运气式”调用。
4. 批处理要看场景,不要机械套用
批处理可以减少网络往返,但也会带来新的复杂度:
- 错误定位更难
- 超时更难拆分
- 单个请求失败可能影响整批处理
所以不要把“能批量”误认为“应该批量”。
对于相互独立且轻量的查询,批处理很合适;对于高风险写操作,则要更谨慎。
5.3 可观测性与排障建议
如果要把 MCP 真正用到生产环境,至少要把这几类信息打出来:
- 请求 ID / trace ID
- 工具名
- 调用参数摘要
- 响应状态
- 错误码
- 耗时
这些字段不能只停留在“应该记录”,而应明确落到链路中的具体位置:
- 在 Host 层 打用户请求、路由决策、上下文摘要和最终响应耗时。
- 在 Client 层 打协议请求与响应、重试、超时、连接状态。
- 在 Server 层 打工具名、参数校验结果、真实执行耗时、下游依赖错误。
如果团队已经有现成的 tracing 体系,例如 OpenTelemetry、Jaeger、Zipkin 或云厂商 APM,更合理的做法是把 MCP 调用链直接接入,而不是单独维护一套新的日志体系。一个常见做法是:
- Host 生成顶层
trace_id - Client / Server 沿用同一个
trace_id - 每次工具调用生成自己的
span
这样在一次调用失败时,就可以沿着同一条链路直接定位:
- 是模型没有正确表达意图
- 还是 Host 路由错了
- 还是 Client 发请求超时
- 还是 Server 校验失败
- 或者真正的问题出在下游 DB / API
否则,一旦链路出问题,你就会分不清问题到底出在:
- 模型意图识别
- Host 路由
- Client 传输
- Server 参数校验
- 工具执行本身
对于后端工程,这比“模型会不会调用工具”更关键。
5.4 一句话结论
如果要用一句话概括 MCP:
Function Calling 让模型学会“调用工具”,而 MCP 让工程系统学会“把工具调用这件事做成标准化、可治理、可维护的基础设施”。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)