☰
ASP.NET Core 集成 MCP:将 .NET 接口暴露给 AI 的完整实践
2026/9/25 16:12:40 网站建设 项目流程

1. 为什么我要把 .NET 接口直接暴露给 AI

去年年底我接手了一个内部工具平台,后端是标准的 ASP.NET Core,接口文档靠 Swagger 撑着,日常调用方是前端和几个内部脚本。后来团队开始用各种 AI 助手做辅助开发,问题就来了:AI 能写代码,但它看不到我们系统里真实的数据结构和业务接口。每次让它帮忙生成一段调用代码,我都得手动把 Swagger 里的 JSON 结构复制粘贴过去,来回折腾,效率极低。

这个痛点其实很普遍。AI 大模型本身有很强的推理和生成能力,但它和你的业务系统之间隔着一堵墙。MCP(Model Context Protocol)就是用来拆这堵墙的。简单说,它是一套让 AI 模型能够发现并调用外部工具、读取外部资源的协议规范。你把自己的 .NET 接口包装成 MCP 服务端,AI 客户端就能像调用内置函数一样直接调你的接口,拿到真实数据再继续推理。

这篇文章要讲的就是怎么落地这件事:用 ASP.NET Core 搭一个 MCP 服务端,把现有的 REST 接口暴露出去;再配一个 MCP 客户端,让 AI 能真正调起来。适合有 .NET 基础、想把自己系统接入 AI 工作流的后端开发,也适合正在评估 MCP 方案的技术负责人。我会把踩过的坑、选型的理由、以及实际跑通之后的经验都摊开讲。

2. MCP 到底解决了什么问题,和直接写 Function Calling 有什么区别

2.1 从"贴 JSON"到"自动发现"的转变

大多数人第一次让 AI 调外部接口,用的是 Function Calling。你在请求里手动声明一个函数签名,描述参数和返回值,模型决定要不要调。这个方式能用,但有几个硬伤:函数声明是写死在每次请求里的,接口一多,prompt 就爆炸;接口改了,声明得同步改,容易漏;多个 AI 客户端之间没法复用同一套工具定义。

MCP 的思路不一样。它把"工具定义"和"工具调用"拆成了两层:服务端负责声明自己有哪些工具、每个工具接受什么参数,客户端负责把这些工具信息喂给模型,并在模型决定调用时转发请求。工具的定义是服务端自描述的,客户端不需要提前知道任何细节。这就意味着你改一次接口,所有接入的 AI 客户端自动感知,不用挨个改配置。

我打个比方。Function Calling 像是你每次打电话前都要手写一份菜单递给对方;MCP 像是你开了一家餐厅,菜单挂在门口,谁来都能看,看完直接点。前者适合临时用,后者适合长期维护。

2.2 MCP 的三种能力原语

MCP 协议里服务端能暴露的东西分三类,理解这三类是设计接口的前提:

  • Tools(工具):可以被 AI 主动调用的操作,比如"查询订单""创建用户"。这是最常用的,也是本文重点。
  • Resources(资源):只读的数据,AI 可以读取但不能修改,比如"当前配置""日志文件内容"。
  • Prompts(提示模板):预定义的提示词模板,客户端可以拉取使用,适合标准化一些常见任务。

实际落地时,90% 的场景用的是 Tools。Resources 适合把一些静态数据暴露给 AI 做上下文,Prompts 用得相对少。我建议一开始只做 Tools,跑通之后再考虑扩展。

2.3 和 Swagger 的关系:不是替代,是互补

有人会问,我都有 Swagger 了,为什么还要 MCP?这两者解决的不是同一个问题。Swagger 是给人看的接口文档,描述的是 HTTP 层面的契约;MCP 是给 AI 用的工具描述,描述的是"这个操作能干什么、需要什么输入"。Swagger 里的一个POST /api/orders在 MCP 里可能被拆成"创建订单"和"校验订单参数"两个工具,因为 AI 需要的是语义化的操作粒度,而不是 HTTP 动词。

我的做法是:Swagger 继续保留,作为人工查阅和前端联调的文档;MCP 服务端单独写一层适配,把核心业务接口包装成 AI 友好的工具。两层各司其职,互不干扰。

3. 用 ASP.NET Core 搭 MCP 服务端的完整过程

3.1 环境准备与包选择

先说环境。我用的是 .NET 8,ASP.NET Core 项目,MCP 的官方 C# SDK 目前可以通过 NuGet 引入。核心包是ModelContextProtocol和ModelContextProtocol.AspNetCore,后者提供了和 ASP.NET Core 集成的扩展方法。

dotnet add package ModelContextProtocol dotnet add package ModelContextProtocol.AspNetCore

