首页 › tutorials › MCP Server TypeScript全栈:双传输到远程部署

MCP Server TypeScript全栈:双传输到远程部署

概述

本教程从零构建一个完整的 Model Context Protocol (MCP) 服务器,覆盖 TypeScript SDK 的核心原语(Tools / Resources / Prompts)、两种传输层(Stdio + Streamable HTTP)、2025-06-18 规范新特性(Elicitation、结构化输出、Tool Annotations),最终部署到 Cloudflare Workers。适合有 TypeScript 基础、希望构建生产级 MCP 工具链的开发者。

适用人群:有 Node.js / TypeScript 经验,想为 Claude Code 或其他 MCP 客户端构建自定义工具服务器的开发者。

学习目标: - 理解 MCP 协议架构与三大原语的分工 - 用官方 @modelcontextprotocol/sdk 构建完整 MCP 服务器 - 实现双传输模式:本地 Stdio + 远程 Streamable HTTP - 掌握 Elicitation、结构化输出、Tool Annotations 等新特性 - 将服务器部署到 Cloudflare Workers 并接入 OAuth 2.1 认证


前置条件

项目 要求
Node.js v20+(建议 v22 LTS)
TypeScript 5.x
包管理器 npm / pnpm / bun 均可
Claude Code 最新版(用于测试 MCP 连接)
基础知识 TypeScript 类型系统、async/await、HTTP 基础

第一章:MCP 协议架构全景

1.1 MCP 是什么

Model Context Protocol(MCP)是 Anthropic 主导的开放协议,为 LLM 应用提供标准化的上下文获取方式。它的核心思想很简单:AI 模型需要工具,工具需要标准接口。

┌─────────────┐     MCP Protocol     ┌─────────────┐
│  MCP Client │ ◄──────────────────► │  MCP Server │
│ (Claude Code│    JSON-RPC 2.0      │  (你的工具)   │
│  VS Code    │                      │             │
│  任意客户端) │                      │             │
└─────────────┘                      └─────────────┘

MCP 把"AI 能做什么"抽象为三大原语:

graph TB
    MCP[MCP Server 能力] --> T[Tools 工具]
    MCP --> R[Resources 资源]
    MCP --> P[Prompts 提示模板]

    T --> T1["执行动作:调用API、操作文件<br/>读写数据库、发HTTP请求"]
    R --> R1["提供数据:文件内容、数据库记录<br/>API响应、配置信息"]
    P --> P1["预设模板:代码审查模板<br/>分析框架、报告格式"]

    style T fill:#e74c3c,color:#fff
    style R fill:#3498db,color:#fff
    style P fill:#2ecc71,color:#fff

1.2 选择正确的原语

很多开发者分不清何时用 Tool、何时用 Resource。判断规则如下:

场景 用什么 原因
调用外部 API 执行操作 Tool 有副作用
查询数据库并返回数据 Tool 或 Resource 只读可做 Resource,需参数过滤做 Tool
提供静态配置文件 Resource 纯数据、无参数
返回项目目录结构 Resource 静态数据
生成代码审查报告 Prompt 需要结构化模板
搜索文件并高亮匹配 Tool 需要输入参数
获取当前时间 Tool 虽然无副作用,但每次结果不同

核心判断:有副作用或需要参数 → Tool;纯数据暴露 → Resource;模板化输出 → Prompt。

1.3 协议版本演进

MCP 规范经历了三个关键版本:

timeline
    title MCP 协议版本演进
    2024-11 : 协议首次发布
              : JSON-RPC batching
              : SSE 传输
    2025-03 : 2025-03-26 版本
              : Streamable HTTP 传输
              : OAuth 2.1 认证框架
    2025-06 : 2025-06-18 版本
              : Elicitation(人工确认)
              : 结构化 Tool 输出
              : Tool Annotations
              : 移除 JSON-RPC batching

2025-06-18 版本的三大新特性:

  1. Elicitation:服务器可以请求客户端向用户确认信息,实现 human-in-the-loop
  2. 结构化输出:Tool 返回值支持 JSON Schema 验证的强类型结构
  3. Tool Annotations:为工具添加元数据标记(readOnlyHint、destructiveHint 等)

第二章:项目初始化与最小服务器

2.1 创建项目

mkdir mcp-devtool-server && cd mcp-devtool-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx

初始化 TypeScript 配置:

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true
  },
  "include": ["src/**/*"]
}

2.2 最小可运行服务器

创建一个只有 Tool 的最小 MCP 服务器:

