Trung cấpHướng dẫnClaude CodeNguồn: Anthropic

Testing và Debug MCP Server — Đảm bảo chất lượng cho production

Nghe bài viết
00:00

Điểm nổi bật

Nhấn để đến mục tương ứng

  1. 1 Điều này giúp test logic mà không cần MCP framework.
  2. 2 Không cần khởi động MCP server hay kết nối Claude — chỉ test hàm xử lý thuần túy.
  3. 3 Performance Benchmarking Đo lường hiệu suất MCP server giúp phát hiện bottleneck và đảm bảo response time đáp ứng yêu cầu.
  4. 4 Xây dựng MCP server là bước đầu tiên.
  5. 5 MCP server chạy như một process riêng biệt, giao tiếp qua JSON-RPC, và được gọi bởi AI model — tạo ra nhiều điểm có thể xảy ra lỗi mà testing truyền thống không bao phủ.
turned-on MacBook Pro

Xây dựng MCP server là bước đầu tiên. Đảm bảo nó hoạt động đúng, ổn định và sẵn sàng cho production là một thách thức hoàn toàn khác. MCP server chạy như một process riêng biệt, giao tiếp qua JSON-RPC, và được gọi bởi AI model — tạo ra nhiều điểm có thể xảy ra lỗi mà testing truyền thống không bao phủ. Bài viết này hướng dẫn bạn xây dựng test suite hoàn chỉnh cho MCP server, từ unit test đến CI/CD.

Tại sao testing MCP server đặc biệt quan trọng?

MCP server khác biệt so với API thông thường ở nhiều điểm:

  • Caller là AI model: Claude có thể gọi tool với input không lường trước, kết hợp các tool theo cách bạn chưa nghĩ đến
  • Schema là contract: Nếu schema sai, Claude sẽ gửi input sai format và tool sẽ fail silently hoặc trả kết quả sai
  • Error propagation phức tạp: Lỗi từ MCP server được truyền qua JSON-RPC rồi mới đến Claude, dễ bị mất context
  • Side effects nguy hiểm: MCP tool có thể thực hiện các thao tác không thể hoàn tác (gửi email, xóa dữ liệu, deploy code)
  • Concurrency: Nhiều tool call có thể xảy ra đồng thời

Unit Testing MCP Tools

Unit test cho MCP tools tập trung vào việc kiểm tra logic xử lý của từng tool handler. Không cần khởi động MCP server hay kết nối Claude — chỉ test hàm xử lý thuần túy.

Thiết lập test environment

// Cai dat dependencies
// npm install --save-dev vitest @types/node

// vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    globals: true,
    environment: "node",
    coverage: {
      provider: "v8",
      reporter: ["text", "json", "html"],
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 75,
      },
    },
  },
});

Tách logic khỏi MCP framework

Nguyên tắc quan trọng nhất: tách business logic ra khỏi MCP server definition. Điều này giúp test logic mà không cần MCP framework.

// src/handlers/search-products.ts
// Logic thuan tuy, khong phu thuoc MCP
export interface SearchParams {
  query: string;
  category?: string;
  maxPrice?: number;
  limit?: number;
}

export interface Product {
  id: string;
  name: string;
  price: number;
  category: string;
}

export async function searchProducts(
  params: SearchParams,
  db: DatabaseClient
): Promise<Product[]> {
  const { query, category, maxPrice, limit = 20 } = params;

  if (!query || query.trim().length === 0) {
    throw new Error("Query khong duoc de trong");
  }

  if (limit < 1 || limit > 100) {
    throw new Error("Limit phai tu 1 den 100");
  }

  let sql = "SELECT * FROM products WHERE name ILIKE $1";
  const sqlParams: any[] = ["%" + query + "%"];

  if (category) {
    sql += " AND category = $" + (sqlParams.length + 1);
    sqlParams.push(category);
  }

  if (maxPrice !== undefined) {
    sql += " AND price <= $" + (sqlParams.length + 1);
    sqlParams.push(maxPrice);
  }

  sql += " LIMIT $" + (sqlParams.length + 1);
  sqlParams.push(limit);

  const result = await db.query(sql, sqlParams);
  return result.rows;
}


// src/index.ts - MCP server chi la wrapper mong
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { searchProducts } from "./handlers/search-products.js";

server.tool(
  "search_products",
  "Tim kiem san pham",
  {
    query: z.string(),
    category: z.string().optional(),
    maxPrice: z.number().optional(),
    limit: z.number().optional(),
  },
  async (params) => {
    const results = await searchProducts(params, db);
    return {
      content: [{ type: "text", text: JSON.stringify(results) }],
    };
  }
);

