【MCP 與 FastMCP 實戰】#02 用 FastMCP 打造你的第一個 MCP Server

測驗:用 FastMCP 打造你的第一個 MCP Server

共 5 題,點選答案後會立即顯示結果

1. 在 FastMCP 裡,@mcp.tool 這個裝飾器的主要作用是什麼?

  • A. 啟動 server 並開始監聽網路請求
  • B. 把函式的回傳值自動轉成網頁畫面
  • C. 把一個普通 Python 函式註冊成 AI 能呼叫的工具
  • D. 檢查程式碼有沒有語法錯誤

2. 以下這段工具程式碼,FastMCP 會拿 docstring """把兩個整數相加""" 來做什麼?

@mcp.tool def add(a: int, b: int) -> int: “””把兩個整數相加””” return a + b
  • A. 當作工具的名稱
  • B. 當作工具的說明(description)給 AI 看
  • C. 當作參數的預設值
  • D. 只是給人看的註解,FastMCP 會忽略

3. 文章說「型別註解在 FastMCP 裡是真的驗證規則」。這句話的意思最接近下列哪一個?

  • A. 型別註解只是提示,寫錯照樣能跑
  • B. 一定要用 Pydantic model 才能驗證
  • C. 型別註解會被轉成註解字串顯示給使用者
  • D. client 傳進來的參數型別不符時,FastMCP 會擋下並回報錯誤

4. 你執行 python my_server.py(server 內是純 mcp.run()),畫面停住、沒有印出網址。這代表什麼?

  • A. server 當機了,要重寫
  • B. 正常,預設是 stdio 傳輸,正在等 client 透過標準輸入輸出連線
  • C. 少裝了套件,所以沒反應
  • D. 它已經在 http://localhost:8000 開好網頁了

5. 想「不開子程序、不走網路」在同一支程式裡驗收 server 有沒有正確暴露工具,最適合用哪種方式?

  • A. 用 Client(mcp) 建立 in-memory client,呼叫 list_tools()call_tool()
  • B. 直接讀原始碼數有幾個 @mcp.tool
  • C. 把 server 部署到雲端再用瀏覽器測
  • D. 用 print() 在每個工具裡印訊息

系列第 2 篇,共 4 篇|難度:L2-進階

前置:已讀本系列 #01(MCP 核心概念與原語)、看得懂基本 Python 型別註解。

上一篇你搞懂了 MCP 的核心原語:Tool(工具)/Resource(資源)/Prompt(提示),以及 client 與 server 怎麼透過協定溝通。

這一篇要動手了。不過對 Vibe Coder 來說,重點不是「背 API」,而是:AI 幫你生了一個 MCP Server,你要看得懂它在幹嘛、判斷它有沒有寫對、能不能收。

我們用的框架是 FastMCP——目前寫 MCP Server 最主流的 Python 框架(官方文件 gofastmcp.com,套件 fastmcp)。


一句話說明

FastMCP 讓你「把一個普通 Python 函式,變成 AI 能呼叫的工具」,schema、驗證、文件全自動生成。


30 秒範例:一個能跑的 Server

先安裝(建議用 uv):

uv add fastmcp

然後這就是一個完整可運作的 MCP Server,存成 my_server.py

from fastmcp import FastMCP

mcp = FastMCP("My MCP Server")

@mcp.tool
def add(a: int, b: int) -> int:
    """把兩個整數相加"""
    return a + b

if __name__ == "__main__":
    mcp.run()
Code language: JavaScript (javascript)

這段代碼做了什麼

  1. 建立一個叫 My MCP Server 的 server 實例
  2. @mcp.tooladd 這個普通函式「註冊」成一個 AI 能呼叫的工具
  3. mcp.run() 啟動 server,等 client 來連

跑起來:

python my_server.py
Code language: CSS (css)

看起來什麼都沒發生(游標停在那)——這是對的。因為預設是 stdio 傳輸(下面會解釋),它在等別人透過標準輸入輸出跟它講話,不是一個網頁伺服器。按 Ctrl+C 結束。


逐行翻譯

from fastmcp import FastMCP        # 從 fastmcp 套件拿出 FastMCP 這個類別
mcp = FastMCP("My MCP Server")     # 建一台 server,取名 My MCP Server

