Chapter 10

实战:构建代码审查 MCP 工具集

综合运用前九章知识,构建一个生产级的代码审查工作流,整合 Git、代码分析与测试运行

项目概述

本章构建一个完整的 代码审查 MCP 工具集(Code Review MCP Server),这是 MCP 在实际工程中最常见的应用场景之一。这个 Server 将整合多种能力:

  Code Review MCP Server
  ╔═══════════════════════════════════════════════════════════════╗
  ║                                                               ║
  ║  Tools(工具)              Resources(资源)                   ║
  ║  ┌─────────────────┐       ┌─────────────────────────┐       ║
  ║  │ git_diff        │       │ git://repo/diff/HEAD    │       ║
  ║  │ git_log         │       │ git://repo/file/{path}  │       ║
  ║  │ run_tests       │       │ analysis://report       │       ║
  ║  │ lint_code       │       └─────────────────────────┘       ║
  ║  │ analyze_deps    │                                          ║
  ║  │ check_security  │       Prompts(模板)                    ║
  ║  └─────────────────┘       ┌─────────────────────────┐       ║
  ║                             │ full_code_review        │       ║
  ║  Sampling                  │ review_pr_checklist     │       ║
  ║  ┌─────────────────┐       │ explain_test_failure    │       ║
  ║  │ AI 代码质量评估  │       └─────────────────────────┘       ║
  ║  └─────────────────┘                                          ║
  ╚═══════════════════════════════════════════════════════════════╝

项目结构

code-review-mcp/
├── src/
│   ├── index.ts           # 入口:注册所有能力
│   ├── tools/
│   │   ├── git.ts         # Git 操作工具
│   │   ├── testing.ts     # 测试运行工具
│   │   ├── linting.ts     # 代码检查工具
│   │   └── security.ts    # 安全扫描工具
│   ├── resources/
│   │   └── git-resources.ts  # Git 相关资源
│   ├── prompts/
│   │   └── review-prompts.ts # 代码审查提示词模板
│   └── utils/
│       ├── exec.ts        # Shell 命令执行工具
│       └── security.ts    # 安全检查工具
├── package.json
└── tsconfig.json

核心工具实现

工具层:Shell 命令安全执行

// src/utils/exec.ts
import { exec } from "child_process";
import { promisify } from "util";

const execAsync = promisify(exec);

export interface ExecResult {
  stdout: string;
  stderr: string;
  exitCode: number;
}

/**
 * 安全执行 Shell 命令
 * 注意:只接受预定义命令白名单,禁止直接拼接用户输入
 */
