本教程面向已有MCP Server开发经验的工程师,系统讲解MCP三种传输协议(Stdio、SSE、Streamable HTTP)的架构差异,手把手完成从本地Stdio到远程Streamable HTTP的完整迁移。你将掌握双传输兼容、会话管理、状态隔离、生产部署等关键技能,并在实战项目中构建一个同时支持Stdio和Streamable HTTP的MCP Server。
适用人群:已了解MCP基础概念,有至少一个Stdio MCP Server开发经验的开发者。
学习目标: - 理解三种传输协议的架构差异与选型策略 - 掌握Streamable HTTP的核心机制:会话管理、无状态设计、流式响应 - 完成Stdio → Streamable HTTP的完整迁移流程 - 构建双传输兼容的MCP Server并部署到生产环境
| 项目 | 要求 |
|---|---|
| Node.js | >= 20.0.0 |
| npm/pnpm | 最新稳定版 |
| MCP TypeScript SDK | @modelcontextprotocol/sdk >= 1.12.0 |
| Claude Code | 最新版(支持 claude mcp add 命令) |
| 基础知识 | MCP协议概念、TypeScript、HTTP基础 |
验证环境:
node --version # v20+
npx tsc --version # 5.x
claude --version # 最新版
timeline
title MCP传输协议演进
2024-11 : MCP规范发布 : Stdio + SSE两种传输
2025-03 : 规范更新 : SSE成为HTTP标准传输
2025-06 : Streamable HTTP引入 : 新传输协议草案
2025-11 : 规范2025-11-25 : Streamable HTTP正式替代SSE
2026-04 : SSE正式废弃 : 所有平台完成迁移
2026-05 : Streamable HTTP成为唯一HTTP传输
graph TB
subgraph "Stdio传输(本地)"
CC1["Claude Code"]
subgraph "同一进程组"
S1["MCP Server\n(stdin/stdout)"]
end
CC1 -->|"spawn 子进程\nstdin JSON-RPC"| S1
S1 -->|"stdout JSON-RPC"| CC1
end
subgraph "SSE传输(已废弃)"
CC2["Claude Code"]
S2["MCP Server"]
CC2 -->|"HTTP POST\n/messages"| S2
S2 -->|"SSE /sse\nEventStream"| CC2
end
subgraph "Streamable HTTP(当前标准)"
CC3["Claude Code"]
S3["MCP Server"]
CC3 -->|"HTTP POST /mcp\nJSON-RPC or NDJSON"| S3
S3 -->|"HTTP Response\nJSON or SSE Stream"| CC3
end
| 维度 | Stdio | SSE(已废弃) | Streamable HTTP |
|---|---|---|---|
| 通信方式 | 进程stdin/stdout | HTTP POST + SSE | HTTP POST + 可选SSE流 |
| 适用场景 | 本地工具 | 远程服务 | 远程/本地/云端 |
| 连接数 | 1对1 | 需要两条连接 | 单条HTTP连接 |
| 会话管理 | 进程生命周期 | GET /sse建立会话 | Mcp-Session-Id头 |
| 状态要求 | 有状态(进程内) | 有状态(服务端session) | 可无状态 |
| 部署复杂度 | 低(本地运行) | 高(需长连接) | 中(标准HTTP) |
| 防火墙友好 | N/A | 一般(需长连接) | 好(标准HTTP) |
| 水平扩展 | 不支持 | 困难 | 支持(无状态模式) |
| 流式响应 | 原生支持 | 原生支持 | NDJSON或SSE |
| SSE废弃 | 不受影响 | 2026-04已废弃 | 不受影响 |
flowchart TD
A[开始选择传输协议] --> B{需要远程访问?}
B -->|否| C{仅本地使用?}
C -->|是| D[Stdio<br/>最简单,本地运行]
C -->|否| E{需要云部署?}
B -->|是| E
E -->|是| F{需要水平扩展?}
F -->|是| G[Streamable HTTP<br/>无状态模式]
F -->|否| H[Streamable HTTP<br/>有状态模式]
E -->|否| I{团队共享?}
I -->|是| G
I -->|否| D
D --> J[推荐:同时支持两种]
G --> J
H --> J
核心建议:生产级MCP Server应同时支持Stdio和Streamable HTTP,让用户按需选择。
Streamable HTTP传输基于标准HTTP协议,所有通信通过单个端点(通常是/mcp)完成:
客户端 → 服务端:HTTP POST /mcp
Content-Type: application/json (普通JSON-RPC)
或
Content-Type: application/x-ndjson (流式NDJSON)
服务端 → 客户端:HTTP Response
Content-Type: application/json (单次响应)
或
Content-Type: text/event-stream (SSE流式响应)
Streamable HTTP通过Mcp-Session-Id请求头管理会话:
第一次请求:
→ POST /mcp (无Session头)
← 200 OK, Mcp-Session-Id: abc123
后续请求:
→ POST /mcp, Mcp-Session-Id: abc123
← 200 OK
关键特性:
- 服务端在初始化响应中返回Mcp-Session-Id
- 客户端必须在后续所有请求中携带此头
- 服务端可以拒绝无效或过期的Session
- 无状态模式下可以忽略Session
sequenceDiagram
participant C as Claude Code
participant S as MCP Server
Note over C,S: 模式1:直接JSON响应
C->>S: POST /mcp {method: "tools/call"}
S->>C: 200 OK {result: {...}}
Note over C,S: 模式2:SSE流式响应
C->>S: POST /mcp {method: "tools/call"}
S->>C: 200 OK, Content-Type: text/event-stream
S->>C: event: message\ndata: {"jsonrpc":"2.0",...}
S->>C: event: message\ndata: {"jsonrpc":"2.0",...}
Note over C,S: 模式3:NDJSON流式
C->>S: POST /mcp, Content-Type: application/x-ndjson
S->>C: 200 OK, Content-Type: application/x-ndjson
S->>C: {"jsonrpc":"2.0",...}\n
S->>C: {"jsonrpc":"2.0",...}\n
我们从一个文件管理MCP Server开始。这是迁移前的Stdio版本:
// server.ts - Stdio版本
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "file-manager",
version: "1.0.0",
});
// 工具:读取文件摘要
server.tool(
"read_summary",
"读取文件内容的摘要信息",
{
path: z.string().describe("文件路径"),
maxLines: z.number().optional().describe("最大行数,默认50"),
},
async ({ path, maxLines = 50 }) => {
try {
const fs = await import("fs/promises");
const content = await fs.readFile(path, "utf-8");
const lines = content.split("\n").slice(0, maxLines);
return {
content: [
{
type: "text",
text: `文件: ${path}\n总行数: ${content.split("\n").length}\n摘要:\n${lines.join("\n")}`,
},
],
};
} catch (err: any) {
return {
content: [{ type: "text", text: `错误: ${err.message}` }],
isError: true,
};
}
}
);
// 工具:搜索文件
server.tool(
"search_files",
"按名称模式搜索文件",
{
directory: z.string().describe("搜索目录"),
pattern: z.string().describe("文件名匹配模式(支持glob)"),
},
async ({ directory, pattern }) => {
try {
const { glob } = await import("glob");
const files = await glob(pattern, { cwd: directory });
return {
content: [
{
type: "text",
text: `在 ${directory} 中找到 ${files.length} 个匹配文件:\n${files.slice(0, 100).join("\n")}`,
},
],
};
} catch (err: any) {
return {
content: [{ type: "text", text: `搜索错误: ${err.message}` }],
isError: true,
};
}
}
);
// 启动Stdio传输
const transport = new StdioServerTransport();
await server.connect(transport);
对应的package.json:
{
"name": "file-manager-mcp",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node dist/server.js",
"build": "tsc"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0",
"zod": "^3.23.0",
"glob": "^11.0.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0"
}
}
在Claude Code中注册(Stdio模式):
claude mcp add file-manager -- node /path/to/file-manager-mcp/dist/server.js
首先安装Express作为HTTP服务器框架:
npm install express
npm install -D @types/express
创建新的传输适配文件:
// transports.ts - 传输层抽象
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
export async function startWithTransport(
server: McpServer,
mode: "stdio" | "http"
) {
if (mode === "stdio") {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP Server running on Stdio transport");
} else {
// Streamable HTTP模式
const httpTransport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // 无状态模式,不生成session
});
await server.connect(httpTransport);
const express = await import("express");
const app = express.default();
app.use(express.json());
// MCP端点
app.post("/mcp", async (req, res) => {
await httpTransport.handleRequest(req, res, req.body);
});
// SSE连接(用于服务端推送通知)
app.get("/mcp", async (req, res) => {
await httpTransport.handleRequest(req, res);
});
// DELETE请求(关闭会话)
app.delete("/mcp", async (req, res) => {
await httpTransport.handleRequest(req, res);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.error(`MCP Server running on Streamable HTTP, port ${PORT}`);
});
}
}
将工具注册与传输层分离:
// tools.ts - 工具注册(与传输无关)
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export function registerTools(server: McpServer) {
server.tool(
"read_summary",
"读取文件内容的摘要信息",
{
path: z.string().describe("文件路径"),
maxLines: z.number().optional().describe("最大行数,默认50"),
},
async ({ path, maxLines = 50 }) => {
try {
const fs = await import("fs/promises");
const content = await fs.readFile(path, "utf-8");
const lines = content.split("\n").slice(0, maxLines);
return {
content: [
{
type: "text",
text: `文件: ${path}\n总行数: ${content.split("\n").length}\n摘要:\n${lines.join("\n")}`,
},
],
};
} catch (err: any) {
return {
content: [{ type: "text", text: `错误: ${err.message}` }],
isError: true,
};
}
}
);
server.tool(
"search_files",
"按名称模式搜索文件",
{
directory: z.string().describe("搜索目录"),
pattern: z.string().describe("文件名匹配模式(支持glob)"),
},
async ({ directory, pattern }) => {
try {
const { glob } = await import("glob");
const files = await glob(pattern, { cwd: directory });
return {
content: [
{
type: "text",
text: `在 ${directory} 中找到 ${files.length} 个匹配文件:\n${files.slice(0, 100).join("\n")}`,
},
],
};
} catch (err: any) {
return {
content: [{ type: "text", text: `搜索错误: ${err.message}` }],
isError: true,
};
}
}
);
// 新增工具:获取文件统计信息
server.tool(
"file_stats",
"获取文件或目录的统计信息",
{
path: z.string().describe("文件或目录路径"),
},
async ({ path }) => {
try {
const fs = await import("fs/promises");
const stat = await fs.stat(path);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
path,
size: stat.size,
isFile: stat.isFile(),
isDirectory: stat.isDirectory(),
modified: stat.mtime.toISOString(),
created: stat.birthtime.toISOString(),
},
null,
2
),
},
],
};
} catch (err: any) {
return {
content: [{ type: "text", text: `统计错误: ${err.message}` }],
isError: true,
};
}
}
);
}
新的主入口文件:
// server.ts - 主入口(支持双传输)
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerTools } from "./tools.js";
import { startWithTransport } from "./transports.js";
const server = new McpServer({
name: "file-manager",
version: "2.0.0",
});
registerTools(server);
// 通过环境变量选择传输模式
const mode = (process.env.MCP_TRANSPORT || "stdio") as "stdio" | "http";
await startWithTransport(server, mode);
无状态模式适合简单场景。对于需要服务端推送通知(如进度更新)的场景,需要启用会话管理:
// transports-stateful.ts - 有状态传输
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { randomUUID } from "crypto";
// 会话存储
const sessions = new Map<
string,
{
transport: StreamableHTTPServerTransport;
server: McpServer;
createdAt: Date;
}
>();
// 会话超时(毫秒)
const SESSION_TIMEOUT = 30 * 60 * 1000; // 30分钟
// 定期清理过期会话
setInterval(() => {
const now = Date.now();
for (const [id, session] of sessions) {
if (now - session.createdAt.getTime() > SESSION_TIMEOUT) {
sessions.delete(id);
console.error(`Session ${id} expired and cleaned up`);
}
}
}, 60 * 1000);
export function createStreamableHttpApp() {
return async (req: any, res: any) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
if (sessionId) {
// 已有会话
const session = sessions.get(sessionId);
if (!session) {
res.status(404).json({
jsonrpc: "2.0",
error: { code: -32001, message: "Session not found or expired" },
});
return;
}
await session.transport.handleRequest(req, res, req.body);
} else {
// 新会话
const newSessionId = randomUUID();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => newSessionId,
});
const server = new McpServer({
name: "file-manager",
version: "2.0.0",
});
// 这里需要注册工具(延迟导入避免循环依赖)
const { registerTools } = await import("./tools.js");
registerTools(server);
await server.connect(transport);
sessions.set(newSessionId, {
transport,
server,
createdAt: new Date(),
});
await transport.handleRequest(req, res, req.body);
console.error(`New session created: ${newSessionId}`);
}
};
}
注意:有状态模式下每个会话创建独立的McpServer实例,适合需要隔离状态的服务。如果不需要状态隔离,无状态模式更简单、更易扩展。
// app.ts - 完整的HTTP服务器
import express from "express";
import { createStreamableHttpApp } from "./transports-stateful.js";
const app = express();
// JSON解析中间件
app.use(express.json());
// CORS支持(生产环境需限制域名)
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
if (req.method === "OPTIONS") {
return res.sendStatus(204);
}
next();
});
// MCP端点 - 处理所有HTTP方法
const mcpHandler = createStreamableHttpApp();
app.post("/mcp", mcpHandler);
app.get("/mcp", mcpHandler); // SSE流
app.delete("/mcp", mcpHandler); // 会话关闭
// 健康检查端点
app.get("/health", (req, res) => {
res.json({ status: "ok", uptime: process.uptime() });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.error(`File Manager MCP Server listening on port ${PORT}`);
console.error(`MCP endpoint: http://localhost:${PORT}/mcp`);
});
更新package.json的scripts:
{
"scripts": {
"start:stdio": "MCP_TRANSPORT=stdio node dist/server.js",
"start:http": "node dist/app.js",
"build": "tsc",
"dev:stdio": "MCP_TRANSPORT=stdio tsx src/server.ts",
"dev:http": "tsx src/app.ts"
}
}
# 添加远程MCP服务器(Streamable HTTP)
claude mcp add file-manager-http \
--transport http \
http://localhost:3000/mcp
验证连接:
# 列出已注册的MCP服务器
claude mcp list
# 测试工具可用性
claude -p "使用file-manager-http工具列出当前目录的文件统计"
在项目根目录创建.claude/settings.json:
{
"mcpServers": {
"file-manager": {
"type": "stdio",
"command": "node",
"args": ["./mcp-servers/file-manager/dist/server.js"]
},
"file-manager-http": {
"type": "url",
"url": "http://localhost:3000/mcp"
}
}
}
flowchart TD
A[MCP Server代码库] --> B[构建产物 dist/]
B --> C{部署环境?}
C -->|本地开发| D[Stdio模式<br/>claude mcp add --stdio]
C -->|团队共享| E[HTTP模式<br/>内部服务器部署]
C -->|云服务| F[HTTP模式<br/>Vercel/AWS部署]
D --> G[配置:command + args]
E --> H[配置:url + header]
F --> I[配置:url + OAuth]
让我们构建一个完整的、可运行的文件分析MCP Server,支持双传输、会话管理和生产级特性。
file-analyzer-mcp/
├── src/
│ ├── index.ts # 统一入口
│ ├── tools/
│ │ ├── analyze.ts # 文件分析工具
│ │ ├── search.ts # 搜索工具
│ │ └── stats.ts # 统计工具
│ ├── resources/
│ │ └── templates.ts # 资源模板
│ ├── transports/
│ │ ├── stdio.ts # Stdio传输
│ │ └── http.ts # Streamable HTTP传输
│ └── utils/
│ └── validation.ts # 输入校验
├── tsconfig.json
├── package.json
└── .env.example
// src/utils/validation.ts
import { z } from "zod";
// 路径安全校验:防止路径遍历攻击
export const safePath = z.string().refine(
(p) => {
const normalized = p.replace(/\\/g, "/");
return (
!normalized.includes("..") &&
!normalized.startsWith("/") &&
!normalized.includes("~")
);
},
{ message: "路径不允许包含 .. 或绝对路径" }
);
// 文件分析参数
export const AnalyzeSchema = {
path: safePath.describe("相对于工作目录的文件路径"),
analysisType: z
.enum(["structure", "complexity", "dependencies"])
.describe("分析类型"),
};
// 搜索参数
export const SearchSchema = {
directory: safePath.describe("搜索的根目录"),
pattern: z.string().min(1).describe("glob匹配模式"),
maxResults: z.number().min(1).max(500).optional().describe("最大结果数"),
};
// 统计参数
export const StatsSchema = {
path: safePath.describe("文件或目录路径"),
recursive: z.boolean().optional().describe("是否递归统计子目录"),
};
// src/tools/analyze.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { AnalyzeSchema, safePath } from "../utils/validation.js";
export function registerAnalyzeTools(server: McpServer, workDir: string) {
server.tool(
"analyze_file",
"分析代码文件的结构、复杂度或依赖关系",
AnalyzeSchema,
async ({ path, analysisType }) => {
const fullPath = `${workDir}/${path}`;
try {
const fs = await import("fs/promises");
const content = await fs.readFile(fullPath, "utf-8");
const lines = content.split("\n");
switch (analysisType) {
case "structure":
return analyzeStructure(fullPath, content, lines);
case "complexity":
return analyzeComplexity(fullPath, content, lines);
case "dependencies":
return analyzeDependencies(fullPath, content, lines);
}
} catch (err: any) {
return {
content: [{ type: "text", text: `分析失败: ${err.message}` }],
isError: true,
};
}
}
);
}
function analyzeStructure(path: string, content: string, lines: string[]) {
const functions = content.match(
/(?:function|const|let|var)\s+\w+|export\s+(?:default\s+)?(?:function|class|const)/g
) || [];
const classes = content.match(/class\s+\w+/g) || [];
const imports = content.match(/import\s+.*?from\s+['"].*?['"]/g) || [];
return {
content: [
{
type: "text" as const,
text: [
`文件结构分析: ${path}`,
`━━━━━━━━━━━━━━━━━━━━`,
`总行数: ${lines.length}`,
`非空行: ${lines.filter((l) => l.trim()).length}`,
`注释行: ${lines.filter((l) => l.trim().startsWith("//") || l.trim().startsWith("*") || l.trim().startsWith("/*")).length}`,
`函数/方法: ${functions.length}`,
`类定义: ${classes.length}`,
`导入语句: ${imports.length}`,
``,
`函数列表:`,
...functions.slice(0, 20).map((f) => ` - ${f.trim()}`),
functions.length > 20 ? ` ... 还有 ${functions.length - 20} 个` : "",
].join("\n"),
},
],
};
}
function analyzeComplexity(path: string, content: string, lines: string[]) {
const conditions =
(content.match(/\bif\b/g) || []).length +
(content.match(/\belse\b/g) || []).length +
(content.match(/\bfor\b/g) || []).length +
(content.match(/\bwhile\b/g) || []).length +
(content.match(/\bswitch\b/g) || []).length +
(content.match(/\bcatch\b/g) || []).length;
const nesting = calculateNesting(content);
return {
content: [
{
type: "text" as const,
text: [
`复杂度分析: ${path}`,
`━━━━━━━━━━━━━━━━━━━━`,
`条件分支数: ${conditions}`,
`最大嵌套深度: ${nesting}`,
`认知复杂度估算: ${conditions + nesting * 2}`,
`代码行/函数比: ${(lines.length / Math.max(1, (content.match(/function/g) || []).length)).toFixed(1)}`,
``,
`建议: ${
nesting > 5
? "嵌套过深,建议提取子函数"
: conditions > 20
? "分支过多,考虑使用策略模式"
: "复杂度在合理范围内"
}`,
].join("\n"),
},
],
};
}
function analyzeDependencies(path: string, content: string, lines: string[]) {
const importLines = lines.filter((l) => l.trim().startsWith("import"));
const localDeps = importLines.filter((l) => l.includes("./") || l.includes("../"));
const npmDeps = importLines.filter(
(l) => !l.includes("./") && !l.includes("../") && l.includes("from")
);
return {
content: [
{
type: "text" as const,
text: [
`依赖分析: ${path}`,
`━━━━━━━━━━━━━━━━━━━━`,
`总导入数: ${importLines.length}`,
`本地模块: ${localDeps.length}`,
`npm包: ${npmDeps.length}`,
``,
`本地依赖:`,
...localDeps.map((d) => ` ${d.trim()}`),
``,
`npm依赖:`,
...npmDeps.map((d) => ` ${d.trim()}`),
].join("\n"),
},
],
};
}
function calculateNesting(content: string): number {
let maxDepth = 0;
let currentDepth = 0;
for (const char of content) {
if (char === "{") {
currentDepth++;
maxDepth = Math.max(maxDepth, currentDepth);
} else if (char === "}") {
currentDepth = Math.max(0, currentDepth - 1);
}
}
return maxDepth;
}
// src/index.ts - 统一入口
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
import { registerAnalyzeTools } from "./tools/analyze.js";
import { SearchSchema } from "./utils/validation.js";
const WORK_DIR = process.env.WORK_DIR || process.cwd();
function createServer(): McpServer {
const server = new McpServer({
name: "file-analyzer",
version: "2.0.0",
});
registerAnalyzeTools(server, WORK_DIR);
// 注册搜索工具
server.tool(
"search_code",
"搜索代码文件",
SearchSchema,
async ({ directory, pattern, maxResults = 100 }) => {
try {
const { glob } = await import("glob");
const files = await glob(pattern, {
cwd: `${WORK_DIR}/${directory}`,
maxFiles: maxResults,
});
return {
content: [
{
type: "text",
text: `找到 ${files.length} 个文件:\n${files.join("\n")}`,
},
],
};
} catch (err: any) {
return {
content: [{ type: "text", text: `搜索失败: ${err.message}` }],
isError: true,
};
}
}
);
return server;
}
// 传输模式选择
const transport = process.env.MCP_TRANSPORT || "stdio";
if (transport === "stdio") {
const server = createServer();
const stdioTransport = new StdioServerTransport();
await server.connect(stdioTransport);
console.error(`[${new Date().toISOString()}] File Analyzer MCP (stdio) started`);
} else {
const app = express();
app.use(express.json());
// CORS
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
if (req.method === "OPTIONS") return res.sendStatus(204);
next();
});
// MCP Streamable HTTP端点
const httpTransport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // 无状态模式
});
const server = createServer();
await server.connect(httpTransport);
const handler = (req: express.Request, res: express.Response) => {
httpTransport.handleRequest(req, res, req.body);
};
app.post("/mcp", handler);
app.get("/mcp", handler);
app.delete("/mcp", handler);
app.get("/health", (req, res) => {
res.json({
status: "ok",
transport: "streamable-http",
uptime: process.uptime(),
});
});
const PORT = parseInt(process.env.PORT || "3000", 10);
app.listen(PORT, () => {
console.error(
`[${new Date().toISOString()}] File Analyzer MCP (streamable-http) on :${PORT}`
);
});
}
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
# 安装依赖
npm install @modelcontextprotocol/sdk zod glob express
npm install -D typescript @types/node @types/express tsx
# 构建
npx tsc
# 本地Stdio模式运行
MCP_TRANSPORT=stdio node dist/index.js
# HTTP模式运行
MCP_TRANSPORT=http PORT=3000 node dist/index.js
# 开发模式(热重载)
MCP_TRANSPORT=http npx tsx --watch src/index.ts
# 初始化连接
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": {"name": "test-client", "version": "1.0.0"}
}
}'
# 预期响应:
# {
# "jsonrpc": "2.0",
# "id": 1,
# "result": {
# "protocolVersion": "2025-03-26",
# "capabilities": {"tools": {"listChanged": false}},
# "serverInfo": {"name": "file-analyzer", "version": "2.0.0"}
# }
# }
# 列出工具
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
}'
# 调用工具
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "analyze_file",
"arguments": {
"path": "src/index.ts",
"analysisType": "structure"
}
}
}'
# 健康检查
curl http://localhost:3000/health
# Dockerfile
FROM node:22-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist/ ./dist/
ENV MCP_TRANSPORT=http
ENV PORT=3000
ENV NODE_ENV=production
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
CMD ["node", "dist/index.js"]
# 构建与运行
docker build -t file-analyzer-mcp .
docker run -d \
-p 3000:3000 \
-e WORK_DIR=/workspace \
-v $(pwd):/workspace:ro \
--name file-analyzer \
file-analyzer-mcp
// api/mcp.ts - Vercel Serverless Function
import type { VercelRequest, VercelResponse } from "@vercel/node";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
// Vercel无状态:每次请求创建新实例
const mcpHandler = async (req: VercelRequest, res: VercelResponse) => {
// CORS
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
if (req.method === "OPTIONS") {
return res.status(204).end();
}
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // Serverless必须无状态
});
const server = new McpServer({
name: "file-analyzer",
version: "2.0.0",
});
// 注册工具(从共享模块导入)
const { registerAnalyzeTools } = await import("../dist/tools/analyze.js");
registerAnalyzeTools(server, "/tmp");
await server.connect(transport);
await transport.handleRequest(req as any, res as any, req.body);
};
export default mcpHandler;
# /etc/nginx/conf.d/mcp-server.conf
upstream mcp_backend {
server 127.0.0.1:3000;
keepalive 64; # 保持连接复用
}
server {
listen 80;
server_name mcp.example.com;
location /mcp {
proxy_pass http://mcp_backend;
proxy_http_version 1.1;
# SSE流式响应支持
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
# 超时设置(流式响应需要较长超时)
proxy_read_timeout 300s;
proxy_send_timeout 300s;
# 传递Session头
proxy_set_header Mcp-Session-Id $http_mcp_session_id;
}
location /health {
proxy_pass http://mcp_backend;
}
}
| 问题 | 原因 | 解决方案 |
|---|---|---|
Connection refused |
服务未启动或端口错误 | 检查PORT和docker ps |
404 Session not found |
会话过期或服务重启 | 无状态模式下不应出现此错误 |
CORS error |
浏览器限制 | 配置Access-Control-Allow-*头 |
Method not found |
协议版本不匹配 | 确认protocolVersion一致 |
timeout |
工具执行过长 | 增加Nginx proxy_read_timeout |
flowchart LR
subgraph "Stdio模式"
A1[Claude Code spawn子进程] --> A2[单连接<br/>进程生命周期]
A2 --> A3[进程退出=断开]
end
subgraph "HTTP模式"
B1[Claude Code HTTP连接] --> B2[多连接可能<br/>需会话管理]
B2 --> B3[服务重启=会话丢失]
end
A3 --> D[注意:错误信息<br/>在stderr而非stdout]
B3 --> E[注意:需要<br/>显式错误处理]
关键差异提醒:
日志输出:Stdio模式下console.log会污染JSON-RPC通道,必须用console.error写日志。HTTP模式下两者都可以用。
进程管理:Stdio模式由Claude Code管理进程生命周期。HTTP模式需要自行管理(systemd、Docker等)。
并发:Stdio是单连接,无需考虑并发。HTTP可能同时收到多个请求,工具内部需要考虑并发安全。
环境变量:Stdio模式继承Claude Code的环境。HTTP模式需要自行配置环境变量。
# 启用MCP SDK调试日志
DEBUG="mcp:*" MCP_TRANSPORT=http node dist/index.js
# 监控HTTP请求
curl -v http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"debug","version":"1.0.0"}}}'
# Claude Code中查看MCP日志
claude mcp list # 确认服务器状态
完成迁移后,按此清单逐项验证:
flowchart TD
A[开始迁移验证] --> B{Stdio模式正常?}
B -->|否| B1[检查工具注册逻辑<br/>console.error日志]
B -->|是| C{HTTP模式正常?}
C -->|否| C1[检查端口/CORS<br/>curl测试端点]
C -->|是| D{Claude Code连接?}
D -->|否| D1[检查claude mcp add命令<br/>确认URL格式]
D -->|是| E{工具调用成功?}
E -->|否| E1[检查Schema定义<br/>验证输入参数]
E -->|是| F{流式响应正常?}
F -->|否| F1[检查proxy_buffering<br/>Content-Type设置]
F -->|是| G[迁移完成!]
B1 --> B
C1 --> C
D1 --> D
E1 --> E
F1 --> F
逐项检查:
claude mcp add 后工具可见/health 端点返回200curl 初始化请求成功| 维度 | 评估 |
|---|---|
| 适合训练营类型 | MCP进阶训练营、AI工具链工程师培训 |
| 教学时长 | 4-6小时(含实操) |
| 学员基础要求 | MCP基础概念、TypeScript、HTTP协议 |
| 实操占比 | 70%(代码迁移+部署演练) |
| 硬件要求 | 笔记本电脑,Docker Desktop,Node.js 20+ |
| 分组建议 | 2-3人一组,每组完成一个完整迁移项目 |
| 考核方式 | 提交双传输MCP Server代码 + Docker部署截图 |
[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