这里有个坑要提前说:MCP 的 C# SDK 迭代比较快,不同版本之间的 API 有变化。我建议锁定一个具体版本,别用浮动版本号,否则某天dotnet restore之后编译不过,排查半天发现是 SDK 升级了。我锁的是当时最新的稳定版,具体版本号你按 NuGet 上的最新稳定版来。

项目结构上,我建议单独建一个 MCP 服务项目,不要和主业务 API 混在一起。原因是 MCP 服务端的生命周期、认证方式、部署端口都可能和主 API 不同,混在一起后期维护会很乱。我的结构是这样的:

Solution/ MyApp.Api/ # 主业务 API,带 Swagger MyApp.McpServer/ # MCP 服务端,引用 Api 的领域服务 MyApp.Shared/ # 共享的 DTO 和领域模型

MCP 服务端通过依赖注入拿到主 API 的领域服务,不直接走 HTTP 调自己的 API。这样避免了一次内部 HTTP 往返,性能更好,也少了一层网络故障点。

3.2 定义第一个 MCP 工具

MCP 工具的定义方式很直观,用特性标注一个方法就行。下面是我实际项目里一个查询订单的工具:

using ModelContextProtocol.Server; using System.ComponentModel; [McpServerToolType] public class OrderTools { private readonly IOrderService _orderService; public OrderTools(IOrderService orderService) { _orderService = orderService; } [McpServerTool, Description("根据订单号查询订单详情,返回订单状态、金额和商品列表")] public async Task<OrderDetailDto> GetOrderDetail( [Description("订单号,格式为 ORD 开头的字符串")] string orderId) { var order = await _orderService.GetByIdAsync(orderId); if (order is null) { throw new McpException($"订单 {orderId} 不存在"); } return order.ToDetailDto(); } }

几个关键点值得展开说。[McpServerToolType]标注在类上,告诉 SDK 这个类里有工具;[McpServerTool]标注在方法上,标记这是一个可被调用的工具。Description特性非常重要,它写的内容会直接进入模型的上下文,模型靠它来判断什么时候该调这个工具。所以描述要写清楚"做什么、返回什么",别写"查询订单"这种模糊的四个字。

参数上的Description同样重要。模型需要知道orderId的格式,否则它可能传一个数字 ID 进来,你的代码直接抛异常。我踩过一次坑:没写参数描述,模型传了个12345,而我的订单号是ORD20240101001这种格式,结果查询一直返回空。加上格式说明之后,模型基本能传对。

3.3 在 Program.cs 里注册 MCP 服务

工具定义好之后,要在启动时注册。ASP.NET Core 的集成方式很简洁:

var builder = WebApplication.CreateBuilder(args); builder.Services.AddMcpServer() .WithToolsFromAssembly() .WithHttpTransport(); var app = builder.Build(); app.MapMcp("/mcp"); app.Run();

WithToolsFromAssembly()会自动扫描当前程序集里所有带[McpServerToolType]的类,把工具注册进去。WithHttpTransport()启用 HTTP 传输,MapMcp("/mcp")把 MCP 端点挂到/mcp路径上。

这里有个细节:MCP 支持两种传输方式,stdio 和 HTTP。stdio 适合本地进程间通信,比如 AI 客户端直接启动你的服务端进程;HTTP 适合服务端部署在远程、多个客户端共享的场景。我选的是 HTTP,因为我们的 MCP 服务要部署在内网服务器上,多个开发者的 AI 客户端都要连。

注意:HTTP 传输模式下,MCP 端点默认没有认证。如果你部署在公网或者多人共享的内网,一定要加认证中间件,否则任何人都能调你的业务接口。我是在MapMcp之前加了一层 API Key 校验的中间件。

3.4 工具粒度的设计经验

这是我认为整个落地过程中最需要经验的部分。工具设计得好不好,直接决定 AI 用起来顺不顺。

我的原则是:一个工具对应一个完整的业务意图,而不是一个 HTTP 接口。举个例子,创建订单这个业务,后端可能是"校验库存 → 扣减库存 → 创建订单 → 发送通知"四个接口。如果我把四个接口都暴露成四个工具,模型得自己编排调用顺序,很容易出错。更好的做法是暴露一个CreateOrder工具,内部把这四步串起来。

但也不能太粗。如果一个工具叫DoEverything,模型根本不知道什么时候该调它。粒度要卡在"模型能清楚判断调用时机"和"一次调用能完成一个完整意图"之间。

我总结了一个判断标准:如果这个工具的描述里出现了"并且""然后"这类连接词,说明它可能太粗了;如果两个工具的描述高度相似、模型经常分不清该调哪个,说明太细了,该合并。

