試-MCP-Inspector與自動(dòng)化測(cè)試)
【MCP 全棧教程】第 21 篇MCP Server 測(cè)試——MCP Inspector 與自動(dòng)化測(cè)試本系列定位從協(xié)議原理到 Server 開(kāi)發(fā)、Client 開(kāi)發(fā)、再到各大平臺(tái)實(shí)戰(zhàn)集成系統(tǒng)化掌握 MCPModel Context Protocol全棧技術(shù)體系。本篇你將學(xué)到MCP Inspector 可視化調(diào)試工具的使用方法手動(dòng)測(cè)試流程Claude Desktop 配置與驗(yàn)證Python pytest 集成測(cè)試框架搭建TypeScript 測(cè)試框架與測(cè)試用例編寫(xiě)覆蓋率要求與常見(jiàn) Bug 排查學(xué)完本篇你將為 MCP Server 建立完整的測(cè)試體系確保每次變更都有質(zhì)量保障。一、測(cè)試體系總覽MCP Server 的測(cè)試分為三個(gè)層次測(cè)試層次工具目的執(zhí)行頻率可視化調(diào)試MCP Inspector交互式驗(yàn)證工具和資源開(kāi)發(fā)階段手動(dòng)測(cè)試Claude Desktop真實(shí)環(huán)境驗(yàn)證發(fā)布前自動(dòng)化測(cè)試pytest / vitest回歸保障每次提交測(cè)試金字塔┌───────────┐ │ 手動(dòng)測(cè)試 │ ← 少量驗(yàn)證端到端體驗(yàn) ├───────────┤ │ 集成測(cè)試 │ ← 中等驗(yàn)證 Client-Server 交互 ├───────────┤ │ 單元測(cè)試 │ ← 大量驗(yàn)證單個(gè)工具邏輯 └───────────┘二、MCP Inspector 使用MCP Inspector 是官方提供的交互式調(diào)試工具可以在可視化界面中連接 Server、列出工具、調(diào)用工具、查看完整的 JSON-RPC 消息。啟動(dòng) Inspector# 方式一通過(guò) npx 啟動(dòng)無(wú)需安裝npx modelcontextprotocol/inspector# 方式二指定要調(diào)試的 Server 命令npx modelcontextprotocol/inspector python server.py# 方式三調(diào)試 TypeScript Servernpx modelcontextprotocol/inspectornodedist/server.js啟動(dòng)后瀏覽器自動(dòng)打開(kāi) Inspector 界面。Inspector 核心功能功能區(qū)域說(shuō)明連接配置配置 Server 啟動(dòng)命令、環(huán)境變量、傳輸方式Tools 面板列出所有工具點(diǎn)擊調(diào)用填寫(xiě)參數(shù)Resources 面板瀏覽和讀取資源Prompts 面板選擇并獲取 Prompt消息日志完整的 JSON-RPC 請(qǐng)求/響應(yīng)記錄通知面板查看收到的通知如 list_changed調(diào)試流程1. 在連接配置中輸入 Server 啟動(dòng)命令 2. 點(diǎn)擊 Connect 連接 Server 3. 查看 Tools 面板確認(rèn)工具列表 4. 選擇一個(gè)工具填寫(xiě)參數(shù) 5. 點(diǎn)擊 Run Tool 執(zhí)行 6. 查看返回結(jié)果和消息日志 7. 如有問(wèn)題在消息日志中檢查 JSON-RPC 消息Inspector 調(diào)試技巧技巧說(shuō)明檢查 discover 響應(yīng)確認(rèn) capabilities 聲明正確對(duì)比參數(shù) Schema確認(rèn) inputSchema 與實(shí)際期望一致查看原始消息切換到原始 JSON 視圖排查格式問(wèn)題測(cè)試通知觸發(fā)工具列表變更觀察通知面板多版本測(cè)試切換協(xié)議版本測(cè)試兼容性三、手動(dòng)測(cè)試Claude Desktop 配置配置 Server編輯 Claude Desktop 配置文件macOS:~/Library/Application Support/Claude/claude_desktop_config.json{mcpServers:{weather-server:{command:python,args:[/path/to/server.py],env:{API_KEY:your-api-key}}}}驗(yàn)證步驟步驟操作預(yù)期結(jié)果1重啟 Claude Desktop加載新配置2打開(kāi)對(duì)話(huà)窗口左側(cè)顯示已連接的 Server3輸入查詢(xún)北京天氣Claude 調(diào)用get_weather工具4觀察工具調(diào)用過(guò)程顯示工具名和參數(shù)5檢查返回結(jié)果結(jié)果正確顯示6測(cè)試異常場(chǎng)景如輸入空城市名查看錯(cuò)誤處理手動(dòng)測(cè)試檢查清單檢查項(xiàng)說(shuō)明Server 正常啟動(dòng)Claude Desktop 日志無(wú)報(bào)錯(cuò)工具列表正確所有工具在對(duì)話(huà)中可用參數(shù)校驗(yàn)生效非法參數(shù)返回明確錯(cuò)誤中文支持中文參數(shù)和返回正常環(huán)境變量env 中的變量正確傳遞多工具協(xié)作連續(xù)調(diào)用多個(gè)工具無(wú)異常四、Python pytest 集成測(cè)試項(xiàng)目結(jié)構(gòu)weather-server/ ├── pyproject.toml ├── server.py └── tests/ ├── __init__.py ├── conftest.py ← 測(cè)試夾具 ├── test_tools.py ← 工具測(cè)試 └── test_resources.py ← 資源測(cè)試安裝測(cè)試依賴(lài)uvadd--devpytest pytest-asyncio pytest-covconftest.py測(cè)試夾具importpytestimportasynciofrommcp.client.sessionimportClientSessionfrommcp.client.stdioimportstdio_client,StdioServerParametersfromserverimportmcpasmcp_serverpytest.fixtureasyncdefmcp_client():創(chuàng)建一個(gè)連接到 Server 的測(cè)試 Client。# 方式一通過(guò) STDIO 連接真實(shí) Server 進(jìn)程server_paramsStdioServerParameters(commandpython,args[server.py],env{API_KEY:test-key})asyncwithstdio_client(server_params)as(read,write):asyncwithClientSession(read,write)assession:# 初始化會(huì)話(huà)awaitsession.initialize()yieldsessionpytest.fixtureasyncdefmcp_client_inprocess():方式二進(jìn)程內(nèi)連接更快不啟動(dòng)子進(jìn)程。frommcp.shared.memoryimportcreate_connected_server_and_clientasyncwithcreate_connected_server_and_client(mcp_server)as(client,server):awaitclient.initialize()yieldclienttest_tools.py工具測(cè)試importpytestpytest.mark.asyncioasyncdeftest_list_tools(mcp_client_inprocess):測(cè)試工具列表。toolsawaitmcp_client_inprocess.list_tools()# 驗(yàn)證工具存在tool_names[t.namefortintools]assertget_weatherintool_namesassertget_forecastintool_names# 驗(yàn)證工具結(jié)構(gòu)weather_toolnext(tfortintoolsift.nameget_weather)assertweather_tool.descriptionisnotNoneassertcityinweather_tool.inputSchema[properties]assertcityinweather_tool.inputSchema[required]pytest.mark.asyncioasyncdeftest_call_weather_valid(mcp_client_inprocess):測(cè)試正常調(diào)用天氣工具。resultawaitmcp_client_inprocess.call_tool(get_weather,{city:北京})assertresult.isErrorisFalseassertlen(result.content)0assertresult.content[0].typetextassert北京inresult.content[0].textpytest.mark.asyncioasyncdeftest_call_weather_empty_city(mcp_client_inprocess):測(cè)試空城市名應(yīng)返回錯(cuò)誤。resultawaitmcp_client_inprocess.call_tool(get_weather,{city:})# 應(yīng)返回 isErrorassertresult.isErrorisTrueassert城市inresult.content[0].textor無(wú)效inresult.content[0].textpytest.mark.asyncioasyncdeftest_call_weather_missing_param(mcp_client_inprocess):測(cè)試缺少必需參數(shù)。withpytest.raises(Exception)asexc_info:awaitmcp_client_inprocess.call_tool(get_weather,{})# 應(yīng)返回參數(shù)錯(cuò)誤assertcityinstr(exc_info.value)or參數(shù)instr(exc_info.value)pytest.mark.asyncioasyncdeftest_unknown_tool(mcp_client_inprocess):測(cè)試調(diào)用不存在的工具。withpytest.raises(Exception):awaitmcp_client_inprocess.call_tool(nonexistent_tool,{})pytest.mark.asyncioasyncdeftest_weather_chinese_city(mcp_client_inprocess):測(cè)試中文城市名。resultawaitmcp_client_inprocess.call_tool(get_weather,{city:烏魯木齊})assertresult.isErrorisFalse測(cè)試 Elicitation 流程pytest.mark.asyncioasyncdeftest_elicitation_flow(mcp_client_inprocess):測(cè)試需要用戶(hù)輸入的工具流程。resultawaitmcp_client_inprocess.call_tool(book_flight,{origin:北京,destination:上海,date:2026-08-15})# 第一次調(diào)用應(yīng)返回 input_requiredassertresult.resultTypeinput_requiredassertlen(result.inputRequests)0# 驗(yàn)證請(qǐng)求結(jié)構(gòu)reqresult.inputRequests[0]assertreq[method]elicitation/createassertreq[params][mode]formassertrequestedSchemainreq[params]assertrequestStateinstr(result)# requestState 存在# 模擬用戶(hù)選擇后重試result2awaitmcp_client_inprocess.call_tool(book_flight,{origin:北京,destination:上海,date:2026-08-15},input_responses[{selectedFlight:CA1501}],request_stateresult.requestState)# 最終應(yīng)完成assertresult2.resultTypecomplete運(yùn)行測(cè)試# 運(yùn)行所有測(cè)試uv run pytest tests/-v# 運(yùn)行并查看覆蓋率uv run pytest tests/--covserver --cov-reportterm-missing# 只運(yùn)行工具相關(guān)測(cè)試uv run pytest tests/test_tools.py-v五、TypeScript 測(cè)試框架項(xiàng)目結(jié)構(gòu)weather-server/ ├── package.json ├── tsconfig.json ├── src/ │ └── server.ts └── tests/ ├── tools.test.ts └── resources.test.ts安裝依賴(lài)npminstall--save-dev vitest modelcontextprotocol/sdk測(cè)試代碼import{describe,test,expect,beforeAll,afterAll}fromvitest;import{Client}frommodelcontextprotocol/sdk/client/index.js;import{StdioClientTransport,}frommodelcontextprotocol/sdk/client/stdio.js;import{spawn}fromchild_process;describe(Weather Server,(){letclient:Client;lettransport:StdioClientTransport;beforeAll(async(){// 啟動(dòng) Server 子進(jìn)程并連接transportnewStdioClientTransport({command:node,args:[dist/server.js],env:{...process.env,API_KEY:test-key},});clientnewClient({name:test-client,version:1.0.0},{capabilities:{}});awaitclient.connect(transport);});afterAll(async(){awaittransport.close();});test(tools/list 返回正確的工具列表,async(){constresultawaitclient.request({method:tools/list},{});expect(result.tools).toBeDefined();consttoolNamesresult.tools.map((t:any)t.name);expect(toolNames).toContain(get_weather);});test(get_weather 正常返回天氣,async(){constresultawaitclient.request({method:tools/call,params:{name:get_weather,arguments:{city:北京},},},{});expect(result.content[0].type).toBe(text);expect(result.content[0].text).toContain(北京);});test(空城市名返回 isError,async(){constresultawaitclient.request({method:tools/call,params:{name:get_weather,arguments:{city:},},},{});expect(result.isError).toBe(true);});test(不存在的工具返回錯(cuò)誤,async(){awaitexpect(client.request({method:tools/call,params:{name:nonexistent,arguments:{},},},{})).rejects.toThrow();});test(中文城市名正常工作,async(){constresultawaitclient.request({method:tools/call,params:{name:get_weather,arguments:{city:哈爾濱},},},{});expect(result.content[0].text).toContain(哈爾濱);});});package.json 測(cè)試腳本{scripts:{build:tsc,test:vitest run,test:watch:vitest,test:coverage:vitest run --coverage}}運(yùn)行測(cè)試# 先構(gòu)建npmrun build# 運(yùn)行測(cè)試npmtest# 覆蓋率報(bào)告npmrun test:coverage六、覆蓋率要求推薦覆蓋率標(biāo)準(zhǔn)指標(biāo)最低要求推薦值說(shuō)明行覆蓋率70%85%執(zhí)行的代碼行比例分支覆蓋率60%80%if/else 分支覆蓋比例函數(shù)覆蓋率75%90%調(diào)用的函數(shù)比例工具覆蓋率100%100%每個(gè)工具至少有一個(gè)測(cè)試配置覆蓋率門(mén)檻Pythonpyproject.toml[tool.pytest.ini_options] addopts --covserver --cov-fail-under80 [tool.coverage.report] exclude_lines [ if __name__, pass, ... ]TypeScriptvitest.config.tsimport{defineConfig}fromvitest/config;exportdefaultdefineConfig({test:{coverage:{provider:v8,reporter:[text,html],thresholds:{lines:80,branches:75,functions:85,},},},});七、常見(jiàn) Bug 排查Bug 1stdout 污染癥狀Server 啟動(dòng)后 Client 連接失敗JSON 解析錯(cuò)誤原因Server 用print()輸出到 stdout破壞了 JSON-RPC 消息流排查檢查代碼中所有print()和sys.stdout.write()修復(fù)將所有輸出改為logging輸出到 stderr# ? 錯(cuò)誤污染 stdoutprint(Server 啟動(dòng))# ? 正確輸出到 stderrimportlogging logging.basicConfig(levellogging.INFO)loggerlogging.getLogger(__name__)logger.info(Server 啟動(dòng))Bug 2工具參數(shù)類(lèi)型不匹配癥狀工具調(diào)用結(jié)果異?;驁?bào)錯(cuò)原因LLM 傳來(lái)的參數(shù)類(lèi)型與代碼期望不符如數(shù)字傳成字符串排查在工具處理器入口打印arguments的類(lèi)型修復(fù)顯式類(lèi)型轉(zhuǎn)換 JSON Schema 校驗(yàn)mcp.tool()asyncdefsearch(page:int,keyword:str)-str:# 防御性類(lèi)型轉(zhuǎn)換pageint(page)ifpageelse1keywordstr(keyword).strip()...Bug 3異步函數(shù)未 await癥狀工具返回 coroutine 對(duì)象而非結(jié)果原因異步調(diào)用忘記await排查檢查返回值是否為coroutine object修復(fù)添加awaitBug 4inputSchema 定義錯(cuò)誤癥狀I(lǐng)nspector 中工具參數(shù)表單渲染異常原因inputSchema 不符合 JSON Schema 規(guī)范排查在 Inspector 中查看參數(shù)區(qū)域是否正常渲染修復(fù)校驗(yàn) Schema 格式# ? 錯(cuò)誤缺少 typeinputSchema{properties:{city:{}}}# 缺少 type: object# ? 正確inputSchema{type:object,properties:{city:{type:string}},required:[city]}Bug 5環(huán)境變量未傳遞癥狀Server 在測(cè)試環(huán)境工作在 Claude Desktop 中失敗原因配置文件的env字段缺少必需環(huán)境變量排查在 Server 啟動(dòng)時(shí)打印環(huán)境變量到 stderr修復(fù)在claude_desktop_config.json的env中補(bǔ)充Bug 排查決策樹(shù)連接失敗 ├── Server 是否啟動(dòng) → 檢查進(jìn)程/日志 ├── stdout 是否被污染 → 檢查 print 語(yǔ)句 ├── 命令路徑是否正確 → 檢查 command/args 配置 └── 協(xié)議版本是否匹配 → 檢查 _meta.protocolVersion 工具調(diào)用失敗 ├── 工具是否存在 → 檢查 tools/list ├── 參數(shù)是否合法 → 檢查 inputSchema ├── 是否異步問(wèn)題 → 檢查 await └── 外部依賴(lài)是否正常 → 檢查網(wǎng)絡(luò)/數(shù)據(jù)庫(kù) 通知收不到 ├── listChanged 是否聲明 → 檢查 capabilities ├── 是否已訂閱 → 檢查 subscriptions/listen └── 連接是否存活 → 檢查長(zhǎng)連接狀態(tài)本篇小結(jié)知識(shí)點(diǎn)核心內(nèi)容MCP Inspector官方可視化調(diào)試工具支持工具調(diào)用、消息查看Claude Desktop手動(dòng)測(cè)試的真實(shí)環(huán)境驗(yàn)證Python 測(cè)試pytest pytest-asyncio 進(jìn)程內(nèi) Client 測(cè)試TypeScript 測(cè)試vitest StdioClientTransport 子進(jìn)程測(cè)試測(cè)試夾具create_connected_server_and_client實(shí)現(xiàn)進(jìn)程內(nèi)快速測(cè)試覆蓋率要求行覆蓋 80%工具覆蓋 100%stdout 污染最常見(jiàn) Bug改用 logging 輸出 stderr參數(shù)類(lèi)型LLM 傳參可能不符合類(lèi)型需防御性轉(zhuǎn)換排查決策樹(shù)連接失敗 → 工具失敗 → 通知問(wèn)題的分層排查下篇預(yù)告第 22 篇Server 安全最佳實(shí)踐——輸入驗(yàn)證與權(quán)限控制JSON Schema 輸入驗(yàn)證防止注入攻擊、工具執(zhí)行前的用戶(hù)授權(quán)機(jī)制、資源訪問(wèn)范圍限制、日志審計(jì)與憑證管理。如果本篇內(nèi)容對(duì)你有幫助歡迎點(diǎn)贊收藏有任何疑問(wèn)歡迎在評(píng)論區(qū)交流。