☰
ASP.NET Core 集成 MCP:让 AI 直接调用你的接口
2026/9/25 4:24:45 网站建设 项目流程

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

1.1 从一个真实痛点说起

去年底我接手了一个内部工单系统的维护工作,前端同事跑过来跟我说:“能不能让 AI 直接帮我查工单状态?我不想每次都在 Swagger 页面里翻接口、填参数、点 Try it out。”当时我的第一反应是——这不就是个 API 调用吗,写个脚本不就行了?但仔细一想,问题没那么简单。

传统的做法是:AI 模型生成一段代码,人去执行,拿到结果再贴回给 AI。这个链路里,人始终是中间人。而 MCP(Model Context Protocol)要解决的核心问题,就是把这个“人”从链路里拿掉,让 AI 直接调用你的接口,拿到结果,继续推理。

我花了大概两周时间,把一个中等规模的 ASP.NET Core 项目改造成了 MCP 服务端,同时写了一个客户端做验证。踩了不少坑,也积累了一些经验。这篇文章就是把这整个过程拆开揉碎讲清楚,包括为什么要这么设计、每一步怎么做、哪些地方容易翻车。

1.2 MCP 到底是什么,用大白话解释

MCP 全称 Model Context Protocol,翻译过来叫“模型上下文协议”。你可以把它理解成 AI 世界里的“USB 接口标准”。以前每个 AI 工具要调用外部能力,都得自己定一套协议,A 工具用 HTTP,B 工具用 WebSocket,C 工具用 gRPC,乱得很。MCP 就是把这些统一起来,定义了一套标准的“工具描述”和“调用方式”。

具体到 .NET 场景,你的 ASP.NET Core 接口本来是通过 HTTP 暴露给前端或者别的服务的。现在你想让 AI 也能调,有两种思路:

  • 思路一:把现有接口包装成 MCP 工具,AI 通过 MCP 协议调用,MCP 服务端再转发到你的业务接口。
  • 思路二:直接在 ASP.NET Core 项目里集成 MCP 服务端能力,让同一个进程既提供 REST API,又提供 MCP 工具。

我选的是思路二,原因后面会详细说。先给结论:思路二的好处是复用现有的依赖注入、认证、日志体系,不用额外维护一个中间层。

1.3 适合谁来读这篇内容

如果你满足以下任意一条,这篇内容应该对你有用:

  • 手里有现成的 ASP.NET Core 项目,想让 AI 助手直接调用里面的接口
  • 正在做 AI Agent 相关开发,需要把内部系统能力暴露给模型
  • 对 MCP 协议感兴趣,想找一个 .NET 技术栈的落地案例
  • 已经在用 Swagger 管理接口,想进一步把接口“AI 化”

不需要你提前了解 MCP 协议细节,但需要你熟悉 ASP.NET Core 的基本开发流程,知道什么是依赖注入、中间件、Controller。如果这些还不熟,建议先补一下基础。

2. 整体方案设计与技术选型

2.1 两种集成方式的取舍

前面提到了两种思路,这里展开说一下为什么选思路二。

思路一的做法是单独起一个 MCP 服务端进程,里面定义工具,每个工具的实现就是去 HTTP 调用你的业务接口。这种做法的好处是解耦彻底,MCP 服务端挂了不影响业务接口。但坏处也很明显:多了一层网络跳转,延迟增加;认证要透传,Token 管理麻烦;业务接口改了参数,MCP 服务端也得跟着改,维护成本翻倍。

思路二是在同一个 ASP.NET Core 进程里,既注册 Controller 提供 REST API,又注册 MCP 服务端提供工具调用。两者共享同一个依赖注入容器,业务逻辑直接调 Service 层,不走 HTTP。这样做的好处是:

  • 零网络开销,工具调用直接走内存
  • 认证体系复用,MCP 调用和 REST 调用走同一套授权逻辑
  • 业务逻辑只写一遍,Service 层同时服务两种入口
  • 部署简单,还是一个进程

坏处是 MCP 服务端的稳定性会影响主进程。但实际测试下来,MCP 服务端的资源消耗很低,只要工具实现里不做阻塞操作,基本不会拖垮主进程。

2.2 核心依赖包的选择

.NET 生态里做 MCP 服务端,目前主流的选择是ModelContextProtocol这个 NuGet 包。我用的版本是 0.1.0-preview,虽然还是预览版,但核心功能已经稳定了。

安装命令很简单:

dotnet add package ModelContextProtocol --prerelease dotnet add package ModelContextProtocol.AspNetCore --prerelease