Viết unit test

// tests/handlers/search-products.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { searchProducts, SearchParams } from
  "../../src/handlers/search-products.js";

// Mock database client
const mockDb = {
  query: vi.fn(),
};

describe("searchProducts", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("tra ve san pham phu hop voi tu khoa", async () => {
    const mockProducts = [
      { id: "1", name: "iPhone 15", price: 25000000,
        category: "dien-thoai" },
      { id: "2", name: "iPhone 14", price: 20000000,
        category: "dien-thoai" },
    ];
    mockDb.query.mockResolvedValue({ rows: mockProducts });

    const result = await searchProducts(
      { query: "iPhone" }, mockDb as any
    );

    expect(result).toEqual(mockProducts);
    expect(mockDb.query).toHaveBeenCalledWith(
      expect.stringContaining("ILIKE"),
      expect.arrayContaining(["%iPhone%"])
    );
  });

  it("throw error khi query rong", async () => {
    await expect(
      searchProducts({ query: "" }, mockDb as any)
    ).rejects.toThrow("Query khong duoc de trong");
  });

  it("throw error khi limit vuot gioi han", async () => {
    await expect(
      searchProducts({ query: "test", limit: 200 }, mockDb as any)
    ).rejects.toThrow("Limit phai tu 1 den 100");
  });

  it("ap dung filter category khi co", async () => {
    mockDb.query.mockResolvedValue({ rows: [] });

    await searchProducts(
      { query: "phone", category: "dien-thoai" },
      mockDb as any
    );

    expect(mockDb.query).toHaveBeenCalledWith(
      expect.stringContaining("category = $2"),
      expect.arrayContaining(["%phone%", "dien-thoai"])
    );
  });

  it("ap dung filter maxPrice khi co", async () => {
    mockDb.query.mockResolvedValue({ rows: [] });

    await searchProducts(
      { query: "laptop", maxPrice: 30000000 },
      mockDb as any
    );

    expect(mockDb.query).toHaveBeenCalledWith(
      expect.stringContaining("price <="),
      expect.arrayContaining([30000000])
    );
  });

  it("mac dinh limit la 20", async () => {
    mockDb.query.mockResolvedValue({ rows: [] });

    await searchProducts({ query: "test" }, mockDb as any);

    expect(mockDb.query).toHaveBeenCalledWith(
      expect.any(String),
      expect.arrayContaining([20])
    );
  });
});

Integration Testing với MCP Protocol

Integration test kiểm tra toàn bộ luồng từ MCP request đến response, bao gồm serialization, schema validation và error handling.

