Tóm tắt: Test theo 3 tầng. (1) Unit test gọi thẳng hàm Python của tool, không cần MCP. (2) Test giao thức: pytest khởi chạy server qua stdio_client, list_tools rồi call_tool như một client thật. (3) Thử tay: MCP Inspector, sau đó claude mcp add và /mcp trong Claude Code. Cách viết server xem Viết MCP server bằng Python.
Luồng hoạt động
flowchart LR A["Hàm tool (Python)"] -->|"unit test"| B["pytest"] A --> C["Server FastMCP"] C -->|"stdio_client + ClientSession"| D["pytest e2e"] C -->|"mcp dev"| E["MCP Inspector"] C -->|"claude mcp add"| F["Claude Code"] F --> G["/mcp, thử prompt thật"]
Cấu hình
Chuẩn bị
uv add "mcp[cli]<2" # code dưới đây dùng API 1.x (mcp.server.fastmcp)
uv add --dev pytest anyioGói mcp bản 2.x đổi API: FastMCP thành MCPServer (from mcp.server.mcpserver import MCPServer), CallToolResult.isError thành is_error, và có thêm mcp.Client để test in-memory. Trang này viết cho 1.x; nếu dùng 2.x hãy đọc migration guide của SDK. uv add "mcp[cli]" không ghim sẽ kéo bản mới nhất.
Tầng 1 - unit test hàm tool
Decorator @mcp.tool() trả lại chính hàm gốc, nên gọi trực tiếp được:
import pytest
from server import list_devices, ping
def test_list_devices_filter():
names = {d["name"] for d in list_devices(role="pe")}
assert names == {"pe1-hni", "pe2-hcm"}
def test_ping_rejects_hostname():
with pytest.raises(ValueError):
ping("router.example.com") # chỉ nhận IPVới tool gọi thiết bị/API ngoài, mock tầng I/O (monkeypatch subprocess.run, client HTTP), đừng để unit test đụng thiết bị thật.
Tầng 2 - test giao thức qua stdio
Khởi chạy server như tiến trình con và nói chuyện bằng JSON-RPC thật, nên bắt được lỗi schema, lỗi print() ra stdout, lỗi import:
import sys
import pytest
from mcp import ClientSession, StdioServerParameters, stdio_client
@pytest.fixture
def anyio_backend():
return "asyncio"
@pytest.mark.anyio
async def test_tools_over_stdio():
params = StdioServerParameters(command=sys.executable, args=["server.py"])
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
assert "ping" in {t.name for t in tools.tools}
ok = await session.call_tool("list_devices", {"role": "pe"})
assert not ok.isError
bad = await session.call_tool("ping", {"target": "not-an-ip"})
assert bad.isError # lỗi trả về cho model, server không chếtuv run pytest -qĐiểm cần nhớ:
- Plugin
anyiocó sẵn trong pytest khi càianyio;@pytest.mark.anyio+ fixtureanyio_backendlà đủ, không cầnpytest-asyncio. - Exception trong tool không làm
call_toolraise: kết quả cóisError=Truevàcontentchứa thông báo (dạngError executing tool <tên>: <lỗi>). Assert trênisError, đừngpytest.raises. - Muốn kiểm tra schema model nhìn thấy: assert trên
t.inputSchemacủa từng tool (kiểu,required) để bắt hồi quy khi đổi chữ ký hàm. - Kết quả dạng list/dict trả về trong
content(mỗi phần tử là mộtTextContentchứa JSON); tool trảstrthì đọccontent[0].text. Có thể parse bằngjson.loadsđể assert.
Tầng 3a - MCP Inspector
uv run mcp dev server.py # Inspector qua CLI của mcp
npx @modelcontextprotocol/inspector uv run server.py # hoặc chạy Inspector trực tiếpTrong Inspector: Connect → tab Tools → List Tools → chọn tool, nhập tham số → Run Tool. Xem cả tab Notifications (log server) khi tool lỗi. Trường hợp cần thử: tham số thiếu, sai kiểu, giá trị biên, tool chạy lâu.
Tầng 3b - Claude Code
claude mcp add netops-test -- uv run --directory /opt/netops-mcp server.py
claude mcp list
claude mcp get netops-testTrong phiên Claude Code gõ /mcp xem trạng thái và danh sách tool, rồi hỏi một câu buộc model phải gọi tool (vd "liệt kê thiết bị role pe"). Kiểm tra: model có chọn đúng tool không, tham số có đúng không, kết quả lỗi có đủ gợi ý để tự sửa không. Nếu model chọn sai, sửa docstring/tên tool trước khi sửa code (xem Thiết kế tool cho agent và Đánh giá agent).
Lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách xử lý |
|---|---|---|
Test e2e treo hoặc lỗi parse JSON khi initialize |
Code print() ra stdout, làm hỏng kênh JSON-RPC |
Bỏ print, dùng logging (stderr) |
ModuleNotFoundError: mcp.server.fastmcp |
Cài mcp 2.x |
Ghim mcp<2 hoặc đổi sang MCPServer theo migration guide |
AttributeError: ... has no attribute 'isError' |
Test viết cho 1.x chạy trên 2.x | Dùng is_error ở 2.x, hoặc ghim mcp<2 |
async def functions are not natively supported |
Thiếu plugin async cho pytest | Cài anyio + @pytest.mark.anyio, hoặc pytest-asyncio |
Test pass nhưng Claude Code báo failed |
Môi trường khác (venv, cwd, biến môi trường) | Chạy đúng lệnh của claude mcp get <tên> trong terminal |
| Inspector không thấy tool vừa sửa | Server chưa được restart | Disconnect rồi Connect lại |
assert bad.isError fail, tool trả chuỗi lỗi bình thường |
Tool tự return "error ..." thay vì raise |
Raise exception để client nhận isError |
Kiểm tra nhanh
uv run pytest -q # unit + e2e
uv run mcp dev server.py # thử tay bằng Inspector
claude mcp list # server đã đăng ký và connected?