@mcp.tool                          # 「把下面這個函式登記成一個工具」
def add(a: int, b: int) -> int:    # 工具叫 add,收兩個整數,回傳一個整數
    """把兩個整數相加"""            # 這句話會變成工具的「說明」給 AI 看
    return a + b                   # 實際邏輯:相加

if __name__ == "__main__":         # 「這個檔案被直接執行時才跑」
    mcp.run()                      # 啟動 server(預設 stdio 傳輸)
Code language: PHP (php)

核心:@mcp.tool 到底做了什麼

這是整篇最重要的一段。當 AI 幫你寫 MCP Server,99% 的代碼長得就是「一個函式 + @mcp.tool」。你要看懂函式的長相怎麼決定工具的長相

FastMCP 在你註冊工具時,自動做四件事:

你寫的東西 FastMCP 自動生成
函式名稱 add 工具名稱 add
docstring """把兩個整數相加""" 工具的說明(description)
參數 + 型別註解 a: int, b: int 輸入參數的 schema(叫什麼、什麼型別、必填與否)
回傳註解 -> int 輸出的 schema

換句話說,你不用手寫任何 JSON schema。你把函式簽章寫清楚,schema 就生出來了。這就是「FastMCP」快的原因。

型別註解不是裝飾,是合約

對一般 Python,型別註解 a: int 只是給人看的提示,寫錯了也照跑。但在 FastMCP 裡,型別註解會變成真正的驗證規則。

@mcp.tool
def add(a: int, b: int) -> int:
    return a + b
Code language: CSS (css)

翻譯:「這個工具叫 add,一定要給我兩個整數 a 和 b。」如果 AI client 傳了個文字 "abc" 進來,FastMCP 會擋下來、回報錯誤,而不是讓你的函式炸掉。

所以看到 AI 生的工具時,先看型別註解有沒有寫齊——這是它有沒有認真做的第一個訊號。


常見變化:AI 可能這樣寫

變化 1:裝飾器帶參數 @mcp.tool(...)

@mcp.tool(
    name="find_products",
    description="搜尋商品目錄,可依分類過濾",
    tags={"catalog", "search"},
)
def search(query: str, category: str | None = None) -> list[dict]:
    ...
Code language: PHP (php)

翻譯:不想用函式名稱和 docstring 當工具名/說明時,可以在裝飾器裡「手動指定」。@mcp.tool(不加括號)和 @mcp.tool(...)(加括號帶參數)兩種都對,不是誰新誰舊,看它要不要客製化而已。

變化 2:選填參數=有預設值

@mcp.tool
def search(query: str, max_results: int = 10) -> list[dict]:
    ...
Code language: CSS (css)

翻譯query 沒有預設值 → 必填;max_results = 10 有預設值 → 選填,不給就用 10。這跟一般 Python 函式規則一模一樣,FastMCP 直接沿用。

變化 3:用 Pydantic model 收複雜參數

當一個工具要收「一包結構化資料」,AI 常會定義一個 Pydantic model:

from pydantic import BaseModel

class User(BaseModel):
    name: str
    age: int = 18

@mcp.tool
def create_user(user: User) -> str:
    return f"建立了 {user.name},年齡 {user.age}"

翻譯:「這個工具收一個 user,裡面一定要有 name(文字),可以有 age(數字,預設 18)。」FastMCP 會把這個 model 展開成巢狀的 schema,client 要傳 {"user": {"name": "Alice"}} 這種 JSON 物件進來。

認得就好(舊寫法):Optional[float] = 現在的 float | NoneList[dict] = 現在的 list[dict]。AI 維護舊專案時可能出現,意思一樣。

變化 4:換傳輸方式(stdio vs http)

if __name__ == "__main__":
    mcp.run()                              # 預設:stdio(本機、給桌面 client 用)
    # 或
    mcp.run(transport="http", port=8000)   # http:可被遠端連
Code language: PHP (php)

翻譯

  • stdio:server 當成一個子程序被啟動,透過標準輸入輸出溝通。Claude Desktop、Cursor 這類桌面工具用的就是這種。本機開發預設用這個。
  • http:把 server 掛成一個網路服務,別人用網址連。要遠端部署才需要。

本篇我們只用 stdio。看到 AI 沒指定 transport(純 mcp.run()),就是 stdio,正常。


回傳值會被怎麼處理

