.NET AI 实战:用 MCP 把 Claude Code 变成数据库 DBA
.NET AI 实战:用 MCP 把 Claude Code 变成数据库 DBA
本文是一个 .NET AI 实战的完整记录:给我自研的数据库自治诊断平台(DBPilot)加上 MCP(Model Context Protocol)出口,让 Claude Code 能直接"读懂"数据库发生了什么、甚至反查出问题代码。
ModelContextProtocol.AspNetCore 和 Microsoft Agent Framework 让.Net开发AI智能体真的很方便,所以咱们就不要总是局限于 RAG、MCP的DEMO,要来就来实战。
本篇相关代码的地址:https://github.com/SkyChenSky/DBPilot.App
文末有意见征集——如果你也维护着没上云的 SQL Server,或者其他数据库引擎也需要,欢迎拍砖。
前言
说实话,真羡慕买了云数据库产品的团队。完整的数据库治理让��发定位问题非常高效。想当年,我的团队虽然上了云,但基于成本问题只是在服务器上自行安装了 SQL Server,因此后续我们需要额外给数据库与对应的服务器增加不少监控系统,以此来排查数据库问题。所谓的监控系统是零散的、不完善的,也没有历史快照记录,偶发的性能飙高如同幽灵,来无影去无踪,排查全靠经验与运气。
这块心病困扰我多年。在这段清闲的时间里,我用 AI 协助完成了一套数据库自治诊断平台 —— DBPilot,以辅助数据库没上云的团队定位数据库性能问题。
首个版本聚焦 SQL Server,目前尚处于内测阶段,计划成熟后开源。借这篇文章,也想听听大家的想法。
我始终认为,程序员的工作本质不是写代码,而是解决工程问题。无论是业务、技术还是管理问题,解题思路殊途同归:
定位问题(需求采集 / 现象观察)→ 分析问题(根因分析 / 数据收集)→ 制定方案(技术选型 / 方案设计)→ 实施落地(编码实现 / 工具建设)→ 验证效果(测试验证 / 监控反馈)→ 持续优化(迭代改进 / 复盘沉淀)
而我一直坚信一个原则:能把问题讲清楚,就已经解决了 80%,剩下的 20% 只是在寻找最优解的路上。
DBPilot 的诞生,正是这个思路的产物。而 MCP 的加入,则让"AI 辅助诊断"这一步从"玩具"变成了"生产力工具"——这也是本文的主线:我会按 原理 → MCP Server 搭建 → MCP Client 使用 → Claude Code 集成 → 实战演示 的顺序,把整条链路讲透。

一、DBPilot:三分钟背景铺垫
DBPilot 是我做的一套数据库自治诊断平台,对齐阿里云 DAS 的核心能力,面向"服务器上自装 SQL Server、没买云数据库治理"的团队。为什么要造这个轮子?当年的日常是:
- 节假日凌晨告警炸群,Zabbix 只能看到 CPU 90%,找不到作恶的 SQL,看日志全部都是被阻塞的语句;
- 历史断档,偶发性能飙高事后只能凭【最近的改动项】、【零散的���志碎片】和【可能出问题的点】拼凑真相;
- 慢性病变,一个上线一年多的功能,突然就报超时问题,没有持续追踪,你甚至不知道它是什么时候开始"生病"的
这些本质上不是技术难题,而是"信息分散 + 工具缺失"的工程问题——Zabbix 缺 SQL Server 特有的锁/阻塞/执行计划分析,Profiler / XE 门槛高且没人肉解读不动,商业工具贵且黑盒。
DBPilot 第一版的能力一句话带过:性能洞察(AAS / Top SQL)、锁阻塞与死锁诊断、慢日志、缺失索引与索引使用率、执行计划变更追踪、实例指标趋势。

架构是经典三层:Quartz 定时 Job 采集 DMV / Extended Events 快照落平台库,Web 控制台与 MCP Server 两种方式消费同一份数据:

本文不再展开 DBPilot 本身的实现细节,聚焦 MCP 这条出口:怎么搭 Server、怎么写 Client、怎么接进 Claude Code,以及它能真刀真枪解决什么问题。
二、MCP 是什么?
在动手写代码之前,我觉得有必要把 MCP 的"为什么"讲清楚。这关系到后面架构设计的合理性。
2.1 为什么需要 MCP?#
在没有 MCP 之前,如果你想让 Claude 访问你的平台或系统——数据库、内部 ERP、工单系统都一样——你需要:
- 写一堆 Prompt 告诉它数据结构和接口;
- 用 Function Calling 封装一套查询接口;
- 如果换到 Cursor,又要重新适配一遍。
这就像是"每个电器都要配一个专用充电器"。
MCP(Model Context Protocol)是 Anthropic 推出的开放标准,它定义了一套通用的"AI 工具 USB 接口"。开发者只需要实现一次 MCP Server,任何支持 MCP 的 AI 客户端(Claude Code、Cursor、Windsurf 等)都能即插即用。对 DBPilot 来说,这个"实现一次"的东西,就是把数据库诊断能力做��一个 MCP Server。
2.2 MCP 的三层架构#
MCP 的架构非常清晰,只有三个角色:

