首页 › tutorials › MCP协议深度与TS工具开发实战

MCP协议深度解析与TypeScript工具开发实战

概述

本教程面向有一定Claude Code使用经验、想要深入理解MCP(Model Context Protocol)协议原理并使用TypeScript SDK构建生产级MCP Server的开发者。你将学到MCP协议的架构设计、JSON-RPC消息流、stdio与Streamable HTTP双传输模式、Tools/Resources/Prompts三大原语的完整实现,以及OAuth 2.1认证集成。教程最终交付一个完整的「代码质量分析」MCP Server实战项目。

适用人群:有TypeScript/Node.js基础,已熟悉Claude Code基本操作,想要从协议层面理解MCP并构建可发布的自定义工具的开发者。


前置条件

依赖 最低版本 验证命令
Node.js 20.x+ node -v
npm/pnpm 10.x+ / 9.x+ npm -v
TypeScript 5.x+ npx tsc -v
Claude Code CLI 最新版 claude -v
Git 2.x+ git -v

确保你已拥有Anthropic API Key或Claude Code订阅。本教程所有代码在macOS和Linux上测试通过,Windows用户建议使用WSL2。


第一章 MCP协议架构全景

1.1 什么是MCP

Model Context Protocol是Anthropic于2024年11月发布的开放标准协议,旨在解决LLM应用与外部工具/数据源之间的标准化连接问题。MCP的核心理念可以类比USB-C接口——一个通用协议连接所有AI工具。

MCP协议定义了三个核心角色:

graph TB
    subgraph Host["Host(宿主应用)"]
        Client["MCP Client<br/>协议客户端"]
        App["Claude Code / Claude Desktop<br/>IDE / 自定义应用"]
    end

    subgraph Server1["MCP Server A"]
        T1["Tools<br/>可调用函数"]
        R1["Resources<br/>数据资源"]
        P1["Prompts<br/>提示模板"]
    end

    subgraph Server2["MCP Server B"]
        T2["Tools"]
        R2["Resources"]
        P2["Prompts"]
    end

    subgraph External["外部世界"]
        API["REST APIs"]
        DB["数据库"]
        FS["文件系统"]
        Git["Git仓库"]
    end

    App --> Client
    Client -->|"JSON-RPC 2.0<br/>双向通信"| Server1
    Client -->|"JSON-RPC 2.0<br/>双向通信"| Server2
    Server1 --> API
    Server1 --> DB
    Server2 --> FS
    Server2 --> Git

关键设计原则:一个Host可以运行多个Client实例,每个Client连接一个Server。这种架构保证了安全隔离和模块化。

1.2 JSON-RPC 2.0消息流

MCP基于JSON-RPC 2.0协议,所有消息分为三类:

请求(Request):

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "analyze_code",
    "arguments": {
      "file_path": "/src/index.ts",
      "metrics": ["complexity", "maintainability"]
    }
  }
}

响应(Response):

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "代码复杂度分析报告:..."
      }
    ]
  }
}

通知(Notification):无ID,不期望响应

{
  "jsonrpc": "2.0",
  "method": "notifications/progress",
  "params": {
    "progressToken": "abc123",
    "progress": 50,
    "total": 100
  }
}

1.3 生命周期:连接建立四步曲

sequenceDiagram
    participant Client as MCP Client
    participant Server as MCP Server

    Note over Client,Server: 第1步:初始化握手
    Client->>Server: initialize {capabilities, clientInfo}
    Server-->>Client: {capabilities, serverInfo}
    Client->>Server: notifications/initialized

    Note over Client,Server: 第2步:能力发现
    Client->>Server: tools/list
    Server-->>Client: [tool1, tool2, ...]
    Client->>Server: resources/list
    Server-->>Client: [resource1, ...]

    Note over Client,Server: 第3步:正常操作
    Client->>Server: tools/call {name, arguments}
    Server-->>Server: 执行工具逻辑
    Server-->>Client: {content: [...]}

    Note over Client,Server: 第4步:关闭
    Client->>Server: 关闭连接/通知

初始化握手是MCP协议最关键的环节。Client在initialize请求中声明自己的能力(如支持sampling),Server在响应中声明自己提供的能力(tools/resources/prompts)。握手成功后进入正常操作阶段。

1.4 传输模式对比

特性 stdio Streamable HTTP
连接方式 标准输入/输出管道 HTTP POST + SSE
部署场景 本地CLI工具 远程服务、云端
认证 无需(本地进程) OAuth 2.1 / API Key
多客户端 不支持 支持
延迟 极低(进程间通信) 网络延迟
配置复杂度 低 中高
Claude Code支持 ✅ 原生 ✅ 需URL配置

