AI 工具 | | 约 32 分钟 | 12,786 字

MCP Server 测试与发布指南

MCP Server 的单元测试、集成测试和发布到 npm 的完整流程

为什么要测试 MCP Server

MCP Server 直接与 AI 模型交互,如果工具返回错误的结果,AI 会基于错误信息做出错误的判断。更糟糕的是,对于数据库或文件操作类的 Server,Bug 可能导致数据丢失或安全问题。

所以,测试不是可选的,而是必须的。


测试策略

MCP Server 的测试分为三个层次:

单元测试 → 集成测试 → 端到端测试
  ↓           ↓           ↓
业务逻辑    MCP 协议    真实环境
测试类型测试对象工具
单元测试解析器、工具函数Vitest / Jest
集成测试MCP Server 工具调用MCP SDK Client
端到端测试完整的 Client-Server 交互MCP Inspector

项目配置

以我们之前开发的数据库 MCP Server 为例,添加测试配置:

npm install -D vitest @vitest/coverage-v8
// vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    globals: true,
    environment: "node",
    coverage: {
      provider: "v8",
      reporter: ["text", "html"],
      include: ["src/**/*.ts"],
      exclude: ["src/index.ts"],
    },
    testTimeout: 10000,
  },
});
// package.json 添加脚本
{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "test:coverage": "vitest run --coverage"
  }
}

测试文件结构:

tests/
├── unit/
│   ├── database.test.ts
│   └── tools.test.ts
├── integration/
│   └── server.test.ts
└── helpers/
    └── mock-db.ts

单元测试

测试数据库操作

// tests/unit/database.test.ts
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { DatabaseConnection } from "../../src/database.js";

// 使用测试数据库
const TEST_DB_URL = process.env.TEST_DATABASE_URL
  || "postgresql://test:test@localhost:5432/test_mcp";

describe("DatabaseConnection", () => {
  let db: DatabaseConnection;

  beforeAll(async () => {
    db = new DatabaseConnection(TEST_DB_URL);

    // 创建测试表
    // 注意:实际项目中应该用 migration 工具
  });

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

  describe("listTables", () => {
    it("should return an array of tables", async () => {
      const tables = await db.listTables();

      expect(Array.isArray(tables)).toBe(true);
      tables.forEach((table) => {
        expect(table).toHaveProperty("name");
        expect(table).toHaveProperty("schema");
        expect(table).toHaveProperty("rowCount");
      });
    });
  });

  describe("describeTable", () => {
    it("should return column information", async () => {
      const columns = await db.describeTable("users");

      expect(columns.length).toBeGreaterThan(0);
      columns.forEach((col) => {
        expect(col).toHaveProperty("name");
        expect(col).toHaveProperty("type");
        expect(col).toHaveProperty("nullable");
        expect(col).toHaveProperty("isPrimaryKey");
      });
    });

    it("should reject invalid table names", async () => {
      await expect(
        db.describeTable("users; DROP TABLE users;")
      ).rejects.toThrow("Invalid table name");
    });
  });

  describe("query", () => {
    it("should execute SELECT queries", async () => {
      const result = await db.query("SELECT 1 as num");

      expect(result.columns).toContain("num");
      expect(result.rows).toHaveLength(1);
      expect(result.rows[0].num).toBe(1);
    });

    it("should reject non-SELECT queries", async () => {
      await expect(
        db.query("DELETE FROM users")
      ).rejects.toThrow("Only SELECT");
    });

    it("should reject queries with forbidden keywords", async () => {
      await expect(
        db.query("SELECT * FROM users; DROP TABLE users")
      ).rejects.toThrow("Forbidden keyword");
    });

    it("should respect the limit parameter", async () => {
      const result = await db.query("SELECT * FROM users", 5);
      expect(result.rows.length).toBeLessThanOrEqual(5);
    });
  });
});

测试工具函数

// tests/unit/tools.test.ts
import { describe, it, expect } from "vitest";