// src/index.ts
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: "devtool-server",
  version: "1.0.0",
});

// 注册一个 Tool
server.tool(
  "get_timestamp",
  "获取当前时间戳和格式化的日期时间",
  {},  // 无参数
  async () => ({
    content: [
      {
        type: "text",
        text: JSON.stringify({
          unix: Math.floor(Date.now() / 1000),
          iso: new Date().toISOString(),
          local: new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }),
        }, null, 2),
      },
    ],
  })
);

// 启动 Stdio 传输
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("MCP DevTool Server running on stdio");
}

main().catch(console.error);

注意:console.error 而不是 console.log。Stdio 传输下,stdout 用于 MCP 协议通信,只有 stderr 可以输出调试信息。如果你用 console.log,协议数据会被污染。

在 package.json 中添加启动脚本:

{
  "type": "module",
  "scripts": {
    "dev": "tsx src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

2.3 接入 Claude Code 测试

在项目目录创建 .mcp.json:

{
  "mcpServers": {
    "devtool": {
      "command": "npx",
      "args": ["tsx", "src/index.ts"],
      "type": "stdio"
    }
  }
}

在项目目录下启动 Claude Code,然后输入:

请调用 get_timestamp 工具获取当前时间

如果一切正常,Claude Code 会调用你的 MCP Tool 并返回格式化的时间数据。

常见问题排查:

问题 原因 解决
"Server not responding" TypeScript 编译错误 运行 npx tsx src/index.ts 看报错
Tool 不可见 .mcp.json 路径不对 确保在项目根目录启动 Claude Code
内容解析失败 返回格式不对 content 必须是数组,每项有 type 和 text
"Method not found" SDK 版本太旧 更新到 @modelcontextprotocol/sdk@latest

第三章:三大原语完整实现

3.1 Tools —— 执行动作的核心

Tool 是 MCP 中使用最广泛的原语。每个 Tool 由三部分组成:名称、描述、输入 Schema(Zod 定义)。

// src/tools/code-metrics.ts
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

export function registerCodeMetricsTools(server: McpServer) {
  // Tool 1:代码复杂度分析
  server.tool(
    "analyze_complexity",
    "分析 TypeScript 代码的圈复杂度(Cyclomatic Complexity)",
    {
      code: z.string().describe("要分析的 TypeScript 代码"),
      threshold: z.number().optional().default(10).describe("复杂度告警阈值"),
    },
    async ({ code, threshold }) => {
      // 简化的圈复杂度计算
      const patterns = [
        /\bif\b/g, /\belse\b/g, /\bfor\b/g, /\bwhile\b/g,
        /\bcase\b/g, /\bcatch\b/g, /\&\&/g, /\|\|/g, /\?\?/g,
      ];

      let complexity = 1; // 基础复杂度
      for (const pattern of patterns) {
        const matches = code.match(pattern);
        if (matches) complexity += matches.length;
      }

      const lines = code.split("\n").length;
      const functions = (code.match(/\bfunction\b|\b=>\b|\w+\s*\(/g) || []).length;

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              complexity,
              lines,
              functions,
              rating: complexity <= (threshold ?? 10) ? "✅ GOOD" : "⚠️ HIGH",
              threshold,
              suggestion: complexity > (threshold ?? 10)
                ? `复杂度 ${complexity} 超过阈值 ${threshold},建议拆分函数`
                : "代码复杂度在合理范围内",
            }, null, 2),
          },
        ],
      };
    }
  );

  // Tool 2:JSON 格式化与验证
  server.tool(
    "format_json",
    "格式化和验证 JSON 字符串,支持自定义缩进",
    {
      input: z.string().describe("要格式化的 JSON 字符串"),
      indent: z.number().optional().default(2).describe("缩进空格数"),
      sort_keys: z.boolean().optional().default(false).describe("是否排序键名"),
    },
    async ({ input, indent, sort_keys }) => {
      try {
        let parsed = JSON.parse(input);
        if (sort_keys) {
          parsed = sortObjectKeys(parsed);
        }
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify(parsed, null, indent),
            },
          ],
        };
      } catch (e) {
        return {
          content: [
            {
              type: "text",
              text: `JSON 解析错误: ${(e as Error).message}`,
            },
          ],
          isError: true,
        };
      }
    }
  );
}

function sortObjectKeys(obj: unknown): unknown {
  if (Array.isArray(obj)) return obj.map(sortObjectKeys);
  if (obj && typeof obj === "object") {
    return Object.keys(obj as Record<string, unknown>)
      .sort()
      .reduce((acc: Record<string, unknown>, key) => {
        acc[key] = sortObjectKeys((obj as Record<string, unknown>)[key]);
        return acc;
      }, {});
  }
  return obj;
}