第二章 TypeScript SDK开发环境搭建

2.1 创建项目骨架

# 创建项目目录
mkdir code-analyzer-mcp && cd code-analyzer-mcp

# 初始化npm项目
npm init -y

# 安装TypeScript SDK和开发依赖
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx

# 初始化TypeScript配置
npx tsc --init

编辑tsconfig.json:

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

2.2 项目目录结构

code-analyzer-mcp/
├── src/
│   ├── index.ts          # 入口:stdio传输模式
│   ├── server-http.ts    # 入口:Streamable HTTP模式
│   ├── server.ts         # Server核心逻辑(共享)
│   ├── tools/
│   │   ├── complexity.ts  # 复杂度分析工具
│   │   ├── dependency.ts  # 依赖分析工具
│   │   └── security.ts    # 安全检查工具
│   ├── resources/
│   │   └── config.ts     # 配置资源
│   ├── prompts/
│   │   └── review.ts     # 代码审查提示模板
│   └── utils/
│       ├── parser.ts     # 代码解析工具
│       └── formatter.ts  # 报告格式化
├── package.json
├── tsconfig.json
└── README.md

2.3 配置package.json

{
  "name": "code-analyzer-mcp",
  "version": "1.0.0",
  "description": "代码质量分析 MCP Server - TypeScript实现",
  "type": "module",
  "bin": {
    "code-analyzer-mcp": "./dist/index.js"
  },
  "scripts": {
    "build": "tsc",
    "dev": "tsx src/index.ts",
    "dev:http": "tsx src/server-http.ts",
    "start": "node dist/index.js",
    "start:http": "node dist/server-http.js",
    "inspect": "npx @modelcontextprotocol/inspector tsx src/index.ts"
  },
  "files": ["dist"]
}

常见问题:"type": "module"是必需的,MCP TypeScript SDK使用ESM模块。如果忘记设置,会遇到require() of ES Module错误。


第三章 Tools原语:构建可调用函数

3.1 Tool注册机制

Tool是MCP最核心的原语——它让LLM能够调用你定义的函数。每个Tool由三部分组成:

  1. name:唯一标识符(小写字母、数字、下划线、连字符)
  2. description:自然语言描述(LLM根据此描述决定何时调用)
  3. inputSchema:JSON Schema定义的参数结构
// src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

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

  // 注册所有工具
  registerComplexityTool(server);
  registerDependencyTool(server);
  registerSecurityTool(server);

  // 注册资源
  registerConfigResource(server);

  // 注册提示模板
  registerReviewPrompt(server);

  return server;
}

3.2 实战:代码复杂度分析工具

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

export function registerComplexityTool(server: McpServer): void {
  server.tool(
    // 工具名称
    "analyze_complexity",
    // 工具描述 - 这段文字直接影响LLM是否选择调用
    "分析指定TypeScript/JavaScript文件的代码复杂度指标。返回圈复杂度、认知复杂度、" +
    "函数行数统计、嵌套深度等指标。支持单个文件和目录批量分析。",
    // 参数Schema - 使用Zod定义,自动转换为JSON Schema
    {
      path: z.string().describe("要分析的文件或目录路径(绝对路径)"),
      metrics: z
        .array(z.enum([
          "cyclomatic",      // 圈复杂度
          "cognitive",       // 认知复杂度
          "lines",           // 行数统计
          "nesting",         // 嵌套深度
          "all"              // 全部指标
        ]))
        .default(["all"])
        .describe("要计算的复杂度指标列表"),
      threshold: z
        .number()
        .default(10)
        .describe("复杂度告警阈值(超过此值标记为高风险)"),
      include_pattern: z
        .string()
        .default("**/*.{ts,tsx,js,jsx}")
        .describe("文件匹配模式(glob格式)"),
    },
    // 工具实现函数
    async ({ path, metrics, threshold, include_pattern }) => {
      try {
        const results = await analyzeComplexity({
          path,
          metrics: metrics.includes("all") 
            ? ["cyclomatic", "cognitive", "lines", "nesting"]
            : metrics,
          threshold,
          includePattern: include_pattern,
        });

        // 高风险函数统计
        const highRisk = results.filter(r => r.cyclomatic > threshold);

        const report = formatReport(results, highRisk, threshold);

        return {
          content: [
            {
              type: "text",
              text: report,
            },
            // 如果有高风险函数,同时返回结构化数据
            ...(highRisk.length > 0 ? [{
              type: "text",
              text: `\n⚠️ 发现 ${highRisk.length} 个高风险函数(复杂度 > ${threshold})`,
            }] : []),
          ],
        };
      } catch (error) {
        return {
          content: [
            {
              type: "text",
              text: `❌ 分析失败:${error instanceof Error ? error.message : String(error)}`,
            },
          ],
          isError: true,
        };
      }
    }
  );
}