describe("SQL Safety", () => {
  function isSafeQuery(sql: string): boolean {
    const trimmed = sql.trim().toUpperCase();
    if (!trimmed.startsWith("SELECT") && !trimmed.startsWith("WITH")) {
      return false;
    }
    const forbidden = ["INSERT", "UPDATE", "DELETE", "DROP", "ALTER", "TRUNCATE"];
    return !forbidden.some((kw) => trimmed.includes(kw));
  }

  it("should allow SELECT queries", () => {
    expect(isSafeQuery("SELECT * FROM users")).toBe(true);
    expect(isSafeQuery("SELECT COUNT(*) FROM orders")).toBe(true);
  });

  it("should allow WITH (CTE) queries", () => {
    expect(
      isSafeQuery("WITH cte AS (SELECT 1) SELECT * FROM cte")
    ).toBe(true);
  });

  it("should reject INSERT queries", () => {
    expect(isSafeQuery("INSERT INTO users VALUES (1)")).toBe(false);
  });

  it("should reject UPDATE queries", () => {
    expect(isSafeQuery("UPDATE users SET name = 'x'")).toBe(false);
  });

  it("should reject DROP queries", () => {
    expect(isSafeQuery("DROP TABLE users")).toBe(false);
  });

  it("should reject queries with embedded dangerous keywords", () => {
    expect(
      isSafeQuery("SELECT * FROM users; DROP TABLE users")
    ).toBe(false);
  });
});

集成测试

集成测试验证 MCP Server 的工具调用是否正确工作。我们使用 MCP SDK 的 Client 来模拟真实的调用:

// tests/integration/server.test.ts
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { createServer } from "../../src/server.js";
import { DatabaseConnection } from "../../src/database.js";

describe("MCP Server Integration", () => {
  let client: Client;
  let db: DatabaseConnection;

  beforeAll(async () => {
    db = new DatabaseConnection(
      process.env.TEST_DATABASE_URL ||
        "postgresql://test:test@localhost:5432/test_mcp"
    );

    const server = createServer(db);

    // 使用内存传输进行测试
    const [clientTransport, serverTransport] =
      InMemoryTransport.createLinkedPair();

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

    await Promise.all([
      client.connect(clientTransport),
      server.connect(serverTransport),
    ]);
  });

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

  describe("tools/list", () => {
    it("should list all available tools", async () => {
      const result = await client.listTools();

      expect(result.tools).toHaveLength(4);

      const toolNames = result.tools.map((t) => t.name);
      expect(toolNames).toContain("list_tables");
      expect(toolNames).toContain("describe_table");
      expect(toolNames).toContain("query");
      expect(toolNames).toContain("table_stats");
    });

    it("should have proper descriptions for each tool", async () => {
      const result = await client.listTools();

      result.tools.forEach((tool) => {
        expect(tool.description).toBeTruthy();
        expect(tool.description.length).toBeGreaterThan(10);
      });
    });
  });

  describe("tools/call - list_tables", () => {
    it("should return table list", async () => {
      const result = await client.callTool({
        name: "list_tables",
        arguments: {},
      });

      expect(result.content).toHaveLength(1);
      expect(result.content[0].type).toBe("text");
      expect((result.content[0] as any).text).toContain("张表");
    });
  });

  describe("tools/call - query", () => {
    it("should execute valid queries", async () => {
      const result = await client.callTool({
        name: "query",
        arguments: { sql: "SELECT 1 as test_value" },
      });

      expect(result.isError).toBeFalsy();
      expect((result.content[0] as any).text).toContain("test_value");
    });

    it("should reject dangerous queries", async () => {
      const result = await client.callTool({
        name: "query",
        arguments: { sql: "DROP TABLE users" },
      });

      expect(result.isError).toBe(true);
    });
  });
});

使用 MCP Inspector 测试

MCP Inspector 是官方提供的可视化测试工具:

# 安装并启动 Inspector
npx @modelcontextprotocol/inspector node dist/index.js \
  "postgresql://user:pass@localhost:5432/mydb"

Inspector 提供了一个 Web 界面,可以:

  1. 查看 Server 信息和能力声明
  2. 浏览所有注册的工具、资源和提示
  3. 手动调用工具并查看返回结果
  4. 查看完整的 JSON-RPC 通信日志

Inspector 测试清单

检查项说明
Server 信息名称和版本是否正确
工具列表所有工具是否正确注册
工具描述描述是否清晰准确
参数定义必填/可选参数是否正确
正常调用工具是否返回预期结果
错误处理错误参数是否返回友好的错误信息
边界情况空输入、超大输入等

调试技巧

1. 日志输出

在开发阶段,添加详细的日志:

// 使用 stderr 输出日志(stdout 被 MCP 协议占用)
function log(message: string, data?: unknown): void {
  const timestamp = new Date().toISOString();
  const logLine = data
    ? `[${timestamp}] ${message}: ${JSON.stringify(data)}`
    : `[${timestamp}] ${message}`;
  process.stderr.write(logLine + "\n");
}

