MCP 是什么,为什么需要它
MCP(Model Context Protocol)是 Anthropic 在 2024 年底开源的一套协议,目标是解决一个核心矛盾:AI 大模型能力很强,但它被锁在自己的上下文窗口里,没有办法主动访问外部世界。
过去,让 AI 调用外部工具的方式五花八门——有的直接拼 prompt,有的自己造 Function Calling 的格式,有的写一堆 wrapper。每家工具、每个 AI 平台各有各的接法,生态碎片化严重。
MCP 做的事情,是把"AI 调用工具"这件事标准化:定义统一的协议格式,让任何 AI 客户端都能用同一套接口与任何工具服务器通信,就像 HTTP 统一了 Web 通信一样。
一句话定义:MCP 是 AI 模型与外部工具/数据源之间的通信协议,基于 JSON-RPC 2.0,通过标准化的"工具声明 + 调用 + 返回"三步完成交互。
MCP 解决的核心问题
- 标准化:工具只需实现一次 MCP 接口,所有兼容 MCP 的 AI 客户端都能用
- 解耦:AI 模型和工具各自独立演进,互不依赖实现细节
- 可组合:多个 MCP Server 可以叠加,给 AI 配置不同能力组合
MCP 生态里有两个角色:
- MCP Client:集成在 AI 应用里,负责发起请求、解析结果(比如 Claude Desktop、Cursor)
- MCP Server:我们要写的,负责注册工具、处理调用、返回结果
协议核心:JSON-RPC 2.0 + 三个方法
MCP 建立在 JSON-RPC 2.0 之上。所有消息都是这个格式:
// 请求
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { ... }
}
// 响应
{
"jsonrpc": "2.0",
"id": 1,
"result": { ... }
}
// 错误响应
{
"jsonrpc": "2.0",
"id": 1,
"error": { "code": -32603, "message": "Internal error" }
}
MCP Server 最少需要实现三个方法:
| 方法 | 触发时机 | 必须返回 |
|---|---|---|
initialize | 握手阶段,连接建立后第一个请求 | 协议版本、服务信息、能力声明 |
tools/list | 客户端发现工具 | 工具列表(名称、描述、参数 schema) |
tools/call | AI 决定调用某个工具 | 执行结果,content 数组格式 |
这三个方法就是 MCP Server 的最小实现面。理解了这个,后面所有代码都是围绕这三个方法展开的。
工具描述格式
每个工具用 JSON Schema 描述自己的输入参数,这是 AI 理解"这个工具要怎么用"的依据:
{
"name": "get_weather",
"description": "获取指定城市的天气信息。当用户询问天气时使用。",
"inputSchema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,例如:北京、上海"
}
},
"required": ["city"]
}
}
description 决定 AI 是否会调用这个工具,inputSchema 决定 AI 传什么参数。两者都要写清楚。
传输层的三种形态
MCP 协议本身不限定传输层,消息可以通过不同的通道传输。实际上有三种主流形态:
三种形态的协议消息格式完全相同,只是传输通道不同。下面逐一实现。
实现一:stdio 版(最小可用)
stdio 版是最纯粹的 MCP 实现,去掉一切网络层,只剩协议本身。非常适合理解 MCP 核心逻辑。
整体结构
Server 监听 stdin 的每一行,每行是一个 JSON-RPC 请求;处理完后把 JSON-RPC 响应写到 stdout,每个响应一行。stderr 用来输出日志,不干扰协议通信。
const readline = require('readline');
class MCPServer {
constructor() {
// 注册工具列表
this.tools = [
{
name: 'get_weather',
description: '获取指定城市的天气信息。当用户询问天气时使用。',
inputSchema: {
type: 'object',
properties: {
city: { type: 'string', description: '城市名称,如:北京、上海' }
},
required: ['city']
}
},
{
name: 'calculate',
description: '执行数学计算。支持 add/subtract/multiply/divide。',
inputSchema: {
type: 'object',
properties: {
operation: {
type: 'string',
enum: ['add', 'subtract', 'multiply', 'divide']
},
a: { type: 'number' },
b: { type: 'number' }
},
required: ['operation', 'a', 'b']
}
}
];
}
// 路由 JSON-RPC 方法
async handleRequest(request) {
const { method, params } = request;
try {
switch (method) {
case 'initialize':
return {
protocolVersion: '2024-11-05',
serverInfo: { name: 'my-mcp-server', version: '1.0.0' },
capabilities: { tools: {} }
};
case 'tools/list':
return { tools: this.tools };
case 'tools/call':
const result = await this.executeTool(params.name, params.arguments);
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
};
default:
throw { code: -32601, message: `Method not found: ${method}` };
}
} catch (err) {
// 区分 MCP 错误对象和普通 Error
const error = err.code ? err : { code: -32603, message: err.message };
return { error };
}
}
// 工具执行逻辑
async executeTool(name, args) {
switch (name) {
case 'get_weather':
return this.getWeather(args.city);
case 'calculate':
return this.calculate(args.operation, args.a, args.b);
default:
throw new Error(`Unknown tool: ${name}`);
}
}
getWeather(city) {
const data = {
'北京': { temperature: '15°C', condition: '晴天', humidity: '45%' },
'上海': { temperature: '18°C', condition: '多云', humidity: '60%' },
'深圳': { temperature: '25°C', condition: '阵雨', humidity: '80%' },
};
return { city, ...(data[city] || { temperature: '--', condition: '暂无数据' }) };
}
calculate(op, a, b) {
const ops = { add: a+b, subtract: a-b, multiply: a*b, divide: b===0 ? null : a/b };
if (b === 0 && op === 'divide') throw new Error('除数不能为 0');
return { expression: `${a} ${op} ${b}`, result: ops[op] };
}
// 启动:监听 stdin 逐行处理
start() {
console.error('[MCP] Server started, waiting for requests...');
const rl = readline.createInterface({
input: process.stdin,
terminal: false
});
rl.on('line', async (line) => {
if (!line.trim()) return;
let request;
try {
request = JSON.parse(line);
} catch {
// JSON 解析失败,返回 parse error
process.stdout.write(JSON.stringify({
jsonrpc: '2.0', id: null,
error: { code: -32700, message: 'Parse error' }
}) + '\n');
return;
}
const result = await this.handleRequest(request);
// 响应里带上原始请求的 id
const response = { jsonrpc: '2.0', id: request.id ?? null, ...result };
process.stdout.write(JSON.stringify(response) + '\n');
});
}
}
new MCPServer().start();
关键细节
- 每条消息占一行,换行符是消息边界,不要在消息内容中用真实换行
- 日志写 stderr,不能写 stdout(会污染协议流)
- 响应必须带上请求的
id,客户端靠 id 对应请求和响应 initialize的protocolVersion用最新的2024-11-05
配置到 Claude Desktop
编辑 ~/Library/Application Support/Claude/claude_desktop_config.json(Mac):
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/absolute/path/to/mcp-server.js"]
}
}
}
重启 Claude Desktop,你的工具就出现在 AI 的工具列表里了。
实现二:HTTP 版(对外暴露)
stdio 只能本地用。如果要让多个客户端共享一个 MCP Server、或者部署到远端服务器,就需要 HTTP 版。
核心思路:把 JSON-RPC 消息放进 HTTP POST 的 body,Server 解析后处理,结果通过 HTTP response 返回。
const http = require('http');
class MCPHTTPServer {
constructor() {
// 工具定义与 stdio 版相同,省略
this.tools = [ /* ... */ ];
}
async handleRequest(request) {
// 与 stdio 版完全相同的路由逻辑
// ...
}
start(port = 3000, host = '127.0.0.1') {
const server = http.createServer(async (req, res) => {
// CORS 预检
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
// 健康检查
if (req.url === '/health' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', timestamp: new Date().toISOString() }));
return;
}
// MCP 主端点
if (req.url === '/mcp' && req.method === 'POST') {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', async () => {
let request;
try {
request = JSON.parse(body);
} catch {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
jsonrpc: '2.0', id: null,
error: { code: -32700, message: 'Parse error' }
}));
return;
}
const result = await this.handleRequest(request);
const response = { jsonrpc: '2.0', id: request.id ?? null, ...result };
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(response));
});
return;
}
res.writeHead(404);
res.end('Not Found');
});
// 绑定 127.0.0.1,让 Nginx 反代,不直接暴露公网
server.listen(port, host, () => {
console.log(`MCP HTTP Server listening on ${host}:${port}`);
});
}
}
new MCPHTTPServer().start(3000, '127.0.0.1');
用 curl 测试
# 列出工具
curl -X POST http://localhost:3000/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
# 调用工具
curl -X POST http://localhost:3000/mcp \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": { "city": "北京" }
}
}'
Nginx 反代配置
不要直接把 MCP Server 端口暴露到公网。用 Nginx 做反代,统一走 HTTPS:
server {
listen 443 ssl;
server_name mcp.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/mcp.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mcp.yourdomain.com/privkey.pem;
# 可选:简单的 Bearer Token 鉴权
# if ($http_authorization != "Bearer your-secret-token") {
# return 401;
# }
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
实现三:SSE 版(支持流式推送)
HTTP 版是请求-响应模式,工具执行完才返回。对于耗时长的工具(比如:大文件处理、实时爬取、进度跟踪),用户等待体验很差。
SSE 版在 HTTP 基础上加入 Server-Sent Events 长连接,Server 可以在工具执行过程中主动推送进度事件。
SSE 的核心概念
SSE 是一种单向的长连接:客户端发起一个普通 GET 请求,Server 不关闭连接,持续往里写数据,每条消息格式如下:
event: progress
data: {"step": 1, "total": 5, "message": "正在处理..."}
event: complete
data: {"result": "done"}
每条消息以两个换行结束。客户端用 EventSource API 或 eventsource 包接收。
SSE Server 实现
const http = require('http');
class MCPSSEServer {
constructor() {
this.clients = new Set(); // 维护所有 SSE 连接
this.tools = [ /* ... 同前 */ ];
}
// 向单个 SSE 客户端推送事件
sendEvent(client, event, data) {
if (!client.destroyed) {
client.write(`event: ${event}\n`);
client.write(`data: ${JSON.stringify(data)}\n\n`);
}
}
// 工具执行:支持流式推送
async executeTool(name, args, client) {
switch (name) {
case 'get_weather':
return this.getWeather(args.city);
case 'long_task':
// 有 SSE client 时,边执行边推进度
return this.longTask(args, client);
default:
throw new Error(`Unknown tool: ${name}`);
}
}
async longTask(args, client) {
const steps = args.steps || 5;
for (let i = 1; i <= steps; i++) {
// 推送进度事件给客户端
if (client) {
this.sendEvent(client, 'progress', {
step: i, total: steps,
percent: Math.round((i / steps) * 100)
});
}
// 模拟耗时操作
await new Promise(r => setTimeout(r, 500));
}
return { status: 'completed', steps };
}
// SSE 连接建立
handleSSE(req, res) {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
});
this.clients.add(res);
console.log(`SSE client connected, total: ${this.clients.size}`);
// 发送连接确认
this.sendEvent(res, 'connected', { clientId: Date.now() });
// 心跳,防止代理/负载均衡断掉空闲连接
const heartbeat = setInterval(() => {
this.sendEvent(res, 'heartbeat', { ts: Date.now() });
}, 25000);
req.on('close', () => {
clearInterval(heartbeat);
this.clients.delete(res);
});
}
// HTTP POST 处理 MCP 请求(SSE client 透传给工具)
handlePost(req, res) {
let body = '';
req.on('data', c => body += c);
req.on('end', async () => {
try {
const request = JSON.parse(body);
// 从请求头找对应的 SSE 客户端(按 clientId)
const clientId = req.headers['x-client-id'];
const client = [...this.clients].find(c => c._clientId === clientId) || null;
const result = await this.handleMCPRequest(request, client);
const response = { jsonrpc: '2.0', id: request.id ?? null, ...result };
res.writeHead(200, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
});
res.end(JSON.stringify(response));
} catch (e) {
res.writeHead(400);
res.end(JSON.stringify({ error: { code: -32700, message: e.message } }));
}
});
}
async handleMCPRequest(request, client = null) {
const { method, params } = request;
try {
switch (method) {
case 'initialize':
return {
protocolVersion: '2024-11-05',
serverInfo: { name: 'mcp-sse-server', version: '1.0.0' },
capabilities: { tools: {}, streaming: true }
};
case 'tools/list':
return { tools: this.tools };
case 'tools/call':
const r = await this.executeTool(params.name, params.arguments, client);
return { content: [{ type: 'text', text: JSON.stringify(r, null, 2) }] };
default:
throw { code: -32601, message: `Method not found: ${method}` };
}
} catch (err) {
return { error: err.code ? err : { code: -32603, message: err.message } };
}
}
start(port = 3001, host = '127.0.0.1') {
const server = http.createServer((req, res) => {
if (req.method === 'OPTIONS') {
res.writeHead(204, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, X-Client-Id'
});
res.end();
return;
}
if (req.url === '/events' && req.method === 'GET')
return this.handleSSE(req, res);
if (req.url === '/mcp' && req.method === 'POST')
return this.handlePost(req, res);
if (req.url === '/status' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ clients: this.clients.size, tools: this.tools.length }));
return;
}
res.writeHead(404);
res.end('Not Found');
});
server.listen(port, host, () => {
console.log(`MCP SSE Server listening on ${host}:${port}`);
});
}
}
new MCPSSEServer().start();
JavaScript 客户端接入
// 建立 SSE 长连接
const evtSource = new EventSource('http://localhost:3001/events');
evtSource.addEventListener('progress', (e) => {
const data = JSON.parse(e.data);
console.log(`进度: ${data.percent}%`);
});
evtSource.addEventListener('connected', (e) => {
const { clientId } = JSON.parse(e.data);
// 调用工具时带上 clientId,Server 据此推送进度
fetch('http://localhost:3001/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Client-Id': clientId
},
body: JSON.stringify({
jsonrpc: '2.0', id: 1,
method: 'tools/call',
params: { name: 'long_task', arguments: { steps: 5 } }
})
}).then(r => r.json()).then(console.log);
});
工具定义的艺术
工具写得好不好,直接决定 AI 能不能正确调用它。有几个实践值得注意:
description 要同时描述"是什么"和"何时用"
// 差:只说是什么
"description": "天气查询工具"
// 好:说清楚触发时机
"description": "获取指定城市的实时天气信息。当用户询问天气、出行建议、穿衣指南等需要天气数据的场景时使用。返回温度、天气状况、湿度。"
参数描述要精确
{
"city": {
"type": "string",
"description": "城市名称,使用中文,如:北京、上海、广州。不要传入省份或地区名。"
}
}
工具粒度要合适
粒度设计原则
- ❌ 一个工具做太多事("查天气+预订机票+订酒店")—— AI 难以判断参数,出错率高
- ❌ 工具太细("获取温度"、"获取湿度"分成两个工具)—— 无谓的多次调用
- ✅ 一个工具完成一件独立的事,参数不超过 5 个,返回完整且有用的信息
错误信息要对 AI 友好
工具执行失败时,错误信息是 AI 的输入,AI 会据此决定下一步。写得清楚,AI 才能做出正确反应:
// 差
throw new Error('Failed');
// 好
throw new Error('城市 "XYZ" 不在支持范围内。目前支持:北京、上海、广州、深圳。请使用标准城市名。');
部署与安全
把 MCP Server 暴露到公网时,有几个安全问题必须处理:
绑定 127.0.0.1,通过 Nginx 反代
// 永远不要这样做
server.listen(3000); // 监听 0.0.0.0,直接暴露公网
// 正确做法
server.listen(3000, '127.0.0.1'); // 只监听本地,让 Nginx 代理
加鉴权
任何公网 MCP Server 都应该有鉴权,最简单的是 Bearer Token:
// 在 HTTP 处理函数里加
const token = req.headers['authorization'];
if (token !== `Bearer ${process.env.MCP_SECRET}`) {
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Unauthorized' }));
return;
}
输入验证不能省
AI 传来的参数不能无脑信任,特别是会影响系统的操作(文件路径、shell 命令、数据库查询):
// 文件路径工具示例:防路径穿越
const safePath = path.resolve('/allowed/base/dir', userInput);
if (!safePath.startsWith('/allowed/base/dir')) {
throw new Error('路径不在允许范围内');
}
// SQL 工具示例:使用参数化查询,绝不拼字符串
db.query('SELECT * FROM users WHERE id = ?', [userId]);
用 PM2 管理进程
# 安装 PM2
npm install -g pm2
# 启动 MCP Server
pm2 start mcp-server.js --name my-mcp
# 开机自启
pm2 startup
pm2 save
# 查看日志
pm2 logs my-mcp
总结
从零写一个 MCP Server,核心只有三件事:
| 层次 | 做什么 | 关键点 |
|---|---|---|
| 协议层 | 实现 initialize / tools/list / tools/call | JSON-RPC 2.0,id 对应,错误格式规范 |
| 传输层 | 选择 stdio / HTTP / SSE | 本地用 stdio,远端用 HTTP+Nginx,流式用 SSE |
| 工具层 | 定义工具、实现执行逻辑 | description 写清楚,inputSchema 要准确,错误信息对 AI 友好 |
MCP 的设计哲学是最小化协议层的复杂度,把所有领域知识放在工具定义里。协议本身只有三个方法,其余的,是你对业务逻辑的理解。
一个好的 MCP Server 不是工具越多越好,而是每个工具职责单一、描述清晰、错误有意义——让 AI 可以可靠地信赖它。
有问题欢迎交流。完整代码见服务器 /root/tv/mcp-lib/ 和 /home/ubuntu/tv/mcp-lib/。
10实战:部署一个线上 MCP 服务
前面九章讲完了原理和三种传输层的手写实现。现在来看一个 真正跑在生产环境的完整案例——我自己的 engle-mcp-server,部署在 mcp.engledev.cloud,任何支持 MCP SSE 的客户端都可以直接接入。
10.1 项目结构
mcp-server/
├── http-server.js # 主服务文件(SSE 传输)
├── package.json # 依赖管理
├── nginx-mcp.conf # Nginx 反向代理配置
└── test-page.html # SSE 调试页面
依赖极少,只需要一个 uuid:
{
"name": "engle-mcp-server",
"version": "1.0.0",
"dependencies": {
"uuid": "^11.1.0"
}
}
10.2 完整源码
以下是 http-server.js 的完整实现。结构分为四大模块:工具定义 → 工具实现 → JSON-RPC 处理 → HTTP 路由。
#!/usr/bin/env node
/**
* Engle MCP Server - 标准 MCP SSE 传输协议
* 手动实现,兼容 Node.js 16+ (Ubuntu 18.04)
*
* 端点:
* GET /sse - SSE 连接端点,返回 endpoint 事件
* POST /messages - 客户端发送 JSON-RPC 消息
* GET /health - 健康检查
* GET / - API 文档页
*/
const http = require('http');
const { v4: uuidv4 } = require('uuid');
const { URL } = require('url');
const PORT = process.env.PORT || 3000;
// ========== 工具定义 ==========
const TOOLS = [
{
name: 'get_weather',
description: '获取指定城市的天气信息(模拟数据)',
inputSchema: {
type: 'object',
properties: {
city: {
type: 'string',
description: '城市名称,例如:北京、上海、深圳'
}
},
required: ['city']
}
},
{
name: 'calculate',
description: '执行数学计算,支持加减乘除',
inputSchema: {
type: 'object',
properties: {
operation: {
type: 'string',
enum: ['add', 'subtract', 'multiply', 'divide'],
description: '计算操作:add(加), subtract(减), multiply(乘), divide(除)'
},
a: { type: 'number', description: '第一个数字' },
b: { type: 'number', description: '第二个数字' }
},
required: ['operation', 'a', 'b']
}
},
{
name: 'server_status',
description: '获取 MCP 服务器运行状态',
inputSchema: { type: 'object', properties: {} }
}
];
// ========== 工具实现 ==========
function getWeather(city) {
const weatherData = {
'北京': { temperature: '15°C', condition: '晴天', humidity: '45%', wind: '北风3级' },
'上海': { temperature: '18°C', condition: '多云', humidity: '60%', wind: '东南风2级' },
'深圳': { temperature: '25°C', condition: '阴天', humidity: '75%', wind: '南风2级' },
// ... 更多城市
};
const weather = weatherData[city];
if (weather) {
return { city, ...weather, updatedAt: new Date().toISOString(), source: 'engle-mcp-server' };
}
// 未知城市返回随机模拟数据
return {
city,
temperature: `${Math.floor(Math.random() * 30 + 5)}°C`,
condition: ['晴天', '多云', '阴天', '小雨'][Math.floor(Math.random() * 4)],
humidity: `${Math.floor(Math.random() * 50 + 30)}%`,
wind: '未知',
updatedAt: new Date().toISOString(),
source: 'engle-mcp-server (随机模拟)',
};
}
function calculate(operation, a, b) {
const ops = {
add: { fn: (a, b) => a + b, sym: '+' },
subtract: { fn: (a, b) => a - b, sym: '-' },
multiply: { fn: (a, b) => a * b, sym: '×' },
divide: { fn: (a, b) => b === 0 ? null : a / b, sym: '÷' },
};
const op = ops[operation];
if (!op) return { error: `未知操作: ${operation}` };
if (operation === 'divide' && b === 0) return { error: '除数不能为零' };
return { expression: `${a} ${op.sym} ${b} = ${op.fn(a, b)}`, result: op.fn(a, b) };
}
function serverStatus() {
return {
name: 'engle-mcp-server', version: '1.0.0',
uptime: `${Math.floor(process.uptime())}s`,
memory: `${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB`,
activeConnections: sessions.size,
timestamp: new Date().toISOString(),
};
}
// 工具调用路由
async function callTool(name, args) {
switch (name) {
case 'get_weather':
return [{ type: 'text', text: JSON.stringify(getWeather(args.city), null, 2) }];
case 'calculate': {
const result = calculate(args.operation, args.a, args.b);
if (result.error) return [{ type: 'text', text: result.error }];
return [{ type: 'text', text: result.expression }];
}
case 'server_status':
return [{ type: 'text', text: JSON.stringify(serverStatus(), null, 2) }];
default:
throw new Error(`Unknown tool: ${name}`);
}
}
// ========== MCP JSON-RPC 处理 ==========
const SERVER_INFO = { name: 'engle-mcp-server', version: '1.0.0' };
const SERVER_CAPABILITIES = { tools: {} };
function handleJsonRpc(request) {
const { method, params, id } = request;
switch (method) {
case 'initialize':
return {
jsonrpc: '2.0', id,
result: {
protocolVersion: '2024-11-05',
capabilities: SERVER_CAPABILITIES,
serverInfo: SERVER_INFO,
},
};
case 'tools/list':
return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
case 'tools/call':
return null; // 异步处理
case 'ping':
return { jsonrpc: '2.0', id, result: {} };
case 'notifications/initialized':
case 'notifications/cancelled':
return null; // 通知不需要返回
default:
return {
jsonrpc: '2.0', id,
error: { code: -32601, message: `Method not found: ${method}` },
};
}
}
// ========== SSE 会话管理 ==========
const sessions = new Map();
function createSession(res) {
const sessionId = uuidv4();
sessions.set(sessionId, { res, createdAt: Date.now() });
return sessionId;
}
function sendSSE(res, data, event) {
if (event) res.write(`event: ${event}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
}
// ========== HTTP 路由 ==========
const server = http.createServer(async (req, res) => {
setCorsHeaders(res);
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
const pathname = url.pathname;
// CORS 预检
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
// ===== SSE 连接端点 =====
if (pathname === '/sse' && req.method === 'GET') {
const sessionId = createSession(res);
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no', // 关键:告诉 Nginx 不要缓冲
});
// 发送 endpoint 事件
res.write(`event: endpoint\ndata: /messages?sessionId=${sessionId}\n\n`);
// 心跳保活(每 30s)
const heartbeat = setInterval(() => {
try { res.write(': heartbeat\n\n'); } catch (e) { clearInterval(heartbeat); }
}, 30000);
req.on('close', () => {
clearInterval(heartbeat);
sessions.delete(sessionId);
});
return;
}
// ===== 消息处理端点 =====
if (pathname === '/messages' && req.method === 'POST') {
const sessionId = url.searchParams.get('sessionId');
const session = sessions.get(sessionId);
if (!session) { res.writeHead(404); res.end(); return; }
const body = await readBody(req);
const request = JSON.parse(body);
// 通知类不需要响应
if (!request.id) { res.writeHead(202); res.end(); return; }
// 工具调用 - 异步处理
if (request.method === 'tools/call') {
const { name, arguments: args } = request.params;
const content = await callTool(name, args || {});
const response = {
jsonrpc: '2.0', id: request.id,
result: { content, isError: false },
};
sendSSE(session.res, response, 'message');
res.writeHead(202); res.end();
return;
}
// 其他 JSON-RPC 方法
const response = handleJsonRpc(request);
if (response) sendSSE(session.res, response, 'message');
res.writeHead(202); res.end();
return;
}
// 404
res.writeHead(404); res.end();
});
server.listen(PORT, '0.0.0.0');
10.3 关键设计要点
这份代码虽然只有 ~500 行,但有几个值得注意的设计决策:
整个服务只依赖 uuid,MCP 协议完全手写。好处是没有黑盒,协议交互一目了然,调试方便,且兼容 Node.js 16+(一些老服务器上跑不了高版本 Node)。
每个 GET /sse 连接生成唯一 sessionId,后续 POST /messages?sessionId=xxx 通过 sessionId 关联到对应的 SSE 流。这是 MCP SSE 传输的核心模式。
每 30 秒发送 : heartbeat\n\n(SSE 注释格式),防止 Nginx、负载均衡器、CDN 等中间层因为"空闲超时"而断开长连接。
X-Accel-Buffering: no这个响应头告诉 Nginx 不要缓冲 SSE 流。没有它,Nginx 会把 SSE 事件攒在缓冲区里,客户端收不到实时消息——这是 Nginx + SSE 最常见的坑。
10.4 Nginx 反向代理配置
SSE 服务放在 Nginx 后面需要特殊配置,否则 SSE 流会被 Nginx 缓冲导致客户端收不到实时事件:
# HTTPS 主配置
server {
listen 443 ssl;
server_name mcp.engledev.cloud;
ssl_certificate /etc/letsencrypt/live/mcp.engledev.cloud/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mcp.engledev.cloud/privkey.pem;
# SSE 连接端点 - 禁用 Nginx 缓冲(关键!)
location /sse {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection ''; # 清空 Connection,保持长连接
proxy_buffering off; # 禁用代理缓冲
proxy_cache off; # 禁用缓存
chunked_transfer_encoding off; # 关闭分块传输
proxy_read_timeout 86400s; # 超时设为 24 小时
proxy_send_timeout 86400s;
}
# 消息端点(普通 HTTP 代理即可)
location /messages {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# 其他路由
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
}
proxy_buffering off— 不缓冲上游响应,SSE 事件实时转发proxy_cache off— 不缓存 SSE 流chunked_transfer_encoding off— 防止 Nginx 对 SSE 流二次编码proxy_set_header Connection ''— 清空 Connection 头,保持长连接proxy_read_timeout 86400s— SSE 是长连接,默认 60s 超时会断开- 服务端也要设置
X-Accel-Buffering: no响应头,双保险
10.5 PM2 进程管理 & 部署
# 安装依赖
cd /root/mcp-server && npm install
# PM2 启动并设为开机自启
pm2 start http-server.js --name mcp-server
pm2 save && pm2 startup
# Nginx 配置
cp nginx-mcp.conf /etc/nginx/sites-enabled/mcp-server.conf
nginx -t && systemctl reload nginx
# 申请 SSL 证书
certbot --nginx -d mcp.engledev.cloud
# 验证
curl https://mcp.engledev.cloud/health
10.6 接入配置
部署完成后,任何支持 MCP SSE 的客户端都可以一行配置接入:
{
"mcpServers": {
"engle-mcp": {
"url": "https://mcp.engledev.cloud/sse"
}
}
}
接入后,AI 客户端就能直接调用服务器上的工具了——查天气、做计算、看服务器状态。整个过程就是我们前面讲的 SSE 传输协议的完整实现。
访问 https://mcp.engledev.cloud 查看服务状态和完整的接入文档。