MCP (Model Context Protocol) 原理与实战
从零开始理解 MCP 协议,通过实战案例掌握 MCP Server 开发
目录
- [MCP 是什么?](#MCP 是什么? "#1-mcp-%E6%98%AF%E4%BB%80%E4%B9%88")
- [核心原理:stdio 管道通信](#核心原理:stdio 管道通信 "#2-%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86stdio-%E7%AE%A1%E9%81%93%E9%80%9A%E4%BF%A1")
- [JSON-RPC 协议](#JSON-RPC 协议 "#3-json-rpc-%E5%8D%8F%E8%AE%AE")
- [MCP Server 生命周期](#MCP Server 生命周期 "#4-mcp-server-%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F")
- [实战:剖析 next-ai-drawio MCP Server](#实战:剖析 next-ai-drawio MCP Server "#5-%E5%AE%9E%E6%88%98%E5%89%96%E6%9E%90-next-ai-drawio-mcp-server")
- [动手开发一个简单的 MCP Server](#动手开发一个简单的 MCP Server "#6-%E5%8A%A8%E6%89%8B%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84-mcp-server")
- 常见问题与最佳实践
1. MCP 是什么?
1.1 问题背景
在 MCP 出现之前,让 AI 助手(如 Claude、ChatGPT)调用外部工具面临很多问题:
css
┌─────────────────────────────────────────────────────────────────────────┐
│ 传统工具集成的问题 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ❌ 每个工具都需要单独集成 │
│ ❌ API 格式不统一,适配成本高 │
│ ❌ 安全性问题(API Key 如何传递) │
│ ❌ 无法跨平台使用(Claude Desktop、Cursor、VS Code 各自有各自的方式) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
1.2 MCP 的解决方案
MCP (Model Context Protocol) 是 Anthropic 推出的开放协议,标准化了 AI 应用与外部工具的交互方式。
scss
┌─────────────────────────────────────────────────────────────────────────┐
│ MCP 架构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────┐ │
│ │ MCP Host (客户端) │ │
│ │ Claude Desktop / Cursor │ │
│ │ / VS Code │ │
│ └──────────────┬──────────────┘ │
│ │ │
│ │ MCP 协议 (统一标准) │
│ │ │
│ ┌────────────────────────┼────────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ MCP Server │ │ MCP Server │ │ MCP Server │ │
│ │ (文件系统) │ │ (数据库) │ │ (绘图工具) │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
1.3 MCP 的核心概念
| 概念 | 说明 | 类比 |
|---|---|---|
| MCP Host | 运行 AI 应用的客户端 | 浏览器 |
| MCP Server | 提供工具能力的服务端 | Web Server |
| MCP Client | Host 内部的客户端逻辑 | HTTP Client |
| Tool | AI 可调用的函数 | API Endpoint |
| Resource | AI 可访问的数据 | 文件/数据库 |
| Prompt | 预定义的提示词模板 | 模板文件 |
2. 核心原理:stdio 管道通信
2.1 什么是 stdio?
每个进程都有三个标准流:
scss
┌─────────────────────────────────────────────────────────────────────────┐
│ 进程的标准流 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 进程 (Process) │ │
│ │ │ │
│ │ stdin (fd=0) ─────► 标准输入 │ │
│ │ (从外部读取数据) │ │
│ │ │ │
│ │ stdout (fd=1) ─────► 标准输出 │ │
│ │ (向外输出数据) │ │
│ │ │ │
│ │ stderr (fd=2) ─────► 标准错误 │ │
│ │ (输出错误信息) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
2.2 父子进程与管道
MCP 使用 父子进程 + 管道 的方式进行通信:
c
┌─────────────────────────────────────────────────────────────────────────┐
│ 父子进程管道通信 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 父进程 (MCP Host) 子进程 (MCP Server) │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ │ │ │ │
│ │ 写入数据 ──┼───[管道1]───► stdin │ 读取数据 │ │
│ │ │ │ │ │
│ │ 读取数据 ◄─┼──[管道2]──── stdout │ 写入数据 │ │
│ │ │ │ │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ 关键点: │
│ • 管道是单向的,需要两个管道组成双向通信 │
│ • 子进程只能操作自己的 stdin/stdout,无法访问父进程的流 │
│ • 父进程关闭时,子进程的 stdin 会收到 EOF │
│ │
└─────────────────────────────────────────────────────────────────────────┘
2.3 为什么选择 stdio?
| 优势 | 说明 |
|---|---|
| 简单 | 不需要网络端口,不需要认证 |
| 安全 | 只有父子进程能通信,外部无法访问 |
| 通用 | 所有编程语言都支持 stdin/stdout |
| 自动生命周期 | 父进程关闭,子进程自动收到信号 |
| 跨平台 | Windows、Linux、macOS 都支持 |
3. JSON-RPC 协议
MCP 在 stdio 之上使用 JSON-RPC 2.0 协议进行消息传递。
3.1 消息格式
json
// 请求 (Request)
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "start_session",
"arguments": {}
}
}
// 响应 (Response)
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "Session started! Session ID: mcp-abc123"
}
]
}
}
// 错误 (Error)
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32602,
"message": "Invalid params"
}
}
3.2 MCP 协议层级
bash
┌─────────────────────────────────────────────────────────────────────────┐
│ MCP 协议栈 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 应用层 │ │
│ │ Tools / Resources / Prompts │ │
│ │ (工具调用、资源访问、提示词模板) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ MCP 协议层 │ │
│ │ initialize / initialized │ │
│ │ tools/list / tools/call │ │
│ │ resources/list / resources/read │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ JSON-RPC 2.0 │ │
│ │ 请求/响应/通知 的序列化格式 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ stdio 传输层 │ │
│ │ 进程间通信的基础设施 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
4. MCP Server 生命周期
4.1 完整生命周期
arduino
┌─────────────────────────────────────────────────────────────────────────┐
│ MCP Server 生命周期 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 启动阶段 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ MCP Host 读取配置 │ │
│ │ ↓ │ │
│ │ spawn 子进程 (启动 MCP Server) │ │
│ │ ↓ │ │
│ │ 建立 stdio 管道连接 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 2. 初始化阶段 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Host 发送: initialize 请求 │ │
│ │ ↓ │ │
│ │ Server 返回: capabilities (支持的功能) │ │
│ │ ↓ │ │
│ │ Host 发送: initialized 通知 │ │
│ │ ↓ │ │
│ │ 连接建立完成,可以开始工作 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 3. 运行阶段 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ AI 决定调用工具 │ │
│ │ ↓ │ │
│ │ Host 发送: tools/call 请求 │ │
│ │ ↓ │ │
│ │ Server 执行工具,返回结果 │ │
│ │ ↓ │ │
│ │ 结果返回给 AI,AI 继续处理 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 4. 关闭阶段 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 用户关闭 MCP Host │ │
│ │ ↓ │ │
│ │ Host 关闭 stdin 管道 │ │
│ │ ↓ │ │
│ │ Server 检测到 stdin close 事件 │ │
│ │ ↓ │ │
│ │ Server 执行清理,退出进程 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
4.2 关闭机制代码
typescript
// MCP Server 的关闭处理
let isShuttingDown = false
function gracefulShutdown(reason: string) {
if (isShuttingDown) return
isShuttingDown = true
console.log(`Shutting down: ${reason}`)
// 清理资源...
process.exit(0)
}
// 监听 stdin 关闭 (主要方式)
process.stdin.on("close", () => gracefulShutdown("stdin closed"))
process.stdin.on("end", () => gracefulShutdown("stdin ended"))
// 监听信号 (备用方式)
process.on("SIGINT", () => gracefulShutdown("SIGINT"))
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"))
// 监听 stdout 错误 (管道断裂)
process.stdout.on("error", (err) => {
if (err.code === "EPIPE") {
gracefulShutdown("stdout error")
}
})
5. 实战:剖析 next-ai-drawio MCP Server
5.1 项目结构
bash
packages/mcp-server/
├── src/
│ ├── index.ts # MCP Server 主入口
│ ├── http-server.ts # 内嵌 HTTP 服务器
│ ├── diagram-operations.ts # 图表操作逻辑
│ ├── xml-validation.ts # XML 验证
│ ├── history.ts # 历史记录管理
│ └── logger.ts # 日志工具
├── package.json
└── tsconfig.json
5.2 核心代码分析
入口文件 (index.ts)
typescript
#!/usr/bin/env node
// 1. 设置 DOM polyfill (Node.js 环境需要)
import { DOMParser } from "linkedom"
;(globalThis as any).DOMParser = DOMParser
// 2. 导入 MCP SDK
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
import { z } from "zod"
// 3. 创建 MCP Server 实例
const server = new McpServer({
name: "next-ai-drawio",
version: "0.2.0",
})
// 4. 注册工具
server.registerTool(
"start_session",
{
description: "Start a new diagram session and open the browser...",
inputSchema: {}, // 无参数
},
async () => {
// 启动 HTTP 服务器
const port = await startHttpServer(6002)
// 创建 session
const sessionId = `mcp-${Date.now().toString(36)}`
// 打开浏览器
await open(`http://localhost:${port}?mcp=${sessionId}`)
return {
content: [{ type: "text", text: `Session started!` }]
}
}
)
// 5. 注册更多工具...
server.registerTool("create_new_diagram", { ... }, async ({ xml }) => { ... })
server.registerTool("edit_diagram", { ... }, async ({ operations }) => { ... })
server.registerTool("get_diagram", { ... }, async () => { ... })
// 6. 启动服务
async function main() {
const transport = new StdioServerTransport()
await server.connect(transport)
}
main().catch(console.error)
5.3 双向通信架构
next-ai-drawio MCP Server 有一个独特的设计:同时支持 stdio 和 HTTP 通信。
scss
┌─────────────────────────────────────────────────────────────────────────┐
│ next-ai-drawio MCP Server 架构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Claude Desktop / Cursor │
│ │ │
│ │ stdio (JSON-RPC) │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ MCP Server (Node.js) │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ MCP Handler │ │ HTTP Server │ │ State Store │ │ │
│ │ │ (stdio) │ │ (port 6002) │ │ (内存 Map) │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │ ▲ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ ┌─────────────┐ │ │ │
│ │ │ │ 浏览器页面 │──────────┘ │ │
│ │ │ │ (draw.io) │ HTTP 轮询 │ │
│ │ │ └─────────────┘ │ │
│ │ │ │ │
│ └──────────┼──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 工具调用结果 │
│ (返回给 AI) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
5.4 为什么需要 HTTP 服务器?
因为 MCP 通过 stdio 通信,但浏览器无法直接连接 stdio。解决方案:
arduino
┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ 问题: 浏览器无法连接 MCP Server 的 stdio │
│ │
│ 解决方案: MCP Server 内嵌一个 HTTP 服务器 │
│ │
│ ┌───────────┐ stdio ┌───────────┐ │
│ │ Claude │◄────────────────►│ MCP │ │
│ │ Desktop │ JSON-RPC │ Server │ │
│ └───────────┘ │ │ │
│ │ ┌───────┤ │
│ │ │ HTTP │◄──── HTTP ────┐ │
│ │ │ Server│ 轮询 │ │
│ │ └───────┘ │ │
│ └───────────┘ ┌──────┴──────┐ │
│ │ 浏览器 │ │
│ │ (draw.io) │ │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
6. 动手开发一个简单的 MCP Server
6.1 项目初始化
bash
# 创建项目
mkdir my-mcp-server
cd my-mcp-server
npm init -y
# 安装依赖
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
6.2 编写代码
typescript
// src/index.ts
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
import { z } from "zod"
// 创建 Server
const server = new McpServer({
name: "my-mcp-server",
version: "1.0.0",
})
// 定义工具
server.registerTool(
"hello",
{
description: "向用户问好",
inputSchema: {
name: z.string().describe("用户名称"),
},
},
async ({ name }) => {
return {
content: [
{
type: "text",
text: `你好,${name}!很高兴认识你!`,
},
],
}
}
)
// 定义资源
server.resource(
"config",
"config://app",
async (uri) => {
return {
contents: [
{
uri: uri.href,
text: JSON.stringify({ version: "1.0.0", name: "my-app" }),
},
],
}
}
)
// 定义提示词
server.prompt(
"greeting",
"问候提示词",
{ name: z.string().describe("用户名称") },
async ({ name }) => {
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `请用热情的语气向 ${name} 问好`,
},
},
],
}
}
)
// 启动服务
const transport = new StdioServerTransport()
await server.connect(transport)
6.3 配置 package.json
json
{
"name": "my-mcp-server",
"version": "1.0.0",
"type": "module",
"main": "dist/index.js",
"bin": {
"my-mcp-server": "./dist/index.js"
},
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}
6.4 在 Claude Desktop 中配置
json
// ~/Library/Application Support/Claude/claude_desktop_config.json (macOS)
// 或 %APPDATA%\Claude\claude_desktop_config.json (Windows)
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/path/to/my-mcp-server/dist/index.js"]
}
}
}
7. 常见问题与最佳实践
7.1 常见问题
Q1: 多个 AI 客户端同时启动 MCP Server 会怎样?
arduino
┌─────────────────────────────────────────────────────────────────────────┐
│ 多实例场景 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Codex 启动 MCP Server → localhost:6002 │
│ Claude Code 启动 MCP Server → 6002 被占用 → 自动切换到 6003 │
│ │
│ ✅ 不会失败(端口自动递增) │
│ ⚠️ 但状态不共享(每个实例独立) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Q2: 关闭客户端后 MCP Server 会关闭吗?
lua
┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ ⚠️ 不会自动关闭!需要开发者自己处理! │
│ │
│ 很多人的误解: │
│ ❌ "客户端关闭后,MCP Server 进程会自动退出" │
│ │
│ 事实是: │
│ 1. 客户端关闭 → stdin 管道关闭 → 子进程收到 EOF │
│ 2. 但 Node.js 进程默认不会因为 stdin 关闭而退出 │
│ 3. 如果有 HTTP Server 或定时器,进程会继续运行 │
│ 4. 必须监听 stdin close 事件,手动调用 process.exit() │
│ │
│ 如果不处理,进程会变成孤儿进程,继续占用端口和内存! │
│ │
└─────────────────────────────────────────────────────────────────────────┘
必须的关闭处理代码:
typescript
// MCP Server 必须自己处理关闭逻辑
// 1. 监听 stdin 关闭(主要方式)
process.stdin.on("close", () => {
// 清理资源...
process.exit(0) // 必须手动退出!
})
// 2. 监听信号(备用方式)
process.on("SIGINT", () => process.exit(0))
process.on("SIGTERM", () => process.exit(0))
// 3. 监听 stdout 错误(管道断裂)
process.stdout.on("error", (err) => {
if (err.code === "EPIPE") {
process.exit(0)
}
})
7.2 最佳实践
| 实践 | 说明 |
|---|---|
| 优雅关闭 | 监听 stdin close、SIGINT、SIGTERM |
| 参数验证 | 使用 zod 定义 inputSchema |
| 错误处理 | 返回 isError: true 和错误信息 |
| 日志输出 | 输出到 stderr,避免污染 stdout |
| 超时处理 | 工具执行设置合理超时 |
总结
MCP 的核心要点
- 协议层:JSON-RPC 2.0 + stdio 传输
- 通信机制:父子进程管道,单向数据流
- 生命周期:启动 → 初始化 → 运行 → 关闭
- 三大能力:Tools(工具)、Resources(资源)、Prompts(提示词)
架构图总览
scss
┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ 用户 │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ MCP Host │ (Claude Desktop / Cursor / VS Code) │
│ │ │ │
│ │ MCP Client │ │
│ └──────┬──────┘ │
│ │ │
│ │ stdio (JSON-RPC) │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ MCP Server │ (你开发的服务) │
│ │ │ │
│ │ • Tools │ (可调用的函数) │
│ │ • Resources │ (可访问的数据) │
│ │ • Prompts │ (提示词模板) │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
参考资料
作者:胡哈
本文基于 next-ai-drawio 项目的 MCP Server 实现,深入讲解了 MCP 协议的原理与实战。