export async function safeExec(
  command: string,
  args: string[],                    // 参数独立传递,不拼接
  options: { cwd?: string; timeout?: number } = {}
): Promise<ExecResult> {
  // 命令白名单,只允许这些命令
  const ALLOWED_COMMANDS = new Set([
    "git", "npm", "npx", "node",
    "eslint", "tsc", "vitest", "jest",
  ]);

  if (!ALLOWED_COMMANDS.has(command)) {
    throw new Error(`命令 "${command}" 不在允许列表中`);
  }

  // 转义参数,防止 shell 注入
  const sanitizedArgs = args.map(arg =>
    arg.replace(/[;&|`$(){}[\]\\]/g, "\\")
  );

  const fullCommand = [command, ...sanitizedArgs].join(" ");

  try {
    const { stdout, stderr } = await execAsync(fullCommand, {
      cwd: options.cwd || process.cwd(),
      timeout: options.timeout || 30000,  // 默认 30 秒超时
      maxBuffer: 1024 * 1024 * 10,        // 10MB 输出上限
    });
    return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0 };
  } catch (err: any) {
    return {
      stdout: (err.stdout || "").trim(),
      stderr: (err.stderr || err.message).trim(),
      exitCode: err.code || 1,
    };
  }
}

Git 工具实现

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

export function registerGitTools(server: McpServer, repoPath: string) {

  // ─── git diff ───────────────────────────────────────────────
  server.tool(
    "git_diff",
    `获取 Git diff 输出,显示代码变更内容。
可以比较:工作区变更、暂存区变更、两个提交之间的差异、特定文件的变更。
返回标准 diff 格式,包含文件路径、变更行数和具体内容。`,
    {
      base: z.string().default("HEAD").describe("比较基准(提交 SHA、分支名、HEAD)"),
      target: z.string().optional().describe("目标(默认为工作区)"),
      file: z.string().optional().describe("限制到特定文件(可选)"),
      stat: z.boolean().default(false).describe("只显示统计摘要,不显示详细内容"),
    },
    async ({ base, target, file, stat }) => {
      const args = ["diff"];
      if (stat) args.push("--stat");
      args.push(base);
      if (target) args.push(target);
      if (file) args.push("--", file);

      const result = await safeExec("git", args, { cwd: repoPath });

      if (result.exitCode !== 0) {
        return {
          content: [{ type: "text", text: `Git diff 失败:${result.stderr}` }],
          isError: true,
        };
      }

      const output = result.stdout || "(没有变更)";
      return { content: [{ type: "text", text: output }] };
    }
  );

  // ─── git log ───────────────────────────────────────────────
  server.tool(
    "git_log",
    "获取 Git 提交历史,返回提交 SHA、作者、日期和提交信息",
    {
      limit: z.number().int().min(1).max(100).default(10).describe("最多返回的提交数"),
      branch: z.string().optional().describe("查看特定分支的历史"),
      author: z.string().optional().describe("按作者过滤"),
      since: z.string().optional().describe("起始时间(如 '2 weeks ago')"),
    },
    async ({ limit, branch, author, since }) => {
      const args = [
        "log",
        `--max-count=${limit}`,
        "--pretty=format:%H|%an|%ad|%s",
        "--date=short",
      ];
      if (branch) args.push(branch);
      if (author) args.push(`--author=${author}`);
      if (since) args.push(`--since=${since}`);

      const result = await safeExec("git", args, { cwd: repoPath });

      if (result.exitCode !== 0) {
        return {
          content: [{ type: "text", text: `Git log 失败:${result.stderr}` }],
          isError: true,
        };
      }

      // 格式化为可读表格
      const lines = result.stdout.split("\n").filter(Boolean);
      const formatted = lines.map(line => {
        const [sha, author, date, message] = line.split("|");
        return `${sha.slice(0, 8)} ${date} [${author}] ${message}`;
      }).join("\n");

      return { content: [{ type: "text", text: formatted || "(没有提交记录)" }] };
    }
  );
}

测试运行工具

// src/tools/testing.ts
export function registerTestingTools(server: McpServer, repoPath: string) {

  server.tool(
    "run_tests",
    `运行项目的测试套件并返回测试结果。
支持 npm test、vitest、jest 等测试框架。
返回通过/失败统计、失败的测试名称和错误信息。
警告:这会实际执行代码,请确保只在受信任的仓库中使用。`,
    {
      pattern: z.string().optional().describe("测试文件匹配模式(如 '**/*.test.ts')"),
      coverage: z.boolean().default(false).describe("是否生成覆盖率报告"),
    },
    async ({ pattern, coverage }) => {
      const args = ["test", "--run"];
      if (coverage) args.push("--coverage");
      if (pattern) args.push(pattern);

      const result = await safeExec("npx", ["vitest", ...args], {
        cwd: repoPath,
        timeout: 120000,  // 测试可能耗时较长,设 2 分钟
      });

      const output = result.stdout + (result.stderr ? `\n${result.stderr}` : "");
      const passed = result.exitCode === 0;

      return {
        content: [{
          type: "text",
          text: `测试${passed ? "通过" : "失败"} (exit ${result.exitCode})\n\n${output}`,
        }],
        isError: !passed,
      };
    }
  );
}

Resources 实现

// src/resources/git-resources.ts
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";

export function registerGitResources(server: McpServer, repoPath: string) {

  // 静态资源:HEAD diff 摘要(代码审查的起点)
  server.resource(
    "git-head-diff",
    "git://repo/diff/HEAD",
    {
      name: "HEAD 变更摘要",
      description: "当前工作区相对于 HEAD 的变更统计",
      mimeType: "text/plain",
    },
    async (uri) => {
      const result = await safeExec("git", ["diff", "HEAD", "--stat"], {
        cwd: repoPath,
      });
      return {
        contents: [{
          uri: uri.toString(),
          mimeType: "text/plain",
          text: result.stdout || "(无变更)",
        }],
      };
    }
  );

  // 动态资源:按路径读取任意文件
  server.resource(
    "repo-file",
    new ResourceTemplate("git://repo/file/{path}", { list: undefined }),
    { name: "仓库文件", description: "读取仓库中任意文件的当前内容" },
    async (uri, variables) => {
      const filePath = path.join(repoPath, variables.path as string);
      const resolved = path.resolve(filePath);
      const base = path.resolve(repoPath);

      if (!resolved.startsWith(base)) {
        throw new Error("路径超出仓库范围");
      }

      const content = await fs.readFile(resolved, "utf-8");
      return {
        contents: [{
          uri: uri.toString(),
          mimeType: getMimeType(resolved),
          text: content,
        }],
      };
    }
  );
}

Prompts 实现

// src/prompts/review-prompts.ts
export function registerReviewPrompts(server: McpServer) {

  server.prompt(
    "full_code_review",
    "启动完整的代码审查流程:获取 diff → 运行测试 → 代码质量分析 → 生成审查报告",
    {
      scope: z.enum(["staged", "head", "branch"])
        .default("head")
        .describe("审查范围"),
      focus: z.array(
        z.enum(["bugs", "security", "performance", "style", "tests"])
      ).default(["bugs", "security"]).describe("审查重点"),
    },
    ({ scope, focus }) => {
      const focusDesc = focus.join("、");
      return {
        messages: [{
          role: "user",
          content: {
            type: "text",
            text: `请对当前代码变更进行系统的代码审查。

审查范围:${scope === "staged" ? "暂存区变更" : scope === "head" ? "最新提交" : "当前分支所有变更"}
重点关注:${focusDesc}

请按以下步骤进行:
1. 使用 git_diff 工具获取代码变更
2. 使用 run_tests 工具检查测试是否通过
3. 使用 lint_code 工具检查代码风格
4. 综合分析,生成详细的审查报告

审查报告应包含:
- **变更摘要**:改动了哪些文件,主要变更内容
- **问题列表**:按严重程度排列(Critical / Warning / Info)
- **测试覆盖**:测试结果和覆盖情况
- **安全检查**:是否存在安全风险(重点关注:${focus.includes("security") ? "是" : "否"})
- **改进建议**:具体的修改方案

请开始审查。`,
          },
        }],
      };
    }
  );
}

Server 入口文件

// src/index.ts — 整合所有模块
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import * as path from "path";
import { registerGitTools } from "./tools/git.js";
import { registerTestingTools } from "./tools/testing.js";
import { registerGitResources } from "./resources/git-resources.js";
import { registerReviewPrompts } from "./prompts/review-prompts.js";

// 从环境变量获取仓库路径
const REPO_PATH = path.resolve(process.env.REPO_PATH || process.cwd());

// 创建 Server
const server = new McpServer({
  name: "code-review-mcp",
  version: "1.0.0",
});

// 注册所有能力
registerGitTools(server, REPO_PATH);
registerTestingTools(server, REPO_PATH);
registerGitResources(server, REPO_PATH);
registerReviewPrompts(server);

// 启动
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  process.stderr.write(`Code Review MCP Server 已启动\n仓库路径:${REPO_PATH}\n`);
}

main().catch(err => {
  process.stderr.write(`启动失败:${err.message}\n`);
  process.exit(1);
});

Claude Desktop 配置

{
  "mcpServers": {
    "code-review": {
      "command": "node",
      "args": ["/path/to/code-review-mcp/dist/index.js"],
      "env": {
        "REPO_PATH": "/path/to/your/project"
      }
    }
  }
}

完整工作流演示

配置好 Server 后,在 Claude Desktop 中的一次完整代码审查对话如下:

  用户:我刚提交了一些代码修改,请帮我做代码审查

  Claude:好的,我来用代码审查工具进行系统分析。

  ► 调用 git_diff(base="HEAD~1", target="HEAD")
  ► 调用 run_tests()
  ► 调用 lint_code()

  分析结果:

  变更摘要:修改了 3 个文件
  - src/auth.ts (+45/-12 行) - 新增 JWT 验证
  - src/routes/user.ts (+8/-2 行) - 使用新认证
  - tests/auth.test.ts (+32/-0 行) - 新增测试

  ⚠ 警告(2 个):
  1. auth.ts:28 - JWT secret 从环境变量读取,但缺少非空检查
  2. user.ts:15 - 缺少对过期 token 的错误处理

  ℹ 建议(1 个):
  1. 建议增加 token 刷新逻辑的测试覆盖

  ✓ 测试全部通过(18/18)
  ✓ Lint 无错误
教程总结 至此,你已掌握 MCP 协议开发的全部核心知识:从协议原理(第 1-2 章)、基础开发(第 3-4 章)、三大能力实现(第 5-7 章)、安全与质量(第 8-9 章)到完整实战(第 10 章)。MCP 是快速演进的技术,建议持续关注 modelcontextprotocol.io 官方文档和 github.com/modelcontextprotocol 仓库的最新动态。