3.2 Resources —— 暴露数据的窗口

Resource 是只读的数据源。Claude Code 可以主动发现和读取 Resource,无需你显式调用。

// src/resources/project-context.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import * as fs from "fs";
import * as path from "path";

export function registerProjectResources(server: McpServer) {
  // 静态 Resource:项目配置
  server.resource(
    "project-config",
    "config://project",
    async (uri) => ({
      contents: [
        {
          uri: uri.href,
          mimeType: "application/json",
          text: JSON.stringify({
            name: "mcp-devtool-server",
            version: "1.0.0",
            description: "Development toolchain MCP server",
            supportedTools: ["analyze_complexity", "format_json", "generate_scaffold"],
            supportedResources: ["project-config", "changelog"],
          }, null, 2),
        },
      ],
    })
  );

  // 动态 Resource 模板:读取项目文件
  server.resource(
    "file-reader",
    new URL("file://projects/{path}"),
    async (uri, { path: filePath }) => {
      // 安全检查:防止路径遍历
      const safePath = filePath.replace(/\.\./g, "").replace(/^\//, "");
      const fullPath = path.join(process.cwd(), safePath);

      if (!fullPath.startsWith(process.cwd())) {
        return {
          contents: [{
            uri: uri.href,
            mimeType: "text/plain",
            text: "Error: Path traversal detected",
          }],
        };
      }

      try {
        const content = fs.readFileSync(fullPath, "utf-8");
        const ext = path.extname(fullPath);
        const mimeMap: Record<string, string> = {
          ".ts": "text/typescript",
          ".js": "text/javascript",
          ".json": "application/json",
          ".md": "text/markdown",
          ".yaml": "text/yaml",
          ".yml": "text/yaml",
        };

        return {
          contents: [{
            uri: uri.href,
            mimeType: mimeMap[ext] || "text/plain",
            text: content,
          }],
        };
      } catch {
        return {
          contents: [{
            uri: uri.href,
            mimeType: "text/plain",
            text: `Error: File not found - ${safePath}`,
          }],
        };
      }
    }
  );
}

3.3 Prompts —— 模板化的交互框架

Prompt 是预定义的提示模板,可以接收参数并生成结构化的提示词。

// src/prompts/code-review.ts
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

export function registerCodeReviewPrompts(server: McpServer) {
  server.prompt(
    "review-code",
    "生成结构化的代码审查提示",
    {
      language: z.string().describe("编程语言"),
      focus: z.enum(["security", "performance", "readability", "all"]).describe("审查重点"),
      code: z.string().describe("待审查的代码"),
    },
    async ({ language, focus, code }) => {
      const focusMap = {
        security: "安全漏洞(OWASP Top 10、注入、XSS、认证问题)",
        performance: "性能瓶颈(时间复杂度、内存泄漏、不必要的计算)",
        readability: "可读性(命名规范、函数长度、注释质量)",
        all: "以上所有方面",
      };

      return {
        messages: [
          {
            role: "user",
            content: {
              type: "text",
              text: `请审查以下 ${language} 代码,重点关注 ${focusMap[focus]}。

## 代码
\`\`\`${language}
${code}
\`\`\`

## 审查要求
1. 列出发现的问题,按严重程度排序(Critical > Warning > Info)
2. 每个问题给出具体的修改建议
3. 总结代码的整体质量评分(1-10)`,
            },
          },
        ],
      };
    }
  );
}

3.4 组装主入口

// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { registerCodeMetricsTools } from "./tools/code-metrics.js";
import { registerProjectResources } from "./resources/project-context.js";
import { registerCodeReviewPrompts } from "./prompts/code-review.js";

const server = new McpServer({
  name: "devtool-server",
  version: "1.0.0",
});

// 注册所有能力
registerCodeMetricsTools(server);
registerProjectResources(server);
registerCodeReviewPrompts(server);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("DevTool MCP Server v1.0.0 running on stdio");
}

main().catch(console.error);
graph LR
    subgraph "MCP Server (devtool-server)"
        T[Tools] --> T1[analyze_complexity]
        T --> T2[format_json]
        T --> T3[generate_scaffold]

        R[Resources] --> R1["config://project"]
        R --> R2["file://projects/{path}"]

        P[Prompts] --> P1[review-code]
    end

    CC[Claude Code] -->|JSON-RPC| Server

    style Server fill:#1a1a2e,color:#fff
    style CC fill:#6c5ce7,color:#fff

