跳至內容

實作課程 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.

執行範例:

dotnet run --project src/AgentOrchestrator/samples/SdkLabs -- mcp

已驗證執行作業的預期輸出:

== 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 沒有伺服器名稱,因此雖會記錄,卻不會計為成功:

if (!string.IsNullOrWhiteSpace(start.Data.McpServerName))
{
    mcpTools.Add(toolName);
}

⚠️ 此範例的舊版本曾計算 任何 工具執行,卻高興地回報 ✅ MCP tool(s) invoked: web_fetch — 與 MCP 完全無關的工具成功事件。計算錯誤項目比完全不檢查更糟,因為它會製造虛假信心。

若未執行任何 MCP 工具,範例會列印:

⚠️  No MCP tool was invoked.
    The model used non-MCP tool(s) instead: web_fetch

並以非零狀態結束,讓指令碼不會將看似合理、但完全由模型自行產生的答案誤認為 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 會損毀串流,使伺服器看似停止回應:

builder.Logging.AddConsole(options =>
{
    options.LogToStandardErrorThreshold = LogLevel.Trace;
});

CopilotChatService 會使用以下結構連接: McpStdioServerConfig 步驟 2 的結構:

McpServers = new Dictionary<string, McpServerConfig>
{
    ["retail-analytics"] = new McpStdioServerConfig
    {
        Command = "dotnet",
        Args = [serverDll],
        WorkingDirectory = apiDirectory
    }
};

真正的重點是兩項設計決策。

1. 在連線層級落實最低權限,而不只是在程式碼中。 內容會以以下方式開啟: Mode=ReadOnly,因此寫入會由 SQLite 本身拒絕:

options.UseSqlite($"Data Source={dbPath};Mode=ReadOnly");

即使提示中夾帶指示,也無法修改資料,因為根本未授予該能力。請將此作法與「我們只是沒有撰寫任何 SaveChangesAsync 呼叫」,這只是一項慣例,不是控制措施。

2. 使用領域工具,而不是原始 SQL。 模型會取得 get_customer_summary,而不是 run_query。使用通用 SQLite MCP 伺服器雖不必撰寫任何程式碼,卻也會向模型提供可執行任意 SQL 的逃生通道。工具介面 安全性邊界,因此請維持小而明確的範圍。

請注意哪些項目確實 變更:REST API 仍透過以下項目直接讀取資料庫: RetailAnalyticsService。MCP 適用於 模型,而不適合用於直接存取自身資料庫的應用程式 — 將您自己的 CRUD 經由 LLM 工具通訊協定路由,只會增加一次子處理程序跳轉,還會徒然犧牲交易一致性與型別安全。

試試看:

dotnet run --project src/AgentOrchestrator/AgentHQDemo.Api
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.McpServerMode=ReadOnly 連接字串。

💡 延伸練習

基本執行成功後,請嘗試以下其中一項:

  • 新增第二個 MCP 伺服器,並比較模型如何在不同工具組之間選擇。
  • 將伺服器名稱新增至 DisabledMcpServers,請重新執行相同提示,比較有無 Microsoft Learn 工具時的答案。
  • 探索相關 SDK 設定,例如 McpOAuthTokenStorage, GitHubMcpToolConfig 以及 EnableMcpApps ,適用於需要驗證或更豐富 MCP 情境的情況。

✅ 檢查點

您現在可以說明:

  • [x] 瞭解 MCP 與直接使用 C# 撰寫工具的差異
  • [x] 何時應使用 McpHttpServerConfigMcpStdioServerConfig
  • [x] 如何 SessionConfig.McpServers 會依名稱連接伺服器
  • [x] 瞭解為何看似合理的答案,不能證明已呼叫 MCP 工具
  • [x] 如何 .vscode/mcp.json ,而 SDK 設定也可以指向相同伺服器
  • [x] 瞭解此存放庫為何 提供 除了使用 MCP 伺服器之外,也能提供 MCP 伺服器,以及為何 REST API 仍直接與資料庫通訊
  • [x] 為何 Mode=ReadOnly 是真正的控制措施,而 ReadOnly = true 只是一項提示