前言
2024年底 Anthropic 发布了 MCP(Model Context Protocol),短短几个月内 GitHub 星标突破 8 万。这个协议解决了一个核心问题:如何让大模型标准化地连接外部工具和数据源。
本文将从协议设计原理出发,手把手带你实现一个完整的 MCP Server,并讨论生产环境中的架构选型。
一、MCP 解决了什么问题?
1.1 背景:AI 应用的"最后一公里"
大模型的能力已经很强,但实际落地时总绕不开这几个问题:
- 如何让 LLM 调用我的数据库? ------ 写个 API 包装层
- 如何让 LLM 读取本地文件? ------ 再写个文件读取接口
- 如何让 LLM 操作浏览器? ------ 又得写一套 Puppeteer 封装
结果就是每个 AI 应用都在重复造轮子,而且每个工具的接入方式都不一样。
1.2 MCP 的核心思路
MCP 的思路很简单:定义一套统一的 Client-Server 通信协议,类似 USB-C 的角色------只要你的设备(工具)支持这个接口,任何主机(LLM 客户端)都能即插即用。
二、协议详解
2.1 三种传输方式
| 传输方式 | 协议 | 适用场景 | 延迟 |
|---|---|---|---|
| stdio | 标准输入输出 | 本地工具、CLI 场景 | 最低 |
| streamable-http | HTTP + 流式响应 | 远程服务、微服务架构 | 中等 |
| SSE | Server-Sent Events | 服务端推送场景 | 中等 |
stdio 是最常用的模式。通信过程如下:
Client 启动 Server 子进程
↓ 通过 stdin/stdout 通信
Client 发送 JSON-RPC 请求 → stdin → Server 处理 → stdout → 返回 JSON-RPC 响应
2.2 JSON-RPC 2.0 消息格式
MCP 底层使用 JSON-RPC 2.0 作为消息编码格式:
json
// 请求
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}
// 响应
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [{
"name": "search_files",
"description": "在项目目录中搜索匹配的文件",
"inputSchema": {
"type": "object",
"properties": {
"pattern": { "type": "string", "description": "搜索模式" }
},
"required": ["pattern"]
}
}]
}
}
2.3 核心能力类型
MCP 定义了四种核心能力:
Tools(工具)------ 最常用,让 LLM 调用自定义函数
Resources(资源)------ 结构化数据访问接口
Prompts(提示词模板)------ 预定义模板,用户快速填充使用
Sampling(采样)------ 允许 Server 反向请求 LLM 能力(如自动生成摘要)
三、动手实现完整 MCP Server
下面实现一个项目管理 MCP Server,包含任务管理、文档检索和代码分析三个模块。
3.1 项目结构
project-mcp/
├── server.py # 主入口
├── tools/
│ ├── tasks.py # 任务管理
│ ├── docs.py # 文档检索
│ └── code.py # 代码分析
└── config.json # 配置示例
3.2 核心代码
python
# server.py
from mcp.server.fastmcp import FastMCP
from tools.tasks import *
from tools.docs import *
from tools.code import *
mcp = FastMCP("project-assistant")
register_task_tools(mcp)
register_doc_tools(mcp)
register_code_tools(mcp)
if __name__ == "__main__":
mcp.run(transport="stdio")
python
# tools/tasks.py --- 任务管理
import json
from datetime import datetime
from pathlib import Path
TASKS_FILE = Path(".mcp_tasks.json")
def _load_tasks():
if TASKS_FILE.exists():
return json.loads(TASKS_FILE.read_text(encoding="utf-8"))
return {"tasks": [], "next_id": 1}
def _save_tasks(data):
TASKS_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
def register_task_tools(mcp):
@mcp.tool()
def task_create(title: str, priority: str = "medium", tags: str = "") -> str:
"""创建新任务。priority: low/medium/high/critical"""
data = _load_tasks()
task = {
"id": data["next_id"],
"title": title,
"priority": priority,
"tags": [t.strip() for t in tags.split(",") if t.strip()],
"status": "todo",
"created_at": datetime.now().isoformat()
}
data["tasks"].append(task)
data["next_id"] += 1
_save_tasks(data)
return f"✅ 任务已创建 [#{task['id']}] {title} ({priority})"
@mcp.tool()
def task_list(status: str = "", tag: str = "") -> str:
"""列出任务,可按状态和标签过滤。status: todo/doing/done"""
data = _load_tasks()
tasks = data["tasks"]
if status:
tasks = [t for t in tasks if t["status"] == status]
if tag:
tasks = [t for t in tasks if tag in t.get("tags", [])]
if not tasks:
return "📭 没有找到匹配的任务"
lines = [f"{'ID':<6}{'状态':<8}{'优先级':<10}{'标题'}"]
lines.append("-" * 50)
for t in tasks:
lines.append(f"#{t['id']:<5}{t['status']:<8}{t['priority']:<10}{t['title']}")
return "\n".join(lines)
@mcp.tool()
def task_update(task_id: int, status: str = None, title: str = None) -> str:
"""更新任务状态或标题"""
data = _load_tasks()
for t in data["tasks"]:
if t["id"] == task_id:
if status: t["status"] = status
if title: t["title"] = title
t["updated_at"] = datetime.now().isoformat()
_save_tasks(data)
return f"✅ 任务 #{task_id} 已更新"
return f"❌ 未找到 ID={task_id} 的任务"
python
# tools/docs.py --- 文档检索
from pathlib import Path
def register_doc_tools(mcp):
@mcp.tool()
def doc_search(keyword: str, directory: str = ".") -> str:
"""在目录中搜索包含关键词的 Markdown 文档"""
results = []
dir_path = Path(directory)
for md_file in dir_path.rglob("*.md"):
try:
content = md_file.read_text(encoding="utf-8")
matches = [(i+1, line.rstrip())
for i, line in enumerate(content.split('\n'))
if keyword.lower() in line.lower()]
if matches:
results.append(f"\n📄 {md_file.relative_to(dir_path)}")
for line_no, line_text in matches[:3]:
results.append(f" L{line_no}: {line_text[:100]}")
except Exception:
continue
if not results:
return f'未找到包含 "{keyword}" 的文档'
return "找到相关文件:" + "\n".join(results[:20])
@mcp.resource("docs://readme")
def get_readme() -> str:
"""返回项目的 README 文档"""
readme = Path("README.md")
return readme.read_text(encoding="utf-8") if readme.exists() else "README.md 不存在"
python
# tools/code.py --- 代码分析
import subprocess
from pathlib import Path
def register_code_tools(mcp):
@mcp.tool()
def code_stats(directory: str = ".", language: str = "") -> str:
"""统计目录下的代码行数,可按语言过滤"""
ext_map = {
"python": [".py"], "javascript": [".js", ".jsx", ".ts", ".tsx"],
"go": [".go"], "rust": [".rs"], "java": [".java"]
}
dir_path = Path(directory)
stats = {"files": 0, "total_lines": 0, "by_ext": {}}
target_exts = ext_map.get(language.lower(),
[ext for exts in ext_map.values() for ext in exts])
for ext in target_exts:
count = 0; lines = 0
for f in dir_path.rglob(f"*{ext}"):
try:
c = f.read_text(encoding="utf-8")
count += 1; lines += len(c.splitlines())
except: pass
if count > 0:
stats["by_ext"][ext] = {"files": count, "lines": lines}
stats["files"] += count; stats["total_lines"] += lines
result = [f"📊 代码统计 ({directory})"]
result.append(f"总文件数: {stats['files']}, 总行数: {stats['total_lines']}")
for ext, info in sorted(stats["by_ext"].items(), key=lambda x: -x[1]["lines"]):
result.append(f" {ext}: {info['files']} 文件, {info['lines']} 行")
return "\n".join(result)
@mcp.tool()
def git_log(count: int = 5) -> str:
"""查看最近的 Git 提交记录"""
try:
r = subprocess.run(
["git", "log", f"-{count}", "--pretty=format:%h %s (%an, %ar)"],
capture_output=True, text=True, encoding="utf-8"
)
return r.stdout if r.returncode == 0 else "当前目录不是 Git 仓库"
except FileNotFoundError:
return "Git 未安装"
3.3 配置与运行
json
{
"mcpServers": {
"project-assistant": {
"command": "python",
"args": ["server.py"],
"cwd": "/your/project/path"
}
}
}
四、架构设计与生产实践
4.1 什么时候该用 MCP?
适合:
- 多客户端复用工具集
- 工具逻辑复杂,需独立维护
- 团队共享工具能力
不适合:
- 一次性简单脚本(直接 Function Calling)
- 对延迟极敏感
- 需要复杂双向流式交互
4.2 多 Server 架构
按职责拆分 Server:文件操作走 stdio、数据库查询走 HTTP、浏览器控制走 SSE。每个 Server 职责单一,Server 之间不直接通信,都通过 Client 中转。
4.3 错误处理要点
生产环境必须做好:
- 输入校验:防止注入攻击(SQL 注入、命令注入、路径穿越)
- 异常捕获:区分不同错误类型返回友好提示
- 权限控制:危险操作需要额外确认机制
- 日志记录:完整的操作链路用于审计和调试
4.4 性能优化
| 问题 | 方案 |
|---|---|
| 冷启动慢 | 常驻进程 / preload |
| 大数据传输 | 分页 + 流式返回 |
| 频繁重复调用 | 结果缓存(TTL) |
| 工具列表过长 | 按需加载 / 分类注册 |
五、与其他方案对比
| 特性 | MCP | LangChain Tools | Function Calling |
|---|---|---|---|
| 标准化程度 | 开放规范 | 框架内部标准 | 各家自实现 |
| 跨客户端兼容 | ✅ 多客户端通用 | ❌ 仅 LangChain | ❌ 单一 API |
| 独立部署 | ✅ 独立进程 | ❌ 同进程 | ❌ 内嵌 |
选择建议:
- 快速原型 → Function Calling
- 需要 Agent 编排 → LangChain Tools
- 跨平台复用 / 长期维护 → MCP
六、常见问题 FAQ
Q: MCP 和 RAG 是什么关系?
互补关系。RAG 解决"给 LLM 提供知识",MCP 解决"给 LLM 提供能力"。可以用 MCP Server 封装 RAG 检索逻辑。
Q: 一个 Client 能连多少个 Server?
理论上无上限,建议控制在 10 个以内。Server 过多会增加 token 开销和管理复杂度。
Q: 如何调试 MCP Server?
大多数 SDK 支持
--log-level debug模式查看完整 JSON-RPC 消息流。官方提供mcp-inspector可视化调试工具。
Q: 支持流式输出吗?
streamable-http 和 stdio 都支持增量输出,适合长时间运行的工具(编译、大数据查询)。
总结
MCP 的价值不在于技术有多复杂,而在于它提供了一个简单但足够好的标准。当整个社区遵循同一套协议时,生态的力量就会显现------你写的工具可以被任何兼容的 LLM 客户端使用,别人写的工具你也可以直接拿来用。
对于开发者来说,现在投入时间学习 MCP 是高性价比的选择:协议不难掌握,但先发优势会让你在 AI 应用开发领域占据有利位置。
本文基于 MCP 协议规范编写,代码示例经过实测可用。如有问题欢迎评论区讨论。