<!-- title: MCP传输实战：Stdio到Streamable HTTP迁移 -->

# MCP传输实战：Stdio到Streamable HTTP迁移

## 概述

本教程面向已有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基础 |

验证环境：

```bash
node --version    # v20+
npx tsc --version # 5.x
claude --version  # 最新版
```

---

## 一、三种传输协议全景对比

### 1.1 协议演进时间线

```mermaid
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传输
```

### 1.2 架构对比

```mermaid
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
```

### 1.3 关键差异表

| 维度 | 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已废弃 | 不受影响 |

### 1.4 选型决策树

```mermaid
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核心机制详解

### 2.1 请求-响应模型

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流式响应)
```

### 2.2 会话管理

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

### 2.3 三种响应模式

```mermaid
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
```

---

## 三、实战：从Stdio迁移到Streamable HTTP

### 3.1 起点：一个Stdio MCP Server

我们从一个文件管理MCP Server开始。这是迁移前的Stdio版本：

```typescript
// 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`：

```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模式）：

```bash
claude mcp add file-manager -- node /path/to/file-manager-mcp/dist/server.js
```

### 3.2 迁移步骤一：引入Streamable HTTP传输

首先安装Express作为HTTP服务器框架：

```bash
npm install express
npm install -D @types/express
```

创建新的传输适配文件：

```typescript
// 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}`);
    });
  }
}
```

### 3.3 迁移步骤二：重构主入口

将工具注册与传输层分离：

```typescript
// 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,
        };
      }
    }
  );
}
```

新的主入口文件：

```typescript
// 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);
```

### 3.4 迁移步骤三：添加有状态会话支持

无状态模式适合简单场景。对于需要服务端推送通知（如进度更新）的场景，需要启用会话管理：

```typescript
// 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实例，适合需要隔离状态的服务。如果不需要状态隔离，无状态模式更简单、更易扩展。

### 3.5 迁移步骤四：完善Express服务器

```typescript
// 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：

```json
{
  "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"
  }
}
```

---

## 四、在Claude Code中注册Streamable HTTP服务器

### 4.1 本地注册

```bash
# 添加远程MCP服务器（Streamable HTTP）
claude mcp add file-manager-http \
  --transport http \
  http://localhost:3000/mcp
```

验证连接：

```bash
# 列出已注册的MCP服务器
claude mcp list

# 测试工具可用性
claude -p "使用file-manager-http工具列出当前目录的文件统计"
```

### 4.2 项目级配置

在项目根目录创建`.claude/settings.json`：

```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"
    }
  }
}
```

### 4.3 双传输并存的配置策略

```mermaid
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，支持双传输、会话管理和生产级特性。

### 5.1 项目结构

```
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
```

### 5.2 输入校验工具

```typescript
// 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("是否递归统计子目录"),
};
```

### 5.3 文件分析工具

```typescript
// 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;
}
```

### 5.4 双传输入口

