ASP.NET Core Web API 接入 MCP:让 Claude 与 Cursor 直接调用 .NET 接口
发布时间:2026/9/28 21:34:20来源:尧图网络
1. 为什么我要把 .NET 接口直接交给 AI 调用先说结论把现有的 ASP.NET Core Web API 包装成 MCP 服务端让 Claude、Cursor、VS Code 里的 AI Agent 直接调用你的业务接口这件事的投入产出比远超我的预期。我手上有一套跑了三年的订单中台Swagger 文档一百多个接口以前对接 AI 的方式是复制接口文档 → 粘贴给模型 → 模型生成调用代码 → 我手动跑来回折腾。改成 MCP 之后AI 自己就能查接口、拼参数、发请求、读返回值我只需要在对话里说帮我查一下上周华东区未发货的订单它自己就把链路走完了。MCP 全称 Model Context Protocol是 Anthropic 在 2024 年底开源的一套协议核心思路是把工具能力从模型里解耦出来用标准协议暴露给任意支持 MCP 的客户端。你可以把它理解成 AI 世界的 USB-C以前每个 AI 工具都要自己写一套插件系统现在只要你的服务端说 MCP 话Claude Desktop、Cursor、Continue、VS Code Copilot 都能直接接。对 .NET 开发者来说这意味着你不需要学 Python、不需要碰 Node用熟悉的 ASP.NET Core 就能把自己的接口变成 AI 可调用的工具。这篇文章面向三类人一是手里有现成 .NET Web API、想让 AI 直接调用的后端二是想给团队内部搭一个AI 能查数据的轻量网关的架构师三是单纯想搞明白 MCP 到底怎么落地、不想只看概念文的开发者。我会从协议本质讲起然后给出服务端和客户端两套可跑的代码最后重点讲我在实测中踩到的坑——这部分才是真正值钱的东西官方文档基本不会告诉你。需要提前说明的是MCP 目前生态还在快速演进SDK 版本迭代很快我下面给的代码基于 .NET 8 官方 C# SDK 的稳定版本如果你用的是预览版API 签名可能有差异以你本地 NuGet 拉到的为准。2. MCP 协议到底在传什么把工具抽象成一次 RPC2.1 三个核心概念Tools、Resources、Prompts很多人一上来就写代码结果写到一半发现不知道自己在实现什么。我建议先把 MCP 的三个原语搞清楚这决定了你后面怎么设计接口暴露粒度。Tools工具是最常用的本质是AI 可以主动调用的函数。每个 Tool 有名字、描述、输入参数的 JSON SchemaAI 根据描述决定要不要调、传什么参数。你的 .NET 接口绝大多数会映射成 Tool。Resources资源是AI 可以读取的数据类似文件或数据库记录是被动的、由客户端决定要不要加载进上下文。适合放配置、文档、静态数据。Prompts提示模板是预置的提示词模板用户可以在客户端里选。这个用得最少我基本没在业务里用过。对让 AI 调 .NET 接口这个场景99% 的工作量都在 Tools 上。所以下面重点讲 Tools。2.2 传输层stdio 和 HTTP 该怎么选MCP 支持两种传输方式选错了后面会很痛苦。传输方式适用场景优点缺点stdio本地工具、单机 CLI零网络配置、进程隔离干净无法远程、每个客户端要单独起进程Streamable HTTP服务端部署、多客户端共享可远程、可鉴权、可水平扩展需要处理会话、CORS、超时我的建议很直接如果只是自己本机用stdio 起步最快只要涉及团队共享或者要部署到服务器直接上 Streamable HTTP别犹豫。我一开始图省事用 stdio结果同事想用的时候发现每个人都要装一遍 .NET 运行时、配一遍路径维护成本爆炸后来全部迁到 HTTP。Streamable HTTP 是 2025 年 3 月规范里替代旧 SSE 的方案单端点同时支持 POST 和 GET服务端可以选择用 SSE 流式返回。相比老的 HTTPSSE 双端点方案它更简单也更稳。2.3 一次完整的调用长什么样理解协议最好的方式是看一次真实交互。AI 客户端连上你的服务端后大致流程是客户端发initialize请求双方交换协议版本和能力声明客户端发tools/list你的服务端返回所有 Tool 的定义名字、描述、参数 Schema模型根据用户问题决定调用某个 Tool客户端发tools/call带上参数你的服务端执行实际逻辑比如查数据库、调内部 API返回结果结果作为上下文喂回模型模型继续推理或给出最终回答关键点在于模型看到的只有 Tool 的描述和参数 Schema看不到你的实现。所以描述写得好不好直接决定 AI 会不会正确调用。这一点后面会专门讲。3. 服务端落地把现有 Web API 包装成 MCP Server3.1 项目结构与依赖选择我用的方案是新建一个独立的 MCP 服务端项目而不是把 MCP 塞进现有的业务 API 里。原因有两个一是 MCP 的会话管理和业务 API 的生命周期模型不一样混在一起容易出诡异问题二是独立部署后MCP 层可以单独做鉴权和限流业务 API 不用动。依赖就一个核心包dotnet add package ModelContextProtocol --prerelease dotnet add package ModelContextProtocol.AspNetCore --prerelease第一个是协议核心第二个是 ASP.NET Core 的宿主集成。注意--prerelease这个包目前还在预览阶段正式版发布后去掉即可。项目结构我习惯这样分McpGateway/ ├── Tools/ # 每个 Tool 一个类 │ ├── OrderTools.cs │ └── CustomerTools.cs ├── Services/ # 实际业务逻辑复用现有 Service ├── Program.cs └── appsettings.json3.2 用特性声明一个 Tool从方法签名到 JSON Schema官方 SDK 提供了[McpServerTool]特性加在方法上就自动注册成 Tool。看一个真实例子using ModelContextProtocol.Server; using System.ComponentModel; [McpServerToolType] public class OrderTools { private readonly IOrderService _orderService; public OrderTools(IOrderService orderService) { _orderService orderService; } [McpServerTool(Name query_orders)] [Description(按条件查询订单列表。支持按区域、状态、时间范围过滤返回订单摘要。)] public async TaskOrderQueryResult QueryOrdersAsync( [Description(区域代码如 east_china、north_china不传则查全部)] string? region null, [Description(订单状态pending/shipped/completed/cancelled)] string? status null, [Description(起始时间ISO 8601 格式如 2025-01-01T00:00:00Z)] DateTime? from null, [Description(结束时间ISO 8601 格式)] DateTime? to null, [Description(返回条数上限默认 20最大 100)] int limit 20) { limit Math.Clamp(limit, 1, 100); return await _orderService.QueryAsync(region, status, from, to, limit); } }这里有几个细节值得展开。方法名和 Tool Name 的关系。默认情况下方法名就是 Tool 名但我强烈建议显式指定Name用下划线命名法query_orders而不是QueryOrders。原因是模型对下划线命名的工具识别率更高而且跨语言调用时不会有大小写歧义。参数描述比方法描述更重要。我实测下来模型决定传什么参数主要看参数的[Description]。方法级描述决定要不要调这个工具参数级描述决定怎么调。两个都要写而且参数描述要具体到格式和取值范围。上面region那个描述如果不写如 east_china模型很可能传华东这种中文然后你的接口报错。默认值和可空性。参数有默认值或可空Schema 里就是 optional模型可以选择不传。这个特性很有用比如limit给默认 20模型不传时就用默认值避免它瞎猜一个数字。3.3 注册与启动Program.cs 的最小可用配置var builder WebApplication.CreateBuilder(args); builder.Services.AddSingletonIOrderService, OrderService(); builder.Services.AddSingletonICustomerService, CustomerService(); builder.Services .AddMcpServer() .WithHttpTransport() .WithToolsFromAssembly(); var app builder.Build(); app.MapMcp(/mcp); app.Run();WithToolsFromAssembly()会自动扫描当前程序集里所有带[McpServerToolType]的类把里面的[McpServerTool]方法注册进去。MapMcp(/mcp)把 MCP 端点挂到/mcp路径上。跑起来之后用 curl 测一下握手curl -X POST http://localhost:5000/mcp \ -H Content-Type: application/json \ -H Accept: application/json, text/event-stream \ -d {jsonrpc:2.0,id:1,method:initialize,params:{protocolVersion:2025-03-26,capabilities:{},clientInfo:{name:test,version:1.0}}}能返回serverInfo和capabilities就说明服务端活了。注意Accept头必须同时包含application/json和text/event-stream只写一个会被拒这个坑我踩过报错信息还特别含糊。3.4 鉴权别让 MCP 端点裸奔MCP 端点默认没有任何鉴权谁都能调你的接口。生产环境必须加。我的做法是在MapMcp前面挂一层中间件校验 Bearer Tokenapp.Use(async (ctx, next) { if (ctx.Request.Path.StartsWithSegments(/mcp)) { var auth ctx.Request.Headers.Authorization.ToString(); if (!auth.StartsWith(Bearer ) || !ValidateToken(auth[Bearer .Length..])) { ctx.Response.StatusCode 401; return; } } await next(); });Token 的校验逻辑复用你现有的认证体系就行。客户端那边在配置里带上Authorization头即可。这里有个细节Streamable HTTP 的 GET 请求用于 SSE 流也要带鉴权头有些客户端只在 POST 时带会导致流建立失败需要在客户端配置里确认。4. 客户端接入让 Claude、Cursor、VS Code 都能连上4.1 客户端配置的通用结构MCP 客户端的配置基本都是 JSON结构大同小异。以 Claude Desktop 为例配置文件在claude_desktop_config.json{ mcpServers: { order-gateway: { url: http://localhost:5000/mcp, headers: { Authorization: Bearer your-token-here } } } }Cursor 的配置在.cursor/mcp.jsonVS Code 的在.vscode/mcp.json字段名基本一致。stdio 模式下把url换成commandargs即可。4.2 连接失败的排查顺序客户端连不上是最高频的问题我总结了一个排查顺序按这个走基本能定位服务端是否在跑curl一下/mcp端点确认返回正常协议版本是否匹配客户端和服务端的protocolVersion要对得上差一个大版本会直接握手失败Accept 头HTTP 模式下必须同时接受application/json和text/event-stream鉴权头401 就是 Token 问题检查是否过期、格式是否正确CORS如果客户端是浏览器扩展跨域会被拦服务端要配 CORS防火墙/端口本地一般没事远程部署要确认端口开放我遇到最坑的一次是客户端一直显示连接中日志里啥也没有最后发现是服务端返回的Content-Type是application/json但客户端期望text/event-stream因为服务端没正确处理流式响应。解决办法是在MapMcp时确保传输层配置正确别自己手写响应。4.3 让 AI 正确选择工具描述工程的实战技巧工具注册好了AI 不一定用得对。我踩过的坑里一半以上是描述写得不好导致的。分享几条实测有效的经验工具数量控制在 20 个以内。我一开始把一百多个接口全暴露出去结果模型选择困难经常调错工具。后来按业务域拆成多个 MCP Server每个 Server 只暴露 10-15 个高频工具准确率明显提升。工具名要有业务语义。query_orders比get_data好create_customer比add好。模型靠名字做第一轮筛选名字模糊它就得读描述读描述就有理解偏差的风险。描述里写清楚什么时候用和什么时候不用。比如[Description(查询订单列表。当用户询问订单状态、订单数量、订单明细时使用。 注意如果用户问的是订单金额统计请用 summarize_orders 工具不要用这个。)]这种负向指引能显著减少误调用。参数描述给出示例值。前面提过region参数写如 east_china模型就不会传中文。日期参数写如 2025-01-01T00:00:00Z模型就不会传上周。返回值要结构化。别返回一大坨字符串用强类型对象SDK 会自动序列化成 JSON。模型对结构化数据的理解能力远强于自然语言段落。5. 实测踩坑那些文档不会告诉你的问题5.1 Swagger 转 MCP 的自动化陷阱热词里swagger 转 mcp出现频率很高我也试过自动转换。思路是读 Swagger JSON把每个 operation 映射成一个 Tool。听起来很美实际用下来问题不少。问题一Swagger 的 description 质量参差不齐。很多项目的 Swagger 注释是自动生成的或者干脆没写转出来的 Tool 描述是空的模型根本不知道怎么用。我试过一个项目转出来 80 个 Tool模型一个都调不对。问题二参数 Schema 太复杂。Swagger 里嵌套对象、oneOf、anyOf这些转成 MCP 的 JSON Schema 后模型理解不了。特别是$ref引用转换工具处理不好会丢字段。问题三接口粒度不对。Swagger 里的接口是给程序员看的粒度细、参数多。AI 调用需要的是业务语义的粗粒度工具。比如创建订单在 Swagger 里可能是三个接口创建、加商品、提交但 AI 需要的是一个create_order工具。我的结论是自动转换适合做初稿但必须人工重写描述和合并粒度。我现在的做法是自动生成 Tool 骨架然后人工过一遍把描述补全、把相关接口合并。一百个接口大概花半天时间值得。5.2 长耗时接口的超时与流式返回MCP 的tools/call默认有超时限制不同客户端不一样Claude Desktop 大概是 60 秒。如果你的接口要跑几分钟比如生成报表会直接超时。解决方案有两个。一是把长任务改成异步Tool 立即返回一个task_id再提供一个query_task_status工具让 AI 轮询。二是用 Streamable HTTP 的流式能力服务端边执行边推送进度。前者实现简单我推荐先用前者。[McpServerTool(Name start_report)] public string StartReport(string reportType) { var taskId Guid.NewGuid().ToString(); _ Task.Run(() GenerateReportAsync(taskId, reportType)); return taskId; } [McpServerTool(Name query_report_status)] public ReportStatus QueryReportStatus(string taskId) { return _reportTasks.GetValueOrDefault(taskId) ?? new ReportStatus { State not_found }; }注意Task.Run里的异常要自己捕获否则会静默丢失AI 那边只会看到任务永远进行中。5.3 会话状态与并发别把 MCP Server 写成有状态服务MCP 的 Streamable HTTP 是有会话概念的initialize之后会返回一个Mcp-Session-Id后续请求要带上。但这不意味着你可以把业务状态存在服务端内存里。我一开始图省事把用户上下文存在一个静态字典里结果多客户端并发时串号了。正确做法是MCP Server 保持无状态所有状态要么放请求参数里要么放外部存储Redis、数据库。会话 ID 只用来关联传输层不要拿它当业务主键。另外AddMcpServer()注册的服务默认是单例还是瞬态要注意。我建议业务 Service 用AddScoped或AddSingleton明确指定别用默认否则可能出现 DbContext 跨请求复用的问题。5.4 返回值大小别把整个数据库塞给模型模型有上下文窗口限制你返回一个几万行的列表直接把上下文撑爆。我踩过一次查订单返回了 5000 条客户端直接卡死。控制策略所有列表类 Tool 强制分页默认返回 20 条最大 100 条。在参数里加limit和offset并在描述里明确告诉模型结果可能被截断需要更多请用 offset 翻页。另外返回值里带上total字段让模型知道总数。对于大文本字段比如订单备注、商品详情考虑截断或摘要别原样返回。6. 从能跑到好用性能与可观测性优化6.1 给 MCP 层加日志和追踪MCP 调用出问题时最难的是定位。因为链路是用户 → AI 客户端 → MCP Server → 业务 API中间任何一环出问题表现都是AI 说它调不了。我的做法是在 MCP 层加结构化日志记录每次tools/call的工具名、参数、耗时、返回状态。用ILogger配合 Serilog 输出到文件出问题时直接 grep。[McpServerTool(Name query_orders)] public async TaskOrderQueryResult QueryOrdersAsync(...) { var sw Stopwatch.StartNew(); _logger.LogInformation(MCP call query_orders: region{Region}, status{Status}, region, status); try { var result await _orderService.QueryAsync(...); _logger.LogInformation(MCP call query_orders done in {Elapsed}ms, count{Count}, sw.ElapsedMilliseconds, result.Items.Count); return result; } catch (Exception ex) { _logger.LogError(ex, MCP call query_orders failed); throw; } }更进一步可以接 OpenTelemetry把 MCP 调用和下游 API 调用串成一条 trace。这个对排查性能问题特别有用能一眼看出时间花在哪。6.2 缓存与限流MCP 调用有个特点AI 可能会重复调用同一个工具。比如它不确定参数对不对会试几次。如果你的接口查数据库很慢这会造成压力。对读多写少的查询类 Tool加一层内存缓存很划算。用IMemoryCachekey 用工具名参数哈希TTL 设短一点比如 30 秒既能挡住重复调用又不会返回太旧的数据。限流方面MCP 端点建议单独限流别和业务 API 共用配额。用 ASP.NET Core 的 Rate Limiting 中间件按 Token 或 IP 限流防止某个客户端把服务打满。6.3 错误信息的写法让 AI 能自我纠正Tool 执行失败时返回的错误信息质量直接影响 AI 能不能自己修正。别直接抛异常让框架返回 500那样 AI 只看到调用失败不知道怎么办。我的做法是捕获业务异常返回结构化的错误对象public class ToolError { public string Code { get; set; } public string Message { get; set; } public string Hint { get; set; } }Hint字段专门写给 AI 看比如region 参数只接受 east_china/north_china/south_china请检查输入。实测下来有了 HintAI 自我纠正的成功率能到 80% 以上不用人工介入。7. 我在这套方案上的一些个人体会整套东西跑通到现在大概三个月服务端稳定运行团队里五六个同事都在用。回头看最大的收获不是技术本身而是对接口该怎么设计有了新认识。以前设计 API 是给人用的参数可以很细、可以有很多个接口。现在设计 MCP Tool 是给 AI 用的得站在AI 会怎么理解这句话的角度去想。描述写得好AI 一次调对描述写得烂AI 反复试错用户体验直接崩。这个转变挺反直觉的但确实是我踩了无数坑之后才悟到的。另外一个体会是别追求一步到位。我一开始想把所有接口都 MCP 化结果工具太多、描述太杂效果很差。后来砍到只暴露最高频的十几个工具反而好用。先把核心场景跑通再慢慢扩这个节奏比较稳。最后分享一个小技巧给每个 Tool 写一个自测用例。就是一段固定的用户问法加上期望调用的工具和参数。改完描述后跑一遍看 AI 还能不能正确调用。这个比人工测快得多也能防止改描述时把之前调好的搞坏。我现在的用例集有三十多条每次发版前跑一遍心里有底。
网站建设高端定制企业官网