从零开发一个MCP Server:架构与实践实录
关键词:MCP,Model Context Protocol,Python,FastMCP,StreamableHTTP,AI Agent,Hermes,SQLite
文章目录
从零开发一个 MCP Server:架构、实践与踩坑实录
本文基于 2026-08-15 本机真实开发会话。文中所有命令输出均为实际运行结果,不是示例数据。
适合读者:已有 Python 基础、想给 Agent/LLM 接入外部数据能力的开发者。通读约 15 分钟,完整跟练约 1 小时。
一、MCP 简介:为什么 Agent 需要一套“外设接口”
大模型 Agent 的瓶颈从来不是“会想”,而是“够不到”。模型只能看到对话文本,拿不到数据库、文件系统、内部 API 里的真实数据。早期方案是给每个 Agent 硬编码工具函数,但工具一多,维护就失控:每个 Agent 框架一套工具协议,换个框架全部重写。
MCP(Model Context Protocol)解决的就是这个问题。它由 Anthropic 于 2024 年底提出,是一个开放协议,定义了 MCP Server(能力提供方) 和 MCP Client(能力消费方) 之间的标准通信方式:
- Server 把能力包装成“工具”(tool),暴露给客户端
- Client 启动时自动发现工具列表,运行时可调用
- 传输层支持 stdio(本地子进程)和 StreamableHTTP(远程传输,2025-03 版协议规范引入,取代旧的 HTTP+SSE 方案)
类比一句话:MCP 之于 Agent,就像 USB 之于电脑——外设只要符合协议就能即插即用,不用管里面是什么芯片。
| 对比项 | 传统硬编码工具 | MCP Server |
|---|---|---|
| 工具协议 | 每框架一套,各自为政 | 统一开放协议 |
| 复用性 | 换框架重写 | 一套 Server 到处接 |
| 部署 | 与 Agent 同进程 | 本地子进程 / 远程独立部署 |
| 工具发现 | 手工注册 | 启动自动发现 |
二、MCP Server 应用介绍:
本文的实战场景:开发一个“学生成绩查询”MCP Server,让 Agent 能直接回答“张伟的均分是多少”这类问题,而不是让人先查好再喂给模型。
2.1 需求拆解
| 能力 | 说明 |
|---|---|
| 查学生 | 按学号精确 / 按姓名模糊 |
| 查成绩 | 按学生查全部成绩、按课程查成绩排名 |
| 统计分析 | 平均分、加权均分、最高最低分 |
2.2 技术选型
| 组件 | 选型 | 理由 |
|---|---|---|
| 语言 | Python 3.12 | MCP 官方 SDK 生态成熟 |
| SDK | mcp Python SDK(FastMCP) | 一份代码支持 stdio + HTTP 双传输 |
| 存储 | SQLite | 单文件、零运维,演示/内网够用;可替换 MySQL/API |
| 接入端 | Hermes Agent | 原生 MCP 客户端,配置即用 |
2.3 架构定位
这个 Server 属于“数据访问型 MCP”:后端接 SQLite,前端通过 MCP 协议向 Agent 暴露只读查询工具。后续接真实教务系统时,只改数据层,工具层不动。
三、MCP Server 架构说明
3.1 总体架构

