MCP server đầu tiên tôi publish lên npm vận hành 8 tiếng thì có user mở issue. 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. Tôi unpublish trong vòng 15 phút. Đó là lúc tôi hiểu giữa “chạy được” và “publish được” là một khoảng rất xa, và Inspector là tool tôi đáng lẽ phải xài từ ngày đầu chứ không phải sau khi đã ship.
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, và security checklist 10 mục phải đi qua trước khi push lên registry. Cuối bài tôi chỉ cách wire Inspector vào CI để contract test schema tự động.
Opinion thẳng trước khi đi: Inspector là tool MUST trước production. Bỏ qua nó tương đương ship API endpoint mà không bao giờ mở Postman lên thử. Tôi đã thấy nhiều server MCP bị reject ngay vòng đầu code review chỉ vì author chưa từng tự gọi tool của mình qua wire format thật.
Vì sao test MCP khác test API thường
MCP server không phải REST API. 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ừ model. LLM có thể gọi tool sai tham số, có thể đưa input độc hại từ user message, 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 khác nhau. Protocol layer kiểm tra server handshake đúng, expose tools/list, resources/list, prompts/list, schema có valid JSON Schema. Tool semantics layer kiểm tra tool nhận input đúng schema thì trả output đúng schema, edge case (empty, malformed, oversized) handle ra sao. LLM interaction layer kiểm tra 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, sẽ chạm tới ở cuối bài.
MCP Inspector là gì
Inspector là dev tool open source của Anthropic, repo modelcontextprotocol/inspector. Nó gồm hai phần: 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 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 đã build, hoặc python -m my_mcp_server cho Python. Hoặc gắn server luôn:
npx @modelcontextprotocol/inspector node build/index.js
Inspector spawn process server, connect qua stdio, gửi initialize request, render tab UI cho Tools, Resources, Prompts, Sampling, Notifications.
Lần đầu tôi mở Inspector mất khoảng 30 phút loay hoay vì server tôi viết không trả schema đúng format cho tools/list. Inspector hiển thị form trống, không có error rõ. Tôi tưởng lỗi Inspector, đến khi xem tab Request mới thấy raw JSON-RPC response thiếu field inputSchema. Lesson: nếu form trống, đừng nhìn UI, mở tab Request ra đọc raw.
Trong tab Tools, Inspector gọi tools/list ngay sau khi connect. Mỗi tool hiện ra với tên, description, một form auto-generate từ JSON Schema của input. Bạn điền tham số, bấm “Call Tool”, response hiện ra dạng JSON kèm content blocks (text, image, resource link). Tab Resources liệt kê các URI server expose, click vào thì Inspector gọi resources/read, render theo MIME type. 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 dev loop.
Mặc định Inspector dùng stdio. SSE và Streamable HTTP cũng support, chuyển qua dropdown trong UI hoặc query param trên URL: http://localhost:6274/?transport=sse&serverUrl=https://my-mcp.example.com.
CLI mode cho automation
Cái này mới là điểm shine của Inspector cho CI/CD. 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. Các method support: 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 unit test thường khó cover vì phải mock JSON-RPC layer, Inspector test trực tiếp 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 của tôi: viết tool handler, define Zod schema cho input, build server (npm run build hoặc tsc -w mode). Inspector đã mở sẵn ở localhost:6274, reload để connect lại (nó tự spawn process mới). Vào tab Tools, gọi tool với input đúng, verify output shape. Sau đó gọi lại với input sai cố tình: thiếu field, kiểu sai, oversized string, special chars. Đọc error message Zod trả ra. Một tool tốt là tool mà error message tự explain cách dùng đúng cho LLM. Refine schema, refine .describe() text cho từng field. 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 SDK client
Sau khi Inspector dev loop ổn, bạn cần test programmatic. @modelcontextprotocol/sdk cung cấp class Client để spawn server in-process và gửi request như thật. Ví dụ TypeScript với vitest:
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 () => {
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. 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 wrapper trả response cố định, spin real MCP server, gọi LLM mock theo sequence. Loại test này expensive và brittle, chỉ làm cho happy path quan trọng nhất.
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"
npx @modelcontextprotocol/inspector --cli $SERVER_CMD --method ping > /dev/null
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
echo "$TOOLS_JSON" | jq -e '.tools[] | select(.name == "search_files") | .inputSchema.required | contains(["query"])' > /dev/null
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
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 (config Claude Desktop downstream), tất cả fail CI. Tôi đã dùng pattern này cho 4 server và catch được 11 PR có breaking change schema trong 3 tháng đầu. Phần lớn là tôi tự breaking schema của chính mình mà quên cập nhật consumer config.
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 hiện tại là mcp-evals (open source, repo evalstate/mcp-evals) và mcp-eval của Anthropic. Cả hai cùng pattern: 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. Khác biệt chính: Anthropic version có sẵn metric (tool selection accuracy, parameter correctness, error recovery rate), mcp-evals bạn tự define metric.
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%, khả năng cao bạn vừa làm description khó hiểu hơn cho model. Tôi không chạy eval trong PR CI vì chậm và 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, 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.
- 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 reject explicit. - 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ả
isError: truevới message generic ở client-side. Tôi từng leak absolute home path qua một stack trace JavaScript, đó là wakeup call. - 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. Tôi suýt ship một tool nhận
auth_tokenfield từ user input cho “tiện”, may là Inspector test phát hiện schema không cần token vẫn gọi được, vì server hardcode đọc env. - 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.
- 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ả error có
retry-afterhint khi vượt. Bảo vệ cả server lẫn downstream API. - 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>(tool poisoning). Pin version cụ thể trong config, đừng dùnglatest. - Pin version downstream. Server bạn depend vào package khác (ví dụ wrapper quanh SDK GitHub) phải pin exact version trong
package.jsonhoặcrequirements.txt. Một dependency update silently thay đổi behavior tool của bạn là loại bug khó debug nhất. - Sandbox khi run local. Server local có file system access mặc định toàn home dir 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ể. - 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.
- 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 phải check thủ công. Whitelist explicit origin (Claude Desktop, Claude Code, Cursor, v.v.), 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 năm 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: mcp-scan cho 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 phải 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 ngay.
Bước tiếp
Tools, resources, prompts, transport, deploy, test, security đã đủ cho một server publish được. Bài 12 đặt câu hỏi quan trọng: server MCP bạn viết có chạy được với client nào ngoài Claude Desktop? 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
- modelcontextprotocol/inspector trên GitHub: repo chính thức, đọc README và CLI doc.
- MCP Inspector docs: hướng dẫn config file, transport, query params.
- OWASP MCP Security Cheat Sheet: nguồn chính của 10 item checklist trên.
- MCP Security Vulnerabilities (Practical DevSecOps): bài chuyên sâu prompt injection và tool poisoning.
- Validating MCP Tool Inputs (Fast.io): pattern Zod validation chi tiết.