實作課程 06 — MCP¶
目標: 將Model Context Protocol伺服器附加到agent,使其能夠使用你沒有編寫但其他人已經建置並執行的工具,同時——更重要的是——學習如何 證明 那些工具實際上被使用了。
時間: ~20分鐘
必要條件: 實驗05 完成,加上網際網路訪問以供HTTP MCP伺服器使用。
步驟1 — 為什麼MCP¶
在 實作課程 3 你編寫了一個工具 @define_tool。這是在你的程式碼庫中實作能力的正確方法。
MCP適用於另一種情況:一個其他人已經建置並執行的可用能力。相反,你指向一個伺服器,該伺服器使用協議進行通訊,其工具將對模型可用。
這個實驗使用了 Microsoft Learn MCP伺服器透過HTTP設定的,與這個存放庫已經為VS Code設定的相同伺服器,因此無需安裝。
步驟2 — 設定伺服器¶
開啟
mcp_sample.py
並找到工作階段設定:
session = await client.create_session(
model=model_id,
streaming=False,
mcp_servers={
"microsoft.docs.mcp": {
"type": "http",
"url": "https://learn.microsoft.com/api/mcp",
"tools": ["*"],
}
},
on_permission_request=PermissionHandler.approve_all,
)
💡 簡單的字典是API。 MCPServerConfig 是一個 TypedDict
union,而不是一個你例項化的類。.NET SDK需要一個
IDictionary<string, McpServerConfig> 和在編譯時拒絕鬆散的字典;Python只是接受字面值。
union有兩種形狀:
| 結構 | 必填欄位 | 使時間 |
|---|---|---|
MCPHTTPServerConfig |
type("http" 或 "sse")、url、tools |
伺服器可以透過網路訪問 |
MCPStdioServerConfig |
command, tools |
伺服器作為本地子程序執行 |
⚠️ tools 必須,不是可選的。 使用 ["*"] 允許伺服器釋出的一切,或者列出特定的工具名稱以縮小範圍。缺少鍵會導致型別錯誤。
💡 type 對於 stdio 形狀是可選的。 CLI 從 command中推斷 stdio,就像它從 url中推斷遠端伺服器一樣。可選的 stdio 鍵是 args, env, timeout 和 working_directory — 注意,最後一個一個是 working_directory,而不是 cwd。SDK 將其重新命名為 cwd
在傳輸格式中,因此傳遞 cwd 自己工作今天,但不是型別化的公共 API。
⚠️ on_permission_request 在這裡也非常重要。 正如
實作課程 3,Python 在沒有提供處理器時拒絕工具呼叫。
PermissionHandler.approve_all 在實驗中是合適的,但在任何涉及真實資料的地方是不合適的。
步驟3 — 執行它¶
新增 --model <id> 覆蓋模型:
已驗證輸出:
== Lab 06: mcp ==
Model: claude-haiku-4.5
Prompt: asking the model to consult Microsoft Learn docs
[mcp] session.mcp_server_status_changed
[mcp] session.mcp_servers_loaded
[mcp] session.mcp_server_status_changed
[tool-request] assistant.message: microsoft.docs.mcp/microsoft_docs_search
[tool] tool.execution_start: microsoft.docs.mcp/microsoft_docs_search
[tool] tool.execution_complete: microsoft.docs.mcp/microsoft_docs_search (success=True)
Assistant: Let me fetch the main Azure Container Apps documentation page for a clearer overview:
[tool-request] assistant.message: microsoft.docs.mcp/microsoft_docs_fetch
[tool] tool.execution_start: microsoft.docs.mcp/microsoft_docs_fetch
[tool] tool.execution_complete: microsoft.docs.mcp/microsoft_docs_fetch (success=True)
Assistant: **Azure Container Apps** is a serverless platform for running containerized applications without managing underlying infrastructure—you provide your container, and Azure handles the servers, orchestration, and deployment. It automatically scales based on demand (HTTP traffic, events, or resource load) and supports microservices, APIs, background jobs, and event-driven workloads with built-in features like traffic splitting, secrets management, and HTTPS ingress.
✅ MCP tool(s) invoked: microsoft.docs.mcp/microsoft_docs_fetch, microsoft.docs.mcp/microsoft_docs_search
兩臺真實的 MCP 工具執行了 — microsoft_docs_search 找到相關頁面,然後
microsoft_docs_fetch 閱讀一個。最後的行確認了這一點,並且範例程式退出 零.
💡 在 .NET 跟蹤記錄了不同的結果。 在那次執行中,學習伺服器載入了但從未展示其工具,因此模型退回到內建的,範例程式退出非零。相同的服務,相同的服務協議,不同結果——正是為什麼下一個步驟存在的原因。
web_fetch 和範例退出非零狀態。同伺服器,同協議,不同結果——正是為了下一個步驟的存在。
第4步 —— 證明工具實際上被使用了¶
這是實驗的真正教訓。Azure Container Apps是公開知識,因此一個模型可以產生一個自信、合理的、正確引用的答案 不呼叫任何MCP工具好的答案不是證據
範例訂閱了構成實際證據的事件:
session.mcp_servers_loaded— 設定被接受session.mcp_server_status_changed— 伺服器連線狀態改變mcp.tools.list_changed— 伺服器釋出其工具清單external_tool.requested— 副本請求外部工具tool.execution_start/tool.execution_complete— 工具實際執行
關鍵細節是工具如何被判斷為 MCP. tool.execution_start
攜帶的 mcp_server_name,只有那些設定為的執行才被計數:
生成的 ToolExecutionStartData 攜帶 tool_call_id, tool_name,
arguments, mcp_server_name,以及 mcp_tool_name 其欄位。只有MCP特定欄位證明工具來自設定的MCP伺服器。
# Only a tool carrying an MCP server name came from MCP.
# Built-ins such as web_fetch must not count, or an
# unreachable server still reports success.
if evt.data.mcp_server_name:
mcp_tools.add(tool_name)
invoked_tools.add(tool_name)
如果沒有任何MCP執行,範例會說這一點並退出非零:
if not mcp_tools:
print()
print("⚠️ No MCP tool was invoked.")
if invoked_tools:
print(f" The model used non-MCP tool(s) instead: {', '.join(sorted(invoked_tools))}")
print(" The answer may have come from the model's own knowledge or a built-in")
print(" tool rather than Microsoft Learn. Check the server is reachable and that")
print(" its tools were loaded — look for the [mcp] lines above.")
return 1
⚠️ 錯誤地計數比不檢查更糟糕。 以前版本 任何 工具執行並愉快地報告
✅ MCP tool(s) invoked: web_fetch — 對於與MCP無關的工具成功
💡 設定 SDKLABS_TRACE_EVENTS=1 列印每一條事件,而不是隻列印與MCP相關的事件,當你正在除錯一個不會連線的伺服器時:
第5步 —— 比較與編輯設定器的對比¶
這個存放庫已經設定了相同的伺服器給VS Code在
.vscode/mcp.json:
{
"servers": {
"microsoft.docs.mcp": {
"type": "http",
"url": "https://learn.microsoft.com/api/mcp"
}
}
}
該檔案是給編輯器的。 mcp_servers= 引數是用於Copilot SDK
工作階段的,它在你的應用或範例中執行。
MCP的目的是:一種協議,多個使用者端。
在你的腦海中保持這兩者的分離:編輯器MCP設定幫助你的開發工具,而 mcp_servers= 改變這個SDK工作階段可以提供的模型。
第6步——消費MCP與 提供 它¶
到目前為止,這個工作階段指向了別人的伺服器。這個存放庫也 實作 一個,而且值得理解為什麼。
直到現在 CopilotChatService 將你的提示傳送給模型,沒有訪問到應用本身的資料。問聊天室"誰是最高花費的客戶?",它只能猜測——零售資料庫對它來說是看不見的。
mcp_server/
用五種唯讀工具填補了這個空白, retail.db:
| 工具 | 答案 |
|---|---|
list_transactions |
“顯示 C003 最近的購買記錄” |
get_transaction |
“交易 7 的詳情是什麼?” |
list_segments |
“有哪些客戶細分?” |
get_customer_summary |
“C001 的消費總額是多少?” |
predict_segment |
“C003 屬於哪個客戶細分?” |
copilot_chat.py
作為標準輸入/輸出伺服器——上面表格的形狀:
{
"retail-analytics": {
"command": sys.executable, # same interpreter as the API
"args": ["-m", "mcp_server"],
"working_directory": str(PROJECT_ROOT),
"tools": ["*"],
}
}
兩個設計決策是這裡的實際教訓。
1. 在連線層實施最低權限,而不只是在程式碼中約束。 引擎以 mode=ro 開啟 SQLite,因此驅動程式會拒絕寫入:
即使是指令走私到提示詞中,也無法修改資料,因為能力從未被授予。與“我們實際上沒有寫任何INSERT語句”這樣的慣例不同,這只是一個慣例,而不是控制。
2. 域工具,而不是原始SQL。 模型獲得 get_customer_summary,而不是
run_query. 一個通用的SQLite MCP伺服器將是一段零程式碼,但它也給模型提供了一個任意SQL逃逸門。工具表面 是 是
安全邊界,因此保持它小且具體。
注意,發生了什麼 不 改變:REST API仍然直接讀取資料庫透過 RetailAnalyticsService. MCP適用於 模型, 而不是應用程式與自己的資料庫進行互動——透過LLM工具協議路由CRUD操作將新增子程序跳轉並丟失事務和型別安全性。
嘗試它:
curl -s -X POST http://localhost:5070/api/chat \
-H "Content-Type: application/json" \
-d '{"prompt":"What is customer C003 total spend and which segment are they in?"}'
檢視API記錄以驗證,就像在第4步中一樣:
INFO:app.services.copilot_chat:MCP tool call: retail-analytics/get_customer_summary
INFO:app.services.copilot_chat:MCP tool call: retail-analytics/predict_segment
💡 權限處理器是受限的,而不是 blanket。 approve_all 對於
console實驗來說是合適的,但這個服務可以從瀏覽器訪問,因此它僅批准來自 retail-analytics 的唯讀工具,拒絕一切其他:
if (
isinstance(request, PermissionRequestMcp)
and request.server_name == RETAIL_MCP_SERVER
and request.read_only
):
return PermissionDecisionApproveOnce()
⚠️ 警告¶
tools是強制性的 在兩種設定形狀中;使用["*"]來處理所有- 省略
on_permission_request導致Python工具呼叫被拒絕 - Learn伺服器透過網路訪問。 如果模型不可達
- 一個合理的答案並不等同於證明。 僅
mcp_server_name在工具執行過程中證明了 MCP 工具執行 - MCP 伺服器是第三方程式碼 並且可以暴露強大的能力。在將代理處理實際工作之前,先檢查伺服器、其權限和資料訪問。
working_directory,而不是cwd, 在 stdio 設定中 — SDK 會為你重新命名它,並且只有公共金鑰型別檢查- 唯讀 提示 不是隻讀 保證.
read_only_hint告訴主機安全自動批准;實際上阻止寫入的是什麼mcp_server/是的mode=roSQLite 連線
💡 延伸挑戰¶
- 狹窄
tools。一個早期修訂版傳送了["*"]僅限一個工具名稱並觀察模型適應 - 新增第二 MCP 伺服器並比較模型如何選擇工具集
- 將伺服器名稱傳遞
disabled_mcp_servers=[...], 重複相同的提示,並比較答案與無學習工具可用的情況 - 交換 HTTP 設定為 stdio 設定 —
{"type": "stdio", "command": "...", "tools": ["*"]}—— 與任何本地 MCP 伺服器進行比較 - 探索
mcp_oauth_token_storage,github_mcp_tool_config,以及enable_mcp_apps用於認證或更豐富的 MCP 情景 - 替換
PermissionHandler.approve_all使用一個處理程式,該處理程式記錄並拒絕你不想模型使用的工具
✅ 檢查點¶
現在你可以解釋:
- [x] 在實驗03中,MCP 與你寫的工具有何不同
- [x] 當使用HTTP設定形狀時,何時使用與標準輸入/輸出形狀不同的HTTP設定形狀
- ✓ 那
mcp_servers因為它使用普通的字典,因為設定是TypedDicts,和那tools是必需的 - [x] 為什麼一個合理的答案並不證明MCP工具被呼叫
- [x] 如何描述Pydantic
mcp_server_name區分真實MCP工具與內建工具 - [x] 如何描述Pydantic
.vscode/mcp.json和SDK設定目標指向相同的伺服器 - [x] 這個存放庫 提供服務 一個MCP伺服器以及消費一個,為什麼REST API仍然直接與資料庫交談
- ✓ 為什麼
mode=ro是真正的控制和read_only_hint是僅是一個提示
相關¶
- 上一個: 實作課程 05 — 工作階段
- 下一頁: 實作課程 07 — 總結
- 示範:Copilot SDK 整合
- 疑難排解