通信细节:
- 协议:MCP 基于 JSON-RPC 2.0,核心方法
initialize(握手)、tools/list(发现工具)、tools/call(调用工具) - 传输:本地用 stdio(子进程 stdin/stdout),远程用 StreamableHTTP(
POST /mcp) - 工具命名:接入 Hermes 后自动加前缀,如
mcp_grades_query_student_grades
3.2 项目结构
grade-mcp-server/
├── pyproject.toml # 项目元数据 + 依赖声明
├── grade_mcp/
│ ├── __init__.py
│ ├── server.py # FastMCP Server 入口,工具定义
│ └── db.py # SQLite 数据层:建表/种子数据/查询
├── tests/
│ └── test_db.py # 数据层单元测试(9 个用例)
├── scripts/
│ ├── test_stdio.py # stdio 模式客户端冒烟测试
│ └── test_http.py # HTTP 模式客户端冒烟测试
├── deploy/
│ ├── deploy.sh # rsync + systemd 一键部署脚本
│ ├── grade-mcp.service # systemd 单元文件
│ └── notes.md # 防火墙/反代/认证建议
├── Dockerfile # 容器化部署
└── README.md
3.3 分层职责
- server.py(工具层):只声明工具签名和 docstring,不碰 SQL。docstring 就是给 LLM 看的工具说明,写清楚参数格式(如
S001、2024-秋)能显著提高模型调用准确率。 - db.py(数据层):所有 SQL 在这里,通过
GRADES_DB_PATH环境变量控制库文件位置。换 MySQL 只改这一个文件。 - 传输层:FastMCP 封装,
server.run(transport="stdio")与streamable_http_app()两行切换。
四、MCP Server 开发实践
4.1 环境准备
系统 Python 是 3.9.6,MCP SDK 需要 3.10+,用 uv 建独立环境:
$ cd ~/workspace/hermes && mkdir grade-mcp-server && cd grade-mcp-server
$ uv venv --python 3.12 .venv
Using CPython 3.12.13
Creating virtual environment at: .venv
4.2 依赖声明
[project]
name = "grade-mcp-server"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"mcp>=1.28,<2",
"uvicorn>=0.30.0",
]
这里锁 mcp<2 是本会话踩的第一个大坑,见“测试验证”章节的踩坑 1。
安装依赖(以可编辑模式装进虚拟环境,方便后续改代码即时生效):
$ uv pip install -e .
若不想打可编辑安装,也可以直接 uv pip install "mcp>=1.28,<2" uvicorn,再从项目根目录运行 python -m grade_mcp.server(利用当前目录的包路径)。
4.3 数据层 db.py(节选)
SCHEMA = """
CREATE TABLE IF NOT EXISTS students (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
class_name TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS courses (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
credits INTEGER NOT NULL DEFAULT 3
);
CREATE TABLE IF NOT EXISTS grades (
id INTEGER PRIMARY KEY AUTOINCREMENT,
student_id TEXT NOT NULL REFERENCES students(id),
course_id TEXT NOT NULL REFERENCES courses(id),
semester TEXT NOT NULL,
score REAL NOT NULL CHECK (score >= 0 AND score <= 100),
UNIQUE (student_id, course_id, semester)
);
"""
种子数据:5 名学生、5 门课程、16 条成绩记录。首次启动自动建库写入。
4.4 Server 入口 server.py(节选)
from mcp.server.fastmcp import FastMCP
from grade_mcp import db
server = FastMCP(
"grade-mcp-server",
instructions=(
"成绩查询 MCP server。提供学生成绩查询、课程成绩查询、"
"学生统计等工具。学生 ID 形如 S001,课程 ID 形如 C001。"
),
)
@server.tool()
def query_student_grades(student_id: str, semester: str | None = None) -> dict:
"""查询某学生的全部成绩。student_id 为学号(如 S001);semester 可选,如 '2024-秋'。"""
student = db.get_student_by_id(student_id.strip().upper())
if not student:
return {"error": f"学生 {student_id} 不存在"}
grades = db.get_grades_by_student(student["id"], semester)
return {"student": student, "grades": grades}
入口同时支持两种传输:
if args.transport == "http":
app = server.streamable_http_app() # StreamableHTTP,端点 /mcp
uvicorn.run(app, host=args.host, port=args.port)
else:
server.run(transport="stdio") # 本地子进程模式
4.5 接入 Hermes
本地开发用 stdio,一条命令接入:
$ hermes mcp add grades --command /Users/workspace/hermes/grade-mcp-server/.venv/bin/python \
--connect-timeout 30 --args -m grade_mcp.server
✓ Saved 'grades' to ~/.hermes/config.yaml (6/6 tools enabled)
生成配置:
mcp_servers:
grades:
command: /Users/workspace/hermes/grade-mcp-server/.venv/bin/python
args:
- -m
- grade_mcp.server
connect_timeout: 30.0
enabled: true
4.6 切换到 HTTP 模式(远程部署预览)
$ .venv/bin/python -m grade_mcp.server --transport http --host 127.0.0.1 --port 8000
Grade MCP Server listening on http://127.0.0.1:8000/mcp
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
$ hermes mcp remove grades
✓ Removed 'grades' from config
$ hermes mcp add grades --url http://127.0.0.1:8000/mcp --connect-timeout 30
✓ Saved 'grades' to ~/.hermes/config.yaml (6/6 tools enabled)
HTTP 模式配置:
mcp_servers:
grades:
url: http://127.0.0.1:8000/mcp
connect_timeout: 30.0
headers: {}
enabled: true
⚠️ 注意:HTTP 模式默认没有任何认证。上面的示例刻意只绑定
127.0.0.1;一旦要暴露到内网/公网,务必先加反代 + 鉴权(如 Nginx + Bearer Token,或 OAuth),方案见仓库deploy/notes.md。
五、测试验证过程
5.1 数据层单元测试
先给数据层写 9 个 pytest 用例,覆盖种子数据、精确/模糊查询、学期过滤、成绩降序、统计聚合、异常学生:
$ .venv/bin/python -m pytest tests/ -q
......... [100%]
9 passed in 0.03s
5.2 stdio 模式端到端冒烟
写一个最小 MCP 客户端,真实走一遍 initialize → list_tools → call_tool:
$ .venv/bin/python scripts/test_stdio.py
[tools] 6 registered:
- list_students: 列出所有学生(学号、姓名、班级)。
- list_courses: 列出所有课程(课程号、课程名、学分)。
- find_student: 按学号或姓名关键字查找学生。...
- query_student_grades: 查询某学生的全部成绩。...
- query_course_grades: 查询某课程的所有学生成绩。...
- query_student_stats: 查询学生的成绩统计:...
[query_student_grades S001] {
"student": {"id": "S001", "name": "张伟", "class_name": "软件工程 2023级1班"},
"grades": [
{"semester": "2024-秋", "score": 88.0, "course_id": "C001", "course_name": "高等数学", "credits": 5},
{"semester": "2024-秋", "score": 92.0, "course_id": "C002", "course_name": "数据结构", "credits": 4},
...
]
}
5.3 HTTP 模式端到端验证
$ curl -s http://127.0.0.1:8000/health
{"status":"ok","service":"grade-mcp-server"}
$ hermes mcp test grades
Transport: HTTP → http://127.0.0.1:8000/mcp
Auth: none
✓ Connected (47ms)
✓ Tools discovered: 6
5.4 在 Hermes 会话里真实调用
切换成 HTTP 后,直接在对话中让 Agent 查所有学生成绩统计,5 个并行查询全部返回:
学号 姓名 班级 课程数 平均分 加权均分 最低 最高
S005 陈静 人工智能 2023级3班 3 92.67 92.67 89.0 96.0
S003 王强 计算机科学 2023级2班 3 91.33 91.67 88.0 95.0
S001 张伟 软件工程 2023级1班 4 88.75 88.63 85.0 92.0
S002 李娜 软件工程 2023级1班 3 78.67 78.46 76.0 81.0
S004 刘洋 计算机科学 2023级2班 3 68.33 68.31 65.0 72.0
5.5 验证工具链
用 hermes verify 跑完整验证链:bootstrap → test → 启动服务 → 健康检查:
{"recipe": "grade-mcp-server (uv + pytest)", "ok": true,
"phases": [{"phase": "bootstrap", "ok": true},
{"phase": "test", "ok": true, "outputTail": "9 passed in 0.03s"}],
"readiness": {"ready": true, "statusCode": 200}}
5.6 踩坑记录
坑 1:mcp 2.0 API 大改,导入直接失败
现象:装最新版 mcp==2.0.0 后,from mcp.server.fastmcp import FastMCP 报 ModuleNotFoundError。
原因:mcp 2.0 移除了 mcp.server.fastmcp 模块,改用新的 MCPServer API(mcp.server.mcpserver),且 stdio 握手协议与 1.x 不兼容——Hermes 内置客户端是 1.28.1,两端协议对不上,连接直接 Connection closed。
修复:锁定 mcp>=1.28,<2,代码改回 FastMCP API。手动 spawn 正常、但 Hermes 连不上,是排查这个问题的关键信号——两端版本不一致。
坑 2:hermes mcp add 的 --args 会吞掉后面的选项
现象:执行 hermes mcp add grades --command python --args -m grade_mcp.server --connect-timeout 30,server 报 unrecognized arguments: --connect-timeout 30。
原因:--args 是贪婪参数,会吃掉它后面所有 token,--connect-timeout 被当成 server 的启动参数传进去了。
修复:--connect-timeout 必须放在 --args 之前:
hermes mcp add grades --command <python> --connect-timeout 30 --args -m grade_mcp.server
坑 3:交互式确认被管道输入误答,生成脏配置
现象:用 printf 'y' | hermes mcp add ... 喂交互提示,配置里多出 headers: Authorization: Bearer y(真实值就是 “y”),.env 里多了 MCP_GRADES_API_KEY=y。
原因:add 流程里还有认证相关提示,一个 y 被误认为“使用 header 认证”。
修复:清掉假 token,headers 设为 {}。注意 hermes config set mcp_servers.grades.headers '{}' 会存成字符串,导致 test 崩('str' object has no attribute 'items'),要用 YAML 结构写入真正的空对象。
共性教训:给 CLI 的交互式流程喂管道输入,先确认提示顺序和数量;工具链版本(尤其协议类 SDK)先对齐再开发,省掉一整轮排查。
六、总结说明
这次实践把“开发一个 MCP Server 并接入 Agent”的完整链路走通了:FastMCP 一份代码支持 stdio 和 HTTP 双传输,SQLite 做数据层,接入 Hermes 后对话里直接查成绩。
核心收获:
- MCP 的价值在于协议标准化——工具定义、发现、调用全部标准化,Server 与 Client 解耦,换 Agent 框架不用重写工具。
- 传输模式要按场景选——本地开发用 stdio(免运维、免端口),远程部署用 StreamableHTTP(独立进程、可扩展),一份代码两行切换。
- 工具 docstring 就是 AI 的 API 文档——写清楚参数格式和返回结构,模型调用准确率明显提升,这是 MCP 开发区别于传统 API 开发的地方。
- 版本对齐是第一优先级——协议类 SDK 大版本之间不兼容,开发前先确认 Client 端版本,能省一整轮排障。
边界:当前 Server 是只读查询,没有写权限和鉴权;SQLite 只适合单机/内网,生产接真实教务系统时建议换 MySQL/PostgreSQL 并加 OAuth 认证。后续方向:接真实数据源、加写入工具、部署到云服务器(Docker/systemd 方案已就绪)。
更多推荐



所有评论(0)