實作課程 06 — Model Context Protocol (MCP)¶
目標: 將 Model Context Protocol 伺服器連接至 Copilot SDK 工作階段,讓代理程式取得非自行撰寫的工具。
時間: 約 20 分鐘
必要條件: 實作課程 05 完成,且 HTTP MCP 伺服器需要網際網路連線。
步驟 1 — 瞭解 MCP 帶來的能力¶
在 實作課程 03 您透過撰寫 C# 函式並自行註冊,為模型提供工具。這種方式很強大,但每項能力仍是您必須擁有、測試及維護的程式碼。
MCP 會改變問題的處理方式。Model Context Protocol 伺服器透過標準通訊協定公開完整工具組,而 SDK 可以將該伺服器連接至工作階段。接著,模型會在一般推理迴路中探索並呼叫該伺服器的工具。
在本實作課程中,外部工具組是 Microsoft Learn。您不需要撰寫
SearchDocsAsync 函式,而是連線至 Learn MCP 伺服器,讓代理程式直接查詢產品文件。
步驟 2 — 選擇 MCP 傳輸方式¶
SDK 有兩種 MCP 伺服器設定型別:
McpServers = new Dictionary<string, McpServerConfig>
{
["microsoft.docs.mcp"] = new McpHttpServerConfig
{
Url = "https://learn.microsoft.com/api/mcp"
}
};
請使用 McpHttpServerConfig ,適用於伺服器已在某處執行,且可透過 HTTP 存取的情況。此處正是如此:Microsoft Learn 將伺服器裝載於
https://learn.microsoft.com/api/mcp,因此本機不需要安裝任何項目。
請使用 McpStdioServerConfig ,適用於 SDK 應啟動本機 MCP 處理程序,並透過標準輸入與輸出進行通訊的情況。此結構常用於在電腦上以命令列程式執行的本機檔案系統工具、資料庫輔助工具或特定語言 MCP 伺服器。
從模型的角度來看,兩種傳輸方式產生相同結果:具名 MCP 工具都會在工作階段中可供使用。
步驟 3 — 設定並執行範例¶
開啟
McpSample.cs
並找到 SessionConfig:
var modelId = await ModelPicker.PickAsync(client, requestedModelId);
var config = new SessionConfig
{
Model = modelId,
Streaming = false,
McpServers = new Dictionary<string, McpServerConfig>
{
["microsoft.docs.mcp"] = new McpHttpServerConfig
{
Url = "https://learn.microsoft.com/api/mcp"
}
},
OnPermissionRequest = PermissionHandler.ApproveAll
};
SessionConfig.McpServers 是一個 IDictionary<string, McpServerConfig>。索引鍵是伺服器名稱,值則是各傳輸方式專用的伺服器設定物件。
範例也會設定 OnPermissionRequest = PermissionHandler.ApproveAll。在我們的執行中,主機 CLI 看似已預先核准 MCP 工具使用,因此未觀察到該處理常式觸發。請將它視為額外防護,而不是此行一律必要的證明。權限注意事項與以下內容所述類似:
實作課程 03.
執行範例:
已驗證執行作業的預期輸出:
== Lab 06: mcp ==
Model: claude-haiku-4.5
Prompt: asking the model to consult Microsoft Learn docs
[mcp] SessionMcpServersLoadedEvent
Assistant: I don't have dedicated "Microsoft Learn tools" in my available toolset.
However, I can use the `web_fetch` tool to retrieve current information.
[tool] ToolExecutionStartEvent: web_fetch
[tool] ToolExecutionCompleteEvent: web_fetch (success=True)
Assistant: Based on Microsoft Learn documentation: **Azure Container Apps is a
serverless platform for running containerized applications without managing the
underlying infrastructure.** It supports API endpoints, background jobs,
event-driven processing and microservices, scaling automatically.
⚠️ No MCP tool was invoked.
The model used non-MCP tool(s) instead: web_fetch
The answer may have come from the model's own knowledge or a built-in
tool rather than Microsoft Learn. Check the server is reachable and that
its tools were loaded — look for the [mcp] lines above.
⚠️ 請仔細閱讀該輸出 — 這正是本實作課程的重點。
這個答案看起來很權威,甚至引用了 Microsoft Learn。但它其實也
未 MCP 結果。 SessionMcpServersLoadedEvent 已觸發,因此伺服器設定已獲接受,但工具從未提供給模型 — 所以模型改用內建的 web_fetch ,卻仍產生看似合理的答案。
若沒有最後的檢查,您會將此視為成功的 MCP 示範。這正是本範例要防止的假通過情況,也是範例以 非零 這裡。
此環境中的狀態: Learn MCP 伺服器雖已載入,卻沒有向工作階段公開工具。在此問題解決之前,請將上述 ⚠️ 路徑視為預期輸出。如果 MCP 工具 確實 為您載入,最後一行會顯示
✅ MCP tool(s) invoked: <server>/<tool>,而且結束代碼為 0。
步驟 4 — 確認已使用 MCP 工具¶
範例會訂閱 SDK 事件,並記錄重要事件:
SessionMcpServersLoadedEvent— 伺服器設定已獲接受McpToolsListChangedEvent— 伺服器已發佈工具清單ToolExecutionStartEvent/ToolExecutionCompleteEvent— 工具確實已執行
關鍵細節在於如何判斷工具是否 MCP. ToolExecutionStartEvent
會包含 McpServerName;只有設定該值的執行才會納入計算。像是以下的內建工具: web_fetch 沒有伺服器名稱,因此雖會記錄,卻不會計為成功:
⚠️ 此範例的舊版本曾計算 任何 工具執行,卻高興地回報 ✅ MCP tool(s) invoked: web_fetch — 與 MCP 完全無關的工具成功事件。計算錯誤項目比完全不檢查更糟,因為它會製造虛假信心。
若未執行任何 MCP 工具,範例會列印:
並以非零狀態結束,讓指令碼不會將看似合理、但完全由模型自行產生的答案誤認為 MCP 支援的成功執行。這點很重要,因為 Azure Container Apps 屬於公開知識,即使沒有任何 MCP 工具可用,模型仍可能根據訓練資料回答。
步驟 5 — 與編輯器的 MCP 設定比較¶
此存放庫已在 VS Code 中設定相同伺服器,位置為
.vscode/mcp.json:
{
"servers": {
"microsoft.docs.mcp": {
"type": "http",
"url": "https://learn.microsoft.com/api/mcp"
}
}
}
該檔案供編輯器使用。 SessionConfig.McpServers 供應用程式或範例內執行的 Copilot SDK 工作階段使用。兩者是不同的取用者,但會透過相同通訊協定與同一伺服器通訊。
這正是 MCP 的重點:一套通訊協定,多種用戶端。同一個工具伺服器可同時服務編輯器、CLI、測試工具或應用程式代理程式。
步驟 6 — 使用 MCP 與 提供服務 它¶
目前為止,工作階段都指向其他人提供的伺服器。此存放庫也 實作 一項,而且值得瞭解原因。
到目前為止 CopilotChatService 將提示傳送給模型,但模型無法存取應用程式本身的資料。若詢問聊天功能「哪些客戶的消費最高?」,它只能猜測,因為零售資料庫對它完全不可見。
AgentHQDemo.McpServer
會透過以下項目,以五個唯讀工具補足該缺口: retail.db:
| 工具 | 解答 |
|---|---|
list_transactions |
「顯示 C003 最近的購買紀錄」 |
get_transaction |
「交易 7 的內容是什麼?」 |
list_segments |
「有哪些客群區隔?」 |
get_customer_summary |
「C001 總共消費了多少?」 |
predict_segment |
「C003 屬於哪個客群區隔?」 |
伺服器是建置於以下套件上的主控台應用程式: ModelContextProtocol 套件,其中以屬性完成註冊:
builder.Services
.AddMcpServer(options => options.ServerInfo = new() { Name = "retail-analytics", Version = "1.0.0" })
.WithStdioServerTransport()
.WithToolsFromAssembly();
[McpServerTool(Name = "get_customer_summary", ReadOnly = true, Destructive = false)]
[Description("Summarises one customer's spending: total, average, transaction count ...")]
public static async Task<CustomerSummaryDto> GetCustomerSummaryAsync(...)
⚠️ stdio 會透過 stdout 傳送通訊協定,因此所有記錄都必須傳送至 stderr。多餘的 Console.WriteLine 會損毀串流,使伺服器看似停止回應:
CopilotChatService
會使用以下結構連接: McpStdioServerConfig 步驟 2 的結構:
McpServers = new Dictionary<string, McpServerConfig>
{
["retail-analytics"] = new McpStdioServerConfig
{
Command = "dotnet",
Args = [serverDll],
WorkingDirectory = apiDirectory
}
};
真正的重點是兩項設計決策。
1. 在連線層級落實最低權限,而不只是在程式碼中。 內容會以以下方式開啟: Mode=ReadOnly,因此寫入會由 SQLite 本身拒絕:
即使提示中夾帶指示,也無法修改資料,因為根本未授予該能力。請將此作法與「我們只是沒有撰寫任何
SaveChangesAsync 呼叫」,這只是一項慣例,不是控制措施。
2. 使用領域工具,而不是原始 SQL。 模型會取得 get_customer_summary,而不是
run_query。使用通用 SQLite MCP 伺服器雖不必撰寫任何程式碼,卻也會向模型提供可執行任意 SQL 的逃生通道。工具介面 是 安全性邊界,因此請維持小而明確的範圍。
請注意哪些項目確實 未 變更:REST API 仍透過以下項目直接讀取資料庫: RetailAnalyticsService。MCP 適用於 模型,而不適合用於直接存取自身資料庫的應用程式 — 將您自己的 CRUD 經由 LLM 工具通訊協定路由,只會增加一次子處理程序跳轉,還會徒然犧牲交易一致性與型別安全。
試試看:
curl -s -X POST http://localhost:5050/api/chat \
-H "Content-Type: application/json" \
-d '{"prompt":"What is customer C003 total spend and which segment are they in?"}'
觀察 API 記錄中的證明,方式與步驟 4 完全相同:
MCP tool call: retail-analytics/get_customer_summary
MCP tool call: retail-analytics/predict_segment
💡 權限處理常式具有特定範圍,不是全面套用。 PermissionHandler.ApproveAll
適合主控台實作課程,但此服務可從瀏覽器存取,因此只核准來自以下位置的唯讀工具: retail-analytics:
if (request is PermissionRequestMcp mcp
&& mcp.ServerName == RetailMcpServer
&& mcp.ReadOnly)
{
return Task.FromResult(PermissionDecision.ApproveOnce());
}
請將其視為縱深防禦,而不是主要控制措施 — 如同
權限診斷
記錄;當主機 CLI 已預先授予工具核准時,從未觀察到掛鉤觸發。 Mode=ReadOnly 連線才是真正有效的控制措施。
⚠️ 常見陷阱¶
McpServers不是Dictionary<string, object>。這是一個IDictionary<string, McpServerConfig>,因此這個常見捷徑會因以下編譯錯誤而失敗:CS0266,因為無法轉換值型別。- Learn 伺服器是透過網路存取。若無法連線,模型便沒有 MCP 工具,而且可能悄悄使用自身知識作答。現在,除非範例觀察到以下事件,否則會以非零狀態結束:
ToolExecutionStartEvent. - MCP 伺服器屬於第三方程式碼,可能公開強大的能力。將伺服器加入處理實際工作的代理程式前,請先審查伺服器本身、其權限與資料存取範圍。
- stdio MCP 伺服器絕不能將內容寫入 stdout。請將所有記錄導向 stderr,否則通訊協定串流會損毀,伺服器看起來就像當機。
- 唯讀 提示 不是唯讀的 保證.
ReadOnly = true會告訴主機可以安全地自動核准;實際阻止寫入的是AgentHQDemo.McpServer是Mode=ReadOnly連接字串。
💡 延伸練習¶
基本執行成功後,請嘗試以下其中一項:
- 新增第二個 MCP 伺服器,並比較模型如何在不同工具組之間選擇。
- 將伺服器名稱新增至
DisabledMcpServers,請重新執行相同提示,比較有無 Microsoft Learn 工具時的答案。 - 探索相關 SDK 設定,例如
McpOAuthTokenStorage,GitHubMcpToolConfig以及EnableMcpApps,適用於需要驗證或更豐富 MCP 情境的情況。
✅ 檢查點¶
您現在可以說明:
- [x] 瞭解 MCP 與直接使用 C# 撰寫工具的差異
- [x] 何時應使用
McpHttpServerConfig與McpStdioServerConfig - [x] 如何
SessionConfig.McpServers會依名稱連接伺服器 - [x] 瞭解為何看似合理的答案,不能證明已呼叫 MCP 工具
- [x] 如何
.vscode/mcp.json,而 SDK 設定也可以指向相同伺服器 - [x] 瞭解此存放庫為何 提供 除了使用 MCP 伺服器之外,也能提供 MCP 伺服器,以及為何 REST API 仍直接與資料庫通訊
- [x] 為何
Mode=ReadOnly是真正的控制措施,而ReadOnly = true只是一項提示
相關內容¶
- 上一步: 實作課程 05 — 工作階段
- 下一步: 實作課程 07 — 總結
- 示範:Copilot SDK 整合
- 疑難排解