議實戰(zhàn):從零開發(fā)AI工具與FastMCP應(yīng)用)
先把話說在前面MCP 協(xié)議Model Context Protocol模型上下文協(xié)議最近在 AI 應(yīng)用開發(fā)圈子里的熱度幾乎可以用“刷屏”來形容。凡是做 Agent、做 AI 插件、做企業(yè)內(nèi)部 Copilot 的人都繞不開這個詞。簡單說它是一套標準化協(xié)議負責(zé)讓 AI 模型“接上”外部工具和數(shù)據(jù)源解決的是大模型沒法直接讀文件、查數(shù)據(jù)庫、調(diào) API 的尷尬。再加上工具開發(fā)這個落地點基本上就是把“會說話的模型”變成“會干活的助手”的關(guān)鍵一步。這篇文章我打算直接以實操為主圍繞 MCP 協(xié)議的核心設(shè)計、工具開發(fā)流程、聯(lián)調(diào)技巧和常見坑位展開。無論你是剛接觸協(xié)議的小白還是已經(jīng)寫過一個簡單 Server 但被參數(shù)校驗折磨過的開發(fā)者都能從中找到可以直接照搬的步驟。我會用 Python 生態(tài)作為主力演示環(huán)境附帶協(xié)議層面的原理解讀爭取讓你看完之后能獨立寫出一個干凈、好用、敢上線的 MCP 工具。1. 先搞懂 MCP 協(xié)議的整體設(shè)計1.1 MCP 到底解決什么問題在 MCP 出現(xiàn)之前想讓 AI 調(diào)用一個工具整體路徑是相當痛苦的。模型廠商和工具提供方各自定義各自的接口A 平臺用 REST 自定義鑒權(quán)B 平臺用 WebSocket 私有消息格式C 平臺干脆只支持內(nèi)置函數(shù)調(diào)用。開發(fā)者每接入一個平臺都要從頭實現(xiàn)一輪“適配層”把工具的入?yún)?、出參、錯誤碼、認證方式全部翻譯成目標平臺能懂的語言。這個場景其實很像早年間的硬件外設(shè)打印機有打印機的驅(qū)動掃描儀有掃描儀的驅(qū)動鍵盤鼠標各搞一套。直到 USB 接口統(tǒng)一了外設(shè)連接標準大家才從“每買一個設(shè)備就要裝一套驅(qū)動”的噩夢中解放出來。MCP 就是在 AI 工具集成領(lǐng)域扮演這個 USB 的角色它把“AI 應(yīng)用怎么發(fā)現(xiàn)工具”“怎么描述工具參數(shù)”“怎么發(fā)起調(diào)用”“怎么返回結(jié)果”這幾個環(huán)節(jié)全部標準化。只要你的工具實現(xiàn)了 MCP 協(xié)議任何支持 MCP 的 AI 客戶端都能直接使用不需要再為不同的客戶端分別寫適配代碼。MCP 的核心是一個客戶端-服務(wù)端模型。AI 應(yīng)用本身就是 Host負責(zé)承載整個會話和模型交互應(yīng)用內(nèi)部再啟動一個 MCP Client用于連接外部的 MCP Server。真正干活的工具則運行在 Server 側(cè)通過標準協(xié)議暴露給客戶端。如果你的工具只在本機使用可以用 stdio 管道通信如果希望遠程讓團隊共享也有 HTTP SSE 的傳輸方案。整個協(xié)議基于 JSON-RPC 2.0 的消息格式請求、響應(yīng)、通知三類消息構(gòu)成了全部的通信語義。1.2 四類核心角色與三類原語MCP 協(xié)議里有幾個概念需要在一開始就建立正確印象否則后面寫工具時會混亂。第一是 Host。Host 是用戶直接面對的應(yīng)用程序比如某個 AI 桌面客戶端、某款支持 MCP 的 IDE 插件它負責(zé)創(chuàng)建會話、管理多個 Client 連接并決定哪些工具可以被模型看到。第二是 Client。Client 是 Host 內(nèi)部與 Server 建立一對一定向連接的組件負責(zé)協(xié)議握手和消息轉(zhuǎn)發(fā)。第三是 Server。Server 是工具提供方它暴露能力給 Client本身不關(guān)心最終用戶是誰。第四是 Tool 本身。Tool 是真正可執(zhí)行的函數(shù)單元有名字、有描述、有參數(shù)聲明、有返回值。在 Server 側(cè)除了 Tool 之外還有兩個容易混淆的原語Resource 和 Prompt。Tool 是“可以執(zhí)行的動作”比如發(fā)一封郵件、查詢訂單狀態(tài)、寫一條數(shù)據(jù)庫記錄Resource 是“可以讀取的數(shù)據(jù)”比如一個 CSV 文件、一份配置、一張圖片Prompt 是“可復(fù)用的會話模板”它可以封裝一套精心設(shè)計的提示詞讓客戶端在特定場景下直接套用。三者的定位差異用一句話概括Tool 負責(zé)“操作”Resource 負責(zé)“供給”Prompt 負責(zé)“引導(dǎo)”。我在實際項目中踩過的一個典型誤區(qū)就是把所有能力都塞進 Tool。比如想知道某個文件是否存在本來應(yīng)該用 Resource 暴露文件系統(tǒng)結(jié)果有人寫了一個 check_file_exists 工具。乍一看沒毛病但模型在面對開放場景時會很別扭你沒法在對話中直接“引用”一個還沒有被加載的資源模型只能靠猜。正確做法是能被檢索和引用的靜態(tài)數(shù)據(jù)用 Resource需要參數(shù)化執(zhí)行的動作才用 Tool。這個區(qū)分越清晰模型的行為就越穩(wěn)定。1.3 一次完整調(diào)用的生命周期理解了角色之后有必要把一次標準工具調(diào)用的完整流程走一遍。整個過程看起來簡單但每一步都有協(xié)議約束。第一步Client 與 Server 建立傳輸通道。本地場景通常是啟動子進程并打開 stdin/stdout 管道遠程場景則是建立 HTTP 連接。第二步Client 發(fā)送 initialize 請求攜帶協(xié)議版本、客戶端能力說明和 Client 信息Server 返回協(xié)議版本、服務(wù)端能力說明和 Server 信息。這里最關(guān)鍵的字段是 capabilities它表明雙方各自支持哪些特性后續(xù)所有消息都必須在這個能力范圍內(nèi)活動。第三步雙方發(fā)送 initialized 通知表示初始化完成。第四步Client 發(fā)送 notifications/initialized 之后開始發(fā)送 tools/list 請求Server 返回當前可用的工具列表每個工具都附帶一份 JSON Schema 格式的參數(shù)聲明。第五步用戶或模型決定調(diào)用某個工具Client 發(fā)送 tools/call 請求Server 執(zhí)行工具并返回結(jié)構(gòu)化結(jié)果。整個調(diào)用鏈路與我平時調(diào)試 REST 接口最大的不同在于MCP 是雙通道的。Client 和 Server 在初始化時可以協(xié)商出多種能力比如采樣能力、根目錄能力、日志能力。這些能力的加入使得 Server 不再只是一個“被調(diào)用方”它甚至可以在特定條件下向 Client 發(fā)起請求比如請求模型幫忙補全一段文本。這種雙向交互設(shè)計是傳統(tǒng) API 協(xié)議里少見的也是 MCP 能支持復(fù)雜 Agent 場景的重要原因。2. 開發(fā)前的準備協(xié)議細節(jié)與工程選型2.1 官方 SDK 怎么選不同類型的團隊、不同語言棧選擇 SDK 的策略完全不一樣。目前官方維護的 SDK 覆蓋 Python、TypeScript、Java、Kotlin、C#、Go 等主流語言。如果團隊本身就是做 AI 應(yīng)用大概率已經(jīng)有了 Python 或 TypeScript 的基礎(chǔ)設(shè)施選官方 SDK 是最穩(wěn)妥的路徑。Python 生態(tài)里我默認推薦一個上層封裝FastMCP。它是基于官方低層 MCP SDK 構(gòu)建的高層框架把協(xié)議細節(jié)封裝成了裝飾器風(fēng)格。你用 FastMCP 定義一個工具本質(zhì)上就是寫一個普通函數(shù)加一個 mcp.tool() 裝飾器再配上類型注解和 docstring參數(shù)的 JSON Schema 會自動生成。對于大部分業(yè)務(wù)工具來說這種開發(fā)體驗幾乎是最快的也很適合快速驗證想法。TypeScript 生態(tài)也有對應(yīng)的 FastMCP 移植版用法與 Python 版本保持了一致的裝飾器風(fēng)格。如果你的團隊前端技術(shù)棧更成熟選 TypeScript 版本完全沒問題。而對于那些需要深度定制協(xié)議行為的場景比如自定義傳輸層、實現(xiàn)復(fù)雜鑒權(quán)、處理流式輸出建議直接用低層 SDK不要用高層封裝。FastMCP 做 90% 的標準場景都很舒服但遇到極端定制需求時它替你封裝掉的那部分反而會變成你不容易繞開的墻。2.2 工具聲明的關(guān)鍵點inputSchema 決定一切寫過一段時間 MCP 工具的人都會認同一個結(jié)論工具能不能被模型正確使用七成功勞取決于 inputSchema 寫得好不好。inputSchema 就是工具的 JSON Schema 參數(shù)聲明模型會閱讀這個聲明來決定調(diào)用哪個工具、傳入什么參數(shù)、參數(shù)取什么值。這里有一個初學(xué)者最常見的失誤把參數(shù)描述寫得過于簡單。比如一個發(fā)送通知的工具content 參數(shù)的描述只寫“內(nèi)容”兩個字。模型面對這個參數(shù)時不知道應(yīng)該傳明文還是 Markdown、要不要帶標題、有沒有長度限制。它只能根據(jù)猜測生成參數(shù)結(jié)果必然不穩(wěn)定。正確做法是給每個參數(shù)寫清楚取值范圍、默認行為、單位或格式、和其他參數(shù)的關(guān)系。我見過最有效的描述方式是“像給一個人解釋怎么操作一樣寫參數(shù)說明”。模型不是人但它閱讀說明的能力非常強說明越精確行為越可靠。除此之外必填與可選字段的聲明也值得專門檢查。FastMCP 會根據(jù)函數(shù)簽名推斷沒有默認值的參數(shù)視為必填有默認值的視為可選。這個推斷在大部分場景下是對的但如果你用 dict 或?qū)ο箢愋妥鳛閰?shù)就必須在描述里進一步說明內(nèi)部結(jié)構(gòu)。否則模型看到的只是一個類型為 object 的參數(shù)內(nèi)部字段完全靠猜幾乎必然出錯。第一版工具寫完之后強烈建議把生成的 inputSchema 實際打印出來看一遍。很多開發(fā)者在 FastMCP 里定義完函數(shù)就直接聯(lián)調(diào)結(jié)果模型亂傳參數(shù)他們完全不知道問題出在參數(shù)描述上。只要打開 schema 看一眼很多問題當場就能定位。2.3 服務(wù)端生命周期與安全邊界MCP Server 的生命周期可以拆成三個階段啟動、工作、退出。啟動階段負責(zé)初始化資源和連接工作階段持續(xù)處理 tools/list、tools/call 等請求退出階段負責(zé)清理資源、保存狀態(tài)、關(guān)閉連接。FastMCP 提供了生命周期鉤子比如 lifespan 上下文管理器可以用來加載模型、初始化數(shù)據(jù)庫連接池、創(chuàng)建全局緩存。我建議從一開始就規(guī)劃好這些鉤子不要等 Server 跑起來了再往函數(shù)里硬塞全局變量。安全邊界是工具開發(fā)中最容易被忽略的一環(huán)。必須清醒地認識到一個 MCP 工具對 AI 客戶端而言就是一段“可被模型任意調(diào)用的代碼”。模型會基于用戶的輸入決定是否調(diào)用你的工具工具內(nèi)部如果存在可以刪除文件、執(zhí)行命令、修改數(shù)據(jù)的操作就必須加防護。我的個人習(xí)慣是三層防護。第一層參數(shù)校驗一定要嚴格所有外部輸入都按“不可信數(shù)據(jù)”對待類型不對直接拒絕取值范圍超限直接拋錯。第二層危險操作必須二次確認比如刪除操作寧可讓工具返回“確認刪除請再調(diào)一次”也不要讓模型一步到位。第三層本地 Server 不要隨意監(jiān)聽公網(wǎng)地址。stdio 模式天然安全因為只有啟動它的父進程能訪問管道但 HTTP 傳輸模式一旦監(jiān)聽在 0.0.0.0 上就等于把工具能力暴露給了整個網(wǎng)絡(luò)這是絕對不能接受的。凡是遠程部署的 MCP Server都必須在傳輸層加鑒權(quán)至少做到 token 校驗和來源 IP 白名單。3. 實操從零寫一個備忘錄 MCP Server3.1 環(huán)境準備與工程初始化為了讓整個分享更有“可抄作業(yè)”的價值我把這次要實現(xiàn)的 Demo 定義成一個備忘錄管理服務(wù)。它提供四個工具新增備忘錄、列出備忘錄、按關(guān)鍵詞搜索、刪除指定備忘錄。功能不復(fù)雜但覆蓋了寫 MCP 工具會遇到的大部分典型問題參數(shù)校驗、讀寫本地文件、結(jié)構(gòu)化返回、錯誤處理。第一步是準備環(huán)境。推薦直接用 uv 管理項目依賴這一步能省很多時間。新建一個 notes_server 目錄初始化虛擬環(huán)境然后安裝 mcp 庫。命令如下mkdir notes_server cd notes_server uv init --python 3.11 uv add mcp[cli]安裝完 mcp 之后FastMCP 已經(jīng)內(nèi)置在里面了。驗證一下 Python 能正常導(dǎo)入uv run python -c from mcp.server.fastmcp import FastMCP; print(FastMCP)如果能打印出類信息說明環(huán)境沒問題。接下來我們直接寫代碼。3.2 動手實現(xiàn)四個核心工具在 notes_server.py 中完成全部實現(xiàn)。代碼本身不長但我把注釋寫得密一點方便你理解每個字段的用途。import json import uuid from datetime import datetime from pathlib import Path from typing import Optional from mcp.server.fastmcp import FastMCP # 創(chuàng)建 FastMCP 實例。name 是服務(wù)標識instructions 會注入給模型參考。 mcp FastMCP( notes-server, instructions這是一個備忘錄管理服務(wù)。支持新增、列出、搜索、刪除備忘錄。備忘錄存儲在本地 JSON 文件。, ) DATA_FILE Path(__file__).parent / notes.json def _load_notes() - list[dict]: if not DATA_FILE.exists(): return [] try: return json.loads(DATA_FILE.read_text(encodingutf-8)) except json.JSONDecodeError: return [] def _save_notes(notes: list[dict]) - None: DATA_FILE.write_text( json.dumps(notes, ensure_asciiFalse, indent2), encodingutf-8, ) mcp.tool() def add_note( title: str, content: str , tags: Optional[list[str]] None, ) - str: 新增一條備忘錄。 參數(shù)說明 - title備忘錄標題必填建議控制在 50 字以內(nèi)。 - content備忘錄正文可空最長不要超過 5000 字。 - tags標簽列表可空每個標簽建議不超過 10 個字。 if not title.strip(): raise ValueError(title 不能為空) notes _load_notes() note { id: str(uuid.uuid4()), title: title.strip(), content: content.strip(), tags: [t.strip() for t in (tags or []) if t.strip()], created_at: datetime.now().isoformat(timespecseconds), } notes.append(note) _save_notes(notes) return json.dumps({ok: True, id: note[id]}, ensure_asciiFalse) mcp.tool() def list_notes(keyword: Optional[str] None) - list[dict]: 列出備忘錄。 keyword 可選。為空時返回全部不為空時按標題和正文做包含匹配。 notes _load_notes() if keyword: kw keyword.strip().lower() notes [ n for n in notes if kw in n[title].lower() or kw in n[content].lower() ] # 按創(chuàng)建時間倒序返回 notes.sort(keylambda n: n[created_at], reverseTrue) return notes mcp.tool() def search_notes(keyword: str) - list[dict]: 搜索備忘錄返回標題或正文包含關(guān)鍵詞的所有條目。 與 list_notes 的區(qū)別本工具明確要求提供 keyword用于語義上更聚焦的搜索場景。 if not keyword.strip(): raise ValueError(keyword 不能為空) return list_notes(keyword) mcp.tool() def delete_note(note_id: str) - str: 按 ID 刪除一條備忘錄。刪除操作不可恢復(fù)請謹慎調(diào)用。 notes _load_notes() before len(notes) notes [n for n in notes if n[id] ! note_id] if len(notes) before: raise ValueError(fnote_id{note_id} 不存在) _save_notes(notes) return json.dumps({ok: True, deleted_id: note_id}, ensure_asciiFalse) if __name__ __main__: mcp.run()有幾個設(shè)計細節(jié)值得展開說明一下。第一所有返回結(jié)果都做了結(jié)構(gòu)化處理單值操作用 JSON 字符串返回查詢操作用數(shù)組返回。這樣模型拿到結(jié)果后可以順利解析而不是面對一段來源不明的文本。第二刪除操作在 docstring 里明確標注了“不可恢復(fù)”和“謹慎調(diào)用”就是在給模型一個行為上的提醒讓它更謹慎地執(zhí)行高影響操作。第三參數(shù)校驗放在了工具函數(shù)的最前面一旦觸發(fā)非法入?yún)⒕蛼伋?ValueErrorFastMCP 會把異常轉(zhuǎn)成協(xié)議層面的錯誤碼返回給客戶端模型就能感知到調(diào)用失敗的原因。3.3 用 MCP Inspector 做本地聯(lián)調(diào)代碼寫完不代表能用第一步驗證應(yīng)該用官方聯(lián)調(diào)工具。先啟動服務(wù)uv run python notes_server.py正常情況下進程會保持運行不會打印任何內(nèi)容等待客戶端通過 stdio 管道連接。然后另開一個終端使用 MCP Inspector 連接uv run npx modelcontextprotocol/inspector python notes_server.py這里有一個值得強調(diào)的細節(jié)Inspector 是個可視化的調(diào)試面板它允許你查看 Server 的工具列表、逐個調(diào)用工具、查看返回結(jié)果。我習(xí)慣先看 Tools 列表頁確認四個工具都在。然后手動調(diào)用 add_note傳一個標題和一個標簽看返回結(jié)果是不是包含 ok 和 id。接著調(diào) list_notes看是否能查回到剛才新增的數(shù)據(jù)。最后調(diào) delete_note把剛才那條數(shù)據(jù)刪掉。整個過程走一遍才敢說工具本身沒有邏輯問題。如果在 Inspector 里發(fā)現(xiàn)工具列表為空優(yōu)先檢查函數(shù)裝飾器是否寫成了 mcp.tools()而不是 mcp.tool()。這類問題很隱蔽因為語法不報錯只是函數(shù)沒有被注冊。3.4 用最小客戶端腳本驗證協(xié)議級調(diào)用Inspector 適合人工聯(lián)調(diào)但如果你要在 CI 環(huán)境里做自動化驗證就得寫一個最小客戶端腳本。這個腳本不涉及任何 AI 客戶端它直接扮演一個 MCP Client發(fā)起協(xié)議請求并打印結(jié)果。import asyncio from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client async def main(): server_params StdioServerParameters( commandpython, args[notes_server.py], ) async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools await session.list_tools() print(Tools:, [t.name for t in tools.tools]) result await session.call_tool(add_note, { title: 協(xié)議測試, content: 通過最小客戶端寫入, tags: [test], }) print(Add result:, result.content[0].text) search_res await session.call_tool(list_notes, {keyword: 協(xié)議測試}) print(Search result:, search_res.content[0].text) if __name__ __main__: asyncio.run(main())這個腳本把協(xié)議層的關(guān)鍵步驟都覆蓋了initialize 握手、list_tools、call_tool。跑一次之后你對 MCP 協(xié)議的理解會從“看過文檔”變成“親手用過”。到這里一個完整的本地 MCP Server 已經(jīng)落地。接下來可以把它接入任何支持 MCP 的 AI 客戶端。不同客戶端的配置方式略有區(qū)別但本質(zhì)都是在一份 JSON 配置里注冊 Server 名、啟動命令和參數(shù)例如{ mcpServers: { notes-server: { command: python, args: [/絕對路徑/notes_server.py], env: {} } } }配置完成后重啟客戶端在對話中要求模型“幫我記一條明天的待辦”或者“查一下和協(xié)議測試相關(guān)的備忘錄”模型就會自動選擇并調(diào)用對應(yīng)的 MCP 工具。4. 實戰(zhàn)中的坑問題清單與排查方法4.1 連接與啟動類問題這一類問題通常在 Server 還沒正式開始處理業(yè)務(wù)之前就爆炸了。最常見的現(xiàn)象是客戶端提示 MCP Server 連接失敗或者工具列表一直加載不出來。第一類原因路徑錯誤??蛻舳伺渲美锾畹?command 或 args 如果帶了相對路徑工作目錄一變就找不到文件了。我的習(xí)慣是在配置里使用絕對路徑。另一個高頻問題是用 uv 虛擬環(huán)境創(chuàng)建的 Python 依賴直接用系統(tǒng) python 命令啟動時找不到 mcp 模塊。這種情況下command 應(yīng)該直接指向虛擬環(huán)境里的 python 可執(zhí)行文件或者用 uv run python 作為啟動命令而不是裸寫 python。前者把環(huán)境綁定死后者依賴 uv 能夠在目標目錄找到項目配置兩者都可行但二選一不要混。第二類原因stdout 被污染。stdio 傳輸模式下Server 的所有標準輸出都會被當成協(xié)議數(shù)據(jù)幀任何 print 語句都會破壞數(shù)據(jù)流。很多人在調(diào)試時喜歡在 Server 代碼里隨手 print 一行日志結(jié)果客戶端直接卡死或報解析錯誤。正確做法是協(xié)議運行期間不要向 stdout 輸出任何非協(xié)議內(nèi)容。日志一律走 stderrFastMCP 也支持配置日志級別或者把日志直接寫入文件。第三類原因依賴版本不匹配。mcp 庫版本迭代很快不同版本之間的 API 變動不小。如果你照著某篇文章的代碼寫但裝的 SDK 版本不同很容易出現(xiàn)屬性不存在或者函數(shù)簽名不一致的問題。排查時先鎖定版本uv.lock 或 requirements.txt 里固定住 mcp 的版本再按照對應(yīng)版本文檔調(diào)試。4.2 工具識別與參數(shù)類問題服務(wù)連上了工具列表也能看到但模型調(diào)用時頻繁報參數(shù)錯誤或者干脆不調(diào)用你的工具。這種問題的根源基本集中在 inputSchema 的質(zhì)量上。最容易出現(xiàn)的是參數(shù)類型推斷錯誤。比如函數(shù)簽名里寫了 tags: list[str] NoneFastMCP 可能按順序推斷 tags 為必需參數(shù)雖然你本意是可選。解決方法是把可選參數(shù)寫成 Optional[list[str]] None讓類型注解和默認值完全一致。再比如參數(shù)用了自定義類型或 dataclass生成的 Schema 可能只是一個普通 object內(nèi)部結(jié)構(gòu)模型完全看不到。遇到這種情況要么把復(fù)雜類型拆成多個基礎(chǔ)類型參數(shù)要么在 docstring 里對結(jié)構(gòu)做極為具體的描述。還有一類經(jīng)常被忽略的問題參數(shù)描述里的字段名和實際函數(shù)簽名不一致。如果 docstring 里描述了某個字段但簽名里沒有這個參數(shù)模型可能會試圖傳一個不存在的字段結(jié)果被參數(shù)校驗拒絕。寫工具時docstring 必須和簽名保持嚴格同步不要出現(xiàn)“文檔里有、簽名里沒有”的情況。4.3 返回值與模型表現(xiàn)類問題工具調(diào)用成功了但模型給用戶的回答仍然不準確甚至出現(xiàn)幻讀這類問題是最難排查的因為錯誤不在協(xié)議層而在“工具返回的結(jié)果是否被模型正確理解”。一個很常見的表現(xiàn)是工具返回了一堆 JSON模型卻只摘取其中一部分或者直接告訴用戶“查詢失敗”。原因多半是返回結(jié)果太隨意。比如直接返回一個字典對象而沒有清晰的字段說明或者返回純文本時沒有顯式標注字段含義。協(xié)議要求工具調(diào)用結(jié)果必須是一個 content 數(shù)組里面每個元素是一個結(jié)構(gòu)化 block。FastMCP 會自動幫你包裝但你在定義返回值時仍然要想清楚“模型能從這個結(jié)果里讀出什么”。我自己的一個改進方法是讓所有工具返回結(jié)果都帶上冗余信息。比如新增備忘錄返回的 JSON 里除了 ok 和 id再帶上標題和創(chuàng)建時間。模型在向用戶匯報時可以直接引用標題和創(chuàng)建時間而不需要再猜測。類似地刪除操作返回 deleted_id 的同時可以把被刪條目的標題也帶回來。多傳這幾個字段模型的表現(xiàn)會明顯穩(wěn)定。還有一類問題是內(nèi)容過長。如果工具返回的列表有幾百條記錄模型處理大量文本時容易丟失前面的信息。此時應(yīng)該做分頁或限制返回條數(shù)。工具不是數(shù)據(jù)庫接口沒有必要把全量數(shù)據(jù)一股腦返回給模型。加一個 limit 參數(shù)默認返回前 20 條多出來的情況返回總量提示讓模型決定是否要擴大范圍是更聰明的設(shè)計。4.4 一個順手的問題速查表現(xiàn)象可能原因排查方法解決方案連接失敗路徑錯誤、虛擬環(huán)境不對檢查客戶端配置里的 command 和 args使用絕對路徑指向虛擬環(huán)境 python工具列表為空裝飾器寫錯、函數(shù)未注冊在 Inspector 中查看服務(wù)端工具日志檢查 mcp.tool() 裝飾器與函數(shù)簽名stdout 被污染Server 里誤用 print觀察終端是否有非協(xié)議輸出日志統(tǒng)一走 stderr 或文件參數(shù)缺失可選參數(shù)被推斷為必填打印生成的 inputSchema用 Optional[...] 默認值聲明參數(shù)模型不按說明調(diào)用參數(shù)描述不精確閱讀生成后的 Schema重寫 docstring描述取值范圍與格式返回結(jié)果不完整返回結(jié)構(gòu)太簡單查看模型實際接收到的 content增加冗余字段提供可直接引用的信息4.5 安全加固與遠程部署的注意點本地 Demo 做完之后很多人會順理成章地想把它做成一個遠程服務(wù)給團隊使用。這一步非常容易踩坑因為遠程 MCP Server 和本地 stdio Server 的安全模型完全不一樣。本地 stdio 模式默認只允許啟動自己的父進程通信外部網(wǎng)絡(luò)無法訪問威脅面很小。但一旦通過 HTTP 傳輸暴露到網(wǎng)絡(luò)任何能訪問到這個端口的人都可能調(diào)用你的工具。舉一個最簡單的例子如果把 notes_server 包裝成 HTTP 模式并監(jiān)聽 0.0.0.0:8000那么攻擊者只需要發(fā)送一個格式化好的 JSON-RPC 請求就能刪除所有備忘錄——如果你的工具里有 delete 這類危險操作后果可想而知。所以我給出的遠程部署建議是第一不要直接復(fù)用本地工具的代碼來監(jiān)聽公網(wǎng)必須加一層網(wǎng)關(guān)做鑒權(quán)可以使用簡單的預(yù)共享 token也可以做 OAuth但絕不能裸奔第二把高危操作設(shè)計成“可停用”的模式遠程環(huán)境下默認禁用 delete、update 這類寫操作第三所有遠程工具的操作日志必須留痕記錄每次調(diào)用的來源、時間、參數(shù)和結(jié)果方便事后審計。工具開發(fā)本身不難難的是讓它在一個不安全的環(huán)境里保持安全。最后再分享一點個人體會備忘錄這個 Demo 是我反復(fù)用來講解 MCP 的“最小完整案例”因為它真實覆蓋了工具注冊、參數(shù)校驗、數(shù)據(jù)讀寫、錯誤處理、客戶端驗證這條完整鏈路。做完這一遍之后你對協(xié)議的掌握程度會遠遠超過只看文檔的效果。我個人在實際操作中最大的體會是工具開發(fā)真正花時間的不是寫函數(shù)邏輯而是“讓模型理解你的工具”。很多人的工具邏輯很簡單但參數(shù)描述寫得敷衍結(jié)果模型要么不調(diào)用要么調(diào)用得亂七八糟。從第一次寫工具開始就有意識地訓(xùn)練自己把每個參數(shù)的語義、邊界、格式寫清楚。這種習(xí)慣比掌握任何框架技巧都更能提升最終交付質(zhì)量。如果你打算繼續(xù)深入我建議下一步可以試著把同一個 Server 改造成遠程 HTTP 模式并加一層簡單的 token 鑒權(quán)去感受一下傳輸方式切換對客戶端配置和協(xié)議行為帶來的差異。也可以嘗試給服務(wù)加上 Resource把某個數(shù)據(jù)文件暴露給模型讀取體會 Tool 和 Resource 分工的不同。走完這兩步MCP 工具開發(fā)對你來說就不再是“照著模板抄”而是真的能按業(yè)務(wù)需求自由設(shè)計了。