关键要点:

3.3 依赖分析工具

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

interface DependencyNode {
  name: string;
  version: string;
  dependencies: string[];
  dev: boolean;
}

export function registerDependencyTool(server: McpServer): void {
  server.tool(
    "analyze_dependencies",
    "分析项目的npm依赖关系。检测过时版本、安全漏洞、未使用的依赖、以及依赖树深度。支持package.json和lockfile解析。",
    {
      project_path: z.string().describe("项目根目录路径"),
      check_updates: z.boolean().default(false)
        .describe("是否检查可更新的版本"),
      check_vulnerabilities: z.boolean().default(true)
        .describe("是否检查已知安全漏洞"),
      depth: z.number().default(3)
        .describe("依赖树分析深度"),
    },
    async ({ project_path, check_updates, check_vulnerabilities, depth }) => {
      // 读取package.json
      const pkgJson = await readPackageJson(project_path);

      // 构建依赖树
      const depTree = await buildDependencyTree(project_path, depth);

      // 可选:检查安全漏洞
      const vulnerabilities = check_vulnerabilities 
        ? await checkSecurity(project_path) 
        : [];

      // 可选:检查版本更新
      const updates = check_updates 
        ? await checkForUpdates(pkgJson) 
        : [];

      // 检测未使用依赖
      const unused = await findUnusedDependencies(project_path, pkgJson);

      const report = [
        "## 依赖分析报告\n",
        `📦 直接依赖: ${Object.keys(pkgJson.dependencies || {}).length} 个`,
        `🔧 开发依赖: ${Object.keys(pkgJson.devDependencies || {}).length} 个`,
        `🌳 最大依赖深度: ${depTree.maxDepth} 层`,
        `🌳 总依赖数量: ${depTree.totalCount} 个\n`,
      ];

      if (vulnerabilities.length > 0) {
        report.push(`### ⚠️ 安全漏洞 (${vulnerabilities.length})`);
        vulnerabilities.forEach(v => {
          report.push(`- **${v.package}** ${v.severity}: ${v.title}`);
          report.push(`  修复版本: ${v.fixVersion}`);
        });
        report.push("");
      }

      if (unused.length > 0) {
        report.push(`### 🗑️ 未使用的依赖 (${unused.length})`);
        unused.forEach(d => report.push(`- ${d}`));
        report.push("");
      }

      if (updates.length > 0) {
        report.push(`### 📈 可更新的依赖 (${updates.length})`);
        updates.forEach(u => {
          report.push(`- **${u.name}**: ${u.current} → ${u.latest}`);
        });
      }

      return {
        content: [{ type: "text", text: report.join("\n") }],
      };
    }
  );
}

3.4 安全检查工具

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

export function registerSecurityTool(server: McpServer): void {
  server.tool(
    "security_scan",
    "扫描项目代码中的潜在安全问题。检测硬编码密钥、SQL注入风险、XSS漏洞模式、" +
    "不安全的依赖版本、以及敏感文件泄露风险。基于静态分析规则引擎。",
    {
      target_path: z.string().describe("要扫描的项目路径"),
      rules: z.array(z.enum([
        "hardcoded-secrets",   // 硬编码密钥
        "sql-injection",       // SQL注入
        "xss",                 // XSS漏洞
        "unsafe-deserialize",  // 不安全反序列化
        "sensitive-files",     // 敏感文件检测
        "all"
      ])).default(["all"]).describe("启用的检查规则"),
      severity: z.enum(["low", "medium", "high", "critical"])
        .default("medium")
        .describe("最低报告严重级别"),
    },
    async ({ target_path, rules, severity }) => {
      const activeRules = rules.includes("all")
        ? ["hardcoded-secrets", "sql-injection", "xss", 
           "unsafe-deserialize", "sensitive-files"]
        : rules;

      const findings = await runSecurityScan(target_path, activeRules, severity);

      const grouped = {
        critical: findings.filter(f => f.severity === "critical"),
        high: findings.filter(f => f.severity === "high"),
        medium: findings.filter(f => f.severity === "medium"),
        low: findings.filter(f => f.severity === "low"),
      };

      const lines = [
        "## 🔒 安全扫描报告\n",
        `扫描目标: ${target_path}`,
        `启用规则: ${activeRules.join(", ")}`,
        `发现总数: ${findings.length}\n`,
      ];

      for (const [level, items] of Object.entries(grouped)) {
        if (items.length === 0) continue;
        const icon = level === "critical" ? "🔴" : 
                     level === "high" ? "🟠" : 
                     level === "medium" ? "🟡" : "🟢";
        lines.push(`### ${icon} ${level.toUpperCase()} (${items.length})`);
        items.forEach(f => {
          lines.push(`- **${f.rule}**: ${f.message}`);
          lines.push(`  📄 ${f.file}:${f.line}`);
          if (f.suggestion) lines.push(`  💡 建议: ${f.suggestion}`);
        });
        lines.push("");
      }

      return {
        content: [{ type: "text", text: lines.join("\n") }],
      };
    }
  );
}