第一个包是核心协议实现,第二个包提供了 ASP.NET Core 的集成扩展。如果你只需要做客户端,装第一个就够了。

这里有个坑要注意:预览版的 API 变动比较频繁,建议在csproj里锁定版本号,不要用浮动版本。我就因为没锁版本,某次dotnet restore之后编译直接报错,排查了半天才发现是包升级导致 API 签名变了。

<PackageReference Include="ModelContextProtocol" Version="0.1.0-preview.12" /> <PackageReference Include="ModelContextProtocol.AspNetCore" Version="0.1.0-preview.12" />

2.3 项目结构规划

我建议在现有项目里新建一个Mcp文件夹,专门放 MCP 相关的代码。结构大概是这样:

YourProject/ ├── Controllers/ # 现有 REST API ├── Services/ # 业务逻辑层 ├── Mcp/ │ ├── Tools/ # MCP 工具定义 │ ├── McpExtensions.cs # 注册扩展方法 │ └── McpOptions.cs # 配置类 ├── Program.cs └── appsettings.json

这样做的好处是 MCP 相关代码集中管理,不会和现有代码混在一起。后面如果要单独开关 MCP 功能,也方便。

2.4 认证方案的设计

MCP 客户端调用服务端时,需要携带认证信息。我的做法是复用现有的 JWT 认证体系。MCP 客户端在初始化时配置一个 HTTP Header,里面带上 Bearer Token。服务端这边,MCP 的端点也走同样的 JWT 中间件。

这里有个细节:MCP 协议本身支持在初始化握手时传递认证信息,但不同客户端的实现不太一样。最稳妥的做法还是走标准的 HTTP Authorization Header,兼容性最好。

注意:不要把 MCP 端点暴露在公网上而不加认证。我见过有人图省事,MCP 服务端直接监听 0.0.0.0 且不加任何认证,结果被扫描到之后,AI 工具被恶意调用,产生了大量无效请求。认证这一步绝对不能省。

3. 服务端落地:把接口变成 MCP 工具

3.1 定义第一个 MCP 工具

假设我们有一个查询订单状态的服务IOrderService,里面有个方法GetOrderStatusAsync(string orderId)。现在要把它暴露成 MCP 工具。

首先定义一个工具类:

using ModelContextProtocol.Server; using System.ComponentModel; namespace YourProject.Mcp.Tools; [McpServerToolType] public class OrderTools { private readonly IOrderService _orderService; public OrderTools(IOrderService orderService) { _orderService = orderService; } [McpServerTool, Description("根据订单号查询订单当前状态,返回状态描述和更新时间")] public async Task<string> GetOrderStatus( [Description("订单号,格式为 ORD 开头的字符串")] string orderId) { var result = await _orderService.GetOrderStatusAsync(orderId); return $"订单 {orderId} 当前状态:{result.Status},更新时间:{result.UpdatedAt:yyyy-MM-dd HH:mm:ss}"; } }

几个关键点解释一下:

[McpServerToolType]标记这个类是一个工具容器,MCP 框架会自动扫描里面的方法。[McpServerTool]标记具体的方法是一个可调用的工具。[Description]特性非常重要,它决定了 AI 能不能正确理解这个工具的用途和参数含义。

我实测下来,Description 写得好不好,直接决定了 AI 调用工具的准确率。比如上面这个例子,如果只写“查询订单”,AI 可能会在用户问“帮我看看订单到哪了”的时候犹豫要不要调用。写成“根据订单号查询订单当前状态,返回状态描述和更新时间”,AI 就能明确知道这个工具能解决什么问题。

3.2 参数类型的处理

MCP 工具的参数支持基本类型:string、int、bool、double 等。复杂对象需要序列化成 JSON 字符串传递,或者拆成多个简单参数。

我建议尽量用简单参数,因为 AI 生成参数值时,简单类型的准确率明显更高。比如要传一个日期范围,不要传一个DateRange对象,而是传startDate和endDate两个字符串参数。

