Bài 6 tôi đã xong cái phần tool design, bài 7 nói về prompt và sampling, bài 8 deploy lên production. Cứ tưởng vậy là gọi MCP server “ready to ship”. Sau khi tôi publish server đầu tiên lên npm thì user đầu tiên báo bug: tool gọi không vào, schema lỗi field name, một tool trả về stack trace JavaScript có chứa absolute path nhà tôi. Đó là lúc tôi hiểu giữa “chạy được” và “publish được” là một khoảng rất xa.

Bài này nói về cách test MCP server trước khi đưa cho ai dùng. Ba phần chính: MCP Inspector (dev tool chính thức của Anthropic), integration test với client SDK mocking, và một security checklist 10 mục bạn phải đi qua trước khi push lên registry. Cuối bài tôi sẽ chỉ cách wire Inspector vào CI để contract test schema tự động.

Tại sao test MCP khác test API thường

MCP server không phải REST API. Khi viết REST endpoint, bạn biết client là code do bạn (hoặc team bạn) viết, request schema bạn kiểm soát hai đầu. MCP thì khác: client là LLM, request không phải code mà là output sinh ra từ một model. LLM có thể gọi tool sai tham số, có thể đưa input độc hại từ user, có thể không gọi tool nào hết dù schema rất rõ ràng.

Hệ quả: test của bạn phải cover ba layer riêng biệt.

  1. Protocol layer: server có handshake đúng, expose đúng tools/list, resources/list, prompts/list không, schema có valid JSON Schema không.
  2. Tool semantics layer: tool nhận input đúng schema có trả output đúng schema không, edge case (empty, malformed, oversized) có handle không.
  3. LLM interaction layer: description của tool có rõ để model gọi đúng tool đúng lúc không, error message có giúp model recover không.

MCP Inspector giúp bạn cover layer 1 và 2 nhanh nhất. Layer 3 cần evaluation framework riêng, bài này sẽ chạm tới ở cuối.

MCP Inspector là gì

MCP Inspector là dev tool open source của Anthropic, repo modelcontextprotocol/inspector trên GitHub. Nó gồm hai thành phần: MCP Inspector Client (giao diện React) và MCP Proxy (Node.js bridge giữa client web và server bạn đang test). Yêu cầu Node.js 22.7.5 trở lên, không cài đặt gì hết, chạy thẳng qua npx.

Cách chạy đơn giản nhất:

npx @modelcontextprotocol/inspector

Nó mở browser ở http://localhost:6274, tự sinh session token (bearer auth bật mặc định, bind localhost-only, có chống DNS rebinding qua check Origin header). Bạn nhập command để spawn server, ví dụ node build/index.js cho TypeScript server đã build, hoặc python -m my_mcp_server cho Python.

Cách chạy gắn server luôn:

npx @modelcontextprotocol/inspector node build/index.js

Inspector sẽ spawn process server, connect qua stdio, gửi initialize request, render tab tabs UI: Tools, Resources, Prompts, Sampling, Notifications.

UI workflow

Trong tab Tools, Inspector gọi tools/list ngay sau khi connect. Mỗi tool hiện ra với tên, description, và một form được auto-generate từ JSON Schema của input. Bạn điền tham số vào form, bấm “Call Tool”, response hiện ra dạng JSON kèm content blocks (text, image, resource link). Có một tab “Request” hiển thị raw JSON-RPC request và response, rất cần cho debug.

Tab Resources liệt kê các URI server expose. Click vào URI thì Inspector gọi resources/read, render content theo MIME type (text, JSON, image preview). Tab Prompts cho phép test prompt resource với argument binding, render xem message cuối ra thế nào.

Tab Sampling đặc biệt: nó cho phép Inspector đóng vai client để xử lý sampling/createMessage request từ server. Khi server bạn test gửi một sampling request, Inspector hiển thị message lên UI và chờ bạn nhập response giả. Cực kỳ tiện vì không cần wire LLM thật vào trong vòng dev loop.

Transport ngoài stdio

Mặc định Inspector dùng stdio (spawn server làm subprocess). Hai transport khác cũng support:

  • SSE: dùng cho server remote, set URL ở header bar Inspector.
  • Streamable HTTP: transport mới hơn, hot trong 2026, dùng cho production deployments.