- Host:用户直接交互的 AI 应用(如 Claude Code)。它管理会话、权限控制、决定连接哪些 Server。
- Client:住在 Host 进程里,一个 Client 只连一个 Server,负责 JSON-RPC 2.0 的消息收发、生命周期管理。
- Server:轻量级进程,暴露三种能力:
- Tools:AI 可以调用的函数(如
get_slow_sql、get_deadlock_detail); - Resources:只读数据(如数据库表结构、当前连接数);
- Prompts:预定义的提示模板(如"生成一份数据库健康检查报告")。
DBPilot 第一版只实现了 Tools——对诊断场景来说,Tools 是信息密度最高、最可控的暴露方式。
2.3 传输方式:stdio vs Streamable HTTP#
MCP 支持两类传输层:
| 方式 | 适用场景 | 特点 |
|---|---|---|
| stdio | 本地个人工具 | 零配置、无端口,Client 直接拉起 Server 子进程,通过标准输入输出通信 |
| Streamable HTTP | 远程 / 团队共享服务 | 单一 /mcp 端点走 HTTP POST,支持多并发、认证、跨机访问,适合服务化部署 |
DBPilot 选择的是 Streamable HTTP,理由很实际:
- 统一入口——采集 Job 要 7×24 跑,Web 控制台要在线,MCP 出口直接复用 ASP.NET Core 管道和同一个 5200 端口,不需要额外进程;
- 团队共享——MCP Server 部署在能连数据库的机器上,团队里任何人的 Claude Code 配一个 URL 就能接入,不必每台机器都装一套;
当然,如果你的工具是纯本地的个人脚本,stdio 依然是好选择——零配置即插即用。传输层只是 MCP 的"壳",工具定义才是"魂"。
顺带一提时间线:MCP C# SDK 由 .NET 团队与 Anthropic 共同维护,v2.0 起正式支持基于 ASP.NET Core 的 HTTP 传输(Streamable HTTP,替代早期的 SSE 双端点方案),默认无状态化,对 Web 服务的集成非常友好。
2.4 交互流程:一次完整的 Tool Call#
当你对 Claude Code 说:"最近有没有死锁?" 时,背后发生了什么?