第四章:2025-06-18 规范新特性实战

4.1 Tool Annotations —— 工具元数据标记

Tool Annotations 让客户端提前了解工具的特性,决定是否需要用户确认:

// src/tools/annotations.ts
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

export function registerAnnotatedTools(server: McpServer) {
  // 只读工具 —— 安全,无需确认
  server.tool(
    "search_files",
    { 
      pattern: z.string().describe("搜索模式(glob)"),
      directory: z.string().optional().default(".").describe("搜索目录"),
    },
    {
      title: "搜索文件",
      readOnlyHint: true,        // 只读,无副作用
      openWorldHint: true,       // 访问外部资源
    },
    async ({ pattern, directory }) => {
      // 实际实现用 fast-glob 等
      return {
        content: [{
          type: "text",
          text: `Searching for "${pattern}" in ${directory}...`,
        }],
      };
    }
  );

  // 破坏性工具 —— 需要用户明确确认
  server.tool(
    "delete_files",
    {
      pattern: z.string().describe("要删除的文件模式"),
    },
    {
      title: "删除文件",
      destructiveHint: true,     // 破坏性操作!
      readOnlyHint: false,
    },
    async ({ pattern }) => {
      return {
        content: [{
          type: "text",
          text: `Deleted files matching: ${pattern}`,
        }],
      };
    }
  );
}

Annotation 字段说明:

字段 类型 含义
title string 工具的人类可读标题
readOnlyHint boolean 是否只读(无副作用)
destructiveHint boolean 是否有破坏性(删除、覆盖等)
idempotentHint boolean 多次调用是否等幂
openWorldHint boolean 是否访问外部网络/资源

4.2 结构化输出 —— 强类型 Tool 返回值

2025-06-18 版本允许 Tool 定义输出 Schema,客户端可以验证返回结构:

// src/tools/structured-output.ts
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

// 定义输出 Schema
const SearchResultSchema = z.object({
  query: z.string(),
  totalMatches: z.number(),
  files: z.array(z.object({
    path: z.string(),
    line: z.number(),
    preview: z.string(),
    relevance: z.number().min(0).max(1),
  })),
  executionTimeMs: z.number(),
});

export function registerStructuredTools(server: McpServer) {
  server.tool(
    "grep_code",
    "在代码库中搜索文本模式,返回结构化结果",
    {
      query: z.string().describe("搜索关键词"),
      max_results: z.number().optional().default(20),
    },
    {
      // 输出 Schema —— 客户端可以据此验证
      outputSchema: SearchResultSchema,
    },
    async ({ query, max_results }) => {
      const startTime = Date.now();

      // 模拟搜索逻辑
      const results = [
        { path: "src/index.ts", line: 42, preview: `// found ${query} here`, relevance: 0.95 },
        { path: "src/tools/code-metrics.ts", line: 15, preview: `const ${query} = ...`, relevance: 0.82 },
      ].slice(0, max_results);

      return {
        structuredContent: {
          query,
          totalMatches: results.length,
          files: results,
          executionTimeMs: Date.now() - startTime,
        },
        // 仍然可以提供文本回退给不支持结构化输出的客户端
        content: [{
          type: "text",
          text: `Found ${results.length} matches for "${query}"`,
        }],
      };
    }
  );
}

4.3 Elicitation —— 服务器主动询问用户

Elicitation 是 MCP 2025-06-18 最强大的新特性之一。它允许服务器在执行过程中暂停,向用户确认信息:

// src/tools/elicitation.ts
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

export function registerElicitationTools(server: McpServer) {
  server.tool(
    "deploy_service",
    "部署服务到指定环境(需要用户确认)",
    {
      service_name: z.string().describe("服务名称"),
      environment: z.enum(["staging", "production"]).describe("目标环境"),
    },
    async ({ service_name, environment }, extra) => {
      // 关键操作前,使用 Elicitation 向用户确认
      const confirmation = await extra.elicitInput({
        message: `即将部署 ${service_name} 到 ${environment} 环境。确认继续?`,
        inputSchema: {
          type: "object" as const,
          properties: {
            confirmed: {
              type: "boolean" as const,
              description: "确认部署",
            },
            rollout_percentage: {
              type: "number" as const,
              description: "灰度发布百分比(1-100)",
              default: 100,
            },
          },
          required: ["confirmed"],
        },
      });

      if (!confirmation?.confirmed) {
        return {
          content: [{
            type: "text",
            text: "部署已取消",
          }],
          isError: true,
        };
      }

      const rollout = confirmation.rollout_percentage ?? 100;
      return {
        content: [{
          type: "text",
          text: `✅ ${service_name} 已部署到 ${environment}(灰度 ${rollout}%)`,
        }],
      };
    }
  );
}
sequenceDiagram
    participant User as 用户
    participant Client as MCP Client<br/>(Claude Code)
    participant Server as MCP Server

    User->>Client: 调用 deploy_service
    Client->>Server: tools/call {service_name, environment}
    Server->>Client: elicitation/request {确认部署?}
    Client->>User: 显示确认对话框
    User->>Client: confirmed=true, rollout=30%
    Client->>Server: elicitation/response
    Server->>Client: 工具执行结果
    Client->>User: 显示部署成功

