
让 AI 直接调你的 .NET 接口——这句话放到一年前可能还会被当成“大模型幻觉吹出来的需求”。但现在你再提业内已经有一个非常具体的协议在支撑了就是 MCPModel Context Protocol模型上下文协议。作为 .NET 后端工程师我第一次接触 MCP 的时候脑子里冒出来的第一个问题是“我辛苦写的接口凭什么要让 AI 来猜着调”后来把协议和服务端、客户端都跑通之后我才发现这个问题的核心不是“接口能不能被调”而是“AI 客户端怎么知道你的接口存在知道了之后又怎么按契约来调”。这篇文章我会用一套完整的 .NET 落地实践来讲清楚 MCP 服务端和客户端怎么实现。内容包括协议原理、服务端封装、客户端调用、实战排查所有代码都在 .NET 8 环境下验证过。想给现有系统开一个“AI 通道”又不想为每家模型厂商单独写适配层的同学可以直接照着做。1. 项目背景与核心思路1.1 MCP 到底在解决什么问题先回到那个最朴素的问题AI 模型本身不会主动去调你的 API它只会“生成文本”。但 AI 应用一旦要落地到业务场景比如查库存、发工单、调订单状态就必须让它能操作真实系统的数据和方法。在 MCP 出现之前常见做法有以下两种把接口文档丢进 system prompt让模型自己“猜”应该怎么构造 HTTP 请求为每个接口单独实现一套 Function Calling 适配层把参数、schema、调用逻辑揉在一起。第一种方式很不稳定模型经常把参数名写错接口返回值一变化 prompt 就失灵。第二种方式倒是稳但你每接一个模型供应商或者每次加一个接口都要重新写一套适配代码。时间一长工程里到处都是“half-function-calling”的死代码。MCP 的思路本质上和数据库驱动接口很类似定一套统一的协议客户端和服务端各说各话但都遵守同一个“接口描述语言”。服务端把工具、资源、提示词暴露出来客户端拿到这份描述后自动发现、自动调用。对 .NET 开发者来说就是你写一个标准 ASP.NET Core 服务把现有业务方法包装成 MCP 工具然后各种支持 MCP 的 AI 客户端就能直接拉起来用。1.2 这套方案的适用场景我做了几个测试场景之后觉得最适合用 MCP 落地的是下面这几类内部业务系统接入 AI 助手比如 OA 系统里的请假查余额、CRM 里的客户查询AI 助手通过 MCP 直接读写内部接口不用绕一层中间服务。给大模型工具链增加操作能力像 Playwright MCP、Figma MCP、Blender MCP 这些生态里的名字已经非常响本质都是把某个垂直领域的能力封装成标准工具集。团队内部共享 AI Agent 技能后端维护一份 MCP Server前端、测试、产品都能通过各自的 AI 客户端去调用同一组接口避免了“到处写死接口地址”的混乱。对我而言最大的收益不是“AI 能调我的接口”而是“接口契约变成了标准化产物”。以前给模型写 function schema每个模型一套现在只需要维护一个 MCP Server协议栈统一客户端随便换。2. 协议原理与整体架构2.1 三个核心抽象先搞清楚MCP 协议里最核心的三个抽象概念如果理解不透后面写代码会反复返工。Tools工具这是 AI 客户端真正会“执行”的东西。每个工具对应一个可调用方法有名字、有描述、有参数 schema。AI 会根据用户输入判断要不要调这个工具、传什么参数。工具执行结果可以返回给模型继续生成回答。Resources资源暴露给 AI 客户端读取的数据源相当于“只读数据接口”。比如数据库里的配置表、文件内容、日志片段。AI 客户端可以主动读取资源内容作为上下文补充。Prompts提示词模板预置在服务端的一组提示词模板客户端可以通过模板名获取。这个适合把复用的指令、few-shot 示例封装在服务端避免客户端每次都要拼一大串提示词。实际项目中Tools 用的最多Resources 适合做 RAG 的前置条件Prompts 看起来简单但很少有人真正设计好。真正落地时我的建议是先从 Tools 起步跑通主链路后再设计 Resources 和 Prompts。2.2 消息在 AI 与 .NET 方法之间怎么流转MCP 的底层传输协议是 JSON-RPC 2.0之上定义了初始化、工具列表、工具调用等消息类型。流程可以用一句大白话描述客户端先和服务端握手然后问“你有哪些工具”拿到工具列表后根据用户问题选择一个工具并传入参数服务端执行完把结果返回客户端再把结果交给大模型继续生成。具体到 .NET 服务端一次完整调用长这样AI 客户端向http://localhost:5000/mcp发起连接客户端发送initialize请求确认协议版本和能力服务端返回 server 信息和 capabilities客户端发送tools/list服务端返回所有注册工具的 JSON Schema 描述客户端根据用户意图发送tools/call里面带上工具名和参数字典服务端匹配到 C# 方法执行后把结构化结果返回。这整个交互对使用者来说几乎是透明的。你在客户端里自然地说“帮我查一下北京今天的天气”背后就是上面这些步骤只是客户端帮你隐藏了。2.3 为什么不是“直接把 API 文档丢给 AI”很多人第一反应是既然 AI 能读文档那我直接给它 Swagger JSON 不就行了理论上可以但两个致命问题马上会出现调用语义不明确Swagger 描述的是 HTTP 端点AI 需要理解 RESTful 语义、路径参数、请求头、鉴权方式每一步都有可能产生歧义。操作边界不可控AI 拿到全部 API 文档后理论上可以调用所有端点。你不想让它 DELETE 掉生产库吧MCP Server 注册工具的时候可以明确只暴露允许调用的方法把操作面收得很小。所以 MCP 不是简单“接口文档的翻版”它更像是一个面向 AI 场景的、限制操作面的、契约化方法注册中心。这个定位决定了它在架构上和普通 Web API 有本质区别。3. 服务端落地把 .NET 接口封装成 MCP Server3.1 初始化一个最小 MCP 服务端我用的是官方 Model Context Protocol 仓库下的 C# SDKNuGet 包当前常用的是ModelContextProtocol.AspNetCore、ModelContextProtocol.Server不同预览版命名可能略有差异但整体思路通用。先建一个空的 ASP.NET Core Web API 项目目标框架选 .NET 8。Program.cs 里的最小配置如下using ModelContextProtocol.AspNetCore; var builder WebApplication.CreateBuilder(args); builder.Services.AddMcpServer(); builder.Services.AddMcpToolsFromAssembly(); var app builder.Build(); app.MapMcp(/mcp); app.Run();这段代码做了三件事AddMcpServer()把 MCP Server 的核心服务注入容器AddMcpToolsFromAssembly()扫描当前程序集里带有McpServerTool标记的工具类MapMcp(/mcp)把 SSE 协议的监听端点暴露到/mcp路径。启动后用浏览器直接访问https://localhost:7001/mcp会看到连接挂起这是正常的因为 MCP 是长连接协议不是普通 REST 接口直接返回 JSON。3.2 把现有接口方法注册成工具假设我有个订单查询服务最核心的方法是“根据单号查订单状态”。改成 MCP 工具最省事的做法是直接在方法上加特性using ModelContextProtocol.Server; public class OrderTools { [McpServerTool(query_order_status)] public static async Taskstring QueryOrderStatus( [McpServerToolParameter(Description 订单号必填)] string orderNo, CancellationToken cancellationToken) { // 这里调用你现有的业务服务或仓储层 var order await orderService.GetByNoAsync(orderNo, cancellationToken); if (order null) { return 未找到该订单; } return $订单 {order.OrderNo} 当前状态{order.Status}更新时间{order.UpdatedAt:yyyy-MM-dd HH:mm:ss}; } }这里有几个设计要点方法描述一定要写清楚AI 客户端会拿方法描述和参数描述来理解你的工具。描述越具体模型选择工具的成功率越高。我踩过最痛的一次坑就是参数只写了一个“id”结果模型把用户输入的客户编号、商品 ID、订单号全往上塞。返回格式尽量是自然语言或简单 JSON让模型可以直接引用并继续生成回答。返回一堆堆砌字段的对象模型虽然能读但生成效率会下降。CancellationToken 直接透传调用超时或者用户中途取消时服务端能及时释放资源。如果你的业务方法散落在多个 service 类里不想改动它们也可以写一个包装类在包装类里加特性内部调用现有方法。这样既不会污染业务层又能把 MCP 工具集中管理。3.3 工具注册的两种方式对比除了扫描程序集SDK 通常还支持在服务注册时手动注册工具。两种方式各有场景。注册方式优点适合场景程序集扫描改一个类加特性就能自动暴露扩展快初创项目、内部小工具、快速验证手动注册注册逻辑集中可以加条件判断大项目、有权限控制、动态判断工具是否可用手动注册的伪代码大致长这样builder.Services .AddMcpServer() .AddMcpTool(query_order_status, async (McpServerToolContext ctx, string orderNo) { // 动态处理逻辑 });个人建议接近生产环境时不要过度依赖自动扫描最好加一层“工具注册表”明确哪些类、哪些方法可以对外暴露。不然同事在业务类里随手加个特性这个接口就被 AI 看到了风险面太大。3.4 服务端鉴权与访问控制MCP 服务端和普通 Web API 一样必须考虑“谁在调用”。尤其是在公网部署时不能让任何人连上你的/mcp端点就随便执行工具。我在服务端里做了两层防护第一层路由级别加鉴权中间件校验调用方的 Token第二层工具方法内部做业务级权限判断根据调用方身份决定能不能执行某个具体操作。app.Use(async (context, next) { if (context.Request.Path.StartsWithSegments(/mcp)) { var token context.Request.Headers[Authorization].FirstOrDefault(); if (!IsValidToken(token)) { context.Response.StatusCode StatusCodes.Status401Unauthorized; return; } } await next(); });注意这里的 Token 校验只是示例。生产环境可以考虑接入 OAuth2 Client Credentials或者其他内部统一认证方案。核心原则是MCP Server 不是“内网免登”的借口它只是传输和契约层安全体系一点不能省。4. 客户端落地让 AI 真正调用 .NET 接口4.1 用现成 AI 客户端配置连接服务端跑起来之后第一步可以先用现成的 MCP 客户端验证连通性。以常见的桌面 AI 客户端为例配置文件里的 mcpServers 部分可以这样写{ mcpServers: { net-order-mcp: { url: http://localhost:5000/mcp } } }有些客户端支持transport: sse的显式指定有些则默认stdio需要留意一下。如果是本地开发也可以用 stdio 方式启动dotnet命令来加载 Server 程序集。两种传输方式没有绝对优劣SSE 适合远程部署stdio 适合本机快速联调。配置好之后在客户端里重启会话应该能在工具列表里看到刚才注册的query_order_status。然后你直接对 AI 说帮我查一下订单号 20250101001 的状态。如果一切正常AI 会自己选择query_order_status工具填入订单号参数返回服务端结果然后再组织成自然语言回答你。4.2 在 .NET 程序里自己实现 MCP 客户端如果是要把 MCP 调用能力集成进自己的 .NET 应用里需要以 SDK 方式调用客户端。最小实现大概是下面这样using ModelContextProtocol.Client; var client await McpClientFactory.CreateAsync( new McpClientOptions { ServerName net-order-mcp, ProtocolVersion 2024-11-05 }, new McpTransportOptions { TransportType sse, ConnectionString http://localhost:5000/mcp }); var tools await client.ListToolsAsync(); Console.WriteLine($服务端暴露了 {tools.Count} 个工具); foreach (var tool in tools) { Console.WriteLine($- {tool.Name}: {tool.Description}); } var callResult await client.CallToolAsync( query_order_status, new Dictionarystring, object? { [orderNo] 20250101001 } ); Console.WriteLine(string.Join(\n, callResult.Content.Select(c c.Text)));这里CallToolAsync最后一个参数就是方法参数名到值的映射。工具名称和参数名必须和服务端暴露的完全一致大小写也不能错。4.3 端到端验证一次完整调用我把完整流程跑通过一次日志记录大概是这个手感09:00:01.000 服务端启动监听 /mcp09:00:05.220 客户端连接到 /mcp完成 initialize 握手09:00:05.310 客户端请求 tools/list拿到 1 个工具09:00:08.442 客户端请求 tools/call工具名 query_order_status参数 orderNo2025010100109:00:08.460 服务端执行订单查询数据库命中返回“已发货”09:00:08.501 客户端拿到结果交给模型组织回答如果这四个阶段都在日志里清晰地出现说明整条链路是通的。哪一步少了就按后面的排查清单去定位。4.4 客户端集成时的上下文设计自己写客户端时最容易忽略的是“上下文传递”问题。比如用户在系统里选了“当前登录用户是张三”AI 在调用订单接口时客户端需要在tools/call里自动带上用户上下文参数而不是让模型凭感觉输入。我的做法是在客户端封装一层McpContextAdapter把用户身份、租户号、traceId 统一塞进参数字典。这样服务端拿到的不只是用户的一句话而是完整业务上下文。public async Taskstring SafeCallToolAsync(string toolName, string userInput) { var args new Dictionarystring, object? { [orderNo] ExtractOrderNo(userInput), [operator] _currentUser.Id, [tenant] _currentUser.TenantId }; var result await _client.CallToolAsync(toolName, args); return string.Join(\n, result.Content.Select(c c.Text)); }这种设计能让 AI 调用业务接口时始终带着操作人、租户等关键信息避免出现越权查询。5. 常见问题与排查技巧5.1 连接失败TLS 证书和端口问题本地联调时最经典的报错就是长这样net::ERR_SSL_PROTOCOL_ERROR或者failed to start claudes workspace request error: net::ERR_CONNECTION_TIMED第一个我遇到过很多次基本都是因为 ASP.NET Core 开发证书没有被客户端信任。我的建议是本地调试时直接用 HTTP 而不是 HTTPSMCP 是内部协议链路里一般有网关层做 TLS 终止不需要每个 Server 各自折腾证书。dotnet run --urls http://localhost:5000第二个ERR_CONNECTION_TIMED多半是端口被防火墙拦了或者服务端没监听预期的端口。先用curl http://localhost:5000/mcp看端口通不通再排查客户端配置的地址是否一致。5.2 SSE 流式响应中断还有一个使用本地服务时经常撞见的坑net::ERR_INCOMPLETE_CHUNKED_ENCODING 200 (OK)这个错误表面看是 HTTP 返回 200但响应体被截断了。在 MCP 场景里多半是 SSE 连接没有正确维持或者有反向代理在中间缓存了响应。SSE 是长连接不能走普通 CDN 或响应缓冲型代理需要在 Nginx 里关掉 proxy_buffering同时配置长超时。location /mcp { proxy_pass http://localhost:5000; proxy_buffering off; proxy_read_timeout 3600s; proxy_set_header Connection ; proxy_http_version 1.1; }如果还是断看看服务端有没有配置心跳或者保活消息。很多 MCP Server 库会周期发注释行保持连接如果你的代理把这行吞了连接也可能被判定为超时断开。5.3 工具能找到但调用参数对不上这个属于“逻辑正常但结果奇怪”的经典问题。AI 客户端明明拉到了工具列表但tools/call传进来的参数总是缺一个或者类型不对。第一反应先看服务端日志把收到的参数原样打出来。很多时候是参数名大小写不一致比如服务端定义orderNo客户端根据模型理解传了ordernumber匹配不上。第二反应是检查参数描述是不是太模糊。我给orderNo加的描述是“订单号必填来自订单系统通常以数字开头”模型的命中率立刻提高了。描述越具体模型越不容易自由发挥。5.4 工具执行超时MCP 工具内部如果调了外部 HTTP 服务或数据库建议设置合理的超时时间同时把 CancellationToken 传下去。默认行为经常会让你等满 100 秒才报错体验很差。using var cts new CancellationTokenSource(TimeSpan.FromSeconds(10)); var order await orderService.GetByNoAsync(orderNo, cts.Token);超时之外还要注意工具不要执行太重的事务操作。AI 调用工具的频率可能远高于人工操作一旦出现大量消耗资源的查询数据库容易直接被打满。生产环境建议在 MCP Server 前加限流或者对工具按等级区分最大并发。5.5 排查速查表症状可能原因优先排查动作客户端连不上 /mcp端口监听错误、TLS 证书不受信用 HTTP 启动curl 验证端口initialize 不成功协议版本不匹配、无 TLS检查 SDK 版本查看服务端日志tools/list 返回空工具类没被扫描、特性没写检查 AddMcpToolsFromAssembly 和类访问级别tools/call 报参数错误参数名/类型不匹配打印服务端收到参数对比工具 schema执行结果丢失代理缓冲、SSE 超时关代理缓冲调长超时调用超时业务方法太慢、无 CancellationToken加超时控制给工具分类限流5.6 开发调试的三个小技巧最后分享三个实际干活时的操作习惯开详细日志开发阶段把 MCP SDK 的日志级别调到 Debug能看到完整 JSON-RPC 消息体比盲猜快太多。用 OpenAI 函数调用格式做对比如果发现 MCP 工具调用不稳定可以把同一个服务方法封装成 OpenAI 的 function schema 做 A/B 对比看是不是模型理解问题。做最小复现遇到诡异问题先建一个只有单个工具的空服务端排除业务逻辑干扰。很多问题最后证明根本不在业务代码里而是协议层配置问题。6. 一些落地后的真实体会跑完整个 MCP 服务端和客户端链路之后最大的感受是MCP 的价值不在于让 AI “学会”调接口而在于让接口描述这件事彻底标准化。对 .NET 开发者来说这意味着你可以像写普通 Web API 一样把业务能力开放给所有 AI 客户端不再需要针对每个模型单独做适配。如果项目刚起步我建议先做一个最简单的工具跑通整条链路再逐步把鉴权、限流、上下文传递等工程化能力补上去。MCP 的生态还在快速演进趁着现在把基础实践刷一遍后面不管是接入本地客户端还是自研 Agent都会轻松很多。