1. 为什么我要把 .NET 接口直接暴露给 AI
1.1 从一个真实痛点说起
去年底我接手了一个内部工单系统的维护工作,前端是 Vue,后端是 ASP.NET Core Web API,数据库 SQL Server。日常最烦的事情不是写业务代码,而是每天都有同事跑来问:“帮我查一下订单号 XXX 的状态”“帮我把这个客户的所有历史工单导出来”“这个接口的入参格式是什么,文档在哪”。这些事情本身不复杂,但极其消耗时间,而且提问的人往往不会用 Swagger,也不愿意学。
后来我尝试把大模型接进来,让 AI 直接调用我现有的 .NET 接口。用户只需要在聊天框里说“帮我查一下订单 12345 的状态”,AI 就能自动识别意图、调用对应的 API、把结果整理成自然语言返回。这个过程中,MCP(Model Context Protocol)就是关键的桥梁。
MCP 是什么?简单说,它是一套让 AI 模型能够发现并调用外部工具、数据源的协议标准。你可以把它理解成“AI 世界的 USB 接口”——只要你的服务实现了 MCP 协议,任何支持 MCP 的 AI 客户端都能直接调用你的能力,不需要为每个 AI 平台单独写适配层。对于 .NET 开发者来说,这意味着你现有的 ASP.NET Core 接口、WPF 桌面工具、甚至 WinForm 里封装好的业务逻辑,都有机会被 AI 直接调度。
这篇文章适合谁看?如果你手里有 .NET 项目,想让 AI 帮你调用现有接口,或者你想把自己的 .NET 服务包装成 AI 可调用的工具,那接下来的内容应该能帮你少走不少弯路。我会从协议理解、服务端搭建、客户端对接、Swagger 集成、常见坑排查这几个维度,把整套流程拆开讲清楚。
1.2 MCP 到底解决了什么问题
在没有 MCP 之前,让 AI 调用外部接口通常有三种做法。第一种是写死提示词,把接口文档塞进 System Prompt 里,让模型自己生成 HTTP 请求。这种做法极其脆弱,接口一改就全废,而且模型经常编造不存在的参数。第二种是给每个 AI 平台写插件,OpenAI 的 Function Calling 一套、Claude 的 Tool Use 一套、国内各家平台又各有一套,维护成本高得离谱。第三种是搭一个中间层,把接口包装成特定格式,但中间层本身又需要大量胶水代码。
MCP 的思路不一样。它定义了一套标准的服务端-客户端通信协议,服务端负责暴露“工具”(Tools)、“资源”(Resources)和“提示”(Prompts),客户端负责把这些能力转译给具体的 AI 模型。服务端不需要关心对面是哪个模型,客户端也不需要关心服务端用什么语言写的。这种解耦带来的直接好处就是:我只需要在 .NET 这边实现一次 MCP 服务端,之后不管团队用的是哪家 AI 客户端,都能直接接进来。
从协议层面看,MCP 基于 JSON-RPC 2.0,支持 stdio 和 HTTP+SSE 两种传输方式。stdio 适合本地进程间通信,比如 VS Code 插件调用本地工具;HTTP+SSE 适合远程服务,比如把部署在服务器上的 .NET API 暴露给云端 AI 客户端。对于 .NET 开发者来说,这两种方式都有对应的实现路径,后面我会分别展开。
1.3 整体方案选型与架构思路
我最终落地的方案是这样的:在现有的 ASP.NET Core 项目里增加一个 MCP 服务端模块,通过 HTTP+SSE 对外暴露工具能力;同时写一个轻量的 MCP 客户端,负责把 AI 客户端的请求转发到 MCP 服务端,再把结果回传给 AI。Swagger 这边做了一层自动化转换,把现有的 OpenAPI 描述自动映射成 MCP 工具定义,省去手写工具描述的工作量。
为什么选 HTTP+SSE 而不是 stdio?因为我的 .NET 服务是部署在内部服务器上的,多个客户端需要同时访问,stdio 那种进程绑定的模式不适合。HTTP+SSE 虽然实现起来稍微复杂一点,但扩展性和可维护性都好很多。另外,SSE 是服务器推送事件,天然适合 MCP 这种需要服务端主动通知客户端的场景。
为什么用 Swagger 自动转换?因为手写 MCP 工具定义太痛苦了。一个中等规模的 .NET 项目动辄几十上百个接口,每个接口都要写工具名、描述、参数 schema,写到最后肯定会出现描述不一致、参数遗漏的问题。Swagger 里已经有完整的 OpenAPI 描述,直接解析 JSON 然后映射成 MCP 工具定义,既准确又省事。当然,自动转换不是万能的,有些接口需要人工调整描述和参数约束,这个后面会细说。
架构上分三层:最底层是现有的 ASP.NET Core Web API,保持不动;中间层是 MCP 服务端,负责把 API 能力包装成 MCP 工具;最上层是 MCP 客户端和 AI 客户端,负责发起调用和展示结果。三层之间通过标准协议通信,任何一层替换都不影响其他层。这种设计的好处是,我可以在不修改现有业务代码的前提下,快速把 AI 能力接进来。
2. MCP 服务端在 .NET 里的核心实现细节
2.1 项目结构与依赖选择
我是在现有的 ASP.NET Core 8.0 项目里直接加 MCP 服务端模块的,没有另起一个新项目。这样做的好处是共享现有的依赖注入容器、配置系统和日志组件,减少重复代码。项目结构大致如下:在原有项目下新建一个Mcp文件夹,里面放McpServer.cs、McpToolRegistry.cs、SwaggerToMcpConverter.cs这几个核心文件,然后在Program.cs里注册相关服务。
依赖方面,官方有ModelContextProtocol这个 NuGet 包,但当时我用的时候版本还比较早期,有些 API 不太稳定。后来我选择基于 JSON-RPC 2.0 自己实现核心通信层,只用了System.Text.Json做序列化,Microsoft.AspNetCore.SignalR做 SSE 推送。这样虽然多写了一些代码,但可控性更强,遇到问题也容易排查。如果你现在开始做,建议先试试官方包,如果满足需求就直接用,不满足再考虑自己实现。
提示:MCP 协议本身不复杂,核心就是 JSON-RPC 2.0 的消息格式加上几个约定的方法名。自己实现通信层并不难,难的是把工具注册、参数校验、错误处理这些周边逻辑做扎实。
2.2 工具注册与描述映射
MCP 服务端的核心是“工具注册表”。每个工具需要包含名称、描述、输入参数的 JSON Schema、以及实际的执行逻辑。我设计了一个McpTool类来封装这些信息,然后用一个McpToolRegistry来管理所有已注册的工具。
工具名称我采用了“动词+名词”的命名方式,比如query_order_status、export_customer_tickets、get_api_documentation。这种命名方式对 AI 模型比较友好,模型能直接从名称推断出工具的用途。描述字段我写的是自然语言,尽量用完整的句子说明这个工具做什么、什么时候用、有什么限制。比如query_order_status的描述是“根据订单号查询订单的当前状态,包括待付款、已付款、已发货、已完成、已取消五种状态。订单号必须是 12 位数字。”
参数 Schema 我用System.Text.Json的JsonSchema相关 API 来构建,支持 string、number、boolean、array、object 这几种基本类型。对于枚举类型的参数,我会在 Schema 里用enum关键字列出所有可能的值,这样模型在生成参数时就不会瞎编。对于必填参数,用required数组标注。这些细节看起来琐碎,但直接决定了 AI 调用接口的成功率。
2.3 Swagger 自动转换的实现思路
手写工具定义太累,所以我写了一个SwaggerToMcpConverter,从 Swagger 的 JSON 描述文件里自动提取接口信息,转换成 MCP 工具定义。具体做法是:在应用启动时,通过ISwaggerProvider获取当前项目的 OpenAPI 文档对象,然后遍历所有 Paths 和 Operations,把每个 Operation 转换成一个 MCP 工具。
转换规则是这样的:工具名称取operationId,如果没有就用“HTTP 方法+路径”拼接生成;工具描述取summary和description的拼接;输入参数从parameters和requestBody里提取,路径参数、查询参数、请求体参数分别处理;参数类型映射到 JSON Schema 的基本类型。对于$ref引用的复杂类型,我会递归展开,直到拿到基本类型为止。
这里有个坑要注意:Swagger 里的operationId不一定唯一,有些项目里多个接口用了同一个operationId,转换的时候会冲突。我的处理方式是检测到重复时自动加后缀,比如queryOrderStatus_1、queryOrderStatus_2。另外,Swagger 里的参数描述经常是空的,转换出来的工具描述会很干瘪,影响 AI 的理解。我的做法是在转换后加一层人工审核,对关键接口补充描述信息。
2.4 参数校验与错误处理
AI 模型生成的参数不一定符合预期,所以服务端必须做严格的参数校验。我在McpTool的执行入口加了一个校验层,按照 JSON Schema 检查参数类型、必填项、枚举值范围、字符串长度、数字范围等。校验不通过时,返回结构化的错误信息,告诉模型哪个参数有问题、期望什么格式。
错误处理方面,我把错误分成三类:参数错误、业务错误、系统错误。参数错误返回 400 级别的提示,业务错误返回具体的业务错误码和说明,系统错误返回 500 级别的提示并记录日志。对于 AI 模型来说,错误信息的可读性很重要,所以我在返回错误时尽量用自然语言描述,而不是只给一个错误码。比如“订单号格式不正确,应该是 12 位数字,你提供的是 8 位”,这种描述模型能直接理解并修正。
注意:不要直接把 .NET 的异常堆栈返回给 AI 客户端,一方面不安全,另一方面模型也看不懂。统一转换成结构化的错误响应,既安全又实用。
3. MCP 客户端对接与 AI 集成实操
3.1 客户端通信层实现
MCP 客户端这边,我实现了一个轻量的McpClient类,负责和服务端建立 SSE 连接、发送 JSON-RPC 请求、接收响应和通知。核心逻辑是:先通过 HTTP POST 发送initialize请求建立会话,然后通过 SSE 长连接接收服务端推送的消息。每次调用工具时,发送tools/call请求,带上工具名和参数,等待服务端返回结果。
SSE 连接的维护是个关键点。网络抖动、服务端重启、客户端休眠都可能导致连接断开,所以需要实现自动重连机制。我的做法是监听 SSE 连接的onerror和onclose事件,触发重连时先尝试重新initialize,如果失败就指数退避重试,最多重试 5 次。重连成功后,之前注册的工具列表需要重新拉取,因为服务端可能已经更新了工具定义。
超时处理也很重要。AI 模型调用工具时,如果服务端响应太慢,客户端不能一直等。我设置了 30 秒的默认超时,超时后返回一个明确的错误信息给 AI 客户端,让模型知道这次调用失败了,可以尝试其他方式或告知用户。对于耗时较长的操作,比如导出大量数据,我会在服务端改成异步任务模式,先返回任务 ID,然后客户端轮询任务状态。
3.2 与 AI 客户端的对接方式
MCP 客户端最终要对接具体的 AI 客户端。目前主流的方式有两种:一种是 AI 客户端原生支持 MCP,比如某些代码编辑器和桌面 AI 工具,直接配置 MCP 服务端地址就能用;另一种是 AI 客户端不支持 MCP,需要通过中间层转换,把 MCP 工具定义转换成该客户端支持的 Function Calling 格式。
我两种方式都试过。原生支持 MCP 的客户端接入最简单,基本就是填个地址的事。不支持 MCP 的客户端,我写了一个适配层,把 MCP 工具定义转换成 OpenAI 风格的 Function Calling 定义,然后把模型的调用请求转发到 MCP 服务端。这个适配层的核心是一个转换函数,把 MCP 的inputSchema映射成 Function Calling 的parameters,把tools/call的响应映射成function角色的消息。
这里有个经验:不同 AI 客户端对工具描述的长度限制不一样。有的客户端限制工具描述在 200 字符以内,有的允许 1000 字符。我的做法是在 MCP 服务端维护两套描述,一套详细版用于原生 MCP 客户端,一套精简版用于 Function Calling 适配。精简版只保留最核心的信息,把详细说明放到工具的examples字段里,模型需要时可以通过resources/read拉取。
3.3 多轮对话中的工具调用管理
AI 调用工具往往不是一次性的,而是多轮对话中的一环。比如用户问“帮我查一下订单 12345 的状态,如果已发货就查一下物流信息”,这需要先调用query_order_status,根据返回结果判断是否已发货,再决定是否调用query_logistics。MCP 客户端需要维护对话上下文,把每次工具调用的结果关联到对应的对话轮次。
我的做法是在客户端维护一个ConversationContext对象,记录当前对话的历史消息、已调用的工具、工具返回的结果。每次 AI 客户端发起新的请求时,把上下文一起传给模型,让模型知道之前发生了什么。工具调用结果我做了结构化处理,除了原始数据外,还加了一个summary字段,用自然语言概括结果,方便模型快速理解。
提示:多轮对话中工具调用的顺序和依赖关系,最好在工具描述里说明清楚。比如
query_logistics的描述里写上“需要先调用 query_order_status 确认订单已发货”,这样模型在规划调用步骤时就有据可依。
3.4 安全与权限控制
把 .NET 接口暴露给 AI 调用,安全是绕不开的问题。我的做法是在 MCP 服务端加了三层防护。第一层是认证,客户端连接时必须携带有效的 API Key,服务端验证通过后才允许建立会话。第二层是授权,每个工具都标注了所需的权限等级,客户端只能调用其权限范围内的工具。第三层是审计,所有工具调用都记录日志,包括调用时间、客户端标识、工具名、参数、结果状态,方便事后追溯。
对于敏感操作,比如删除数据、修改配置,我额外加了一个确认机制。AI 模型调用这类工具时,服务端不会直接执行,而是返回一个“待确认”状态,需要用户在 AI 客户端里明确确认后才真正执行。这个机制虽然增加了一步交互,但能有效防止模型误操作。实测下来,这个设计在内部系统里很受欢迎,大家用起来放心很多。
4. 常见问题排查与实战避坑指南
4.1 Swagger 转换中的典型问题
Swagger 转 MCP 工具定义的过程中,我踩过不少坑。最常见的问题是 Swagger JSON 里缺少operationId,导致工具名生成规则混乱。有些项目里operationId是自动生成的 GUID,毫无可读性。我的处理方式是优先用operationId,如果没有或者不可读,就用“HTTP 方法+路径”生成,比如get_api_orders_id。生成后加一层人工审核,把不合适的名字改掉。
另一个问题是复杂类型的递归展开。Swagger 里经常有嵌套的对象类型,比如订单对象里包含客户对象,客户对象里又包含地址对象。递归展开时如果不加深度限制,遇到循环引用就会死循环。我的做法是设置最大展开深度为 5 层,超过深度就用object类型代替,并在描述里说明“详细结构请参考接口文档”。这样既避免了死循环,又保留了基本信息。
还有一个坑是枚举值的处理。Swagger 里的枚举有时候是字符串,有时候是数字,有时候是$ref引用。我在转换时统一转成字符串枚举,并在描述里说明原始类型。对于数字枚举,我会在描述里加上映射关系,比如“1 表示待付款,2 表示已付款”,这样模型生成参数时就不会搞错。
4.2 连接与通信故障排查
SSE 连接不稳定是最常见的问题。表现是客户端时不时报“连接已断开”,或者工具调用超时。排查思路是这样的:先看服务端日志,确认是否有异常抛出;再看网络层,确认是否有代理或防火墙拦截了 SSE 长连接;最后看客户端,确认重连逻辑是否正常工作。
我遇到过一次典型故障:客户端每隔几分钟就断连一次,服务端日志显示正常。后来发现是中间的负载均衡器设置了 60 秒的空闲超时,SSE 连接在 60 秒内没有数据传输就被断开了。解决办法是在服务端定期发送心跳消息,保持连接活跃。心跳间隔我设的是 30 秒,比负载均衡器的超时时间短一半,这样就不会被误断。
另一个常见问题是 JSON-RPC 消息格式错误。MCP 协议对消息格式有严格要求,jsonrpc字段必须是"2.0",id字段必须是字符串或数字,method字段必须是字符串。我有一次因为id字段用了 GUID 对象而不是字符串,导致服务端解析失败。排查这类问题时,把原始消息打印出来逐字段检查,基本都能定位到问题。
4.3 工具调用失败的原因分析
工具调用失败的原因五花八门,我整理了一个速查表,方便快速定位。
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 模型不调用工具 | 工具描述不清晰 | 检查工具描述是否说明了用途和触发条件 | 补充描述,增加示例 |
| 调用参数错误 | 参数 Schema 不准确 | 对比实际接口文档和 Schema 定义 | 修正 Schema,补充枚举值 |
| 调用超时 | 服务端处理太慢 | 查看服务端日志,确认耗时操作 | 改异步任务,增加超时提示 |
| 返回结果模型看不懂 | 结果格式太原始 | 检查返回给模型的数据结构 | 增加 summary 字段,结构化输出 |
| 工具名冲突 | 多个工具同名 | 检查工具注册表 | 自动加后缀或人工重命名 |
| 权限不足 | 客户端权限配置错误 | 检查客户端 API Key 和权限映射 | 调整权限配置,重新授权 |
除了表格里的问题,还有一个隐蔽的坑:模型有时候会“幻觉”出不存在的工具名。比如我注册了query_order_status,模型却调用了get_order_status。这种情况通常是工具描述和模型训练数据里的常见命名不一致导致的。解决办法是在工具描述里加上“别名”说明,比如“本工具也可称为 get_order_status”,这样模型就能正确匹配了。
4.4 性能优化与扩展建议
当工具数量增多、调用频率变高时,性能问题会逐渐暴露。我做了几项优化,效果比较明显。第一项是工具定义的缓存,服务端启动时把 Swagger 转换结果缓存到内存,后续请求直接读缓存,避免每次重新解析。第二项是连接池,MCP 客户端和服务端之间的 HTTP 连接复用,减少握手开销。第三项是结果缓存,对于查询类工具,相同参数的请求在短时间内返回缓存结果,降低后端压力。
扩展性方面,我建议把 MCP 服务端设计成可插拔的模块。工具注册、参数校验、权限控制、日志审计这些功能都做成独立的中间件,需要时启用,不需要时关闭。这样当项目规模变大时,可以按需组合,不会因为功能堆砌导致维护困难。另外,工具定义最好支持热更新,服务端更新工具后,客户端能自动感知并刷新,不需要重启整个服务。
注意:性能优化不要过早进行。先把功能跑通,确认工具调用链路没问题,再根据实际瓶颈做针对性优化。我一开始就想着做缓存、做连接池,结果调试阶段因为缓存导致工具更新不生效,白白浪费了半天时间排查。
4.5 从 Swagger 到 MCP 的自动化流水线
最后分享一个我觉得很实用的做法:把 Swagger 转 MCP 工具定义做成自动化流水线。在 CI/CD 流程里加一个步骤,每次 .NET 项目构建时自动生成最新的 Swagger JSON,然后跑一遍转换脚本,生成 MCP 工具定义文件,最后部署到 MCP 服务端。这样接口一更新,MCP 工具定义就自动同步,不需要人工干预。
转换脚本我用的是 .NET 控制台程序,读取 Swagger JSON,输出 MCP 工具定义 JSON。脚本里加了校验逻辑,如果发现工具名冲突、参数类型不支持、描述为空等问题,直接报错并终止流水线。这样能在早期发现潜在问题,避免把有问题的工具定义部署到生产环境。实测下来,这套流水线把工具定义的维护成本降低了八成以上,接口更新后基本不需要人工介入。
我在实际使用中发现,MCP 这套东西虽然概念不复杂,但落地时的细节特别多。从协议理解到服务端实现,从客户端对接到问题排查,每个环节都有坑。但只要把核心链路跑通一次,后面就是不断优化和扩展的过程。对于 .NET 开发者来说,现有的 ASP.NET Core 项目就是最好的起点,不需要推倒重来,加一个 MCP 模块就能让 AI 直接调用你的接口。这个投入产出比,我觉得很划算。