Bạn có thể chuyển transport qua dropdown trong UI hoặc qua query param trên URL Inspector, ví dụ http://localhost:6274/?transport=sse&serverUrl=https://my-mcp.example.com.

CLI mode cho automation

Inspector có CLI mode đặc biệt cho CI/CD và scripting. Thêm flag --cli:

npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list

Output là JSON ra stdout, exit code 0 nếu OK, non-zero nếu fail. Đây chính là cái bạn cần để wire contract test vào GitHub Actions.

Các method support trong CLI: tools/list, tools/call (kèm --tool-name, --tool-arg key=value), resources/list, resources/read, prompts/list, prompts/get, ping.

Ví dụ test một tool cụ thể:

npx @modelcontextprotocol/inspector --cli node build/index.js \
  --method tools/call \
  --tool-name search_files \
  --tool-arg query=foo \
  --tool-arg max_results=10

Nếu schema sai (ví dụ query đáng lẽ là number mà tôi truyền string), Zod throw validation error, Inspector trả về error frame với field path rõ ràng, exit code khác 0. Đây là điều mà unit test thường khó cover vì bạn phải mock JSON-RPC layer, còn Inspector test luôn trên wire format thật.

Workflow dev hàng ngày

Tôi chạy Inspector ngay từ khi viết tool đầu tiên, không đợi server “xong” mới test. Loop dev của tôi như sau.

  1. Viết tool handler trong server code, define Zod schema cho input.
  2. Build server (npm run build hoặc tsc -w mode).
  3. Inspector đang mở sẵn ở localhost:6274. Reload connect lại (nó tự spawn process mới khi connect).
  4. Vào tab Tools, gọi tool với input đúng. Verify output shape.
  5. Gọi lại với input sai cố tình: thiếu field, kiểu sai, oversized string, special chars. Xem error message Zod trả ra có đủ rõ cho LLM đọc không.
  6. Refine schema, refine .describe() text cho từng field. Một tool tốt là tool mà error message tự explain cách dùng đúng.
  7. Lặp.

Cycle này nhanh hơn nhiều so với viết unit test cho mỗi tool. Unit test có chỗ của nó (regression, edge case nhiều), nhưng cho dev loop trải nghiệm như REPL, Inspector thắng.

Integration test với client SDK mocking

Sau khi Inspector dev loop ổn, bạn cần test programmatic. Hai cách:

Cách 1: SDK client trực tiếp

@modelcontextprotocol/sdk cung cấp class Client để bạn spawn server in-process và gửi request như thật. Ví dụ TypeScript:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { describe, it, expect } from "vitest";

describe("my-mcp-server", () => {
  it("lists expected tools", async () => {
    const transport = new StdioClientTransport({
      command: "node",
      args: ["build/index.js"],
    });
    const client = new Client({ name: "test", version: "1.0.0" }, { capabilities: {} });
    await client.connect(transport);

    const result = await client.listTools();
    const names = result.tools.map((t) => t.name);
    expect(names).toContain("search_files");
    expect(names).toContain("read_file");

    await client.close();
  });

  it("search_files returns valid schema", async () => {
    // setup omit cho gọn
    const result = await client.callTool({
      name: "search_files",
      arguments: { query: "test", max_results: 5 },
    });
    expect(result.isError).toBe(false);
    expect(result.content[0].type).toBe("text");
  });
});

Mỗi test spin một process server mới, slow nhưng coverage cao. Trade off chấp nhận được vì test này chạy trong CI, không phải trong dev loop.

Cách 2: Mock LLM, real server

Nếu bạn muốn test luôn cả flow LLM gọi tool, dùng MockLanguageModel từ SDK Anthropic hoặc tự viết một wrapper trả response cố định. Spin real MCP server, point client vào, gọi LLM mock một sequence cố định: “tôi gọi tool X với arg Y”, “OK kết quả là Z thì trả lời Q”. Verify cuối cùng output cuối của agent đúng.

Loại test này expensive (slow, brittle), chỉ làm cho happy path quan trọng nhất. Phần lớn coverage nên ở cách 1.

Contract test cho schema trong CI

