:(二)圖書館座位查詢與預(yù)約MCP-Server)
1. 從零跑通圖書館座位 MCP-ServerFastMCP 到底能做什么FastMCP 是一個基于 MCP 協(xié)議構(gòu)建的快速開發(fā)框架它把網(wǎng)絡(luò)通信、并發(fā)控制、工具注冊這些底層細節(jié)都封裝好了你只需要專注寫業(yè)務(wù)邏輯。MCP 全稱 Model Calling Protocol是一種模型與工具之間通信的標準協(xié)議允許模型根據(jù)任務(wù)需求動態(tài)調(diào)用外部工具比如 API、數(shù)據(jù)庫等從而增強模型能力。這篇要做的是一個圖書館座位查詢與預(yù)約的 MCP-ServerAI 客戶端可以通過它查詢某棟樓某天某個時間段還有哪些空座位也能直接發(fā)起預(yù)約、查看自己的預(yù)約記錄。適合誰看如果你已經(jīng)寫過一點 Python想把自己的業(yè)務(wù)系統(tǒng)圖書館、會議室、工位、充電樁都行包裝成 AI 能調(diào)用的工具這篇就是可跟做的模板。我會給出完整的server.py骨架、config.toml配置片段以及用 MCP 客戶端發(fā)起查詢和預(yù)約請求的驗證動作。整個流程跑下來你會得到一個監(jiān)聽 8082 端口、支持 SSE 傳輸?shù)目捎梅?wù)。需要提前說明的是本文的后端 API 地址用的是示例域名你在本地調(diào)試時可以把它換成自己的 mock 服務(wù)或者真實接口。重點在于 FastMCP 這一層的工具定義、參數(shù)校驗和調(diào)試方法這部分是通用的。2. TaoToken 前置準備把模型調(diào)用和 MCP 調(diào)試串起來MCP-Server 寫完之后你需要一個能調(diào)用模型的客戶端來驗證工具是否被正確識別和觸發(fā)。這里我用 TaoToken 來做模型側(cè)的接入它的 API 地址是https://taotoken.net/api兼容常見的調(diào)用方式配置起來比較直接。第一步去控制臺創(chuàng)建一個 API Key。打開https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_library_seat在 API Keys 頁面新建一個密鑰復(fù)制保存好。這個 Key 后面會用在客戶端的配置里。第二步如果你打算長期做編碼類或 Agent 類的開發(fā)可以了解一下 Coding Plan地址是https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_library_seat。它適合需要反復(fù)調(diào)試 MCP 工具、頻繁發(fā)起模型請求的場景比單次調(diào)用更省心。第三步驗證模型對話是否正常。打開https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_library_seat在對話框里發(fā)一句「你好幫我列一下你能調(diào)用的工具」確認模型能正常響應(yīng)。這一步是為了排除 Key 或網(wǎng)絡(luò)配置的問題等 MCP-Server 起來之后再回來測工具調(diào)用。接入文檔在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_library_seat里面有不同語言的調(diào)用示例遇到參數(shù)格式問題可以對照查。3. 可復(fù)制配置server.py 骨架與 config.toml先裝依賴。FastMCP 的包名是mcp里面自帶fastmcp模塊pip install mcp requests然后創(chuàng)建server.py。下面這份代碼可以直接復(fù)制運行我加了參數(shù)校驗和錯誤處理比裸調(diào)用更穩(wěn)import json import re import requests from datetime import datetime from typing import Optional from mcp.server.fastmcp import FastMCP mcp FastMCP( LibrarySeatService, descriptionf圖書館座位服務(wù)啟動時間{datetime.now().strftime(%Y-%m-%d %H:%M)}, port8082, ) API_BASE http://127.0.0.1:9000/seat DATE_PATTERN re.compile(r^\d{4}-\d{2}-\d{2}$) TIME_RANGE_PATTERN re.compile(r^\d{2}:\d{2}~\d{2}:\d{2}$) def validate_date(date: str) - Optional[str]: if not DATE_PATTERN.match(date): return 日期格式錯誤應(yīng)為 YYYY-MM-DD return None def validate_time_range(time_range: str) - Optional[str]: if not TIME_RANGE_PATTERN.match(time_range): return 時間段格式錯誤應(yīng)為 HH:mm~HH:mm return None def format_response(response, success_keyNone): if response.status_code 200: data response.json() if success_key and success_key in data: return json.dumps({status: success, data: data}, ensure_asciiFalse) if isinstance(data, (list, dict)): return json.dumps({status: success, data: data}, ensure_asciiFalse) return json.dumps({status: error, message: 未找到有效數(shù)據(jù)}, ensure_asciiFalse) return json.dumps( {status: error, code: response.status_code, message: response.text}, ensure_asciiFalse, ) mcp.tool() async def query_available_seats( building: str, date: str, time_range: str, floor: Optional[str] None, seat_type: Optional[str] None, token: Optional[str] None, ) - str: 查詢可用座位信息 err validate_date(date) or validate_time_range(time_range) if err: return json.dumps({status: error, message: err}, ensure_asciiFalse) params { building: building, date: date, timeRange: time_range, floor: floor, seatType: seat_type, } headers {Authorization: fBearer {token}, Content-Type: application/json} response requests.get(f{API_BASE}/available, headersheaders, paramsparams, timeout10) return format_response(response) mcp.tool() async def book_seat( seat_id: str, date: str, time_range: str, student_name: str, contact: str, token: Optional[str] None, ) - str: 預(yù)約指定座位 err validate_date(date) or validate_time_range(time_range) if err: return json.dumps({status: error, message: err}, ensure_asciiFalse) data { seatId: seat_id, date: date, timeRange: time_range, studentName: student_name, contact: contact, } headers {Authorization: fBearer {token}, Content-Type: application/json} response requests.post(f{API_BASE}/book, headersheaders, jsondata, timeout10) return format_response(response, success_keybookingId) mcp.tool() async def get_my_bookings(token: Optional[str] None) - str: 獲取用戶當前預(yù)約記錄 headers {Authorization: fBearer {token}, Content-Type: application/json} response requests.get(f{API_BASE}/my-bookings, headersheaders, timeout10) return format_response(response) if __name__ __main__: mcp.run(transportsse)幾個關(guān)鍵點解釋一下。FastMCP初始化時傳了port8082默認監(jiān)聽0.0.0.0本地調(diào)試沒問題生產(chǎn)環(huán)境記得加鑒權(quán)。mcp.tool()裝飾器把普通異步函數(shù)注冊成 MCP 工具函數(shù)簽名里的類型注解和 docstring 會被客戶端讀取用來判斷什么時候調(diào)用這個工具。所以 docstring 要寫清楚用途參數(shù)名要語義化。參數(shù)校驗我單獨抽了兩個函數(shù)用正則檢查日期和時間段格式。這一步很重要因為模型生成的參數(shù)不一定規(guī)范提前攔截能避免把臟數(shù)據(jù)打到后端。config.toml是給客戶端用的配置片段放在客戶端能讀到的位置[mcp_servers.library_seat] url http://127.0.0.1:8082/sse transport sse description 圖書館座位查詢與預(yù)約服務(wù)如果你的客戶端支持 stdio 傳輸也可以改成command python、args [server.py]的形式但 SSE 更適合本地調(diào)試因為服務(wù)獨立運行日志看得清楚。4. 驗證請求從啟動服務(wù)到成功預(yù)約先啟動服務(wù)python server.py看到類似Uvicorn running on http://0.0.0.0:8082的輸出就說明起來了。此時打開瀏覽器訪問http://127.0.0.1:8082/sse應(yīng)該能看到 SSE 流保持連接。接下來用 MCP 客戶端驗證。這里我用一個簡單的 Python 客戶端腳本模擬模型發(fā)起工具調(diào)用import asyncio from mcp import ClientSession from mcp.client.sse import sse_client async def main(): async with sse_client(http://127.0.0.1:8082/sse) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools await session.list_tools() print(可用工具, [t.name for t in tools.tools]) result await session.call_tool( query_available_seats, { building: A棟, date: 2025-06-15, time_range: 14:00~16:00, floor: 3樓, }, ) print(查詢結(jié)果, result.content[0].text) asyncio.run(main())運行后你應(yīng)該能看到三個工具名以及查詢返回的 JSON。如果后端 mock 服務(wù)返回了座位列表輸出里會有status: success和座位數(shù)據(jù)。預(yù)約的驗證類似把call_tool換成book_seat傳入seat_id、student_name、contact等參數(shù)。成功時返回里會帶bookingId。我實測下來最容易出問題的是時間格式模型有時會生成14:00-16:00這種用短橫線的寫法被校驗函數(shù)攔下來這時候在客戶端提示里明確格式要求就能解決。如果你想在 TaoToken 的模型對話里直接測打開https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_library_seat把 MCP-Server 配置進去然后發(fā)一句「幫我查一下 A 棟 6 月 15 號下午兩點到四點三樓的空座位」觀察模型是否正確調(diào)用了query_available_seats。5. 本篇常見錯排查報錯一ModuleNotFoundError: No module named mcp說明依賴沒裝對。注意包名是mcp而不是fastmcp裝完用pip show mcp確認版本。如果同時裝了舊的fastmcp包可能會沖突建議先卸載再裝。報錯二Address already in use8082 端口被占了。用lsof -i :8082找到進程殺掉或者改FastMCP初始化時的port參數(shù)。改端口后記得同步改config.toml里的 URL。報錯三工具列表為空客戶端連上了但list_tools返回空。檢查mcp.tool()裝飾器是否加在函數(shù)上以及函數(shù)是否是async def。FastMCP 對同步函數(shù)也支持但異步函數(shù)在高并發(fā)下表現(xiàn)更好。另外確認mcp.run()在if __name__ __main__:里被調(diào)用。報錯四調(diào)用工具返回status: error且 message 是連接錯誤后端 API 地址不通。API_BASE我寫的是127.0.0.1:9000你需要有一個真實或 mock 的服務(wù)在跑??梢杂胮ython -m http.server 9000先起個靜態(tài)服務(wù)測連通性或者用 FastAPI 寫個最簡單的 mock。報錯五日期校驗一直失敗檢查傳入的日期是不是YYYY-MM-DD格式。模型有時會輸出2025/06/15或6月15日這些都會被正則攔下。解決辦法是在工具 docstring 里明確寫「格式Y(jié)YYY-MM-DD」模型看到后生成規(guī)范格式的概率會高很多。報錯六SSE 連接建立后立即斷開通常是客戶端和服務(wù)端的傳輸協(xié)議不匹配。確認服務(wù)端mcp.run(transportsse)客戶端用sse_client。如果客戶端只支持 stdio就改用mcp.run(transportstdio)并調(diào)整配置。6. 下一步把座位服務(wù)接進你的編碼工作流到這里一個能查詢、能預(yù)約、能查記錄的圖書館座位 MCP-Server 就跑通了。你可以把API_BASE換成真實的圖書館接口把參數(shù)校驗按實際業(yè)務(wù)字段調(diào)整工具函數(shù)也可以繼續(xù)加比如取消預(yù)約、修改時間段、查詢歷史記錄。如果你打算把這個服務(wù)長期掛在本地配合編碼助手一起用建議把 API Key 和 MCP 配置統(tǒng)一管理。API Keys 在https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_library_seat創(chuàng)建和管理接入細節(jié)看https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_library_seat。需要長期跑 Agent 任務(wù)的話Coding Plan 在https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_library_seat按需選用。最后留一個實用技巧調(diào)試 MCP 工具時先把format_response里的原始response.text打印出來確認后端返回結(jié)構(gòu)再決定success_key怎么設(shè)。很多「工具調(diào)用失敗」其實是后端字段名和預(yù)期不一致看一眼原始響應(yīng)就能定位。