Sổ tay kỹ thuật

Test MCP server (Inspector, pytest, Claude Code)

how-to#mcp#python#testing· cập nhật 2026-11-08

Dùng khi đã viết MCP server và cần test tool ở 3 tầng - unit test hàm, test giao thức qua stdio bằng pytest, rồi thử tay bằng MCP Inspector và Claude Code.

Sửa trang này
Mục lục

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 anyio

Gó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:

test_unit.py
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 IP

Vớ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:

test_e2e.py
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ết
uv run pytest -q

Điểm cần nhớ:

  • Plugin anyio có sẵn trong pytest khi cài anyio; @pytest.mark.anyio + fixture anyio_backend là đủ, không cần pytest-asyncio.
  • Exception trong tool không làm call_tool raise: kết quả có isError=True và content chứa thông báo (dạng Error executing tool <tên>: <lỗi>). Assert trên isError, đừng pytest.raises.
  • Muốn kiểm tra schema model nhìn thấy: assert trên t.inputSchema củ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ột TextContent chứa JSON); tool trả str thì đọc content[0].text. Có thể parse bằng json.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ếp

Trong 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-test

Trong 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?

Tham khảo