工具回傳的東西 FastMCP 會幫你「序列化」成 client 收得到的格式:

  • 回傳 int / str / bool → 直接變成對應的值
  • 回傳 dict / list → 變成 JSON 結構
  • 回傳 Pydantic model → 攤平成 JSON 物件

你不用自己 json.dumps()。看到 AI 在工具裡手動把結果轉成 JSON 字串再回傳,通常是多此一舉(見紅旗)。


🚩 紅旗:看到這些要警覺

🚩 1:工具沒有型別註解

@mcp.tool
def add(a, b):        # 🚩 沒型別!AI client 根本不知道該傳什麼
    return a + b
Code language: PHP (php)

為什麼危險:沒有型別,生出來的 schema 就是「隨便什麼都行」,FastMCP 幫不了你驗證,AI 也更容易亂傳參數。

跟 AI 這樣說:「幫每個工具的參數和回傳值都補上型別註解,讓 FastMCP 能生出正確的 schema。」

🚩 2:工具沒有 docstring(或說明很爛)

@mcp.tool
def proc(x: str) -> str:   # 🚩 工具叫 proc、沒說明,AI 看不懂這是幹嘛的
    ...
Code language: CSS (css)

為什麼危險:MCP 工具是給 AI 讀的。名字含糊、沒說明,AI 就不知道什麼時候該用這個工具,等於白做。

跟 AI 這樣說:「每個工具都要有清楚的名稱和 docstring,說明它做什麼、什麼時候該用、參數是什麼意思。」

🚩 3:危險操作沒有任何防護

@mcp.tool
def run_shell(cmd: str) -> str:      # 🚩 讓 AI 直接下任意 shell 指令
    import subprocess
    return subprocess.run(cmd, shell=True, capture_output=True).stdout.decode()
Code language: PHP (php)

為什麼危險:你把一把「能執行任意指令」的槍交給 AI。一旦這個 server 接上會亂呼叫工具的 client,等於門戶大開。MCP 工具是 AI 能主動觸發的能力,破壞性操作要特別小心

跟 AI 這樣說:「這個工具會執行任意指令,風險太高。改成只允許白名單內的操作,或加上明確的參數限制。」

🚩 4:工具把錯誤默默吞掉

@mcp.tool
def fetch(url: str) -> str:
    try:
        return download(url)
    except Exception:
        return ""            # 🚩 失敗了卻回空字串,AI 以為成功
Code language: CSS (css)

為什麼危險:工具失敗時回一個看起來正常的空值,AI(和你)都不會知道出事了,只會拿到一堆莫名其妙的結果。

跟 AI 這樣說:「工具失敗時應該讓錯誤往上拋或回傳清楚的錯誤訊息,不要用空字串掩蓋失敗。」


Vibe Coder 驗收檢查點

AI 交給你一個 MCP Server,別只是「看起來對」。用下面的方法實際驗它有沒有正確暴露工具。

方法 A:寫個 5 行 client 自己呼叫(最推薦)

FastMCP 內建 client,可以「在同一個程式裡」直接連上你的 server,不用開子程序、不用網路。這是最快、最穩的驗收方式。把 server 存成 my_server.py,另存一個 check.py

import asyncio
from fastmcp import Client
from my_server import mcp        # 直接把 server 物件拿進來

async def main():
    async with Client(mcp) as client:          # 開連線(in-memory,不用網路)
        tools = await client.list_tools()       # 列出所有工具
        print("工具清單:", [t.name for t in tools])

        result = await client.call_tool("add", {"a": 2, "b": 3})   # 實際呼叫
        print("add(2, 3) =", result.data)

asyncio.run(main())
Code language: PHP (php)

python check.py,預期看到:

工具清單: ['add']
add(2, 3) = 5
Code language: JavaScript (javascript)
  • [ ] list_tools() 有列出你期待的每一個工具名稱
  • [ ] call_tool() 回傳的結果正確

看到這兩個,就代表工具確實有被正確暴露

方法 B:用 CLI 快速看 server 有什麼

不想寫 code,直接問 server「你有哪些工具」:

fastmcp inspect my_server.py
Code language: CSS (css)

它會印出 server 名稱、工具清單、每個工具的參數 schema。

  • [ ] 輸出裡的工具數量、名稱、參數型別,跟你要的一致

