实操 04 MCP 实战:协议机制与 Server 开发
所属:AI 知识图谱 → 实操系列(practice/)。正文只回答了"MCP 该不该用"(03 篇约 20 行),本篇回答"它到底是什么、怎么自己写一个"。
速览卡
| 项 | 内容 |
|---|---|
| 一句话定位 | 把 MCP 从"听说过的协议"变成"我能自己开发并接入一个 Server",解决工具集成的 N×M 问题 |
| 核心内容 | 协议要解决什么、Host/Client/Server 架构、三原语(Tools/Resources/Prompts)、传输方式、开发一个 Server 并接入、调试与安全 |
| 前置知识 | 实操 01(Function Calling 的原理与代码) |
| 读完能做到 | 自己写一个 MCP Server(如封装内部数据库查询),并把它接入支持 MCP 的编码工具 |
| 配套正文 | 协议生态定位与"保持可替换抽象层"的建议见 03 篇 |
MCP(Model Context Protocol)最初由 Anthropic 提出并开源,现已被多家厂商与工具采纳
[易变] 生态采纳情况以官方站点为准。本篇讲机制,不绑定具体 SDK 版本。
一、先搞懂 MCP 解决什么问题
没有 MCP 之前,把 N 个工具接进 M 个 AI 应用,要写 N×M 个适配:
改造前(N×M) 改造后(N+M)
应用1 ─┬─ 工具A 应用1 ─┐ ┌─ 工具A
├─ 工具B ├─ MCP ─┤
应用2 ─┼─ 工具A 应用2 ─┘ 协议 ├─ 工具B
└─ 工具B └─ 工具CMCP 是一层标准协议:工具侧实现一次 Server,应用侧实现一次 Client,之后任意组合。这正是 03 篇把 MCP 归为"工具集成层"的原因——它解决的是连接成本,不是智能能力。
它不是什么(避免误解):
- ❌ 不是 Agent 框架(不负责规划、循环、记忆)
- ❌ 不是模型(不生成内容)
- ✅ 是工具与数据的标准化接入协议,约等于"AI 应用的 USB-C"
二、架构与三原语
2.1 三个角色
┌────────────────────────────────────────────┐
│ Host(宿主应用:IDE / 桌面端 / 你的 Agent) │
│ └── MCP Client(每个 Server 一个连接) │
│ ├────────────▶ MCP Server A(数据库)│
│ ├────────────▶ MCP Server B(文件系统)│
│ └────────────▶ MCP Server C(内部 API)│
└────────────────────────────────────────────┘| 角色 | 谁实现 | 职责 |
|---|---|---|
| Host | 应用方(IDE、桌面 App、你的服务) | 承载模型交互,管理多个 Client |
| Client | 应用方(通常 SDK 内置) | 与一个 Server 维持一条连接 |
| Server | 你要写的部分 | 暴露工具/资源/提示词,处理调用 |
2.2 三原语:Tools / Resources / Prompts
| 原语 | 谁控制 | 作用 | 类比 |
|---|---|---|---|
| Tools | 模型调用 | 让模型"动手":查库、写文件、调 API | Function Calling 的工具 |
| Resources | 应用控制 | 可读取的数据(文件内容、DB 记录) | 只读数据源 |
| Prompts | 用户控制 | 预置的提示词模板(如 /review) | 斜杠命令 |
最关键的是 Tools——它就是把实操 01 第五节的 Function Calling 标准化了:你写一次工具定义,任何 MCP Client 都能发现并调用。
三、自己写一个 MCP Server(Python)
场景:把"查询内部员工信息数据库"封装成工具,让 AI 助手能查。
pip install mcp # 官方 Python SDK [易变]# server.py —— 一个最小可用的 MCP Server
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("hr-tools")
# ① 定义工具:类型标注 + docstring 就是给模型看的"使用说明书"
@mcp.tool()
def get_employee(department: str, limit: int = 10) -> list[dict]:
"""查询指定部门的员工列表。
Args:
department: 部门名称,如 "技术部"、"市场部"
limit: 返回条数上限,默认 10
"""
# 真实实现:查数据库 / 调内部 API
rows = query_hr_db(department, limit)
return [{"name": r.name, "role": r.role, "email": r.email} for r in rows]
@mcp.tool()
def get_oncall(week: str) -> dict:
"""查询某一周的值班安排。
Args:
week: ISO 周格式,如 "2026-W36"
"""
return query_oncall(week)
if __name__ == "__main__":
mcp.run(transport="stdio") # 本地场景走 stdio三个决定成败的细节:
- 工具描述(docstring)决定调用质量。模型只看名字和描述来决定"该不该调用我、参数怎么填"。写得含糊 → 该调不调、参数乱填。这条与实操 01 Function Calling 完全一致。
- 参数用类型标注 + 默认值。
limit: int = 10会被自动转成 JSON Schema,不用手写。 - 返回值要简洁。别把整张表的原始字段全丢回去,只返回模型需要的字段——省 Token 也降低误解。
四、传输方式:stdio 与 HTTP
| 方式 | 场景 | 特点 |
|---|---|---|
| stdio | 本地 Server(默认) | Host 以子进程启动 Server,通过标准输入/输出通信。零网络配置、天然隔离,本地工具首选 |
| SSE / Streamable HTTP | 远程 Server | Server 独立部署,多客户端可共享;需处理鉴权、并发、网络异常 |
选型判断:
工具只在本机跑 / 需访问本地文件 → stdio
工具要多人共享 / 部署在服务器 → HTTP(必须做鉴权)生产环境用 HTTP 时,与 09 篇权限最小化原则对齐:每个客户端独立凭证、按身份过滤可见工具、记录调用审计日志。
五、接入一个 Host(以编码工具为例)
主流支持 MCP 的编码工具配置形态类似(具体文件名 [易变],查所用工具的文档):
{
"mcpServers": {
"hr-tools": {
"command": "python",
"args": ["/abs/path/to/server.py"],
"env": { "HR_DB_URL": "..." }
},
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
}
}
}配置后重启 Host,工具列表会出现 get_employee / get_oncall,模型就能在对话中自行判断调用。注意:凭据走 env 传入,不要写死在代码里(对照 09 篇数据泄露风险点)。
六、调试:出问题先查这三处
| 症状 | 排查方向 |
|---|---|
| Host 里看不到我的工具 | ① Server 进程是否启动成功(看 Host 日志);② 命令路径是否绝对;③ stdio 模式下 stdout 被你的 print 污染会破坏协议 |
| 模型看到工具但不调用 | 工具/参数描述不够明确;改 docstring,写清"什么时候用我" |
| 调用报参数错 | 类型标注缺失或写得含糊;检查必填/选填 |
stdio 模式最常见的一个坑:
print("调试信息") # ❌ 污染 stdout,协议解析直接崩
# 正确:
import sys; print("调试信息", file=sys.stderr) # ✅ 走 stderr
# 或用 SDK 提供的日志接口官方 SDK 一般自带调试器(mcp dev server.py 之类,命令以当期文档为准 [易变]),可交互式列出工具并手动调用——先手动调通,再让模型调。
七、安全:Server 是一段有权限的代码
MCP Server 在你的机器上执行,工具能做什么,模型就能让它做什么。对照 03 篇提示注入与 09 篇:
| 风险 | 防护 |
|---|---|
| 间接提示注入(外部文档里藏"调用删除工具") | 破坏性操作必须人工确认;工具权限按操作类型分级 |
| Server 权限过大 | 最小权限:只给必要的文件目录/数据库只读账号 |
| 第三方 Server 来源不明 | 只用官方/可信来源,审代码后再接;优先 stdio 隔离 |
| 敏感数据外泄 | 工具返回前脱敏;记录调用审计 |
| 无调用上限 | 加频率限制与熔断(对照 05 篇稳定性) |
判断原则:把每个工具都当成"一个可能被诱导的实习生"——它有权做的事,就是最坏情况下会做的事。
八、什么时候别用 MCP
对照 03 篇"保持可替换抽象层"的建议:
| 情况 | 建议 |
|---|---|
| 工具只给你自己的一个 Agent 用,且不打算换 Host | 直接写 Function Calling 更简单,无协议开销 |
| 工具要被多个 Host/多个应用复用 | MCP 价值最大 |
| 需要跨组织分发工具 | MCP(生态标准,别人不用改代码就能接) |
| 工具调用要求极低延迟/高吞吐的核心链路 | 协议有序列化开销,评估后再定 |
协议是手段不是目的:如果你的工具只有一个消费者,先别上 MCP。
九、动手任务
- [ ] 任务 1:跑通第三节的
server.py(先把query_hr_db换成返回固定假数据的函数),用 Host 配置接入,在对话里问"技术部有哪些人",观察模型是否自动调用工具 - [ ] 任务 2(刻意制造 bug):在 Server 里加一行
print("debug")到 stdout,看协议如何崩溃;再用stderr改写,验证恢复正常——亲手踩一次这个坑印象最深 - [ ] 任务 3(描述质量实验):把
get_employee的 docstring 改写成含糊版本(如"查询员工"),对比模型调用的准确率差异,体会"描述即接口契约" - [ ] 任务 4(进阶):给工具加一个破坏性操作(如
delete_employee),实现"调用前必须返回确认请求",思考如何防间接提示注入 - [ ] 自检:能不看资料说出——三原语各自由谁控制?stdio 与 HTTP 怎么选?为什么 Server 的 stdout 不能被污染?什么情况下不该用 MCP?
延伸方向
- Function Calling 基础(MCP Tools 就是它的标准化)→ 实操 01
- 有了工具,如何组织成能自主执行的 Agent → 实操 06 Agent 实战
- 协议生态定位、A2A/ACP/AG-UI 分工与采用建议 → 03 篇
- 工具调用的权限设计与注入防护 → 09 篇