```typescript
// 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}`
    );
  });
}
```

### 5.5 TypeScript配置

```json
// 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"]
}
```

### 5.6 构建与运行

```bash
# 安装依赖
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
```

### 5.7 测试Streamable HTTP端点

```bash
# 初始化连接
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
```

---

## 六、生产部署实战

### 6.1 Docker容器化

```dockerfile
# 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"]
```

```bash
# 构建与运行
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
```

### 6.2 Vercel Serverless部署

```typescript
// 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;
```

### 6.3 反向代理配置（Nginx）

```nginx
# /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;
    }
}
```

---

## 七、常见问题与解决方案

### 7.1 连接问题

| 问题 | 原因 | 解决方案 |
|------|------|----------|
| `Connection refused` | 服务未启动或端口错误 | 检查`PORT`和`docker ps` |
| `404 Session not found` | 会话过期或服务重启 | 无状态模式下不应出现此错误 |
| `CORS error` | 浏览器限制 | 配置`Access-Control-Allow-*`头 |
| `Method not found` | 协议版本不匹配 | 确认`protocolVersion`一致 |
| `timeout` | 工具执行过长 | 增加Nginx `proxy_read_timeout` |

### 7.2 Stdio vs HTTP行为差异

```mermaid
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/>显式错误处理]
```

**关键差异提醒**：

1. **日志输出**：Stdio模式下`console.log`会污染JSON-RPC通道，必须用`console.error`写日志。HTTP模式下两者都可以用。

2. **进程管理**：Stdio模式由Claude Code管理进程生命周期。HTTP模式需要自行管理（systemd、Docker等）。

3. **并发**：Stdio是单连接，无需考虑并发。HTTP可能同时收到多个请求，工具内部需要考虑并发安全。

4. **环境变量**：Stdio模式继承Claude Code的环境。HTTP模式需要自行配置环境变量。

### 7.3 调试技巧

```bash
# 启用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  # 确认服务器状态
```

---

## 八、迁移检查清单

完成迁移后，按此清单逐项验证：

```mermaid
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
```

**逐项检查**：

- [ ] Stdio模式：`claude mcp add` 后工具可见
- [ ] Stdio模式：工具调用返回正确结果
- [ ] HTTP模式：`/health` 端点返回200
- [ ] HTTP模式：`curl` 初始化请求成功
- [ ] HTTP模式：工具列表正确返回
- [ ] HTTP模式：工具调用返回正确结果
- [ ] Claude Code：HTTP服务器注册成功
- [ ] Claude Code：通过HTTP调用工具正常
- [ ] 生产环境：Docker容器正常运行
- [ ] 生产环境：Nginx代理流式响应正常

---

## 延伸阅读

- [MCP Transports官方规范](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports) — 三种传输协议的完整规范定义
- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) — 官方TypeScript SDK及Streamable HTTP传输实现
- [MCP SSE废弃迁移指南](https://botoi.com/blog/mcp-sse-deprecation-streamable-http-migration/) — SSE到Streamable HTTP的迁移步骤
- [Stdio到Streamable HTTP迁移](https://chatforest.com/guides/mcp-server-migration-stdio-to-http/) — 从Stdio迁移到Streamable HTTP的实践指南
- [MCP最佳实践](https://mcp-best-practice.github.io/mcp-best-practice/best-practice/) — MCP Server设计的架构最佳实践

## 训练营适用性评估

| 维度 | 评估 |
|------|------|
| **适合训练营类型** | MCP进阶训练营、AI工具链工程师培训 |
| **教学时长** | 4-6小时（含实操） |
| **学员基础要求** | MCP基础概念、TypeScript、HTTP协议 |
| **实操占比** | 70%（代码迁移+部署演练） |
| **硬件要求** | 笔记本电脑，Docker Desktop，Node.js 20+ |
| **分组建议** | 2-3人一组，每组完成一个完整迁移项目 |
| **考核方式** | 提交双传输MCP Server代码 + Docker部署截图 |

## 参考来源

1. [MCP Transports Specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports) — MCP协议传输层官方规范，定义Stdio和Streamable HTTP两种标准传输
2. [Streamable HTTP vs SSE for MCP Servers](https://toolradar.com/blog/streamable-http-vs-sse) — 对比分析SSE废弃原因与Streamable HTTP技术优势
3. [MCP SSE Deprecated: Migrate to Streamable HTTP](https://botoi.com/blog/mcp-sse-deprecation-streamable-http-migration/) — SSE传输2026年4月废弃公告及迁移实战指南
4. [MCP Best Practices](https://mcp-best-practice.github.io/mcp-best-practice/best-practice/) — MCP Server架构设计最佳实践，涵盖单一职责和安全边界
5. [Building a Streamable HTTP MCP Server: From stdio to Vercel Serverless](https://dev.to/rafsilva85/building-a-streamable-http-mcp-server-from-stdio-to-vercel-serverless-497o) — 从Stdio到Serverless部署的完整教程

> [!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 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
