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 生态里有两个角色:

协议核心: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/callAI 决定调用某个工具执行结果,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
通过标准输入/输出通信。AI 客户端以子进程方式启动 MCP Server,通过 stdin/stdout 交换 JSON-RPC 消息。最简单,本地工具首选,Claude Desktop 默认用这种。
HTTP
Server 暴露 HTTP 端点,Client 通过 POST 请求发送 JSON-RPC。适合远程部署、多客户端共享,可以用 Nginx 反代、加鉴权。
SSE
在 HTTP 基础上加入 Server-Sent Events,Server 可以主动推送事件。适合长耗时工具、流式输出场景,进度推送、实时日志等。

三种形态的协议消息格式完全相同,只是传输通道不同。下面逐一实现。

实现一: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();

关键细节

配置到 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/callJSON-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 项目结构

bash 目录结构
mcp-server/
├── http-server.js      # 主服务文件(SSE 传输)
├── package.json        # 依赖管理
├── nginx-mcp.conf      # Nginx 反向代理配置
└── test-page.html      # SSE 调试页面

依赖极少,只需要一个 uuid

json package.json
{
  "name": "engle-mcp-server",
  "version": "1.0.0",
  "dependencies": {
    "uuid": "^11.1.0"
  }
}

10.2 完整源码

以下是 http-server.js 的完整实现。结构分为四大模块:工具定义 → 工具实现 → JSON-RPC 处理 → HTTP 路由

javascript http-server.js — 完整 MCP SSE 服务
#!/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 行,但有几个值得注意的设计决策:

① 不用任何 MCP SDK

整个服务只依赖 uuid,MCP 协议完全手写。好处是没有黑盒,协议交互一目了然,调试方便,且兼容 Node.js 16+(一些老服务器上跑不了高版本 Node)。

② SSE 会话用 Map 管理

每个 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 缓冲导致客户端收不到实时事件:

nginx nginx-mcp.conf — SSE 反向代理配置
# 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;
    }
}
Nginx SSE 配置要点(踩坑总结):
  • 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 进程管理 & 部署

bash 部署命令
# 安装依赖
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 的客户端都可以一行配置接入:

json MCP 客户端配置(WorkBuddy / Claude Desktop / Cursor 通用)
{
  "mcpServers": {
    "engle-mcp": {
      "url": "https://mcp.engledev.cloud/sse"
    }
  }
}

接入后,AI 客户端就能直接调用服务器上的工具了——查天气、做计算、看服务器状态。整个过程就是我们前面讲的 SSE 传输协议的完整实现。

🔗 在线体验

访问 https://mcp.engledev.cloud 查看服务状态和完整的接入文档。