第五章:双传输模式 —— Stdio 与 Streamable HTTP

5.1 传输层对比

graph TB
    subgraph "本地传输 (Stdio)"
        S1[Claude Code] -->|stdin/stdout| S2[MCP Server 进程]
    end

    subgraph "远程传输 (Streamable HTTP)"
        R1[任意 MCP Client] -->|HTTP POST| R2[MCP Server<br/>Cloudflare Workers]
        R2 -->|SSE Stream| R1
    end

    style S2 fill:#00b894,color:#fff
    style R2 fill:#0984e3,color:#fff
特性 Stdio Streamable HTTP
传输方式 标准输入/输出管道 HTTP POST + SSE 流
适用场景 本地 CLI 工具 远程服务、团队共享
认证 无需 OAuth 2.1
部署 用户本地安装 Cloudflare / Vercel / 自托管
多客户端 一对一 多对一
延迟 极低(进程内) 取决于网络

5.2 实现 Streamable HTTP 传输

// src/transports/http.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamable-http.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import express from "express";
import { randomUUID } from "crypto";

export async function startHttpTransport(server: McpServer, port: number) {
  const app = express();
  app.use(express.json());

  // 会话管理:每个客户端连接对应一个 transport 实例
  const transports: Map<string, StreamableHTTPServerTransport> = new Map();

  // MCP 消息端点
  app.post("/mcp", async (req, res) => {
    const sessionId = req.headers["mcp-session-id"] as string | undefined;
    let transport: StreamableHTTPServerTransport;

    if (sessionId && transports.has(sessionId)) {
      // 已有会话:复用 transport
      transport = transports.get(sessionId)!;
    } else if (isInitializeRequest(req.body)) {
      // 新会话:创建 transport
      transport = new StreamableHTTPServerTransport({
        sessionIdGenerator: randomUUID,
      });
      transports.set(transport.sessionId!, transport);

      // 清理断开的会话
      transport.onclose = () => {
        transports.delete(transport.sessionId!);
      };

      await server.connect(transport);
    } else {
      res.status(400).json({
        jsonrpc: "2.0",
        error: { code: -32000, message: "Bad Request: no valid session" },
        id: null,
      });
      return;
    }

    await transport.handleRequest(req, res, req.body);
  });

  // SSE 端点(用于服务器推送通知)
  app.get("/mcp", async (req, res) => {
    const sessionId = req.headers["mcp-session-id"] as string;
    const transport = transports.get(sessionId);
    if (!transport) {
      res.status(400).send("No active session");
      return;
    }
    await transport.handleRequest(req, res);
  });

  // DELETE 端点(关闭会话)
  app.delete("/mcp", async (req, res) => {
    const sessionId = req.headers["mcp-session-id"] as string;
    const transport = transports.get(sessionId);
    if (transport) {
      await transport.handleRequest(req, res);
      transports.delete(sessionId);
    }
  });

  app.listen(port, () => {
    console.error(`Streamable HTTP server listening on port ${port}`);
  });
}

5.3 双传输主入口

// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { startHttpTransport } from "./transports/http.js";
import { registerCodeMetricsTools } from "./tools/code-metrics.js";
import { registerAnnotatedTools } from "./tools/annotations.js";
import { registerStructuredTools } from "./tools/structured-output.js";
import { registerElicitationTools } from "./tools/elicitation.js";
import { registerProjectResources } from "./resources/project-context.js";
import { registerCodeReviewPrompts } from "./prompts/code-review.js";

function createServer(): McpServer {
  const server = new McpServer({
    name: "devtool-server",
    version: "1.0.0",
  });

  registerCodeMetricsTools(server);
  registerAnnotatedTools(server);
  registerStructuredTools(server);
  registerElicitationTools(server);
  registerProjectResources(server);
  registerCodeReviewPrompts(server);

  return server;
}