方法 C:用 MCP Inspector 視覺化點點看

MCP 官方有一個網頁版測試工具 Inspector,可以用表單填參數、按按鈕呼叫工具、看回傳:

fastmcp dev inspector my_server.py
Code language: CSS (css)

它會啟動 server 並開一個瀏覽器介面(連線方式選 STDIO 後按 connect)。

註:不同 FastMCP 版本 CLI 略有差異,舊版可能是 fastmcp dev my_server.py。指令跑不動時,先 fastmcp --help 看你的版本支援哪個。

  • [ ] 在 Inspector 裡看得到工具、能填參數呼叫、回傳符合預期

看不懂就這樣問 AI

「用白話一段一段解釋這個 FastMCP server 檔案在幹嘛,每個 @mcp.tool 分別暴露了什麼工具、收什麼參數、回傳什麼,我是初學者。」

「這個 MCP Server 裡有沒有危險的工具(例如能執行任意指令、刪檔、打外部 API)?各自的風險是什麼?」

「幫我寫一個最小的 FastMCP client 腳本,連上這個 server、列出所有工具、並各呼叫一次,讓我驗收。」


小結

  • FastMCP = 把普通 Python 函式變成 AI 工具@mcp.tool 是核心。
  • 函式簽章決定工具長相:名稱 → 工具名、docstring → 說明、型別註解 → 參數 schema、回傳註解 → 輸出 schema。
  • 型別註解在這裡是真的驗證規則,不是裝飾——先看它有沒有寫齊。
  • stdio 是本機開發預設,看到純 mcp.run() 就是它。
  • 驗收靠實際呼叫Client(mcp) 寫 5 行自己列工具、呼叫工具,比「看起來對」可靠得多。

下一篇(#03)我們會讓工具真的做點有用的事,並把 server 接上真實的 MCP client。

進階測驗:用 FastMCP 打造你的第一個 MCP Server

測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。

1. AI 幫你生了一個 MCP Server,你想快速確認「工具數量、名稱、參數型別跟需求一致」,又不想自己寫 client 腳本。最直接的做法是? 情境題

  • A. 打開檔案逐行讀,數 @mcp.tool 有幾個
  • B. 執行 fastmcp inspect my_server.py,看它印出的工具清單與參數 schema
  • C. 把 server 部署到 Horizon 再從網頁看
  • D. 在每個工具裡加 print() 後執行

2. 你正在 review AI 生的一個工具,它要收「一包使用者資料」(含姓名、年齡)。下列哪種寫法最符合文章建議、能讓 FastMCP 自動生出結構化的參數 schema? 情境題

  • A. def create_user(data),函式內自己解析
  • B. def create_user(name, age),都不加型別
  • C. 定義 class User(BaseModel),再寫 def create_user(user: User)
  • D. def create_user(data: str),傳進來的是 JSON 字串

3. 你要把一個已寫好的 server 部署給遠端 client 用網址連線。原本是 mcp.run(),該怎麼改最合適? 情境題

if __name__ == “__main__”: mcp.run() # 目前:本機用
  • A. 改成 mcp.run(transport="http", port=8000)
  • B. 加一行 mcp.run(),跑兩次
  • C. 把 @mcp.tool 全改成 @mcp.tool(http=True)
  • D. 移除 if __name__ == "__main__" 就會自動變 http

4. 下面這個 AI 生的工具,依文章的紅旗判斷,最該退回修正的問題是什麼? 錯誤診斷

@mcp.tool def fetch(url: str) -> str: try: return download(url) except Exception: return “”
  • A. 少了 mcp.run(),server 不會啟動
  • B. url 沒加型別註解
  • C. 失敗時回空字串,把錯誤吞掉,AI 會誤以為成功
  • D. 回傳型別應該用 Pydantic model

5. 一個工具長這樣,AI client 常常「傳錯參數」或「不知道何時該用它」。從文章角度看,根本原因是什麼? 錯誤診斷

@mcp.tool def proc(a, b): return a + b
  • A. 函式名稱一定要跟工具名不同
  • B. 少了 if __name__ == "__main__"
  • C. 參數太少,工具至少要三個參數
  • D. 沒有型別註解也沒有 docstring,schema 與說明都生不出來,AI 無從判斷怎麼用

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *