<!-- title: 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 能做什么"抽象为三大原语：

```mermaid
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 规范经历了三个关键版本：

```mermaid
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 创建项目

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

初始化 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/**/*"]
}
```

### 2.2 最小可运行服务器

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

```typescript
// 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` 中添加启动脚本：

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

### 2.3 接入 Claude Code 测试

在项目目录创建 `.mcp.json`：

```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 定义）。

```typescript
// 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，无需你显式调用。

```typescript
// 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 是预定义的提示模板，可以接收参数并生成结构化的提示词。

```typescript
// 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 组装主入口

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

```mermaid
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 让客户端提前了解工具的特性，决定是否需要用户确认：

```typescript
// 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，客户端可以验证返回结构：

```typescript
// 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 最强大的新特性之一。它允许服务器在执行过程中暂停，向用户确认信息：

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

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

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

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

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

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

```bash
# 本地 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 原生模块，需要调整代码：

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

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

[observability]
enabled = true
```

部署命令：

```bash
npx wrangler deploy
```

### 6.4 接入远程服务器

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

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

或通过命令行：

```bash
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 认证流程

```mermaid
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 验证中间件

```typescript
// 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 中集成

```typescript
// 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 中配置认证：

```json
{
  "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 服务器：

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

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

### 8.2 单元测试

```typescript
// 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 服务器，包含以下能力：

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

完整运行命令：

```bash
# 克隆并安装
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 |

---

## 延伸阅读

- [MCP 官方规范 - 2025-06-18 版本变更](https://modelcontextprotocol.io/specification/2025-06-18/changelog) — 了解所有协议变更细节
- [TypeScript SDK 官方文档](https://ts.sdk.modelcontextprotocol.io/documents/server.html) — Server API 完整参考
- [Cloudflare Remote MCP 部署指南](https://developers.cloudflare.com/agents/guides/remote-mcp-server/) — Cloudflare 官方 MCP 部署教程
- [MCP 传输层详解：Stdio vs Streamable HTTP](https://www.truefoundry.com/blog/mcp-stdio-vs-streamable-http-enterprise) — 企业级传输层对比
- [MCP OAuth 2.1 认证规范](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) — 认证机制官方文档

## 参考来源

1. [MCP 2025-06-18 规范变更](https://modelcontextprotocol.io/specification/2025-06-18/changelog) — 2025-06-18 版本完整变更日志，涵盖 Elicitation、结构化输出、Tool Annotations
2. [TypeScript SDK GitHub 仓库](https://github.com/modelcontextprotocol/typescript-sdk) — 官方 TypeScript SDK 源码与文档
3. [MCP Tools vs Resources vs Prompts 对比](https://www.rapidevelopers.com/mcp-tutorial/mcp-tools-vs-resources-vs-prompts) — 三大原语的选择策略
4. [Streamable HTTP 传输详解](https://dev.to/zoricic/understanding-mcp-server-transports-stdio-sse-and-http-streamable-5b1p) — Stdio、SSE、Streamable HTTP 传输机制对比
5. [Cloudflare Workers MCP 部署](https://developers.cloudflare.com/agents/guides/remote-mcp-server/) — 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学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
