实验 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"
}
};
当服务器已在其他位置运行并可通过 HTTP 访问时,请使用 McpHttpServerConfig。本例正是如此:Microsoft Learn 在
https://learn.microsoft.com/api/mcp 托管服务器,因此本地无需安装任何组件。
当 SDK 需要启动本地 MCP 进程,并通过标准输入和标准输出与之通信时,请使用 McpStdioServerConfig。这种形式常用于以命令行程序方式在本机运行的文件系统工具、数据库辅助程序或特定语言的 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 成功调用:
⚠️ 此示例的旧版本会统计所有工具执行,并错误地报告 ✅ 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 整合
- 故障排除