// 在工具处理函数中使用
server.tool("query", "...", schema, async (args) => {
  log("query called", args);
  try {
    const result = await db.query(args.sql);
    log("query result", { rowCount: result.rowCount });
    return { content: [{ type: "text", text: "..." }] };
  } catch (error) {
    log("query error", { error: (error as Error).message });
    throw error;
  }
});

2. 环境变量调试

# 设置 MCP SDK 的调试模式
DEBUG=mcp:* node dist/index.js

3. 常见问题排查

问题可能原因解决方案
Server 无响应stdout 被其他输出占用确保日志输出到 stderr
工具不显示注册时机不对确保在 connect 之前注册
参数验证失败Schema 定义不匹配检查 Zod schema
连接超时Server 启动太慢优化初始化逻辑

发布到 npm

1. 准备 package.json

{
  "name": "mcp-database-server",
  "version": "1.0.0",
  "description": "MCP Server for database queries",
  "type": "module",
  "main": "dist/index.js",
  "bin": {
    "mcp-database-server": "dist/index.js"
  },
  "files": [
    "dist/**/*",
    "README.md",
    "LICENSE"
  ],
  "keywords": [
    "mcp",
    "model-context-protocol",
    "database",
    "postgresql",
    "ai"
  ],
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/yourname/mcp-database-server"
  },
  "engines": {
    "node": ">=18.0.0"
  },
  "scripts": {
    "build": "tsc",
    "test": "vitest run",
    "prepublishOnly": "npm run build && npm test"
  }
}

2. 确保入口文件有 shebang

// src/index.ts 的第一行
#!/usr/bin/env node

构建后需要确保 dist/index.js 也有这行,并且有执行权限:

// package.json scripts
{
  "scripts": {
    "build": "tsc && chmod +x dist/index.js"
  }
}

3. 编写 README

README 对于 MCP Server 来说尤其重要,因为用户需要知道如何配置:

# mcp-database-server

MCP Server for querying PostgreSQL databases.

## Installation

npx mcp-database-server <connection-string>

## Configuration

Add to your Claude Code MCP settings:

{
  "mcpServers": {
    "database": {
      "command": "npx",
      "args": ["-y", "mcp-database-server", "postgresql://..."]
    }
  }
}

## Available Tools

- list_tables - List all database tables
- describe_table - Show table structure
- query - Execute read-only SQL queries
- table_stats - Get table statistics

4. 发布流程

# 1. 确保已登录 npm
npm login

# 2. 检查包名是否可用
npm view mcp-database-server

# 3. 构建和测试
npm run build
npm test

# 4. 试运行发布(不实际发布)
npm publish --dry-run

# 5. 正式发布
npm publish

# 6. 验证发布结果
npx mcp-database-server --help

5. 版本管理

遵循语义化版本(Semantic Versioning):

# 修复 Bug
npm version patch  # 1.0.0 → 1.0.1

# 新增功能(向后兼容)
npm version minor  # 1.0.1 → 1.1.0

# 破坏性变更
npm version major  # 1.1.0 → 2.0.0

发布检查清单

发布前确认以下事项:

检查项状态
所有测试通过
TypeScript 编译无错误
README 包含安装和配置说明
package.json 的 bin 字段正确
入口文件有 shebang 和执行权限
files 字段只包含必要文件
没有包含敏感信息(.env, tokens)
LICENSE 文件存在
keywords 包含 “mcp”
engines 指定了 Node.js 最低版本

CI/CD 自动化

使用 GitHub Actions 自动化测试和发布:

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run build
      - run: npm test

  publish:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && startsWith(github.event.head_commit.message, 'release:')
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          registry-url: https://registry.npmjs.org
      - run: npm ci
      - run: npm run build
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

总结

MCP Server 的测试和发布流程并不复杂,但每一步都很重要:

  • 单元测试确保业务逻辑正确
  • 集成测试确保 MCP 协议交互正常
  • MCP Inspector 提供可视化的端到端验证
  • npm 发布让你的 Server 能被全世界使用

整个 MCP 开发实战系列到这里就结束了。从协议理解到 Server 开发,从数据库到 API 到文件处理,再到测试和发布,我们走完了一个完整的开发周期。

写代码是第一步,测试是第二步,发布是第三步。每一步都不能省略,因为你的 MCP Server 会被 AI 信任和使用。

评论

加载中...

相关文章

分享:

评论

加载中...