用 Python 寫一個 MCP Server
動手寫一個「讀書筆記」MCP Server,提供新增筆記與搜尋筆記兩個工具。先用 MCP Inspector 不經過 AI 測試它,再接到 Claude Code 和 Claude Desktop,讓你在對話裡隨口就能記筆記、找筆記。
上一堂課結尾,請你想一個「最想讓 AI 幫你存取的東西」。這堂課我們先一起做一個很生活化的例子:讀書筆記。
想像你在跟 Claude 聊一本書,聊到一半說:「幫我記下來:好習慣要讓它顯而易見。」Claude 就自動存起來。過了兩個月,你問:「我以前記過哪些跟習慣有關的筆記?」它就找給你。
做完之後,你會有一個任何支援 MCP 的 App 都能用的小工具。程式不到七十行,我們一段一段來。
先看整體長什麼樣
你寫的是中間那支程式。啟動它、和它對話,都是 Host 的工作。
還記得上一堂的 stdio 嗎?Host 會把你的 Server 當作子程式啟動,透過「標準輸入」送請求、從「標準輸出」讀回覆。這件事會影響我們寫程式的一個小習慣,等一下會提到。
建立專案
我們用 uv 管理這個專案。uv 是一個 Python 的專案與套件管理工具,可以幫你建立獨立的環境、安裝套件,MCP 官方教學也是用它。還沒安裝的話,在 Windows 的 PowerShell 執行:
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
macOS 或 Linux 則是:
curl -LsSf https://astral.sh/uv/install.sh | sh
裝好後重新開一個終端機,然後建立專案:
uv init reading-notes
cd reading-notes
uv add "mcp[cli]"
mcp 是 MCP 官方的 Python 套件,[cli] 會順便裝上開發用的指令工具。
完整程式
在 reading-notes 資料夾裡建立 notes_server.py:
import json
import logging
from datetime import date
from pathlib import Path
from mcp.server import MCPServer
# logging 預設寫到 stderr,不會干擾 MCP 訊息
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
mcp = MCPServer("reading-notes")
# 筆記檔放在這支程式旁邊,不管從哪裡啟動都找得到
NOTES_FILE = Path(__file__).parent / "notes.json"
def load_notes() -> list[dict]:
if not NOTES_FILE.exists():
return []
return json.loads(NOTES_FILE.read_text(encoding="utf-8"))
def save_notes(notes: list[dict]) -> None:
NOTES_FILE.write_text(
json.dumps(notes, ensure_ascii=False, indent=2), encoding="utf-8"
)
@mcp.tool()
def add_note(book: str, note: str) -> str:
"""新增一則讀書筆記。使用者說「幫我記下來」或分享讀書心得時使用。
Args:
book: 書名,不含書名號,例如:原子習慣
note: 筆記內容,保留使用者的原意,一到三句話
"""
notes = load_notes()
notes.append({
"book": book.strip(),
"note": note.strip(),
"date": date.today().isoformat(),
})
save_notes(notes)
logger.info("新增了一則《%s》的筆記", book)
return f"已記下《{book}》的筆記,目前共有 {len(notes)} 則。"
@mcp.tool()
def search_notes(keyword: str, limit: int = 5) -> str:
"""用關鍵字搜尋讀書筆記,會比對書名和筆記內容。
使用者想回顧某本書、某個主題,或問「我記過什麼」時使用。
回傳最新的幾則結果,每則包含書名、日期與內容;找不到時會明確說明。
Args:
keyword: 要找的字詞,例如:習慣,或一個書名
limit: 最多回傳幾則,預設 5,最多 20
"""
limit = max(1, min(limit, 20))
kw = keyword.strip().lower()
hits = [
n for n in load_notes()
if kw in n["book"].lower() or kw in n["note"].lower()
]
if not hits:
return f"找不到和「{keyword}」有關的筆記。"
newest_first = hits[::-1][:limit]
return "\n".join(
f"《{n['book']}》({n['date']}):{n['note']}" for n in newest_first
)
if __name__ == "__main__":
mcp.run(transport="stdio")
逐段拆解
MCPServer("reading-notes"):建立一個 Server,括號裡是它的名字。
@mcp.tool():這個裝飾器把下面的函式登記成一個工具。和上個單元比較一下,你不用再手寫工具說明書的字典了,套件會自動幫你產生:
| 你寫的 | 變成說明書裡的 |
|---|---|
函式名稱 add_note |
工具的 name |
| docstring(函式下方三個引號的說明) | 工具的 description |
型別提示 book: str、limit: int = 5 |
參數規格,有預設值的就是選填 |
Args: 底下每個參數的說明 |
每個參數的說明 |
所以「定義工具」那堂課學到的原則全部適用:docstring 就是說明書,要寫清楚做什麼、什麼時候用、參數怎麼給、會回傳什麼。這是整支程式最值得花時間的地方。
limit = max(1, min(limit, 20)):模型給的參數不能完全信任,上個單元的 safe_path 也是同樣的精神。這裡把數量限制在合理範圍,避免一次回傳幾千則塞爆上下文。
logger.info 而不是 print:這就是前面說的小習慣。
mcp.run(transport="stdio"):用 stdio 方式啟動,等待 Host 傳訊息過來。
開發的四個步驟
先排除程式的問題,再排除說明書的問題,卡住時比較好找原因。
用 MCP Inspector 測試
MCP Inspector 是官方的測試工具,會開一個網頁,讓你不經過 AI、直接按按鈕呼叫工具。它需要 Node.js(上一堂裝過了)。在 reading-notes 資料夾裡執行:
npx @modelcontextprotocol/inspector uv run notes_server.py
inspector 後面接的,就是「啟動你的 Server 的指令」。瀏覽器打開後:
- 確認畫面顯示已連線。
- 切到 Tools 分頁,列出工具,應該看到
add_note和search_notes。點開看看說明,是不是就是你寫的 docstring? - 點
add_note,填入書名和筆記,按執行,應該看到「已記下……」。 - 再用
search_notes搜尋剛剛的關鍵字,確認找得到。
資料夾裡也會多出一個 notes.json,打開就能看到你的筆記。
另一個選擇是 uv run mcp dev notes_server.py,也會幫你開啟 Inspector。哪個能用就用哪個。
接到 Claude Code
測試沒問題,就可以接上真正的 Host 了。在 reading-notes 資料夾裡執行(把路徑換成你的資料夾實際位置,Windows 也可以用 / 斜線):
claude mcp add --transport stdio reading-notes -- uv --directory C:/Users/你的名字/reading-notes run notes_server.py
上一堂學過,-- 後面是啟動 Server 的指令。--directory 告訴 uv 要到哪個資料夾執行,所以 Claude Code 不管在哪裡啟動,都找得到你的程式。
用 claude mcp list 確認已連線,然後打開 Claude Code 聊聊看:
我最近在讀《原子習慣》,幫我記下來:好習慣要讓它顯而易見,壞習慣要讓它隱而不現。
Claude 會詢問是否允許使用 add_note,同意後就記下來了。再問:
我記過哪些跟習慣有關的筆記?
這一步檢查的是說明書:模型有沒有自己判斷該用 search_notes?關鍵字給得對不對?如果它選錯工具或關鍵字很奇怪,回去改 docstring,往往比改程式有效。
接到 Claude Desktop
Claude Desktop 是透過設定檔 claude_desktop_config.json 加入 Server。在 Windows 可以用 VS Code 打開它:
code $env:AppData\Claude\claude_desktop_config.json
macOS 的位置是 ~/Library/Application Support/Claude/claude_desktop_config.json。檔案不存在就新建一個,內容是:
{
"mcpServers": {
"reading-notes": {
"command": "uv",
"args": [
"--directory",
"C:\\Users\\你的名字\\reading-notes",
"run",
"notes_server.py"
]
}
}
}
存檔後完全關閉 Claude Desktop 再重新打開。JSON 裡的 Windows 路徑要用兩個反斜線,或改用 /。如果顯示找不到 uv,把 command 換成 uv 的完整路徑,可以在終端機用 where uv(macOS 用 which uv)查到。
這就是 MCP 的威力:同一支程式,兩個 App 都能用,你一行都沒改。
你可能會卡在這裡
- Inspector 連不上:先單獨執行
uv run notes_server.py,看有沒有錯誤訊息。沒有錯誤時它會安靜地等待,這是正常的,按 Ctrl+C 結束。 - Claude Code 顯示連線失敗:最常見的是
--directory的路徑打錯。用claude mcp remove reading-notes刪掉後重新加。 - 改了程式卻沒生效:Host 用的還是先前啟動的 Server。在 Claude Code 裡重開一個對話,或重新啟動 Claude Desktop。
- 中文變成亂碼:確認讀寫檔案都有加
encoding="utf-8",json.dumps有加ensure_ascii=False。
這是我用 Python mcp 套件寫的讀書筆記 MCP Server(附上 notes_server.py)。請幫我新增一個 list_books 工具,列出所有書名和每本書的筆記數量。docstring 要說清楚用途、使用時機與回傳格式,並沿用現有的 load_notes。不要使用 print。
- 照著步驟建立
reading-notes專案,用 Inspector 分別呼叫兩個工具,確認notes.json有被寫入。 - 把 Server 接到 Claude Code,用聊天的方式新增三則不同書的筆記,再請它搜尋其中一個主題。
- 故意把
search_notes的 docstring 改成只有「搜尋」兩個字,重新連線後問同樣的問題,觀察模型的行為有沒有變。記得改回來。 - 用上面的提示詞請 AI 幫你加上
list_books,先用 Inspector 測,再在 Claude Code 裡問「我讀過哪些書?」 - 挑戰:把上一堂寫下的點子,做成你自己的第一個 MCP Server,就算只有一個工具也很好。
讀完了嗎?
標記完成後,單元與專案的進度會自動更新(只存在這台裝置的瀏覽器)。
留言與討論
卡關了、有心得,或發現內容需要更新?都歡迎留言。留言使用 GitHub 帳號登入。