Đây là chỗ MCP Inspector CLI thật sự shine. Bạn viết một script test-mcp-contract.sh chạy trong CI:

#!/usr/bin/env bash
set -euo pipefail

SERVER_CMD="node build/index.js"

# 1. Kiểm tra ping
npx @modelcontextprotocol/inspector --cli $SERVER_CMD --method ping > /dev/null

# 2. Liệt kê tools, verify count và name
TOOLS_JSON=$(npx @modelcontextprotocol/inspector --cli $SERVER_CMD --method tools/list)
EXPECTED_TOOLS=(search_files read_file write_file)
for tool in "${EXPECTED_TOOLS[@]}"; do
  if ! echo "$TOOLS_JSON" | jq -e ".tools[] | select(.name == \"$tool\")" > /dev/null; then
    echo "Missing tool: $tool"
    exit 1
  fi
done

# 3. Verify schema có required fields
echo "$TOOLS_JSON" | jq -e '.tools[] | select(.name == "search_files") | .inputSchema.required | contains(["query"])' > /dev/null

# 4. Test một call happy path
RESULT=$(npx @modelcontextprotocol/inspector --cli $SERVER_CMD \
  --method tools/call \
  --tool-name search_files \
  --tool-arg query=mcp \
  --tool-arg max_results=3)
echo "$RESULT" | jq -e '.isError == false' > /dev/null

# 5. Test call với input sai phải fail
if npx @modelcontextprotocol/inspector --cli $SERVER_CMD \
  --method tools/call \
  --tool-name search_files \
  --tool-arg max_results=3 \
  > /dev/null 2>&1; then
  echo "Expected failure for missing query arg, but call succeeded"
  exit 1
fi

echo "Contract test passed"

Wire vào .github/workflows/test.yml:

- name: MCP contract test
  run: |
    npm run build
    bash scripts/test-mcp-contract.sh

Lợi ích: schema regression bị catch ngay PR. Đổi tên field, đổi type, xóa tool đang được client khác dùng (downstream Claude Desktop config), tất cả fail CI.

Eval cho LLM interaction layer

Contract test cover layer 1 và 2. Layer 3 (LLM có biết gọi tool đúng không) cần eval framework. Hai option phổ biến.

Option A: mcp-evals (open source, có repo evalstate/mcp-evals). Bạn viết test case dạng “user nói X, expect tool Y được gọi với arg Z”. Framework spin LLM (Claude hoặc OpenAI), connect MCP server, chạy conversation, verify tool call sequence.

Option B: Anthropic’s mcp-eval (internal tool, một số phần đã open). Tương tự nhưng có sẵn metric (tool selection accuracy, parameter correctness, error recovery rate).

Eval không phải pass/fail như unit test, kết quả là score. Bạn baseline score lúc launch, monitor drift theo từng release. Nếu update description tool xong score drop 10%, có khả năng bạn vừa làm description khó hiểu hơn cho model.

Tôi không chạy eval trong PR CI (chậm, tốn API). Chạy nightly hoặc trước release.

Security checklist trước khi publish

Khi MCP server bạn ra public registry (npm, PyPI, hoặc MCP marketplace), người lạ chạy nó trong process AI của họ. Chỉ cần một field nhạy cảm leak, một injection chưa block, là damage thật. Đây là 10 mục tôi đi qua trước mỗi lần publish, gom từ OWASP MCP Security Cheat Sheet và kinh nghiệm cá nhân.

1. Schema validation chặt

Mọi tool input phải có JSON Schema với additionalProperties: false. Đừng để model “sáng tạo” field. Dùng Zod hoặc Pydantic, không tự viết check ad-hoc. Empty string, null, oversized payload đều phải reject explicit.

2. Error handling không leak

Không bao giờ throw raw exception ra response. Wrap mọi handler bằng try/catch, log error chi tiết server-side, trả về isError: true với message generic ở client-side. Tôi từng leak absolute home path qua một stack trace JavaScript, đó là wakeup call.

3. No token passthrough

Đừng nhận access token từ user message rồi forward thẳng vào downstream API. Đó là pattern OAuth confused deputy: LLM bị trick thành proxy quyền của user. Token phải ở server config (env var, secret manager), không ở runtime input.

