首页 › tutorials › 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基础

验证环境:

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

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

1.1 协议演进时间线

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 架构对比

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 选型决策树

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 三种响应模式

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版本:

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

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

首先安装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}`);
    });
  }
}

3.3 迁移步骤二:重构主入口

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

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

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

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

// 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服务器

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

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

4.1 本地注册

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

验证连接:

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

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

4.2 项目级配置

在项目根目录创建.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"
    }
  }
}

4.3 双传输并存的配置策略

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 输入校验工具

// 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 文件分析工具

// 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 双传输入口

// 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配置

// 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 构建与运行

# 安装依赖
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端点

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

6.2 Vercel Serverless部署

// 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)

# /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行为差异

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 调试技巧

# 启用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

逐项检查:


延伸阅读

训练营适用性评估

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

参考来源

  1. MCP Transports Specification — MCP协议传输层官方规范,定义Stdio和Streamable HTTP两种标准传输
  2. Streamable HTTP vs SSE for MCP Servers — 对比分析SSE废弃原因与Streamable HTTP技术优势
  3. MCP SSE Deprecated: Migrate to Streamable HTTP — SSE传输2026年4月废弃公告及迁移实战指南
  4. MCP Best Practices — MCP Server架构设计最佳实践,涵盖单一职责和安全边界
  5. Building a Streamable HTTP MCP Server: From stdio to Vercel Serverless — 从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学通信知识星球海报-扫码加入