Test MCP server end-to-end

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

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

  beforeAll(async () => {
    transport = new StdioClientTransport({
      command: "node",
      args: ["dist/index.js"],
      env: {
        ...process.env,
        DATABASE_URL: "postgresql://localhost/test_db",
      },
    });

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

    await client.connect(transport);
  });

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

  it("liet ke tat ca tools", async () => {
    const result = await client.listTools();
    expect(result.tools).toBeInstanceOf(Array);
    expect(result.tools.length).toBeGreaterThan(0);

    const toolNames = result.tools.map(t => t.name);
    expect(toolNames).toContain("search_products");
    expect(toolNames).toContain("get_product_detail");
  });

  it("tool co schema hop le", async () => {
    const result = await client.listTools();
    const searchTool = result.tools.find(
      t => t.name === "search_products"
    );

    expect(searchTool).toBeDefined();
    expect(searchTool!.inputSchema.properties).toHaveProperty("query");
    expect(searchTool!.inputSchema.required).toContain("query");
  });

  it("goi tool thanh cong voi input hop le", async () => {
    const result = await client.callTool("search_products", {
      query: "test",
      limit: 5,
    });

    expect(result.content).toBeInstanceOf(Array);
    expect(result.content[0].type).toBe("text");

    const data = JSON.parse(result.content[0].text);
    expect(data).toBeInstanceOf(Array);
  });

  it("tra ve loi khi input khong hop le", async () => {
    const result = await client.callTool("search_products", {
      query: "",
    });

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

MCP Inspector — Công cụ debug chính thức

MCP Inspector là công cụ chính thức từ Anthropic để debug MCP server. Nó cung cấp giao diện web cho phép bạn tương tác trực tiếp với MCP server, xem request/response, và test tool calls.

Cài đặt và sử dụng

# Chay MCP Inspector
npx @modelcontextprotocol/inspector

# Hoac chi dinh server cu the
npx @modelcontextprotocol/inspector node dist/index.js

MCP Inspector cung cấp:

  • Tool Explorer: Liệt kê tất cả tools với schema, cho phép test từng tool bằng form input
  • Resource Browser: Duyệt các resources mà server cung cấp
  • Prompt Templates: Xem và test các prompt templates
  • Request/Response Log: Xem toàn bộ giao tiếp JSON-RPC giữa client và server
  • Error Details: Hiển thị chi tiết lỗi với stack trace

Debug workflow với Inspector

Quy trình debug hiệu quả với MCP Inspector:

  1. Khởi động Inspector kết nối tới MCP server
  2. Kiểm tra danh sách tools — xác nhận tool xuất hiện đúng tên và schema
  3. Test tool với input đơn giản — xác nhận response format đúng
  4. Test với edge cases — input rỗng, giá trị biên, ký tự đặc biệt
  5. Kiểm tra error handling — gửi input sai format, thiếu required fields
  6. Xem log JSON-RPC — kiểm tra request/response payload

Các lỗi thường gặp và cách xử lý

Lỗi 1: Connection errors

MCP server không khởi động hoặc không kết nối được.

// Nguyen nhan thuong gap:
// 1. Server crash khi khoi dong do missing dependencies
// 2. Port bi chiem (HTTP transport)
// 3. Path toi server binary sai

// Cach debug:
// Chay server truc tiep de xem error output
node dist/index.js

// Kiem tra stderr
// MCP server ghi log ra stderr, stdout danh cho JSON-RPC

// Fix: Them error handling cho startup
process.on("uncaughtException", (err) => {
  console.error("[MCP Server] Uncaught exception:", err);
  process.exit(1);
});

process.on("unhandledRejection", (reason) => {
  console.error("[MCP Server] Unhandled rejection:", reason);
  process.exit(1);
});

Lỗi 2: Schema mismatch

Claude gửi input không khớp với schema mong đợi.

// Van de: Schema dinh nghia "price" la number
// nhung Claude gui string "25000"

// Fix: Su dung Zod coerce de tu dong convert
server.tool(
  "filter_products",
  "Loc san pham theo gia",
  {
    minPrice: z.coerce.number().optional(),
    maxPrice: z.coerce.number().optional(),
    category: z.string().optional(),
  },
  async (params) => {
    // params.minPrice va maxPrice luon la number
    // du Claude gui string hay number
    return await filterProducts(params);
  }
);

Lỗi 3: Timeout

Tool xử lý quá lâu, dẫn đến timeout.

// Van de: Tool goi external API mat 30 giay,
// vuot qua timeout cua client

// Fix 1: Dat timeout cho external calls
async function callExternalAPI(url: string, timeoutMs = 10000) {
  const controller = new AbortController();
  const timeout = setTimeout(
    () => controller.abort(), timeoutMs
  );

  try {
    const response = await fetch(url, {
      signal: controller.signal
    });
    return await response.json();
  } catch (err) {
    if (err instanceof Error && err.name === "AbortError") {
      throw new Error(
        "Request timeout sau " + timeoutMs + "ms"
      );
    }
    throw err;
  } finally {
    clearTimeout(timeout);
  }
}

// Fix 2: Tra ve progress updates cho long-running tasks
server.tool(
  "generate_report",
  "Tao bao cao - co the mat vai phut",
  { reportType: z.string() },
  async ({ reportType }) => {
    // Chia nho thanh cac buoc va cap nhat tien do
    const steps = ["Thu thap du lieu", "Phan tich", "Tao bao cao"];
    const results: string[] = [];

    for (const step of steps) {
      results.push(await executeStep(step, reportType));
    }

    return {
      content: [{
        type: "text",
        text: JSON.stringify({
          status: "completed",
          steps: steps.length,
          result: results.join("
"),
        }),
      }],
    };
  }
);

Lỗi 4: Memory leak

MCP server tiêu tốn ngày càng nhiều memory theo thời gian.

// Nguyen nhan thuong gap:
// 1. Khong dong database connections
// 2. Cache khong co TTL
// 3. Event listeners khong duoc remove

// Fix: Implement resource cleanup
class ResourceManager {
  private resources: Map<string, any> = new Map();
  private cleanupInterval: NodeJS.Timeout;

  constructor() {
    // Cleanup moi 5 phut
    this.cleanupInterval = setInterval(
      () => this.cleanup(), 5 * 60 * 1000
    );
  }

  register(id: string, resource: any, ttlMs: number) {
    this.resources.set(id, {
      resource,
      expiresAt: Date.now() + ttlMs,
    });
  }

  private cleanup() {
    const now = Date.now();
    for (const [id, entry] of this.resources) {
      if (entry.expiresAt < now) {
        if (typeof entry.resource.close === "function") {
          entry.resource.close();
        }
        this.resources.delete(id);
      }
    }
  }

  dispose() {
    clearInterval(this.cleanupInterval);
    for (const [, entry] of this.resources) {
      if (typeof entry.resource.close === "function") {
        entry.resource.close();
      }
    }
    this.resources.clear();
  }
}

Logging Best Practices

Logging cho MCP server cần đặc biệt cẩn thận vì stdout được dùng cho JSON-RPC communication. Tất cả log phải ghi vào stderr hoặc file.

// src/logger.ts
import { createWriteStream } from "fs";
import { join } from "path";

type LogLevel = "debug" | "info" | "warn" | "error";

class McpLogger {
  private logFile;
  private level: LogLevel;

  private levels: Record<LogLevel, number> = {
    debug: 0, info: 1, warn: 2, error: 3,
  };

  constructor(logDir: string, level: LogLevel = "info") {
    this.level = level;
    const filename = "mcp-" + new Date().toISOString().split("T")[0] + ".log";
    this.logFile = createWriteStream(
      join(logDir, filename), { flags: "a" }
    );
  }

  private log(level: LogLevel, message: string, data?: any) {
    if (this.levels[level] < this.levels[this.level]) return;

    const entry = {
      timestamp: new Date().toISOString(),
      level,
      message,
      ...(data && { data }),
    };

    // Ghi ra stderr (khong anh huong JSON-RPC tren stdout)
    console.error("[" + level.toUpperCase() + "] " + message);
    // Ghi ra file
    this.logFile.write(JSON.stringify(entry) + "
");
  }

  debug(msg: string, data?: any) { this.log("debug", msg, data); }
  info(msg: string, data?: any) { this.log("info", msg, data); }
  warn(msg: string, data?: any) { this.log("warn", msg, data); }
  error(msg: string, data?: any) { this.log("error", msg, data); }
}

export const logger = new McpLogger(
  process.env.LOG_DIR || "/tmp/mcp-logs",
  (process.env.LOG_LEVEL as LogLevel) || "info"
);

Những gì cần log

  • Tool invocations: Tên tool, input parameters (sanitized), thời gian xử lý
  • Errors: Error message, stack trace, input gây lỗi
  • External calls: URL, status code, response time
  • Resource usage: Số connections active, memory usage, cache hit rate

Lưu ý: Không log dữ liệu nhạy cảm (passwords, API keys, personal data). Sanitize input trước khi ghi log.

Performance Benchmarking

Đo lường hiệu suất MCP server giúp phát hiện bottleneck và đảm bảo response time đáp ứng yêu cầu.

// tests/benchmark/performance.test.ts
import { describe, it, expect } from "vitest";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";

describe("Performance Benchmarks", () => {
  let client: Client;
  // ... setup

  it("search_products tra ve trong 200ms", async () => {
    const start = performance.now();
    await client.callTool("search_products", {
      query: "test", limit: 10,
    });
    const duration = performance.now() - start;
    expect(duration).toBeLessThan(200);
  });

  it("xu ly 50 concurrent requests", async () => {
    const requests = Array.from({ length: 50 }, (_, i) =>
      client.callTool("search_products", {
        query: "test-" + i, limit: 5,
      })
    );

    const start = performance.now();
    const results = await Promise.all(requests);
    const duration = performance.now() - start;

    // Tat ca requests thanh cong
    expect(results.every(r => !r.isError)).toBe(true);
    // Tong thoi gian duoi 5 giay
    expect(duration).toBeLessThan(5000);
  });

  it("memory khong tang qua 50MB sau 1000 requests",
    async () => {
    const before = process.memoryUsage().heapUsed;

    for (let i = 0; i < 1000; i++) {
      await client.callTool("search_products", {
        query: "stress-" + i, limit: 1,
      });
    }

    const after = process.memoryUsage().heapUsed;
    const increase = (after - before) / 1024 / 1024; // MB
    expect(increase).toBeLessThan(50);
  });
});

CI/CD cho MCP Server

Thiết lập CI/CD pipeline đảm bảo MCP server luôn ở trạng thái deployable.

GitHub Actions workflow

# .github/workflows/mcp-server-ci.yml
name: MCP Server CI/CD

on:
  push:
    branches: [main]
    paths: ["mcp-server/**"]
  pull_request:
    branches: [main]
    paths: ["mcp-server/**"]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_DB: test_db
          POSTGRES_PASSWORD: test
        ports: ["5432:5432"]
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"
          cache-dependency-path: mcp-server/package-lock.json

      - name: Install dependencies
        run: npm ci
        working-directory: mcp-server

      - name: Type check
        run: npx tsc --noEmit
        working-directory: mcp-server

      - name: Unit tests
        run: npx vitest run --coverage
        working-directory: mcp-server

      - name: Build
        run: npm run build
        working-directory: mcp-server

      - name: Integration tests
        run: npx vitest run tests/integration
        working-directory: mcp-server
        env:
          DATABASE_URL: postgresql://postgres:test@localhost/test_db

      - name: Performance tests
        run: npx vitest run tests/benchmark
        working-directory: mcp-server
        env:
          DATABASE_URL: postgresql://postgres:test@localhost/test_db

Test Fixtures cho MCP

Fixtures giúp tạo dữ liệu test nhất quán và dễ maintain.

// tests/fixtures/products.ts
export const sampleProducts = [
  {
    id: "prod-001",
    name: "MacBook Pro 14 inch",
    price: 52000000,
    category: "laptop",
    stock: 15,
  },
  {
    id: "prod-002",
    name: "iPhone 15 Pro Max",
    price: 34000000,
    category: "dien-thoai",
    stock: 50,
  },
  {
    id: "prod-003",
    name: "AirPods Pro 2",
    price: 6500000,
    category: "phu-kien",
    stock: 100,
  },
];

// tests/fixtures/setup-db.ts
import { Pool } from "pg";
import { sampleProducts } from "./products.js";

export async function setupTestDb(pool: Pool) {
  await pool.query("DROP TABLE IF EXISTS products CASCADE");
  await pool.query(`
    CREATE TABLE products (
      id VARCHAR(20) PRIMARY KEY,
      name VARCHAR(255) NOT NULL,
      price INTEGER NOT NULL,
      category VARCHAR(50),
      stock INTEGER DEFAULT 0
    )
  `);

  for (const product of sampleProducts) {
    await pool.query(
      "INSERT INTO products VALUES ($1, $2, $3, $4, $5)",
      [product.id, product.name, product.price,
       product.category, product.stock]
    );
  }
}

export async function teardownTestDb(pool: Pool) {
  await pool.query("DROP TABLE IF EXISTS products CASCADE");
  await pool.end();
}

Checklist trước khi deploy production

Trước khi đưa MCP server lên production, hãy đảm bảo đã hoàn thành các mục sau:

  • Unit test coverage trên 80% cho tất cả tool handlers
  • Integration test verify schema, response format, error handling
  • Performance test đảm bảo response time dưới ngưỡng chấp nhận
  • Logging hoạt động đúng, ghi ra stderr hoặc file (không phải stdout)
  • Error handling cho tất cả edge cases (input rỗng, null, quá dài)
  • Rate limiting nếu tool gọi external API
  • Resource cleanup (database connections, file handles, browser instances)
  • CI/CD pipeline chạy tất cả tests trước khi deploy
  • Monitoring và alerting cho uptime và error rate
  • Documentation cho mỗi tool (description rõ ràng giúp Claude sử dụng đúng)

Bước tiếp theo

Testing và debugging MCP server là quy trình liên tục, không phải chỉ làm một lần. Khi thêm tool mới, luôn viết test trước (TDD). Khi phát hiện bug, viết regression test trước khi fix. Sử dụng MCP Inspector thường xuyên để verify server behavior. Khám phá thêm các hướng dẫn phát triển MCP tại Thư viện Nâng cao Claude.

Tính năng liên quan:Unit TestingIntegration TestingMCP InspectorCI/CD

Bai viet co huu ich khong?

Bản quyền thuộc về tác giả. Vui lòng dẫn nguồn khi chia sẻ.

Bình luận (0)
Ảnh đại diện
Đăng nhập để bình luận...
Đăng nhập để bình luận
  • Đang tải bình luận...

Đăng ký nhận bản tin

Nhận bài viết hay nhất về sản phẩm và vận hành, gửi thẳng vào hộp thư của bạn.

Bảo mật thông tin. Hủy đăng ký bất cứ lúc nào. Chính sách bảo mật.