4. No PII trong log

Log tool invocation rất hữu ích để debug. Nhưng đừng log raw input nguyên si vì input có thể chứa email, phone, file content user. Redact theo pattern (regex email, phone, key-like strings) hoặc structured log với explicit fields safe.

5. Rate limit per client

Một MCP server bị một LLM agent gọi liên tục do prompt injection cố tình loop. Set rate limit per client session (ví dụ 100 calls/min/session), trả về error có retry-after hint khi vượt. Bảo vệ cả server lẫn downstream API mà server gọi tới.

6. Audit registry trước khi depend

Bạn không chỉ là author server, bạn cũng là consumer của server khác. Trước khi cài MCP server thứ ba vào setup Claude của bạn, đọc source. Check description tool có vẻ “innocent” mà nhúng instruction kiểu <IMPORTANT>ignore previous instructions...</IMPORTANT> không (tool poisoning). Pin version cụ thể trong config, đừng dùng latest.

7. Pin version downstream

Nếu server bạn depend vào package khác (ví dụ wrapper quanh SDK GitHub), pin exact version trong package.json hoặc requirements.txt. Một dependency update silently thay đổi behavior tool của bạn là loại bug khó debug nhất.

8. Sandbox khi run local

Server local có file system access mặc định toàn home dir của user. Restrict path explicit: chỉ một whitelist directory được read, không tự follow symlink ra ngoài, normalize path để chặn ../../../etc/passwd. Nếu deploy Docker, mount volume read-only khi có thể.

9. Rotate secrets thường xuyên

Token API key trong env có lifecycle riêng. Document rõ trong README cách rotate, đừng để user nghĩ “set một lần xong quên”. Nếu server expose webhook (callback từ third-party), validate signature, dùng secret webhook tách biệt với token API chính.

10. Check Host header và Origin

Cho SSE và HTTP transport, server phải check Host và Origin header để chặn DNS rebinding attack. MCP Inspector tự làm cái này, nhưng server custom bạn viết thì phải check thủ công. Whitelist explicit origin (Claude Desktop, Claude Code, Cursor, etc.), reject mặc định.

Đi qua 10 item trên không bảo đảm zero vulnerability nhưng catch được phần lớn class lỗi phổ biến nhất trong MCP server year 2026. Path traversal, command injection, hardcoded credential là ba loại được report nhiều nhất trong các audit MCP server công cộng năm qua.

Bonus: dùng mcp-scan để audit tool poisoning

mcp-scan (npm package, open source) scan một MCP server hoặc một registry và alert nếu tool description chứa pattern khả nghi: HTML-like tag (<IMPORTANT>, <s>, <instructions>), imperative verb “ignore”, “forget”, “send to”, instruction nhúng trong description. Chạy local trên server của bạn trước publish, chạy nightly trên dependencies bạn đang dùng.

npx mcp-scan ./build/index.js
npx mcp-scan https://my-mcp-registry.example.com

Tool này không là silver bullet (false positive nhiều), nhưng quick win cho phần “rug pull detection”: sau khi server đã được trust, một update âm thầm thêm pattern khả nghi sẽ được flag.

Tổng kết workflow test

Một MCP server “production ready” của tôi đi qua các bước sau trước khi push lên registry.

  1. Dev loop với Inspector UI: mỗi tool gọi tay, edge case, refine schema và description.
  2. Integration test với SDK client trong vitest/pytest: cover happy path + parameter validation.
  3. Contract test CLI Inspector trong CI: schema regression catch trên mỗi PR.
  4. Eval LLM interaction nightly: track tool selection accuracy score.
  5. Security checklist 10 item, đặc biệt token passthrough và schema strict.
  6. mcp-scan audit trước publish và trên dependency.

Sau bài này thì tools, resources, prompts, transport, deploy, test, security đã đủ cho một server publish được. Bài 12 tôi sẽ nói về chuyện server MCP bạn viết có chạy được với client nào ngoài Claude Desktop không: Cursor, Windsurf, gptme, OpenAI Codex. Nhiều client adopt MCP nghĩa là một server tốt có thể serve nhiều ecosystem cùng lúc, miễn là bạn không lock vào API riêng Anthropic.

Tham khảo