async function main() {
  const mode = process.env.MCP_TRANSPORT || "stdio";
  const server = createServer();

  if (mode === "stdio") {
    const transport = new StdioServerTransport();
    await server.connect(transport);
    console.error("DevTool MCP Server running on stdio");
  } else if (mode === "http") {
    const port = parseInt(process.env.PORT || "3000", 10);
    await startHttpTransport(server, port);
  } else {
    console.error(`Unknown transport: ${mode}. Use "stdio" or "http"`);
    process.exit(1);
  }
}

main().catch(console.error);

通过环境变量切换传输模式:

# 本地 Stdio 模式(Claude Code 使用)
MCP_TRANSPORT=stdio npx tsx src/index.ts

# 远程 HTTP 模式(多客户端共享)
MCP_TRANSPORT=http PORT=3000 npx tsx src/index.ts

第六章:远程部署到 Cloudflare Workers

6.1 为什么选 Cloudflare Workers

优势 说明
全球边缘 300+ 节点,客户端就近访问
零冷启动 V8 isolate,毫秒级启动
免费额度 每天 10 万次请求免费
OAuth 集成 Cloudflare Access 提供内置认证

6.2 适配 Workers 环境

Workers 不支持 Node.js 原生模块,需要调整代码:

// src/workers/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamable-http.js";
import { z } from "zod";

const server = new McpServer({
  name: "devtool-server-remote",
  version: "1.0.0",
});

// 注册 Tools(不能使用 Node.js API)
server.tool(
  "hash_text",
  "计算文本的 SHA-256 哈希值",
  { text: z.string().describe("输入文本") },
  async ({ text }) => {
    const encoder = new TextEncoder();
    const data = encoder.encode(text);
    const hashBuffer = await crypto.subtle.digest("SHA-256", data);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join("");

    return {
      content: [{
        type: "text",
        text: `SHA-256: ${hashHex}`,
      }],
    };
  }
);

server.tool(
  "base64_encode",
  "Base64 编码/解码",
  {
    input: z.string().describe("输入字符串"),
    operation: z.enum(["encode", "decode"]).describe("操作类型"),
  },
  async ({ input, operation }) => {
    const result = operation === "encode"
      ? btoa(unescape(encodeURIComponent(input)))
      : decodeURIComponent(escape(atob(input)));

    return {
      content: [{
        type: "text",
        text: result,
      }],
    };
  }
);

// Workers 入口
export default {
  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname === "/mcp" && request.method === "POST") {
      const transport = new StreamableHTTPServerTransport({
        sessionIdGenerator: undefined, // 无状态模式
      });
      await server.connect(transport);

      const body = await request.json();
      return transport.handleRequest(request, body);
    }

    // 健康检查
    if (url.pathname === "/health") {
      return new Response(JSON.stringify({ status: "ok", version: "1.0.0" }), {
        headers: { "Content-Type": "application/json" },
      });
    }

    return new Response("Not Found", { status: 404 });
  },
};

6.3 Wrangler 配置

# wrangler.toml
name = "mcp-devtool-server"
main = "src/workers/index.ts"
compatibility_date = "2025-06-18"
compatibility_flags = ["nodejs_compat"]

[observability]
enabled = true

部署命令:

npx wrangler deploy

6.4 接入远程服务器

在 Claude Code 的全局配置 ~/.claude.json 中添加远程 MCP:

{
  "mcpServers": {
    "devtool-remote": {
      "type": "streamable-http",
      "url": "https://mcp-devtool-server.<your-subdomain>.workers.dev/mcp"
    }
  }
}

或通过命令行:

claude mcp add devtool-remote --transport streamable-http \
  --url https://mcp-devtool-server.<your-subdomain>.workers.dev/mcp

第七章:OAuth 2.1 认证保护

远程 MCP 服务器必须考虑认证。2025-06-18 规范引入了基于 OAuth 2.1 的授权框架。

7.1 认证流程

sequenceDiagram
    participant Client as MCP Client
    participant Server as MCP Server
    participant Auth as Authorization Server<br/>(Cloudflare Access)

    Client->>Server: POST /mcp (无 token)
    Server-->>Client: HTTP 401 + WWW-Authenticate
    Client->>Auth: OAuth 2.1 Authorization Request
    Auth->>Client: Authorization Code
    Client->>Auth: Token Exchange
    Auth->>Client: Access Token
    Client->>Server: POST /mcp (Bearer Token)
    Server->>Server: 验证 Token
    Server-->>Client: 200 OK + MCP Response