4. 客户端接入:让 AI 真正调起来

4.1 客户端选型与配置

MCP 客户端现在选择不少,主流的 AI 开发工具基本都支持。我实际用过两类:一类是 IDE 内置的 AI 助手,一类是独立的桌面客户端。配置方式大同小异,核心就是告诉客户端"去哪里找 MCP 服务端"。

以配置文件为例,HTTP 传输的配置大概长这样:

{ "mcpServers": { "my-dotnet-api": { "url": "http://localhost:5000/mcp", "headers": { "X-Api-Key": "your-api-key-here" } } } }

url指向你的 MCP 端点,headers里带上认证信息。配置好之后重启客户端,它会在启动时拉取服务端的工具列表。

我第一次配的时候犯了个错:服务端跑在https://localhost:8889,客户端配的也是 https,结果一直报 SSL 协议错误。原因是本地开发证书客户端不信任。后来改成 http 就好了。本地开发阶段,MCP 服务端用 http 就行,别给自己找证书的麻烦。生产环境再上 https,那时候证书是正规签发的,不会有信任问题。

4.2 验证工具是否被正确发现

配置完之后,怎么确认客户端真的看到了你的工具?大多数客户端有个"工具列表"或者"可用工具"的面板,点开能看到服务端暴露的所有工具名称和描述。如果列表是空的,按这个顺序排查:

  1. 服务端是否正常启动,/mcp端点是否可访问
  2. 客户端配置的 url 是否和实际端点一致
  3. 认证 header 是否正确
  4. 服务端日志里有没有收到工具列表请求

我遇到过一次工具列表为空,排查了半天发现是WithToolsFromAssembly()扫描的程序集不对。工具类写在另一个项目里,而启动项目没有引用那个项目,自然扫不到。解决办法是在WithToolsFromAssembly()里显式指定程序集,或者确保启动项目引用了工具所在的项目。

4.3 一次完整的调用链路

工具被发现之后,实际调用是这样的流程:用户在 AI 客户端里提问,比如"帮我查一下订单 ORD20240101001 的状态";模型分析意图,发现需要调GetOrderDetail工具;客户端把调用请求转发到 MCP 服务端的/mcp端点;服务端执行工具方法,返回结果;客户端把结果喂回模型,模型生成最终回答。

整个过程对用户是透明的,用户只看到 AI 回答了订单状态。但对开发者来说,每一环都可能出问题。我在服务端加了详细的日志,记录每次工具调用的入参和出参,排查问题时非常有用。

[McpServerTool, Description("...")] public async Task<OrderDetailDto> GetOrderDetail(string orderId) { _logger.LogInformation("MCP 工具调用 GetOrderDetail,参数 orderId={OrderId}", orderId); try { var result = await _orderService.GetByIdAsync(orderId); _logger.LogInformation("GetOrderDetail 返回成功,订单状态={Status}", result?.Status); return result.ToDetailDto(); } catch (Exception ex) { _logger.LogError(ex, "GetOrderDetail 执行失败,orderId={OrderId}", orderId); throw; } }

这段日志代码看起来啰嗦,但真出问题的时候能救命。MCP 调用是异步的,客户端那边只看到一个错误提示,具体哪里错了全靠服务端日志。

5. 踩过的坑和对应的解法

5.1 返回值序列化的坑

MCP 工具方法的返回值会被序列化成 JSON 传给客户端。这里有个容易忽略的点:返回的对象不能太大。我有一次写了个工具返回订单列表,没加分页,结果一个查询返回了几千条记录,序列化之后几百 KB,客户端处理起来很慢,模型也消化不了这么多内容。

解法很简单:工具方法内部做好分页和字段裁剪,只返回模型真正需要的字段。比如订单列表只需要订单号、状态、金额、创建时间,不需要把整个订单实体所有字段都返回。我在 DTO 层面做了专门的 MCP 返回模型,和 API 返回模型分开。

public record OrderSummaryDto( string OrderId, string Status, decimal Amount, DateTime CreatedAt);

用 record 定义,字段精简,序列化出来干净利落。

5.2 异常处理的边界

工具方法里抛异常,MCP 协议会把它包装成错误响应传给客户端。但异常信息会直接暴露给模型,所以异常消息要写成人能看懂的话,别把堆栈或者内部错误码扔出去。我一开始直接throw new Exception(ex.Message),结果模型收到一堆数据库错误信息,完全没法处理。

正确的做法是捕获底层异常,转换成语义化的McpException:

try { var order = await _orderService.GetByIdAsync(orderId); if (order is null) throw new McpException($"未找到订单 {orderId},请确认订单号是否正确"); return order.ToDetailDto(); } catch (DbException ex) { _logger.LogError(ex, "数据库查询失败"); throw new McpException("订单查询服务暂时不可用,请稍后重试"); }