第四章 Resources原语:暴露数据上下文

4.1 Resources vs Tools的区别

许多开发者会混淆Resources和Tools。核心区别:

维度 Tools Resources
语义 动作/操作(动词) 数据/信息(名词)
调用方式 LLM主动调用 LLM读取嵌入上下文
副作用 可能有(写数据库等) 只读
缓存 不缓存 可缓存(ETag机制)
典型场景 执行分析、调用API 提供配置、文档、数据

4.2 注册静态和动态Resources

// src/resources/config.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

export function registerConfigResource(server: McpServer): void {
  // 静态资源:固定的配置信息
  server.resource(
    "analysis-config",
    "config://analysis-rules",
    async (uri) => ({
      contents: [
        {
          uri: uri.href,
          mimeType: "application/json",
          text: JSON.stringify({
            version: "1.0.0",
            rules: {
              cyclomatic_threshold: 10,
              max_function_lines: 50,
              max_nesting_depth: 4,
              excluded_patterns: [
                "node_modules/**",
                "dist/**",
                "*.min.js",
                "*.d.ts"
              ],
              severity_mapping: {
                hardcoded_secrets: "critical",
                sql_injection: "high",
                xss: "high",
                unsafe_deserialize: "medium",
              }
            }
          }, null, 2),
        },
      ],
    })
  );

  // 动态资源:从文件系统读取的项目元信息
  server.resource(
    "project-info",
    "project://info",
    { mimeType: "application/json" },
    async (uri) => {
      const projectMeta = await gatherProjectMeta();
      return {
        contents: [
          {
            uri: uri.href,
            mimeType: "application/json",
            text: JSON.stringify(projectMeta, null, 2),
          },
        ],
      };
    }
  );

  // 模板资源:支持参数化的资源URI
  server.resource(
    "file-content",
    new ResourceTemplate("file://{+path}", { list: undefined }),
    async (uri, { path }) => {
      const content = await readFileContent(path);
      return {
        contents: [
          {
            uri: uri.href,
            mimeType: guessMimeType(path),
            text: content,
          },
        ],
      };
    }
  );
}

注意:Resources在Claude Code中的可见性有限——目前Claude Code主要使用Tools交互,Resources更多用于Claude Desktop和自定义客户端。但定义Resources可以让你的Server在更多场景下复用。


第五章 Prompts原语:定义提示模板

5.1 为什么需要Prompts

Prompts原语让你定义可复用的提示模板,用户可以通过Claude Desktop的Prompt选择器快速使用。每个Prompt可以接受参数,返回结构化的消息序列。

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

export function registerReviewPrompt(server: McpServer): void {
  server.prompt(
    "code-review",
    "生成结构化代码审查清单。基于传入的文件路径和关注点," +
    "生成一份全面的代码审查指南,引导LLM进行深度审查。",
    {
      file_path: z.string().describe("待审查的文件路径"),
      focus_areas: z.string()
        .default("correctness,performance,security")
        .describe("审查关注领域(逗号分隔)"),
      language: z.string().default("chinese")
        .describe("审查报告语言"),
    },
    async ({ file_path, focus_areas, language }) => {
      const areas = focus_areas.split(",").map(s => s.trim());
      const lang = language === "chinese" ? "中文" : "English";

      return {
        messages: [
          {
            role: "user",
            content: {
              type: "text",
              text: `请对以下文件进行全面的代码审查:${file_path}\n\n` +
                `请使用${lang}输出审查报告,重点关注以下领域:\n` +
                areas.map((a, i) => `${i + 1}. **${a}**`).join("\n") +
                `\n\n审查报告请包含:\n` +
                `- 总体评分(1-10)\n` +
                `- 每个关注领域的具体发现\n` +
                `- 严重程度分级(critical/high/medium/low)\n` +
                `- 具体修复建议和代码示例\n` +
                `- 总结和优先修复建议`,
            },
          },
        ],
      };
    }
  );
}