7.2 实现 Token 验证中间件

// src/middleware/auth.ts
import { verifyJwt } from "./jwt-utils.js";

interface AuthConfig {
  audience: string;
  issuer: string;
  jwksUri: string;
}

export function createAuthMiddleware(config: AuthConfig) {
  return async (request: Request): Promise<{ authorized: boolean; error?: Response }> => {
    const authHeader = request.headers.get("Authorization");

    if (!authHeader?.startsWith("Bearer ")) {
      return {
        authorized: false,
        error: new Response("Unauthorized", {
          status: 401,
          headers: {
            "WWW-Authenticate": `Bearer realm="mcp", audience="${config.audience}"`,
          },
        }),
      };
    }

    const token = authHeader.slice(7);

    try {
      const payload = await verifyJwt(token, {
        audience: config.audience,
        issuer: config.issuer,
        jwksUri: config.jwksUri,
      });

      // Token 有效
      return { authorized: true };
    } catch {
      return {
        authorized: false,
        error: new Response("Invalid token", { status: 401 }),
      };
    }
  };
}

7.3 在 Workers 中集成

// src/workers/index-with-auth.ts
import { createAuthMiddleware } from "../middleware/auth.js";

const auth = createAuthMiddleware({
  audience: "https://mcp-devtool-server.your-domain.workers.dev",
  issuer: "https://your-auth-domain.cloudflareaccess.com",
  jwksUri: "https://your-auth-domain.cloudflareaccess.com/cdn-cgi/access/certs",
});

export default {
  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);

    // MCP 端点需要认证
    if (url.pathname === "/mcp") {
      const { authorized, error } = await auth(request);
      if (!authorized) return error!;

      // ... 处理 MCP 请求
    }

    // 健康检查不需要认证
    if (url.pathname === "/health") {
      return new Response(JSON.stringify({ status: "ok" }));
    }

    return new Response("Not Found", { status: 404 });
  },
};

在 Claude Code 中配置认证:

{
  "mcpServers": {
    "devtool-remote-auth": {
      "type": "streamable-http",
      "url": "https://mcp-devtool-server.<subdomain>.workers.dev/mcp",
      "headers": {
        "Authorization": "Bearer ${MCP_ACCESS_TOKEN}"
      }
    }
  }
}

第八章:测试与调试

8.1 MCP Inspector —— 官方调试工具

MCP 提供了 Inspector 工具,可以在浏览器中可视化调试 MCP 服务器:

npx @modelcontextprotocol/inspector npx tsx src/index.ts

Inspector 会打开一个 Web UI,可以: - 查看服务器声明的 Tools / Resources / Prompts - 直接调用 Tool 并查看返回值 - 测试 Resource 读取 - 查看 JSON-RPC 消息日志

8.2 单元测试

// tests/tools.test.ts
import { describe, it, expect } from "vitest";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { spawn } from "child_process";

describe("DevTool MCP Server", () => {
  let client: Client;

  beforeAll(async () => {
    const transport = new StdioClientTransport({
      command: "npx",
      args: ["tsx", "src/index.ts"],
    });

    client = new Client({
      name: "test-client",
      version: "1.0.0",
    });

    await client.connect(transport);
  });

  it("应该列出所有注册的 Tools", async () => {
    const tools = await client.listTools();
    const toolNames = tools.tools.map(t => t.name);

    expect(toolNames).toContain("get_timestamp");
    expect(toolNames).toContain("analyze_complexity");
    expect(toolNames).toContain("format_json");
  });

  it("应该正确分析代码复杂度", async () => {
    const result = await client.callTool({
      name: "analyze_complexity",
      arguments: {
        code: "if (a) { for (let i = 0; i < 10; i++) { if (b) { console.log(i); } } }",
      },
    });

    const content = result.content as Array<{ type: string; text: string }>;
    const parsed = JSON.parse(content[0].text);
    expect(parsed.complexity).toBeGreaterThan(1);
  });

  it("应该格式化 JSON 并排序键", async () => {
    const result = await client.callTool({
      name: "format_json",
      arguments: {
        input: '{"z":1,"a":2}',
        sort_keys: true,
      },
    });

    const content = result.content as Array<{ type: string; text: string }>;
    expect(content[0].text).toBe('{\n  "a": 2,\n  "z": 1\n}');
  });

  afterAll(async () => {
    await client.close();
  });
});

8.3 调试技巧清单