这样模型收到的错误信息是"未找到订单 XXX",它就能告诉用户订单号可能不对,而不是一脸懵。

5.3 并发调用的资源竞争

MCP 客户端可能同时发起多个工具调用。如果你的工具方法里有共享状态,比如静态变量、单例服务里的可变字段,就会出现竞争。我遇到过一次:两个请求同时调同一个工具,因为共享了一个DbContext,报了"上下文已被释放"的错误。

解法是确保工具方法是无状态的,所有依赖通过构造函数注入,且注入的服务是线程安全的。DbContext这种非线程安全的对象,要么用IDbContextFactory每次创建新的,要么确保生命周期是 Scoped。我最后改成了IDbContextFactory,每次调用创建一个新的上下文,问题消失。

5.4 工具描述被模型忽略

有时候工具定义得好好的,模型就是不调,或者调错工具。这通常是描述写得不够精确。我总结了几条写描述的经验:

  • 描述里要包含"什么时候用这个工具"的触发条件,而不只是"这个工具做什么"
  • 参数描述要写清楚格式和取值范围
  • 如果有多个相似工具,描述里要写清楚它们之间的区别

比如两个查询工具,一个查订单、一个查物流,描述里就要明确"查订单状态用这个""查物流轨迹用那个",别让模型猜。

6. 上线前的检查清单和长期维护建议

6.1 上线前必须确认的几件事

在把 MCP 服务端推到生产之前,我列了一份检查清单,每次部署前过一遍:

检查项确认内容我的实际做法
认证MCP 端点是否有认证API Key 中间件,Key 存在环境变量里
限流是否有调用频率限制按客户端 IP 限流,防止单个客户端打爆
日志是否记录每次调用入参、出参、耗时、错误全记录
超时工具方法是否有超时控制统一 30 秒超时,长任务拆成异步
返回大小返回值是否可控DTO 裁剪字段,列表强制分页
错误信息异常消息是否语义化统一转 McpException,不暴露内部细节

这份清单里的每一项都是我或者同事实际踩过坑之后加上的。尤其是限流,上线第一天就遇到一个客户端配置错误,疯狂重试,把服务端打挂了。加上限流之后稳了。

6.2 工具版本管理

接口会变,工具也会变。我的做法是给工具加版本后缀,比如GetOrderDetailV2,旧版本保留一段时间,等所有客户端都迁移完再下线。这样避免某次接口变更导致所有 AI 客户端突然不可用。

同时,工具的描述里要标注版本和变更说明,方便排查问题时确认客户端用的是哪个版本。

6.3 监控和告警

MCP 服务端的监控和普通 API 一样重要。我接入了应用性能监控,重点看几个指标:工具调用成功率、平均耗时、错误分布。错误率超过阈值就告警。

有个细节:MCP 的错误和普通 HTTP 错误不一样,工具方法抛异常时 HTTP 状态码可能还是 200,错误信息在响应体里。所以监控不能只看 HTTP 状态码,要解析响应体里的错误标记。我在这块踩过坑,一开始只看状态码,结果工具大量报错但监控显示一切正常。

6.4 安全边界

最后说安全。MCP 工具本质上是把你的业务能力暴露给了 AI,而 AI 的行为有一定不可预测性。所以工具方法内部一定要做权限校验,不能假设调用方是可信的。比如查询订单的工具,要校验当前用户有没有权限查这个订单,而不是拿到订单号就直接返回。

我的做法是在工具方法里复用主 API 的权限校验逻辑,把当前用户的身份信息通过 MCP 的上下文传进来。这样 AI 调用和人工调用走的是同一套权限体系,不会出现绕过。

另外,写操作的工具要格外谨慎。查询类工具出问题最多是数据泄露,写操作工具出问题可能直接改坏数据。我的原则是:初期只暴露只读工具,写操作工具等权限体系和审计日志完善之后再逐步开放。我们上线三个月后才开放了第一个写工具,而且加了二次确认机制。

这套东西跑下来,最大的体会是:MCP 本身不难,难的是把工具设计得让 AI 用得顺手,以及把安全和可观测性做扎实。技术选型上,.NET 生态的 MCP SDK 已经足够成熟,ASP.NET Core 的集成也很自然。真正花时间的是那些细节——描述怎么写、粒度怎么切、异常怎么处理、权限怎么控。这些没有标准答案,只能在实际跑的过程中不断调整。我现在回头看第一版工具定义,和现在用的版本已经面目全非了,但每一次调整都是被真实问题逼出来的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询