第六章 传输层:stdio与Streamable HTTP双模实现

6.1 stdio模式(本地开发首选)

stdio是最简单的传输方式——Claude Code通过子进程启动你的Server,通过stdin/stdout通信:

// src/index.ts
#!/usr/bin/env node

import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createServer } from "./server.js";

async function main() {
  const server = createServer();
  const transport = new StdioServerTransport();
  await server.connect(transport);

  // stdio模式下,日志不能写到stdout(会干扰协议通信)
  // 使用stderr或文件日志
  console.error("✅ Code Analyzer MCP Server 已启动 (stdio模式)");
}

main().catch((error) => {
  console.error("❌ 启动失败:", error);
  process.exit(1);
});

调试要点:stdio模式下,console.log()会写入stdout,与MCP消息流冲突导致协议错误。所有日志必须使用console.error()或写入文件。

6.2 Streamable HTTP模式(远程部署)

Streamable HTTP是MCP 2025-03-26规范引入的新传输方式,替代了旧的SSE传输。它支持:

// src/server-http.ts
import express from "express";
import { StreamableHTTPServerTransport } from 
  "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createServer } from "./server.js";

const app = express();
app.use(express.json());

// Streamable HTTP端点
app.post("/mcp", async (req, res) => {
  const server = createServer();
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined, // 无状态模式
  });

  try {
    await server.connect(transport);
    await transport.handleRequest(req, res, req.body);
  } catch (error) {
    console.error("MCP请求处理失败:", error);
    res.status(500).json({
      jsonrpc: "2.0",
      error: { code: -32603, message: "Internal error" },
      id: null,
    });
  }
});

// SSE端点(用于服务器推送)
app.get("/mcp", async (req, res) => {
  const server = createServer();
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined,
  });
  await server.connect(transport);
  await transport.handleRequest(req, res);
});

// DELETE端点(关闭会话)
app.delete("/mcp", async (req, res) => {
  const server = createServer();
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined,
  });
  await server.connect(transport);
  await transport.handleRequest(req, res);
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.error(`✅ Code Analyzer MCP Server 已启动 (HTTP模式)`);
  console.error(`   端点: http://localhost:${PORT}/mcp`);
});

6.3 两种模式的工作流对比

graph LR
    subgraph stdio模式
        CC1["Claude Code CLI"] -->|"启动子进程"| Proc["Node.js 进程<br/>(Server)"]
        Proc -->|"stdin/stdout<br/>JSON-RPC"| CC1
    end

    subgraph Streamable_HTTP模式["Streamable HTTP模式"]
        CC2["Claude Code CLI"] -->|"HTTP POST /mcp<br/>JSON-RPC in body"| GW["Express Server<br/>:3000"]
        GW -->|"JSON-RPC response<br/>或 SSE stream"| CC2
    end

    style stdio模式 fill:#e8f5e9
    style Streamable_HTTP模式 fill:#e3f2fd

第七章 Claude Code集成配置

7.1 项目级配置(推荐)

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

{
  "mcpServers": {
    "code-analyzer": {
      "command": "npx",
      "args": ["-y", "tsx", "/absolute/path/to/code-analyzer-mcp/src/index.ts"],
      "env": {
        "NODE_ENV": "development"
      }
    }
  }
}

7.2 用户级全局配置

编辑~/.claude/settings.json,让所有项目都能使用:

{
  "mcpServers": {
    "code-analyzer": {
      "command": "node",
      "args": ["/usr/local/lib/code-analyzer-mcp/dist/index.js"],
      "env": {}
    }
  }
}

7.3 远程HTTP服务器配置

对于Streamable HTTP模式的Server:

{
  "mcpServers": {
    "code-analyzer-remote": {
      "type": "http",
      "url": "https://your-domain.com/mcp",
      "headers": {
        "Authorization": "Bearer your-api-key"
      }
    }
  }
}

7.4 验证连接

启动Claude Code后,使用/mcp命令查看已连接的MCP服务器状态:

claude
> /mcp

预期输出:

MCP Servers:
  ✅ code-analyzer (connected)
    Tools: analyze_complexity, analyze_dependencies, security_scan
    Resources: config://analysis-rules, project://info
    Prompts: code-review

常见问题排查: - 如果Server未显示,检查JSON配置语法是否正确 - 如果显示❌ connection failed,手动运行npx tsx src/index.ts看是否有错误输出 - 如果Server启动了但Tool不可见,检查server.tool()的description是否为空


第八章 实战项目:完整代码质量分析Server

8.1 工具函数实现

// src/utils/parser.ts
import { readFile, stat, readdir } from "fs/promises";
import { join, extname } from "path";

interface ComplexityResult {
  file: string;
  function_name: string;
  start_line: number;
  end_line: number;
  cyclomatic: number;
  cognitive: number;
  lines: number;
  nesting_depth: number;
}

export async function analyzeComplexity(options: {
  path: string;
  metrics: string[];
  threshold: number;
  includePattern: string;
}): Promise<ComplexityResult[]> {
  const { path, threshold } = options;
  const isDir = (await stat(path)).isDirectory();
  const files = isDir ? await collectFiles(path) : [path];

  const results: ComplexityResult[] = [];

  for (const file of files) {
    if (!isSourceFile(file)) continue;

    const content = await readFile(file, "utf-8");
    const functions = extractFunctions(content, file);

    for (const fn of functions) {
      const cyclomatic = calculateCyclomaticComplexity(fn.body);
      const cognitive = calculateCognitiveComplexity(fn.body);

      if (cyclomatic > threshold || cognitive > threshold) {
        results.push({
          file,
          function_name: fn.name,
          start_line: fn.startLine,
          end_line: fn.endLine,
          cyclomatic,
          cognitive,
          lines: fn.endLine - fn.startLine + 1,
          nesting_depth: calculateMaxNesting(fn.body),
        });
      }
    }
  }

  return results.sort((a, b) => b.cyclomatic - a.cyclomatic);
}