技巧 方法
查看协议消息 Inspector 的 Messages 标签
调试 Tool 执行 console.error(JSON.stringify({args, result}))
检查连接状态 claude mcp list 查看已注册服务器
日志持久化 MCP_LOG=1 npx tsx src/index.ts 2>server.log
热重载 用 tsx watch 替代 tsx,修改代码自动重启
连接超时 检查 server.connect() 是否在 30 秒内完成

第九章:完整项目结构

mcp-devtool-server/
├── src/
│   ├── index.ts                  # 主入口(双传输)
│   ├── tools/
│   │   ├── code-metrics.ts       # 代码复杂度分析
│   │   ├── annotations.ts        # Tool Annotations 示例
│   │   ├── structured-output.ts  # 结构化输出示例
│   │   └── elicitation.ts        # Elicitation 示例
│   ├── resources/
│   │   └── project-context.ts    # 项目上下文资源
│   ├── prompts/
│   │   └── code-review.ts        # 代码审查提示模板
│   ├── transports/
│   │   └── http.ts               # Streamable HTTP 传输
│   ├── middleware/
│   │   └── auth.ts               # OAuth 2.1 认证
│   └── workers/
│       ├── index.ts              # Workers 入口
│       └── index-with-auth.ts    # 带 Auth 的 Workers 入口
├── tests/
│   └── tools.test.ts             # 单元测试
├── .mcp.json                     # 本地 Claude Code 配置
├── wrangler.toml                 # Cloudflare Workers 配置
├── tsconfig.json
└── package.json

实战案例:构建一个 DevOps 工具链 MCP 服务器

把前面所有知识串起来,构建一个真实可用的 DevOps 工具链 MCP 服务器,包含以下能力:

graph TB
    subgraph "DevOps MCP Server"
        direction TB

        subgraph "Tools"
            T1[analyze_complexity<br/>代码复杂度分析]
            T2[format_json<br/>JSON格式化]
            T3[hash_text<br/>哈希计算]
            T4[deploy_service<br/>服务部署+确认]
            T5[grep_code<br/>代码搜索]
        end

        subgraph "Resources"
            R1[config://project<br/>项目配置]
            R2[file://projects/path<br/>文件读取]
        end

        subgraph "Prompts"
            P1[review-code<br/>代码审查模板]
        end
    end

    subgraph "部署方式"
        D1[Stdio 本地]
        D2[HTTP 远程]
        D3[Workers Cloudflare]
    end

    DevOps --> D1
    DevOps --> D2
    DevOps --> D3

    style DevOps fill:#1a1a2e,color:#fff

完整运行命令:

# 克隆并安装
git clone <your-repo> && cd mcp-devtool-server
npm install

# 本地测试
npm run dev

# Inspector 调试
npx @modelcontextprotocol/inspector npm run dev

# 运行测试
npx vitest

# 部署到 Cloudflare
npx wrangler deploy

# Claude Code 接入远程服务器
claude mcp add devtools --transport streamable-http \
  --url https://mcp-devtool-server.your-subdomain.workers.dev/mcp

关键决策速查表

决策点 推荐 替代方案
传输模式 本地用 Stdio,远程用 Streamable HTTP SSE(已废弃)
Schema 库 Zod(官方推荐) JSON Schema 直接写
部署平台 Cloudflare Workers(免费、全球) Vercel / AWS Lambda
认证方案 OAuth 2.1(规范要求) API Key(非标准)
框架选择 官方 SDK @modelcontextprotocol/sdk FastMCP(第三方封装)
测试工具 MCP Inspector + Vitest 手动 curl

延伸阅读

参考来源

  1. MCP 2025-06-18 规范变更 — 2025-06-18 版本完整变更日志,涵盖 Elicitation、结构化输出、Tool Annotations
  2. TypeScript SDK GitHub 仓库 — 官方 TypeScript SDK 源码与文档
  3. MCP Tools vs Resources vs Prompts 对比 — 三大原语的选择策略
  4. Streamable HTTP 传输详解 — Stdio、SSE、Streamable HTTP 传输机制对比
  5. Cloudflare Workers MCP 部署 — Cloudflare 官方远程 MCP 服务器部署指南

训练营适用性评估:本教程适合"AI 工具链开发训练营"中级模块,预计教学时长 4-5 小时(含动手实验)。建议分两节:第一节(2h)完成本地 Stdio 服务器 + 三大原语;第二节(2-3h)实现 Streamable HTTP + Cloudflare 部署 + OAuth 认证。学员需具备 TypeScript 基础和 HTTP 协议知识。

[!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学通信知识星球海报-扫码加入