记录我在学习 Model Context Protocol (MCP) 过程中的思考、疑问和收获
前言
最近在开发 AgentCrew 项目时,第一次深入接触了 MCP(Model Context Protocol)。作为一个优化"氛围式编程"(Vibe Coding)反馈循环的工具,我需要让 Cursor 能够通过 MCP 协议与我的 Agent 系统交互。
在这个过程中,我遇到了很多疑问,也学到了很多。这篇文章记录了我的学习历程,希望能帮助到同样在学习 MCP 的朋友。
第一个困惑:MCP 到底是什么?
最初的误解
刚开始,我以为 MCP 就是一个 npm 包,就像其他工具库一样,安装就能用。但当我看到配置文件中需要指定 index.js 路径时,我困惑了:
json
{
"mcpServers": {
"agentcrew": {
"command": "node",
"args": ["D:\\Code\\agentCrew\\src\\index.js"]
}
}
}
为什么需要文件路径?为什么不能像其他工具一样直接使用?
理解:MCP 是协议,不是包
经过深入学习,我明白了:
MCP = Model Context Protocol(模型上下文协议)
- MCP 是协议标准,类似 HTTP 协议
- MCP 服务器是实现,类似 HTTP 服务器(Express、Nginx 等)
- npm 包只是分发方式,不是 MCP 的本质
类比理解:
HTTP 协议(标准)
↓
HTTP 服务器(实现)
- Express(npm 包)
- Nginx(系统服务)
- Apache(系统服务)
MCP 协议(标准)
↓
MCP 服务器(实现)
- @modelcontextprotocol/server-github(npm 包)
- src/index.js(本地文件)
- https://api.example.com/mcp(远程服务)
这个理解让我豁然开朗:MCP 是一个开放标准,任何人都可以实现自己的 MCP 服务器。
第二个困惑:为什么需要 index.js 路径?
疑问
既然 MCP 是协议,为什么配置文件中还需要指定本地文件路径?这让我觉得 MCP 似乎只能本地使用。
理解:stdio 传输方式
原来,MCP 支持多种传输方式:
-
stdio(标准输入输出) - 本地进程
- 配置:
command+args - 适用:本地开发的 MCP 服务器
- 配置:
-
SSE(Server-Sent Events) - 远程 HTTP 服务
- 配置:
url - 适用:第三方提供的远程服务
- 配置:
-
WebSocket - 实时通信
- 配置:
url(wss://) - 适用:需要双向实时通信的场景
- 配置:
AgentCrew 使用 stdio 方式:
- Cursor 启动本地 Node.js 进程
- 通过 stdin/stdout 通信
- 不需要网络端口
- 数据不离开本地(隐私更好)
工作流程:
Cursor 启动
↓
执行:node D:\Code\agentCrew\src\index.js
↓
启动 MCP 服务器进程
↓
通过 stdin/stdout 通信
第三个困惑:为什么不用简单的 HTTP RPC?
疑问
既然 LLM 是同步等待工具结果的,为什么不直接用简单的 HTTP RPC?每次调用一个 API,等待结果,继续生成。这样不是更简单吗?
理解:长连接的优势
虽然简单场景下,HTTP RPC 确实更简单,但 MCP 选择长连接有更深层的原因:
1. 流式响应
HTTP RPC:
调用工具
↓
[等待 30 秒...]
↓
返回完整结果
MCP 长连接:
调用工具
↓
工具: "开始处理..."(立即返回)
工具: "已处理 50%..."(推送进度)
工具: "完成"(最终结果)
2. 会话保持
HTTP RPC:
- 每次请求都是独立的
- 需要客户端管理状态
- 需要重复传递上下文
MCP 长连接:
- 会话状态自动维护
- 工具调用之间共享上下文
- 不需要重复传递
3. 统一协议
即使简单工具不需要复杂特性,统一协议也便于:
- 扩展和管理
- 未来添加新功能
- 统一接口
类比: HTTP/1.1 也可以工作,但 HTTP/2 提供了更多能力。统一协议更有利于扩展。
第四个困惑:MCP vs Function Call
疑问
大模型一般还提供一个能力:function call 是支持 RPC 的。MCP 和 function call 的定位不一样吗?为什么 GitHub 这种调用不走 function call 而要走 MCP?
理解:两种不同的能力
Function Call(函数调用):
- LLM 内置能力
- 简单的 HTTP REST API 调用
- 适合简单的请求-响应场景
- 无状态,每次调用独立
MCP(Model Context Protocol):
- 标准化协议
- 支持复杂的交互模式
- 支持流式响应、会话保持、主动推送
- 完整的上下文管理
对比表:
| 特性 | Function Call | MCP |
|---|---|---|
| 定位 | LLM 内置能力 | 标准化协议 |
| 流式响应 | ❌ 不支持 | ✅ 支持 |
| 会话保持 | ❌ 无状态 | ✅ 有状态 |
| 主动推送 | ❌ 不支持 | ✅ 支持 |
| 标准化 | ❌ 各厂商不同 | ✅ 统一标准 |
为什么 GitHub 选择 MCP?
- 统一标准:所有工具使用统一协议
- 未来扩展:支持更复杂的交互模式
- 更好集成:统一的接口和管理
- 会话管理:支持多步骤操作
两者的关系:
- 不是替代关系,而是互补关系
- Function Call 适合简单的 API 调用
- MCP 适合复杂的工具交互
第五个困惑:为什么不用 YAML 配置文件?
疑问
既然 MCP 服务器可以通过 npm 包提供,为什么不用 YAML 等配置文件来定义工具能力?这样不是更简单吗?
理解:协议 vs 配置文件
YAML 是配置文件格式,MCP 是通信协议!
YAML 的局限性:
- ❌ 无法实时交互
- ❌ 无法流式响应
- ❌ 无法双向通信
- ❌ 无法会话管理
- ❌ 无法动态执行
MCP 协议的优势:
- ✅ 实时交互
- ✅ 流式响应
- ✅ 双向通信
- ✅ 会话管理
- ✅ 动态执行(执行代码、访问数据库等)
类比理解:
YAML(类似 API 文档):
yaml
# tools.yaml
tools:
- name: parse_prd
description: 解析 PRD
MCP(类似 HTTP 协议):
javascript
// MCP 服务器实现
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'parse_prd') {
// 执行解析逻辑
const result = await parsePRD(request.params.args);
// 流式返回
return {
content: [
{ type: 'text', text: '开始解析...' },
{ type: 'text', text: '解析完成' },
{ type: 'text', text: JSON.stringify(result) }
]
};
}
});
关键理解:
- YAML 是静态配置,无法执行
- MCP 是动态协议,可以执行
- 配置文件无法替代协议
第六个困惑:MCP 支持哪些数据格式?
疑问
通过 stdio 使用 MCP 时,协议支持哪些形式的数据提供给 agent?比如流式数据?或者是 JSON 形式的结果数据?
理解:MCP 的数据格式
基本响应格式:
typescript
{
content: Array<{
type: 'text' | 'image' | 'resource',
text?: string, // 文本内容
data?: string, // Base64 编码的数据(用于图片等)
mimeType?: string, // MIME 类型
uri?: string // 资源 URI
}>,
isError?: boolean // 是否为错误响应
}
支持的格式:
-
JSON 格式(推荐)
javascript{ content: [{ type: 'text', text: JSON.stringify({ session_id: 'default', data: { ... } }, null, 2) }] } -
纯文本格式
javascript{ content: [{ type: 'text', text: '纯文本内容' }] } -
流式响应(模拟)
javascript{ content: [ { type: 'text', text: '步骤1...' }, { type: 'text', text: '步骤2...' }, { type: 'text', text: '完成' } ] }
注意: 在 stdio 传输中,所有 content 项会一次性返回。真正的流式推送需要 SSE 或 WebSocket。
实际代码示例(AgentCrew):
javascript
// src/index.js - handleParsePRD 方法
async handleParsePRD(prdContent, sessionId) {
const parsed = await this.requirementParser.parse(prdContent);
return {
content: [
{
type: 'text',
text: JSON.stringify({
session_id: sessionId,
requirements: parsed,
summary: {
features_count: parsed.features.length,
user_stories_count: parsed.userStories.length,
}
}, null, 2) // 格式化 JSON,缩进 2 空格
}
]
};
}
Transformer 架构的影响
理解 LLM 的行为
在学习过程中,我深入了解了 Transformer 架构,这帮助我理解了为什么 MCP 这样设计:
Transformer 的核心特点:
- 顺序生成:token 按顺序生成
- 上下文依赖:每个 token 依赖前面的所有 token
- 同步等待:必须等待前一个 token 才能生成下一个
工具调用时的行为:
LLM 生成: "我将调用 GitHub 工具"
↓
生成工具调用 token: { "tool": "github_search" }
↓
【暂停生成】← Transformer 停止生成新 token
↓
等待工具结果
↓
收到结果后,继续生成: "根据查询结果..."
关键理解:
- LLM 确实是同步的
- 工具调用时确实会暂停生成
- 必须等待工具结果才能继续
流式响应时 LLM 的行为:
- LLM 会一直等待直到收到最终结果
- LLM 不会异步处理其他内容
- 但可以逐步接收流式消息,提供更好的反馈
这个理解让我明白了为什么 MCP 需要支持流式响应:即使 LLM 是同步等待的,流式响应也能提供更好的用户体验。
实际开发中的收获
1. 会话管理的重要性
在开发 AgentCrew 时,我实现了会话管理功能:
javascript
// 会话存储
this.sessions = new Map();
// 获取或创建会话
getSession(sessionId) {
if (!this.sessions.has(sessionId)) {
this.sessions.set(sessionId, {
id: sessionId,
requirements: null,
tasks: null,
generatedCode: [],
feedbackHistory: [],
createdAt: new Date().toISOString(),
});
}
return this.sessions.get(sessionId);
}
这让我深刻理解了 MCP 会话管理的价值:
- 多步骤操作可以共享上下文
- 不需要重复传递数据
- 更好的用户体验
2. 数据格式的选择
在实现过程中,我选择了 JSON 格式作为主要数据格式:
javascript
return {
content: [{
type: 'text',
text: JSON.stringify(data, null, 2)
}]
};
原因:
- ✅ LLM 可以轻松解析
- ✅ 结构清晰
- ✅ 易于扩展
- ✅ 格式化后更易读
3. 错误处理
MCP 协议支持错误标记:
javascript
return {
content: [{
type: 'text',
text: JSON.stringify({
error: true,
message: error.message
}, null, 2)
}],
isError: true // 标记为错误
};
这让我意识到协议设计的人性化:不仅支持成功响应,还明确支持错误响应。
学习过程中的思考
1. 协议设计的智慧
MCP 的设计体现了几个重要的设计原则:
统一性:
- 所有工具使用统一的协议
- 便于管理和扩展
- 降低学习成本
灵活性:
- 支持多种传输方式(stdio/SSE/WebSocket)
- 支持多种数据格式(text/image/resource)
- 适应不同场景
可扩展性:
- 协议层和实现层分离
- 易于添加新功能
- 未来兼容性好
2. 为什么需要协议?
在学习过程中,我反复思考:为什么需要协议?为什么不能直接用配置文件?
我的理解:
配置文件(YAML):
- 静态定义
- 无法执行
- 无法交互
协议(MCP):
- 动态交互
- 可以执行
- 实时通信
类比:
- API 文档 vs API 协议
- 函数签名 vs 函数实现
- HTML vs HTTP
协议定义了"如何交互",而不仅仅是"有什么能力"。
3. 长连接 vs 短连接
为什么 MCP 选择长连接而不是简单的 HTTP RPC?
我的理解:
虽然简单场景下,HTTP RPC 确实更简单,但长连接提供了:
- 流式响应(更好的用户体验)
- 会话保持(多步骤操作)
- 主动推送(实时反馈)
- 低延迟(连接复用)
类比: HTTP/1.1 vs HTTP/2,虽然 HTTP/1.1 可以工作,但 HTTP/2 提供了更多能力。
总结:我的 MCP 学习收获
核心理解
-
MCP 是协议标准,不是 npm 包
- 类似 HTTP 协议
- 可以有多种实现方式
- 开放标准,任何人都可以实现
-
MCP 支持多种传输方式
- stdio(本地进程)
- SSE(远程 HTTP 服务)
- WebSocket(实时通信)
-
MCP 的优势
- 流式响应
- 会话保持
- 双向通信
- 统一标准
-
MCP vs Function Call
- 不是替代关系,而是互补关系
- Function Call 适合简单场景
- MCP 适合复杂场景
-
协议 vs 配置文件
- 配置文件是静态的
- 协议是动态的
- 协议可以执行,配置文件不能
实际应用
在开发 AgentCrew 的过程中,MCP 让我能够:
- 实现复杂的多步骤工作流
- 维护会话状态
- 提供流式反馈
- 统一接口管理
未来思考
学习 MCP 让我思考了几个问题:
-
协议设计的重要性
- 好的协议设计可以大大简化开发
- 统一标准降低学习成本
- 可扩展性很重要
-
AI 交互的未来
- MCP 为 AI 交互提供了标准化方案
- 未来可能会有更多类似的协议
- 协议设计需要考虑 AI 的特殊性
-
开发者体验
- MCP 的设计考虑了开发者体验
- 支持多种传输方式,适应不同场景
- 错误处理和流式响应都很人性化
写在最后
学习 MCP 的过程让我深刻理解了协议设计的重要性。一个好的协议不仅要功能强大,还要:
- 易于理解
- 易于实现
- 易于扩展
- 考虑实际使用场景
MCP 的设计体现了这些原则,这也是为什么它能够成为 AI 交互的标准协议。
希望这篇文章能够帮助到同样在学习 MCP 的朋友。如果你也有疑问,欢迎一起讨论!
相关资源:
- MCP 协议规范
- MCP SDK 文档
- MCP 知识 Blog - 详细的技术文档
- MCP 数据格式详解 - 数据格式说明