// 圈复杂度:基于决策点计数
function calculateCyclomaticComplexity(code: string): number {
  const patterns = [
    /\bif\b/g,
    /\belse\s+if\b/g,
    /\bfor\b/g,
    /\bwhile\b/g,
    /\bcase\b/g,
    /\bcatch\b/g,
    /\?\s*[^:]+:/g,       // 三元运算符
    /&&/g,                 // 逻辑与
    /\|\|/g,               // 逻辑或
    /\?\?/g,               // 空值合并
  ];

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

// 认知复杂度:考虑嵌套加成
function calculateCognitiveComplexity(code: string): number {
  let complexity = 0;
  let nestingLevel = 0;
  const lines = code.split("\n");

  for (const line of lines) {
    const trimmed = line.trim();
    const indent = line.length - trimmed.length;

    // 进入嵌套
    if (/[\{]$/.test(trimmed) || /\b(if|for|while|switch|try)\b/.test(trimmed)) {
      complexity += 1 + nestingLevel;
      if (/[\{]$/.test(trimmed)) nestingLevel++;
    }
    // 退出嵌套
    if (/^\}/.test(trimmed)) {
      nestingLevel = Math.max(0, nestingLevel - 1);
    }
  }

  return complexity;
}

async function collectFiles(dir: string): Promise<string[]> {
  const entries = await readdir(dir, { withFileTypes: true });
  const files: string[] = [];

  for (const entry of entries) {
    const fullPath = join(dir, entry.name);
    if (entry.isDirectory() && !entry.name.startsWith(".") 
        && entry.name !== "node_modules" && entry.name !== "dist") {
      files.push(...await collectFiles(fullPath));
    } else if (entry.isFile() && isSourceFile(entry.name)) {
      files.push(fullPath);
    }
  }
  return files;
}

function isSourceFile(name: string): boolean {
  return [".ts", ".tsx", ".js", ".jsx"].includes(extname(name));
}

8.2 报告格式化

// src/utils/formatter.ts
import { ComplexityResult } from "./parser.js";

export function formatReport(
  results: ComplexityResult[],
  highRisk: ComplexityResult[],
  threshold: number
): string {
  const lines: string[] = [
    "## 📊 代码复杂度分析报告\n",
    `分析文件数: ${new Set(results.map(r => r.file)).size}`,
    `分析函数数: ${results.length}`,
    `告警阈值: ${threshold}`,
    `高风险函数: ${highRisk.length}\n`,
  ];

  // 按文件分组
  const grouped = groupByFile(results);

  for (const [file, fns] of Object.entries(grouped)) {
    lines.push(`### 📄 ${file}`);
    lines.push("| 函数 | 圈复杂度 | 认知复杂度 | 行数 | 嵌套深度 | 风险 |");
    lines.push("|------|---------|-----------|------|---------|------|");

    for (const fn of fns) {
      const risk = fn.cyclomatic > threshold ? "🔴 高" : 
                   fn.cyclomatic > threshold * 0.7 ? "🟡 中" : "🟢 低";
      lines.push(
        `| \`${fn.function_name}\` (${fn.start_line}-${fn.end_line}) ` +
        `| ${fn.cyclomatic} | ${fn.cognitive} | ${fn.lines} | ` +
        `${fn.nesting_depth} | ${risk} |`
      );
    }
    lines.push("");
  }

  if (highRisk.length > 0) {
    lines.push("### 🚨 优先重构建议\n");
    for (const fn of highRisk.slice(0, 5)) {
      lines.push(
        `1. **${fn.function_name}** (${fn.file}:${fn.start_line})` +
        ` - 圈复杂度 ${fn.cyclomatic},建议拆分为多个小函数`
      );
    }
  }

  return lines.join("\n");
}

function groupByFile(results: ComplexityResult[]): Record<string, ComplexityResult[]> {
  return results.reduce((acc, r) => {
    (acc[r.file] ||= []).push(r);
    return acc;
  }, {} as Record<string, ComplexityResult[]>);
}

8.3 完整构建与测试

# 编译TypeScript
npm run build

# 使用MCP Inspector测试(官方调试工具)
npm run inspect

MCP Inspector是一个浏览器端的交互式调试界面,可以: - 查看Server暴露的所有Tools/Resources/Prompts - 手动调用Tool并查看响应 - 检查JSON-RPC消息流 - 验证输入参数的JSON Schema

启动后在浏览器打开http://localhost:5173,你将看到:

Tools:
  ├── analyze_complexity
  │     Parameters: path, metrics, threshold, include_pattern
  ├── analyze_dependencies
  │     Parameters: project_path, check_updates, check_vulnerabilities, depth
  └── security_scan
        Parameters: target_path, rules, severity

Resources:
  ├── config://analysis-rules
  └── project://info

Prompts:
  └── code-review
        Parameters: file_path, focus_areas, language

第九章 OAuth 2.1认证(远程Server安全)

9.1 为什么需要OAuth

当你的MCP Server通过Streamable HTTP对外提供服务时,必须实现认证以防止未授权访问。MCP规范要求使用OAuth 2.1 + Dynamic Client Registration(DCR)。

9.2 OAuth流程

sequenceDiagram
    participant Client as MCP Client<br/>(Claude Code)
    participant Server as MCP Server<br/>(你的服务)
    participant Auth as Authorization Server<br/>(如Auth0/Clerk)

    Client->>Server: POST /mcp (无token)
    Server-->>Client: 401 + WWW-Authenticate header

    Client->>Auth: GET /authorize (PKCE flow)
    Auth-->>Client: 登录页面
    Client->>Auth: 用户授权
    Auth-->>Client: Authorization Code

    Client->>Auth: POST /token (code + PKCE verifier)
    Auth-->>Client: Access Token

    Client->>Server: POST /mcp (Bearer token)
    Server->>Auth: 验证token (introspect/JWKS)
    Auth-->>Server: Token有效
    Server-->>Client: 正常MCP响应

9.3 Express中间件实现

// src/middleware/auth.ts
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";

const JWKS_URI = process.env.JWKS_URI || "";
const AUDIENCE = process.env.AUTH_AUDIENCE || "code-analyzer-mcp";
const ISSUER = process.env.AUTH_ISSUER || "";

// 简化版JWKS验证中间件
export async function requireAuth(
  req: Request, 
  res: Response, 
  next: NextFunction
): Promise<void> {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith("Bearer ")) {
    res.setHeader("WWW-Authenticate", 
      `Bearer realm="MCP", resource_metadata="${ISSUER}/.well-known/oauth-authorization-server"`);
    res.status(401).json({
      jsonrpc: "2.0",
      error: { 
        code: -32001, 
        message: "Authentication required" 
      },
      id: null,
    });
    return;
  }

  const token = authHeader.slice(7);

  try {
    const decoded = jwt.verify(token, await getSigningKey(), {
      audience: AUDIENCE,
      issuer: ISSUER,
      algorithms: ["RS256"],
    });

    // 将用户信息注入请求上下文
    (req as any).mcp_context = { user: decoded };
    next();
  } catch {
    res.status(401).json({
      jsonrpc: "2.0",
      error: { code: -32001, message: "Invalid or expired token" },
      id: null,
    });
  }
}

async function getSigningKey(): Promise<string> {
  // 生产环境中应从JWKS端点获取公钥并缓存
  // 这里简化处理
  return process.env.JWT_PUBLIC_KEY || "";
}

9.4 保护HTTP端点

更新server-http.ts:

import { requireAuth } from "./middleware/auth.js";

// MCP端点添加认证中间件
app.post("/mcp", requireAuth, async (req, res) => {
  // ...原有逻辑不变
});

// WELL-KNOWN端点:OAuth服务器发现
app.get("/.well-known/oauth-authorization-server", (req, res) => {
  res.json({
    issuer: ISSUER,
    authorization_endpoint: `${ISSUER}/authorize`,
    token_endpoint: `${ISSUER}/oauth/token`,
    registration_endpoint: `${ISSUER}/oauth/register`,
    scopes_supported: ["mcp:tools", "mcp:resources"],
    code_challenge_methods_supported: ["S256"],
  });
});

第十章 发布与分发

10.1 npm包发布

# 确保构建产物是最新的
npm run build

# 登录npm
npm login

# 发布
npm publish --access public

发布后,用户可以这样配置你的Server:

{
  "mcpServers": {
    "code-analyzer": {
      "command": "npx",
      "args": ["-y", "code-analyzer-mcp"]
    }
  }
}

10.2 Docker容器化

# Dockerfile
FROM node:20-slim

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist/ ./dist/

# Streamable HTTP模式
ENV NODE_ENV=production
ENV PORT=3000

EXPOSE 3000
CMD ["node", "dist/server-http.js"]
# 构建并运行
docker build -t code-analyzer-mcp .
docker run -p 3000:3000 \
  -e AUTH_ISSUER=https://your-auth-server.com \
  -e JWT_PUBLIC_KEY="$(cat public.pem)" \
  code-analyzer-mcp

完整工作流总览

flowchart TB
    Start([开始开发]) --> Init[初始化项目<br/>npm init + 安装SDK]
    Init --> Design[设计工具接口<br/>定义Tool/Resource/Prompt]
    Design --> Code[编写工具实现<br/>注册到McpServer]
    Code --> Stdio[实现stdio传输<br/>本地开发调试]
    Stdio --> Inspect[使用MCP Inspector测试]
    Inspect --> Config[配置Claude Code<br/>.claude/settings.json]
    Config --> Verify[/mcp命令验证]
    Verify --> HTTP{需要远程部署?}
    HTTP -->|是| StreamHTTP[实现Streamable HTTP]
    StreamHTTP --> OAuth[添加OAuth认证]
    OAuth --> Docker[Docker容器化]
    Docker --> Publish[发布到npm/镜像仓库]
    HTTP -->|否| LocalPub[npm publish本地包]
    LocalPub --> Done([完成])
    Publish --> Done

    style Start fill:#4caf50,color:white
    style Done fill:#2196f3,color:white
    style HTTP fill:#ff9800,color:white

最佳实践清单

Tool设计

性能优化

安全


延伸阅读

训练营适用性评估

维度 评估
适合训练营类型 全栈开发训练营、AI工具开发训练营、DevOps工程师进阶
建议教学时长 6-8小时(含动手实践),分2个session
学员基础要求 TypeScript中级、Node.js基础、REST API经验
难度评级 ⭐⭐⭐⭐(中高)
实践环节占比 70%编码 + 20%调试测试 + 10%部署发布
推荐分组 2-3人一组,每组完成一个不同主题的MCP Server

参考来源

  1. MCP Specification 2025-03-26 — MCP协议官方规范文档,定义了JSON-RPC消息格式、生命周期和传输协议
  2. TypeScript SDK Server Guide — 官方TypeScript SDK服务端开发API文档
  3. Build a Custom MCP Server for Claude Code - Apidog — 从零构建MCP Server的完整实战指南
  4. How to Build an MCP Server with Python, Docker, and Claude Code - freeCodeCamp — Python + Docker部署MCP Server教程
  5. Complete Guide to MCP in 2026 - DEV Community — 2026年MCP架构、Streamable 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学通信知识星球海报-扫码加入