Model Context Protocol(MCP)怎麼把 Claude 接上外部工具與資料?它用工具、資源和提示三種基本元素,分別處理模型要執行的能力、應用程式要帶入的資料,以及使用者要觸發的工作流程;本篇也整理 MCP 客戶端、伺服器和 Inspector 的實作關係。
前幾天的 Claude Platform 101 把視角放在 API、工具與 agent loop,今天打開 Introduction to Model Context Protocol,開始往整合的另一側走。
這堂課的做法很直接:從零寫一個 MCP 伺服器,再寫一個可以連上它的客戶端。看起來像是在組一個小型聊天 App,實際上是在把平常使用 Claude Code 時已經被包好的功能拆開來看——工具怎麼被列出來、資源怎麼帶進上下文、提示怎麼變成可以重複使用的工作流程。
| 項目 | 內容 |
|---|---|
| 堂數 | 10 堂課 |
| 總時長 | 1 小時 |
| 測驗 | 1 個(7 題) |
| 完成 | 有完成徽章 |
| 先決條件 | 具備 Python 程式設計實務知識,了解 JSON、HTTP request-response、async/await 和 API 基本概念 |
| 適合對象 | 想建立 MCP 伺服器,並把 Claude 接到外部工具與服務的開發人員 |
假設要做一個聊天介面,讓使用者詢問「我所有 repository 中有哪些開放的 pull request?」。Claude 要回答這個問題,就需要能使用 GitHub API 的工具。
如果每個 App 都自己處理 GitHub 的工具定義、參數格式、API 呼叫、錯誤處理和後續維護,整合工作很快就會膨脹。換成 MCP 的想法後,這些工作可以集中在一個專門的 MCP 伺服器裡,App 只要透過標準化的協定連線,就能取得它公開的工具、資源和提示。
MCP 伺服器可以由服務提供商自己維護,也可以由開發者針對自己的資料或內部系統建立。它的角色像是外部服務的介面:把某個服務真正的 API 細節包起來,再用 MCP 能理解的方式公開出去。
這裡有一個容易混在一起的概念:MCP 伺服器和 tool use 不是同一件事。MCP 伺服器負責提供工具的定義與執行入口;Claude 收到這些工具後,才在對話過程中判斷要不要使用其中一個。前者是整合標準,後者是模型如何使用能力。
MCP 客戶端位在你的 App 和 MCP 伺服器之間,負責處理連線、訊息交換,以及 Python SDK 的協定細節。這堂課把 App 裡的 MCP 客戶端寫出來,所以也能看見平常使用產品時被隱藏的那一層。
MCP 的通訊不綁定單一傳輸方式。可以在同一台機器上使用 standard input/output,也可以透過 HTTP、WebSocket 或其他網路協定連線。傳輸方式可以換,客戶端和伺服器交換的訊息概念仍然相同。
最常用的工具訊息可以先記兩組:
| 訊息 | 用途 |
|---|---|
ListToolsRequest / ListToolsResult |
客戶端詢問伺服器有哪些工具,取得工具名稱、描述和參數 schema |
CallToolRequest / CallToolResult |
客戶端要求伺服器執行某個工具,並接收執行結果 |
把一次完整請求拆開後,大概會經過這條路徑:使用者把問題送進 App,App 先向 MCP 伺服器取得工具清單,再把問題和工具一起交給 Claude。Claude 判斷需要使用工具後,App 透過 MCP 客戶端送出工具呼叫,MCP 伺服器執行實際邏輯,結果再回到 App 和 Claude,最後才組成答案給使用者。
這也讓我重新理解 Claude Code 裡「設定完 MCP 後就能用」的感覺。工具清單的探索與連線管理早就由產品處理好了,使用者通常只會看到模型開始使用工具的那一段。
課程選了一個很容易觀察結果的文件管理案例。文件先放在記憶體裡的簡單字典,MCP 伺服器提供讀取文件和編輯文件兩個工具。
Python MCP SDK 的重點是 decorator。用 @mcp.tool() 裝飾一個 Python function,再搭配 type hints 和 Pydantic Field 描述參數,SDK 就能幫忙產生工具 schema,不需要手動拼一大段 JSON。
這裡的描述文字不能隨便寫。工具名稱、用途和每個參數的說明,都是 Claude 判斷要不要使用工具時會看到的資訊。描述越清楚,模型越容易知道這個工具能做什麼、什麼時候適合呼叫,以及每個參數應該放什麼。
有了工具之後,還需要一個不必先接完整 App 的測試方式。MCP SDK 提供 MCP Inspector,可以在瀏覽器介面裡連接伺服器、列出工具、填入參數並直接執行。
這個開發循環很實用:先啟動 server,再從 Inspector 的 Tools 分頁列出工具;接著選擇 read_doc_contents 或 edit_document,填入文件 ID 和必要欄位,執行後檢查回傳值。編輯文件後再立刻讀一次,也能確認伺服器狀態是否如預期保留。
我照著課程在本機跑了一次,過程中遇到兩個版本相關的提醒。當時直接安裝 mcp[cli] 會拿到 MCP 2.x,但課程範例使用的是 1.x 的 FastMCP 語法;為了先照課程完成練習,我把版本釘在 2 以下。啟動 mcp dev 時,Inspector 也會透過 npx 安裝前端套件,第一次執行會看到套件安裝提示和 deprecated warning。
這些訊息不會改變 Inspector 的核心用途,但很提醒人:課程範例和套件現在的版本不一定同時更新。先看錯誤訊息指出的是 API 改名、版本不相容,還是單純的依賴警告,再決定要升級程式碼或固定版本,通常比直接重裝更快。
伺服器可以被 Inspector 測試後,課程開始寫另一半:讓 App 能夠真的使用 MCP 伺服器。
客戶端主要包兩個元件:自訂的 MCP Client 類別,以及 Python SDK 提供的 Client Session。前者負責把常用操作包起來,後者才是實際維持與伺服器連線的物件。連線和資源清理如果散落在各處,很容易在流程結束時漏掉,所以課程把它們集中在自己的類別裡管理。
最基本的兩個方法是 list_tools() 和 call_tool()。前者把伺服器提供的工具清單拿回來交給 App,再由 App 和使用者問題一起傳給 Claude;後者接住 Claude 決定要使用的工具名稱與參數,透過 session 呼叫 MCP 伺服器。
這裡的分工可以這樣看:MCP 伺服器知道怎麼做某個外部服務的事情,MCP 客戶端知道怎麼把這些能力接進自己的 App,而 Claude 負責根據使用者問題判斷要不要使用工具。三者各自有工作,App 才能把結果組回一個完整的對話流程。
工具適合「執行一個動作」,資源則適合「提供一份資料」。課程用 HTTP 的 GET 來類比資源:它通常是唯讀的,重點在取得資訊,不在改變狀態。
MCP 資源有兩種:直接資源使用固定 URI,適合列出全部文件;範本化資源在 URI 中放入參數,適合依照 doc_id 讀取單一文件。SDK 會解析 URI 中的參數,再把它傳給對應的 Python function。
資源 @mcp.resource |
工具 @mcp.tool |
|
|---|---|---|
| 主要用途 | 取得唯讀資料 | 執行動作,可能改變狀態 |
| 誰控制使用時機 | App 應用程式 | Claude 模型 |
| 是否能帶參數 | 直接資源或範本化資源都可以 | 用參數與 Field 定義 |
| 回傳資訊 | 透過 mime_type 告訴客戶端如何解析 |
SDK 依工具定義產生結構 |
mime_type 也很重要。JSON、純文字和二進位資料的處理方式不同,伺服器用 MIME type 告訴客戶端內容應該怎麼解讀,SDK 再協助完成序列化。
這堂課用 @document_name 當作文件提及的使用情境:App 先取得文件清單,使用者選到某份文件後,App 再讀取對應資源,將內容直接放進送給 Claude 的上下文。
這裡最值得留意的是,@ 不是 MCP 協定的一部分。MCP 只定義資源這個抽象概念,以及讀取資源時的 request 和 result;要不要用 @ 觸發、怎麼做自動完成、讀到內容後怎麼塞進 prompt,都是 App 自己的 UI 與流程設計。
所以直接呼叫原始 Claude Messages API 時,@report.pdf 只是一段普通文字。Claude Code 之所以能把 @ 當成檔案引用,是因為 Claude Code 自己在客戶端實作了這層邏輯。這門課把這段被產品包好的流程拆出來,讓人看見資源從列出、選取到讀取的完整路徑。
MCP 的提示(Prompts)可以想成由伺服器作者預先準備好的工作指令。使用者當然可以直接要求 Claude 把文件轉成 Markdown,但如果某個工作流程有固定格式、領域知識和容易漏掉的邊界條件,把這些要求整理成一個可重複使用的提示,結果會更一致。
課程用文件格式化作為例子。使用者從可用提示中選取 format,提供文件 ID,客戶端取得已經填入參數的指示,再交給 Claude 執行。提示本身不等於工具:它負責準備高品質的 instructions,實際要讀取或修改文件時,仍然可以搭配工具完成。
好的提示也需要測試。MCP Inspector 能顯示參數插值後真正送出的訊息,方便在使用者依賴之前檢查內容是否完整、變數是否正確,還有提示是否真的適合這個伺服器的工作範圍。
客戶端端需要提供列出提示和取得個別提示的能力。list_prompts 讓 App 知道有哪些工作流程可以顯示;get_prompt 則依照使用者選的提示名稱和引數,取得最後要交給 Claude 的訊息。
這種設計很像斜線指令:使用者輸入 / 後看到可用的工作流程,選取一個提示並補上文件名稱,App 再把完整指示傳給模型。斜線輸入只是 App 層的互動方式,MCP 負責的是提示如何被定義、列出和取回。
做到這裡,三個 MCP 基本元素的分工可以濃縮成一張決策表:
| 你想完成的事情 | 適合的 MCP 元素 | 控制者 |
|---|---|---|
| 給 Claude 一個可以自主使用的新能力 | 工具 Tools | 模型 |
| 把資料帶進 UI 或對話上下文 | 資源 Resources | 應用程式 |
| 讓使用者點選或輸入指令,啟動預先定義的工作流程 | 提示 Prompts | 使用者 |
這個「誰控制」的角度,比單純記住三個名詞更好用。要讓 Claude 自己決定是否查資料或執行動作,先想工具;要讓 App 決定何時把資料放進畫面或上下文,想資源;要讓人透過按鈕、選單或斜線指令啟動一段流程,想提示。
| 章節 | 實作端 | 內容 |
|---|---|---|
| 3 | 伺服器端 | 工具 |
| 4 | 客戶端 | Inspector 測工具 |
| 5 | 客戶端 | list_tools / call_tool |
| 6 | 伺服器端 | 資源 |
| 7 | 客戶端 | read_resource(@ 觸發) |
| 8 | 伺服器端 | 提示 |
| 9 | 客戶端 | list_prompts / get_prompt(/ 觸發) |
看完這張表,課程的節奏會更清楚:單數課程偏向在伺服器端定義工具、資源和提示,雙數課程則把它們接回客戶端,處理發現、呼叫和測試。
以下把課程中最主要的程式碼集中放在文末。範例中的 FastMCP 是這次依課程版本完成的 1.x 寫法;如果安裝到 MCP 2.x,需依當時 SDK 文件調整 API,或先將版本固定在 2 以下。
mkdir my_mcp && cd my_mcp
uv venv
source .venv/bin/activate
uv pip install "mcp[cli]<2"
mcp dev mcp_server.py
server.py 實測版本"""my_mcp 練習伺服器 —— 對照 ironman Day 8 筆記逐堂課內容組起來的完整版本。
每個區塊註解標的「第 N 堂」對應
ironman/2026_09_22_day08_introduction-to-model-context-protocol.md
"""
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.prompts import base
from pydantic import Field
# 第 3 堂:設定 MCP 伺服器
mcp = FastMCP("DocumentMCP", log_level="ERROR")
# 第 3 堂:文件存在簡單字典結構裡(key 是文件 ID,value 是內容)
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.",
"financials.docx": "These financials outline the project's budget and expenditures",
"outlook.pdf": "This document presents the projected future performance of the system",
"plan.md": "The plan outlines the steps for the project's implementation.",
"spec.txt": "These specifications define the technical requirements for the equipment",
}
# ── 第 3 堂:工具(由模型控制,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]
@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)
# ── 第 6 堂:資源(由 App 控制,唯讀,對應「@」引用文件時被讀取) ────────────
@mcp.resource("docs://documents", mime_type="application/json")
def list_docs() -> list[str]:
"""直接資源:固定網址,列出所有文件 ID(給自動完成用)。"""
return list(docs.keys())
@mcp.resource("docs://documents/{doc_id}", mime_type="text/plain")
def fetch_doc(doc_id: str) -> str:
"""範本化資源:網址帶 {doc_id} 參數,取單一文件內容。"""
if doc_id not in docs:
raise ValueError(f"Doc with id {doc_id} not found")
return docs[doc_id]
# ── 第 8 堂:提示(由使用者觸發,例如 /extract_numbers)───────────────────
# 改用「抓數字」取代課程原本的「重排成 markdown」,結果一眼就能對答案,
# 不用另外寫 markdown 解析/驗證的程式碼。
@mcp.prompt(
name="extract_numbers",
description="Extract all numeric values mentioned in the document.",
)
def extract_numbers_prompt(
doc_id: str = Field(description="Id of the document to scan"),
) -> list[base.Message]:
prompt = f"""
Your goal is to find every number mentioned in a document and list them out clearly, one per line.
The id of the document you need to scan is:
<document_id>
{doc_id}
</document_id>
Use the 'read_doc_contents' tool to read the document first, then extract every number you find.
If there are no numbers, say so explicitly instead of leaving the list empty.
"""
return [base.UserMessage(prompt)]
if __name__ == "__main__":
mcp.run()
有 7 題。以下保留題目與正確答案,中英對照的繁中是自譯。
docs://documents/report.pdf?(想建立一個根據 ID 擷取不同文件的資源,例如 docs://documents/report.pdf,應該用哪種類型的資源?)答案:範本化資源(Template Resource),因為 URI 中帶有 {doc_id} 這類參數。
答案:一個 MCP Client 類別和一個 Client Session。
答案:你必須自己編寫、測試和維護所有 GitHub 工具程式碼。
答案:ListToolsRequest。
答案:在 Python function 上使用 @mcp.tool() decorator。
答案:Prompts。因為工作流程由使用者動作觸發;Resources 是讓 App 取得資料,不是用來表示使用者點擊按鈕啟動的流程。
答案:使用內建的 MCP Inspector,搭配 mcp dev mcp_server.py。
原本以為 MCP 簡介只是把 MCP 的概念、架構和溝通流程講清楚,沒想到它是一堂很硬核的實作課。課程用一個簡單粗略、但前後完整的 App 視角,從零示範要打造一個能接上 MCP 的 AI client,會經過哪些流程;每個節點該由誰處理、App 要補哪些 function,也都拆開來講。
課程刻意把 Claude Code 已經包好的部分拿掉,改成從一個獨立 App 出發。這也是我在 Resource 那堂一直疑惑的地方:為什麼讀取資料也要由 MCP 和 App 來處理?後來才發現,前提就是你沒有 Claude Code 幫你讀 Resource,App 必須自己包含讀取 Resource 的 function。原本看起來有點繞的設計,放回「我要自己打造一個 App」的前提後,整個邏輯就接起來了。
這堂課就是跟著內容一步一步打造 MCP,再透過 Inspector UI 測試整個過程。官方已經提供不錯的工具和範例,實際跟 agent 合作把 Inspector 串起來的感覺也很好;一路把每個節點跑過一次,才真的把 MCP 的知識補齊。下一堂是 MCP 的進階主題,剛好可以接著看這套協定還有哪些更細的邊界。
我是 Jasper,從事軟體開發,目前專注打造 AI 工作流程。
本文同步發佈於我的 Blog,和我一起探討更多 AI 議題 🚀。