Agent 與 MCP

動手寫第一個 MCP server:用 Python SDK 十幾行搞定

用官方 Python SDK 寫 MCP server 非常精簡:一行 FastMCP 初始化,之後用 @mcp.tool、@mcp.resource、@mcp.prompt 三個 decorator 搭配型別提示定義功能,SDK 會自動生成 Claude 需要的 JSON schema。寫完用內建的 MCP Inspector 在瀏覽器裡即時測試,不必接上完整應用。
動手寫第一個 MCP server:用 Python SDK 十幾行搞定:文章重點卡

動手寫第一個 MCP server:用 Python SDK 十幾行搞定

看懂概念之後,最快的學法就是自己寫一個。這篇我們用官方 Python SDK 蓋一個文件管理的 MCP server:文件放在記憶體裡的 dict,提供讀取與編輯兩個工具,再加上 resources 與 prompts,最後用內建的 Inspector 測試。你會發現整件事精簡得出乎意料。

先說一個實務提醒:真實專案裡,你通常只會實作 client 或 server 其中一邊。要把自家服務開放給其他開發者,寫 server;要接上現成的服務,寫 client。教學專案兩邊都寫,純粹是為了看懂它們怎麼配合。

你將學到什麼

一行初始化

FastMCP 一行就能建出完整的 MCP server 骨架。

decorator 定義工具

型別提示自動變 JSON schema,不用手寫規格。

resources 與 prompts

同一套寫法,把資料通道與指令範本一起補上。

Inspector 即時測試

瀏覽器裡列出、執行、驗證工具,不必接完整應用。

動手前:把專案跑起來

起手式是一個 CLI 專案包,照 README 三步就能啟動:把 Anthropic API key 填進 .env、用 UV(推薦)或 pip 安裝依賴、跑一次主程式確認一切正常。啟動後會看到聊天提示,先問個「1 加 1 等於多少」確認 Claude 有回應,再開始動工。

# 用 UV(推薦)
uv run main.py

# 或用標準 Python
python main.py

專案主要就三個檔案:main.py、mcp_client.py、mcp_server.py。這篇我們只動 mcp_server.py,client 那一側留給下一篇。

初始化:一行就有一個 server

官方 Python SDK 提供 FastMCP,初始化一行搞定。文件用最簡單的 dict 存放,key 是文件 id,value 是內容,先不碰資料庫:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("DocumentMCP", log_level="ERROR")

docs = {
    "deposition.md": "This deposition covers the testimony of Angela Smith, P.E.",
    "report.pdf": "The report details the state of a 20m condenser tower.",
    "plan.md": "The plan outlines the steps for the project's implementation.",
}

FastMCP 的第一個引數是 server 名稱,log_level 控制日誌量。這個 server 接下來會提供兩個工具:一個讀文件內容,一個做找字取代的編輯,正好對應「讀」與「寫」兩種最基本的文件操作。

用 decorator 定義工具,不手寫 schema

SDK 最大的好處:不用手寫 JSON schema。你用 @mcp.tool decorator 加上型別提示,SDK 自動生成 Claude 需要的 schema;Pydantic 的 Field 則提供參數說明,幫 Claude 理解每個引數的用途。先看讀取工具:

@mcp.tool(
    name="read_doc_contents",
    description="Read the contents of a document and return it as a string."
)
def read_document(
    doc_id: str = Field(description="Id of the document to read")
):
    if doc_id not in docs:
        raise ValueError(f"Doc with id {doc_id} not found")

    return docs[doc_id]

拆開來看:decorator 的 name 與 description 告訴 Claude 這個工具是誰、能做什麼;參數上的型別提示與 Field 描述,則變成 schema 裡的欄位說明。Claude 挑工具、填引數時讀的就是這些文字,所以描述寫得越清楚,Claude 用得越準。

編輯工具做簡單的找字取代,三個參數:文件 id、要找的字串、要換上的字串。實作直接用 Python 內建的字串 replace:

@mcp.tool(
    name="edit_document",
    description="Edit a document by replacing a string in the documents content with a new string."
)
def edit_document(
    doc_id: str = Field(description="Id of the document that will be edited"),
    old_str: str = Field(description="The text to replace. Must match exactly, including whitespace."),
    new_str: str = Field(description="The new text to insert in place of the old text.")
):
    if doc_id not in docs:
        raise ValueError(f"Doc with id {doc_id} not found")

    docs[doc_id] = docs[doc_id].replace(old_str, new_str)

兩個工具都有基本的錯誤處理:文件不存在就丟出 ValueError,訊息寫清楚,Claude 收到後能理解發生什麼事,甚至據此調整下一步。整理一下 SDK 寫法的好處:

  • 型別提示自動變成 JSON schema,不必手寫也不會寫歪。
  • Pydantic 內建參數驗證,錯的輸入進不來。
  • 錯誤處理就用 Python 例外,寫起來很自然。
  • 樣板程式碼大幅減少,程式乾淨好維護。

加上 resources 與 prompts

同一套 decorator 模式延伸到另外兩個原語。resources 用 @mcp.resource 定義:URI 固定的是 direct resource,URI 帶參數的是 templated resource,SDK 會自動解析參數傳給函式;mime_type 給 client 一個解析提示,回傳值由 SDK 自動序列化,不必自己轉 JSON 字串:

@mcp.resource(
    "docs://documents",
    mime_type="application/json"
)
def list_docs() -> list[str]:
    return list(docs.keys())

@mcp.resource(
    "docs://documents/{doc_id}",
    mime_type="text/plain"
)
def fetch_doc(doc_id: str) -> str:
    if doc_id not in docs:
        raise ValueError(f"Doc with id {doc_id} not found")
    return docs[doc_id]

mime_type 常用的值:application/json 表示結構化資料、text/plain 表示純文字,其他合法的 MIME type 也都可以,甚至二進位資料。

prompts 用 @mcp.prompt 定義,回傳一組可以直接送給 Claude 的訊息。這裡做一個 format 指令,把指定文件重寫成 markdown 格式:

@mcp.prompt(
    name="format",
    description="Rewrites the contents of the document in Markdown format."
)
def format_document(
    doc_id: str = Field(description="Id of the document to format")
) -> list[base.Message]:
    prompt = f"""
Your goal is to reformat a document to be written with markdown syntax.

The id of the document you need to reformat is:
<document_id>
{doc_id}
</document_id>

Add in headers, bullet points, tables, etc as necessary.
Use the 'edit_document' tool to edit the document.
"""
    return [base.UserMessage(prompt)]

留意兩件事:一是 prompt 用 XML 標籤把 doc_id 包起來,界線清楚;二是範本裡明確指示 Claude 用 edit_document 工具改文件,prompt 與工具本來就是設計來搭配使用的。在 Inspector 的 Prompts 分頁可以直接測:填一個 doc_id,看變數安插後產生的訊息長什麼樣,確認沒問題再交給使用者。

用 Inspector 邊寫邊測

寫好的 server 不必接上完整應用才能測。SDK 內建瀏覽器版的 MCP Inspector,一行啟動:

mcp dev mcp_server.py

啟動後打開它給的本機網址,按 Connect 連上 server,連線狀態會從 Disconnected 變成 Connected。接著就能在 Tools、Resources、Prompts 幾個分頁裡逐一測試。以讀取工具為例:進 Tools 分頁按 List Tools 列出全部工具,選 read_doc_contents,在右側欄位填入 deposition.md 這類文件 id,按 Run Tool,下方就會顯示執行狀態與回傳內容。

你也可以串著測,先用 edit_document 改一段文字,馬上再用 read_doc_contents 讀回來,確認修改真的生效;Inspector 會在工具呼叫之間保留 server 的狀態,這種「改完馬上讀回來」的串測完全可行。Inspector 還在快速開發中,介面版本更新很快,但核心功能一致。

除了正常路徑,也順手測錯誤情境:填一個不存在的文件 id,確認回來的是那句寫清楚的錯誤訊息,而不是整個 server 掛掉。邊角情況在這裡先驗過,之後 Claude 真的踩到時,你才知道它會看到什麼。

之後的開發節奏就是固定四拍:改 server 程式、在 Inspector 裡測個別工具、驗證結果、隔離除錯。等這裡全綠,再把 server 接進 client 與 Claude,整合階段就只剩接線,不會邊接邊抓蟲。

改 server 程式 mcp dev 啟動 Inspector 瀏覽器裡測工具 看結果除錯 改完再來一圈,全程不離開瀏覽器
Inspector 把回饋迴圈縮到最短:改完立刻測,測完立刻改。
省下的不只是時間Inspector 省下的不是幾分鐘,是整個回饋迴圈:改程式、測工具、看結果、除錯,全部不用離開瀏覽器,也不用先把 server 接進 Claude 或任何應用。問題在隔離環境裡先抓出來,之後整合才不會兩邊猜。
參考出處本文取材自 Anthropic 官方 Claude Academy 免費課程「Building with the Claude API」,由酒Ann 消化後以自己的視角重新編寫。想看英文原版課程,可到 Claude Academy 修習。

延伸學習

把這篇文章分享給需要的人FacebookLINEThreadsX

常見問答

一定要用 Python 寫 MCP server 嗎?
不一定。MCP 是語言無關的協定,官方規格另外維護,也有 Python、TypeScript 等多種 SDK。本文用 Python SDK 示範,因為 FastMCP 搭配 decorator 的寫法最精簡。
不手寫 JSON schema,Claude 怎麼知道工具長怎樣?
SDK 會從函式的型別提示與 Field 描述自動生成 JSON schema,Claude 拿到的仍是標準的工具規格,只是生成的工作由 SDK 代勞。
MCP Inspector 是什麼?
Python SDK 內建的瀏覽器測試工具,用 mcp dev 指令啟動。可以在網頁上列出並執行 server 的 tools、resources、prompts,不必先接上完整應用。
工具出錯時該怎麼處理?
直接丟出帶清楚訊息的例外即可,例如文件不存在就 raise ValueError。Claude 讀得懂錯誤訊息,並可能據此修正參數再試一次。