这个流程的核心价值在于:LLM 不需要知道 SQL Server 的连接字符串、DMV 语法、XE 事件怎么解析,它只需要知道"调用哪个 Tool、传什么参数",剩下的交给 MCP Server 处理。而且注意第 11~12 步——LLM 会自主决定"再看一眼细节",这个多步推理正是诊断场景需要的。
另外一个容易被忽略的点:DBPilot �� MCP Server 数据源是平台库的历史表,不是直连被监控实例的实时 DMV。采集服务早已把慢日志、死锁、AAS 快照持续落库,MCP 工具只是把这份"历史档案"结构化地交给 AI。好处有两个:
一是历史可回溯("上周三凌晨为什么慢"也能答),
二是不给被监控实例增加额外查询压力(仅 get_blocking_current 一个工具例外——阻塞是"现场"信息,必须打实时 DMV)。
三、MCP Server 搭建实战
好,原理讲完,开始写代码。这一节回答:"在 ASP.NET Core 里挂一个 MCP Server 要几步?" 答案是三步。
3.1 环境与包#
- .NET 10
- NuGet:
ModelContextProtocol.AspNetCore2.2.0(官方 C# SDK 的 ASP.NET Core 集成包)
3.2 三步挂载#
DBPilot 的 Host 是模块化启动的(Program.cs 里各扩展方法链式组合),MCP 出口也是一个模块,核心代码浓缩自 McpExtension.cs:
public static class McpExtension
{
// 第 1 步:注册服务(Add 段)
public static WebApplicationBuilder AddDbpilotMcp(this WebApplicationBuilder builder)
{
var enabled = builder.Configuration.GetSection("DBPilot:Modules:Mcp")
.GetValue<bool?>("Enabled") ?? false;
var options = builder.Configuration.GetSection("DBPilot:Mcp").Get<McpOptions>() ?? new(); </span><span style=class="hljs-string">"color: rgba(class="hljs-number">0
, class="hljs-number">128, class="hljs-number">0, class="hljs-number">1)">class=class="hljs-string">"hljs-comment">//</span><span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">128, class="hljs-number">0, class="hljs-number">1)"> 安全默认关:模块开关默认 false,且 ApiKey 为空同样不注册</span>
<span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">255, class="hljs-number">1)">if</span> (!enabled) { Log.Information(<span style=class="hljs-string">"color: rgba(class="hljs-number">128, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)">class="hljs-string">"</span><span style="color: rgba(class="hljs-number">128, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)class="hljs-string">">MCP 出口模块(Mcp)已在配置中关闭,跳过注册</span><span style="color: rgba(class="hljs-number">128, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)class="hljs-string">">"</span>); <span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">255, class="hljs-number">1)">return</span><span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)"> builder; }
</span><span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">255, class="hljs-number">1)">if</span> (<span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">255, class="hljs-number">1)">string</span><span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)">.IsNullOrWhiteSpace(options.ApiKey))
{ Log.Information(</span><span style=class="hljs-string">"color: rgba(class="hljs-number">128, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)">class="hljs-string">"</span><span style="color: rgba(class="hljs-number">128, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)class="hljs-string">">MCP 出口模块(Mcp)已启用但 ApiKey 为空,为安全起见跳过注册</span><span style="color: rgba(class="hljs-number">128, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)class="hljs-string">">"</span>); <span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">255, class="hljs-number">1)">return</span><span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)"> builder; }
builder.Services.AddSingleton(options);
builder.Services.AddMcpServer(o </span>=><span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)">
{
o.ServerInfo </span>= <span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">255, class="hljs-number">1)">new</span>() { Name = <span style=class="hljs-string">"color: rgba(class="hljs-number">128, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)">class="hljs-string">"</span><span style="color: rgba(class="hljs-number">128, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)class="hljs-string">">dbpilot</span><span style="color: rgba(class="hljs-number">128, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)class="hljs-string">">"</span>, Version = <span style=class="hljs-string">"color: rgba(class="hljs-number">128, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)">class="hljs-string">"</span><span style="color: rgba(class="hljs-number">128, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)class="hljs-string">">class="hljs-number">1.0</span><span style="color: rgba(class="hljs-number">128, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)class="hljs-string">">"</span><span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)"> };
})
.WithHttpTransport() </span><span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">128, class="hljs-number">0, class="hljs-number">1)">class=class="hljs-string">"hljs-comment">//</span><span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">128, class="hljs-number">0, class="hljs-number">1)"> ① Streamable HTTP 传输</span>
.WithToolsFromAssembly(<span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">255, class="hljs-number">1)">typeof</span>(DiagnosticTools).Assembly); <span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">128, class="hljs-number">0, class="hljs-number">1)">class=class="hljs-string">"hljs-comment">//</span><span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">128, class="hljs-number">0, class="hljs-number">1)"> ② 扫描程序集里的工具类</span>
<span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">255, class="hljs-number">1)">return</span><span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)"> builder;
}
</span><span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">128, class="hljs-number">0, class="hljs-number">1)">class=class="hljs-string">"hljs-comment">//</span><span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">128, class="hljs-number">0, class="hljs-number">1)"> 第 class="hljs-number">2、class="hljs-number">3 步:挂中间件 + 映射端点(Use 段;Add 段未启用时 no-op)</span>
<span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">255, class="hljs-number">1)">public</span> <span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">255, class="hljs-number">1)">static</span> WebApplication UseDbpilotMcp(<span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">255, class="hljs-number">1)">this</span><span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)"> WebApplication app)
{
</span><span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">255, class="hljs-number">1)">if</span> (app.Services.GetService<McpOptions>() <span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">255, class="hljs-number">1)">is</span> <span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">255, class="hljs-number">1)">null</span>) <span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">255, class="hljs-number">1)">return</span><span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)"> app;
app.UseMiddleware</span><McpAuthMiddleware>(); <span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">128, class="hljs-number">0, class="hljs-number">1)">class=class="hljs-string">"hljs-comment">//</span><span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">128, class="hljs-number">0, class="hljs-number">1)"> API Key 认证</span>
app.MapMcp(<span style=class="hljs-string">"color: rgba(class="hljs-number">128, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)">class="hljs-string">"</span><span style="color: rgba(class="hljs-number">128, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)class="hljs-string">">/mcp</span><span style="color: rgba(class="hljs-number">128, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)class="hljs-string">">"</span>); <span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">128, class="hljs-number">0, class="hljs-number">1)">class=class="hljs-string">"hljs-comment">//</span><span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">128, class="hljs-number">0, class="hljs-number">1)"> 单一端点:POST /mcp</span>
<span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">255, class="hljs-number">1)">return</span><span style=class="hljs-string">"color: rgba(class="hljs-number">0, class="hljs-number">0, class="hljs-number">0, class="hljs-number">1)"> app;
}
}
评论