[McpServerTool, Description("查询指定时间范围内的订单列表")] public async Task<string> GetOrdersByDateRange( [Description("开始日期,格式 yyyy-MM-dd")] string startDate, [Description("结束日期,格式 yyyy-MM-dd")] string endDate, [Description("每页数量,默认 20")] int pageSize = 20) { // 实现逻辑 }

可选参数给默认值,这样 AI 不传的时候也能正常工作。但要注意,Description 里要说明默认值是多少,否则 AI 不知道不传参数会是什么行为。

3.3 在 Program.cs 里注册 MCP 服务端

注册代码不复杂,但顺序很重要:

var builder = WebApplication.CreateBuilder(args); // 现有的服务注册 builder.Services.AddControllers(); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { /* 配置 */ }); builder.Services.AddScoped<IOrderService, OrderService>(); // 注册 MCP 服务端 builder.Services.AddMcpServer() .WithToolsFromAssembly(); // 自动扫描当前程序集里的工具类 var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); // 映射 MCP 端点 app.MapMcp("/mcp"); app.Run();

WithToolsFromAssembly()会自动扫描当前程序集里所有标记了[McpServerToolType]的类,并注册里面的工具方法。工具类的构造函数参数会从依赖注入容器里解析,所以IOrderService能直接注入进来。

MapMcp("/mcp")把 MCP 端点映射到/mcp路径。客户端连接的时候就用这个地址。

3.4 和 Swagger 共存的处理

现有项目里通常已经有 Swagger 了,加了 MCP 之后,Swagger 页面里可能会多出一些 MCP 相关的端点,看起来比较乱。我的做法是在 Swagger 配置里过滤掉 MCP 相关的路径:

builder.Services.AddSwaggerGen(options => { options.DocInclusionPredicate((docName, apiDesc) => { // 过滤掉 MCP 端点 return !apiDesc.RelativePath?.StartsWith("mcp") ?? true; }); });

这样 Swagger 页面保持干净,只展示业务接口。MCP 端点不需要在 Swagger 里展示,因为它不是给人看的,是给 AI 用的。

另外,如果你之前给 API 加了统一前缀(比如/api/v1),MCP 端点建议不要加这个前缀,单独走/mcp路径。这样职责清晰,也方便后面做独立的限流和监控。

3.5 工具粒度的设计原则

这是我在实际项目中感受最深的一点:工具不是越多越好,也不是越细越好。

一开始我把每个 Service 方法都暴露成了工具,结果 AI 面对几十个工具,选择困难,经常调错。后来我做了合并,把相关的操作整合成一个工具,通过参数来区分具体行为。

比如原来有CreateOrder、CancelOrder、UpdateOrder三个工具,我合并成了一个ManageOrder工具,加一个action参数来区分。这样 AI 只需要知道“有个工具能管理订单”,具体做什么由 action 决定。

但也不能合并得太粗。如果一个工具能做的事情太多,Description 就很难写清楚,AI 反而不知道怎么用。我的经验是:一个工具对应一个明确的业务意图,参数控制在 5 个以内。

粒度优点缺点适用场景
细粒度(一方法一工具)职责清晰工具数量多,AI 选择困难工具总数少于 10 个
中粒度(按业务聚合)数量适中,意图明确需要设计 action 参数大多数场景推荐
粗粒度(一系统一工具)数量极少Description 难写,AI 容易懵不推荐

4. 客户端落地:让 AI 真正调起来

4.1 客户端的基本结构

MCP 客户端的作用是连接服务端,获取工具列表,然后根据 AI 的决策调用对应工具。在 .NET 里,客户端可以是一个控制台程序,也可以集成在 Web 应用里。

我写了一个控制台客户端做验证,核心代码如下:

using ModelContextProtocol.Client; var transport = new HttpClientTransport(new HttpClientTransportOptions { Endpoint = new Uri("https://localhost:8889/mcp"), AdditionalHeaders = new Dictionary<string, string> { ["Authorization"] = "Bearer YOUR_TOKEN_HERE" } }); await using var client = await McpClient.CreateAsync(transport); // 获取工具列表 var tools = await client.ListToolsAsync(); foreach (var tool in tools) { Console.WriteLine($"工具:{tool.Name} - {tool.Description}"); } // 调用工具 var result = await client.CallToolAsync("GetOrderStatus", new Dictionary<string, object?> { ["orderId"] = "ORD20240101001" }); Console.WriteLine(result.Content.First().Text);

HttpClientTransport是 HTTP 传输方式,适合远程连接。如果服务端和客户端在同一台机器上,也可以用StdioTransport,通过标准输入输出通信,延迟更低。

4.2 把工具列表喂给 AI 模型

客户端拿到工具列表后,需要把这些工具的描述转换成 AI 模型能理解的格式,通常是 JSON Schema。MCP 客户端库一般会提供转换方法。

以 OpenAI 风格的 API 为例,转换后的格式大概是这样:

{ "type": "function", "function": { "name": "GetOrderStatus", "description": "根据订单号查询订单当前状态,返回状态描述和更新时间", "parameters": { "type": "object", "properties": { "orderId": { "type": "string", "description": "订单号,格式为 ORD 开头的字符串" } }, "required": ["orderId"] } } }

把这段 JSON 作为tools参数传给模型,模型在需要的时候就会返回一个tool_calls,里面包含要调用的工具名和参数。客户端解析这个返回,调用对应的 MCP 工具,再把结果传回给模型,形成闭环。

4.3 处理工具调用的返回结果

MCP 工具的返回结果是一个CallToolResult对象,里面包含一个Content列表。每个 Content 可能是文本、图片或者其他类型。大多数情况下我们只关心文本内容。

但这里有个坑:如果工具执行过程中抛异常了,MCP 框架会把异常信息包装成错误结果返回,而不是直接抛出。所以客户端需要判断result.IsError属性:

var result = await client.CallToolAsync("GetOrderStatus", args); if (result.IsError) { Console.WriteLine($"工具调用失败:{result.Content.First().Text}"); } else { Console.WriteLine($"工具调用成功:{result.Content.First().Text}"); }

服务端这边,工具方法里抛出的异常会被框架捕获,异常消息会作为错误内容返回。所以工具方法里要写好异常处理,给 AI 返回有意义的错误信息,而不是一堆堆栈跟踪。

4.4 多轮对话中的工具调用管理

实际使用中,AI 可能需要在一次对话里调用多个工具。比如用户问“帮我查一下订单 ORD001 的状态,如果还没发货就取消掉”,AI 需要先调GetOrderStatus,根据结果再决定要不要调CancelOrder。

客户端需要维护一个对话历史,每次工具调用和结果都要追加到历史里,再传给模型进行下一轮推理。这个过程可能循环多次,直到模型不再请求调用工具,而是直接给出最终回答。

我建议设置一个最大循环次数(比如 10 次),防止模型陷入无限调用。同时要记录每次调用的工具名和参数,方便排查问题。

实操心得:在开发调试阶段,把每次工具调用的请求和响应都打到日志里。我一开始没打日志,AI 调错了工具我都不知道它传了什么参数,排查起来很痛苦。后来加了详细日志,一眼就能看出是 Description 写得不够清楚,还是参数格式有问题。

5. 常见问题与排查技巧实录

5.1 连接失败类问题

问题一:客户端连不上服务端,报 SSL 错误

本地开发时,ASP.NET Core 默认用自签名证书,客户端验证会失败。解决办法是在客户端配置里跳过证书验证(仅限开发环境):

var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true }; var httpClient = new HttpClient(handler);

生产环境一定要用受信任的证书,不要跳过验证。

问题二:连接超时

检查服务端是否真的在监听 MCP 端点。可以在浏览器里访问https://localhost:8889/mcp,如果返回 404,说明MapMcp没生效。常见原因是MapMcp写在了app.Run()之后,或者路径写错了。

5.2 工具调用类问题

问题三:AI 不调用工具,直接编造答案

这是最常见的问题,根本原因通常是工具 Description 写得不够明确。AI 不确定这个工具能不能回答用户的问题,就选择自己编。

解决办法:把 Description 写成“当用户问 XXX 时使用此工具”的格式。比如不要写“查询订单状态”,而是写“当用户询问订单当前状态、物流进度时,使用此工具查询”。

问题四:AI 调用工具时参数格式错误

比如日期格式传成了2024/01/01而不是2024-01-01。解决办法是在参数的 Description 里明确写出格式要求,并且服务端做参数校验,格式不对时返回明确的错误提示,让 AI 知道怎么修正。

问题五:工具返回结果太长,AI 处理不了

如果工具返回一个很长的列表,AI 的上下文可能装不下。解决办法是在工具实现里做分页,或者只返回摘要信息,详细内容让 AI 再调一次工具获取。

5.3 性能与稳定性问题

问题六:MCP 调用导致主进程响应变慢

如果工具方法里有耗时操作(比如查数据库、调外部接口),会占用线程池资源。解决办法是把耗时操作改成异步,并且设置超时时间。MCP 框架支持在工具方法上设置超时,超时后会自动取消。

问题七:并发调用时出现资源竞争

多个 AI 客户端同时调用同一个工具时,如果工具方法里有共享状态,可能出现竞争。解决办法是工具类注册为 Scoped 生命周期,每次调用创建新实例,避免共享状态。

5.4 常见问题速查表

问题现象可能原因排查方法解决方案
客户端连不上端点路径错误浏览器访问 /mcp检查 MapMcp 路径
SSL 证书错误自签名证书查看错误详情开发环境跳过验证
AI 不调工具Description 不清晰查看工具列表改写 Description
参数格式错误缺少格式说明查看调用日志补充参数 Description
返回结果过长未分页查看返回内容实现分页或摘要
主进程变慢同步阻塞调用查看线程池状态改异步 + 超时
并发竞争共享状态查看异常日志改 Scoped 生命周期

5.5 几个容易忽略的细节

第一个细节:MCP 工具的命名不要用下划线或特殊字符,用驼峰或帕斯卡命名。有些 AI 模型对工具名的解析规则比较严格,特殊字符可能导致调用失败。

第二个细节:工具返回的文本里不要包含 Markdown 格式。AI 模型可能会把 Markdown 符号当成内容的一部分,影响后续推理。返回纯文本就好,格式化的事情让 AI 自己去做。

第三个细节:如果工具需要调用外部 HTTP 接口,记得设置合理的超时和重试策略。我遇到过外部接口偶发超时,导致 MCP 工具调用卡住,AI 那边一直等不到结果。后来加了 5 秒超时和一次重试,问题就解决了。

第四个细节:在appsettings.json里给 MCP 相关配置单独开一个节点,方便不同环境用不同配置。比如开发环境可以开启详细日志,生产环境关闭。

{ "Mcp": { "Enabled": true, "EndpointPath": "/mcp", "EnableDetailedLogging": false, "ToolTimeoutSeconds": 30 } }

然后在Program.cs里根据配置决定是否注册 MCP 服务端。这样如果某个环境不需要 MCP,直接改配置就行,不用改代码。

6. 从能用到好用:几个进阶优化点

6.1 给工具加上权限控制

不是所有 AI 客户端都应该能调用所有工具。我的做法是在工具方法里检查当前用户的角色,没有权限就返回错误信息。

[McpServerTool, Description("取消指定订单")] public async Task<string> CancelOrder( [Description("订单号")] string orderId, IHttpContextAccessor httpContextAccessor) { var user = httpContextAccessor.HttpContext?.User; if (!user.IsInRole("OrderManager")) { return "当前用户没有取消订单的权限"; } // 执行取消逻辑 }

IHttpContextAccessor可以从依赖注入里拿到,这样工具方法就能访问当前请求的上下文信息。注意要把IHttpContextAccessor注册到容器里:

builder.Services.AddHttpContextAccessor();

6.2 工具调用的审计日志

为了排查问题和满足合规要求,我建议记录每次工具调用的详细信息:谁调的、什么时候调的、调了什么工具、传了什么参数、返回了什么结果、耗时多少。

这些信息可以在 MCP 中间件里统一记录,不用每个工具方法里都写一遍。MCP 框架提供了拦截器机制,可以注册一个IMcpServerToolInterceptor来实现。

6.3 工具版本管理

当业务接口升级时,MCP 工具也可能需要改。为了不影响已经在用的 AI 客户端,我建议给工具加版本号。比如GetOrderStatusV2,旧版本保留一段时间,等所有客户端都升级后再下线。

或者在 Description 里标注版本,让 AI 知道有新版本可用。但这种方式不太可靠,AI 不一定能正确理解版本差异。最稳妥的还是工具名里带版本号。

6.4 和现有 Swagger 文档的联动

既然已经有 Swagger 文档了,能不能自动生成 MCP 工具?理论上可以,但实际做下来效果一般。因为 Swagger 里的接口描述通常比较技术化,不适合直接给 AI 看。MCP 工具的 Description 需要针对 AI 的理解能力做优化,人工编写效果更好。

不过可以做一个辅助工具:从 Swagger 文档里提取接口列表和参数信息,生成 MCP 工具的代码骨架,然后人工补充 Description。这样能省一部分工作量。

7. 一些个人体会

这套方案我在两个项目里落地过,一个内部工单系统,一个对外的小程序后端。工单系统那边,AI 助手接入后,客服人员查订单、改状态的效率大概提升了三成,因为不用再切到后台管理系统里操作了,直接在对话窗口里说一句话就行。

小程序后端那边,主要是给运营同事用,他们不懂技术,但通过 AI 助手能直接查数据、导出报表,省了很多提需求的沟通成本。

踩过的坑主要集中在前两天,工具 Description 写不好、认证配置不对、SSL 证书报错,这些问题排查起来比较费时间。但一旦跑通,后面的维护成本很低。业务逻辑改的时候,Service 层改完,REST API 和 MCP 工具同时生效,不用改两遍。

如果你正准备做类似的事情,我的建议是先从一两个简单的查询类工具开始,跑通整个链路,再逐步增加复杂的操作类工具。不要一上来就把所有接口都暴露出去,那样调试起来会很乱。

另外,MCP 协议本身还在演进中,预览版的 API 可能还会变。建议关注官方仓库的更新,升级版本时先在小项目里验证,没问题再推到主项目。

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

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

立即咨询