尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

ASP.NET Core 集成 MCP:将 .NET 接口暴露给 AI 的完整实践

发布时间:2026/9/25 16:12:42

资讯中心
01
ARTICLE

ASP.NET Core 集成 MCP:将 .NET 接口暴露给 AI 的完整实践

ASP.NET Core 集成 MCP:将 .NET 接口暴露给 AI 的完整实践
1. 为什么我要把 .NET 接口直接暴露给 AI去年年底我接手了一个内部工具平台后端是标准的 ASP.NET Core接口文档靠 Swagger 撑着日常调用方是前端和几个内部脚本。后来团队开始用各种 AI 助手做辅助开发问题就来了AI 能写代码但它看不到我们系统里真实的数据结构和业务接口。每次让它帮忙生成一段调用代码我都得手动把 Swagger 里的 JSON 结构复制粘贴过去来回折腾效率极低。这个痛点其实很普遍。AI 大模型本身有很强的推理和生成能力但它和你的业务系统之间隔着一堵墙。MCPModel 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 8ASP.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 TaskOrderDetailDto 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 验证工具是否被正确发现配置完之后怎么确认客户端真的看到了你的工具大多数客户端有个工具列表或者可用工具的面板点开能看到服务端暴露的所有工具名称和描述。如果列表是空的按这个顺序排查服务端是否正常启动/mcp端点是否可访问客户端配置的 url 是否和实际端点一致认证 header 是否正确服务端日志里有没有收到工具列表请求我遇到过一次工具列表为空排查了半天发现是WithToolsFromAssembly()扫描的程序集不对。工具类写在另一个项目里而启动项目没有引用那个项目自然扫不到。解决办法是在WithToolsFromAssembly()里显式指定程序集或者确保启动项目引用了工具所在的项目。4.3 一次完整的调用链路工具被发现之后实际调用是这样的流程用户在 AI 客户端里提问比如帮我查一下订单 ORD20240101001 的状态模型分析意图发现需要调GetOrderDetail工具客户端把调用请求转发到 MCP 服务端的/mcp端点服务端执行工具方法返回结果客户端把结果喂回模型模型生成最终回答。整个过程对用户是透明的用户只看到 AI 回答了订单状态。但对开发者来说每一环都可能出问题。我在服务端加了详细的日志记录每次工具调用的入参和出参排查问题时非常有用。[McpServerTool, Description(...)] public async TaskOrderDetailDto 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)结果模型收到一堆数据库错误信息完全没法处理。正确的做法是捕获底层异常转换成语义化的McpExceptiontry { 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 的集成也很自然。真正花时间的是那些细节——描述怎么写、粒度怎么切、异常怎么处理、权限怎么控。这些没有标准答案只能在实际跑的过程中不断调整。我现在回头看第一版工具定义和现在用的版本已经面目全非了但每一次调整都是被真实问题逼出来的。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。