尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

.NET实现MCP服务端与客户端:AI调用业务接口的实践指南

.NET实现MCP服务端与客户端:AI调用业务接口的实践指南 MCP这个词近半年在AI圈子里几乎快被说烂了。但真正让我决定上手去搞的是团队里一个真实需求产品经理说能不能让AI自动帮运营同事查订单、改状态、拉报表而不是每次都在对话里贴一份Excel给我于是我开始研究怎么把手上现成的.NET接口直接变成AI能调用的能力。我所说的这个“能力”指的就是MCPModel Context Protocol模型上下文协议。这篇文章算是我对“MCP服务端与客户端落地”的一次完整复盘核心围绕三件事MCP到底解决什么问题、怎么用.NET写一个MCP服务端、以及怎么让AI客户端真正把这个服务用起来。适合正在做AI Agent、或者想把现有业务接口开放给大模型的.NET开发者参考。1. 为什么需要MCP给AI和业务系统之间装一根“通用数据线”1.1 没有MCP的时候AI调用系统接口有多别扭先讲个背景。过去我们想让大模型操作自己的系统最常听到的方案是Function Calling也就是你在调用某个模型API时把函数声明一并传过去模型根据用户输入决定调哪个函数、传什么参数最后把函数返回值带回对话里。Function Calling本身没问题问题在于它跟具体厂商的API绑得比较紧。换一家大模型函数声明的格式可能就不一样了同一个函数想被不同的AI Agent用就得分别适配。而且Function Calling通常只覆盖“调用函数”这一件事像读取文件、查询资源、给模型补充上下文这些需求还是要自己造轮子。MCP就是冲着这个痛点来的。它是Anthropic在2024年底开源的一个开放协议核心思路很简单把AI需要的能力抽象成标准的服务端让任意支持MCP的客户端都能发现、调用这些能力。你不需要关心对面是Claude还是别的Agent只要实现一遍MCP服务端能力就能被整个生态复用。1.2 MCP的三大核心概念Client、Server、Tool要理解MCP记住三个角色就够了。MCP Client运行在AI应用侧负责连接服务端、发现能力、发起调用。Claude Desktop、各种AI编程工具里内置的Agent都属于这个角色。MCP Server运行在你自己的系统侧把你的业务接口包装成统一的能力。这篇文章里的.NET服务就是扮演这个角色。Tool / Resource / Prompt服务端暴露给AI的能力单元。Tool对应“可执行的函数”Resource对应“可读取的数据”Prompt对应“可复用的提示词模板”。在协议层面MCP基于JSON-RPC 2.0通信。你会发现它本质上是定义了一套固定的“方法名”初始化时发initialize查能力时发tools/list调用时发tools/call。这套方法名是协议标准里写死的所以任何语言的任何客户端只要遵守这套规范就能互相通信。1.3 为什么选择MCP而不是自己写一套协议有人可能会问我的接口用REST API暴露得好好的AI想要数据直接HTTP GET不就行了为什么非要套一层MCP我自己的体会是MCP解决的是“发现”和“上下文”的问题。REST API需要人类去读文档才知道有哪些端点、每个参数怎么传而MCP的服务端会把能力清单、参数Schema、方法描述一并暴露出来AI在运行时可以通过tools/list自己“读到”这些信息再决定调用哪个工具、传什么参数。换句话说REST API是给人用的MCP是给模型用的。另外MCP里的Resource和Prompt还能解决上下文注入的问题。比如你想让AI在你内部知识库里检索资料你可以把知识库包装成ResourceAI在回答之前会先读取相关内容。这种东西如果自己实现要考虑协议格式、传输层、鉴权工作量不小用MCP就省事了。从协议设计角度来看MCP更像是一个“AI时代的接口网关”它不是替你做业务逻辑而是把业务能力以模型能理解的方式暴露出去。2. 服务端先行用.NET把自己的接口改造成MCP Server2.1 技术选型用官方C# SDK而不是自己实现JSON-RPC动手前先解决一个选择题用官方C# SDK还是自己照着MCP规范撸一个我的建议是能用SDK就用SDK除非你的需求特别诡异。MCP协议虽然简单但里面有很多容易被忽略的细节比如初始化的能力协商、消息ID匹配、通知机制、流式传输的分帧格式。自己实现一遍不是不行但调试成本会高得让你怀疑人生。目前.NET生态里最省心的选择是官方维护的ModelContextProtocol NuGet包微软自己的AI示例库也在用它。项目要求.NET 8以上我用的是.NET 8的长期支持版本。建一个最简单的控制台项目就能跑通stdio模式如果要走远程HTTP需要引用ASP.NET Core相关的包后面会单独说。2.2 搭建项目一个最小可运行的MCP服务端我建议按下面的步骤搭一个最小项目。先建控制台应用然后加NuGet引用再写Tool最后启动。dotnet new console -n McpDemoServer cd McpDemoServer dotnet add package ModelContextProtocol工具类写法如下。官方SDK支持通过特性标注一个类里的方法成为Tool实现非常清爽using ModelContextProtocol; public class QueryTools { [McpTool(Description 根据用户ID查询用户基本信息包括昵称、注册时间、积分余额)] public async TaskUserInfo GetUserInfoAsync( [McpParameter(Description 用户ID整数)] int userId, CancellationToken cancellationToken) { // 这里调用你自己的业务Service // 我这里用一个模拟数据代替 await Task.Delay(50, cancellationToken); return new UserInfo(userId, 张三, DateTime.UtcNow.AddDays(-100), 3650); } } public record UserInfo(int UserId, string NickName, DateTime RegisterTime, int Points);Program.cs的写法同样很简洁using ModelContextProtocol; var builder Host.CreateApplicationBuilder(args); builder.Services .AddMcpServer() .WithStdioServerTransport() .WithToolsQueryTools(); var host builder.Build(); await host.RunAsync();这段代码被Claude Desktop这类客户端拉起时客户端会通过标准输入输出和这个进程通信。这个模式叫stdio transport适合本地进程调用优点是配置简单缺点是服务必须跑在客户端同一台机器上。如果只是自己开发调试这个模式足够了。2.3 把已有业务接口接入MCP从一个订单查询讲起上面那个例子太玩具了咱们来点真实场景。假设你有一个ASP.NET Core业务系统里面有订单查询接口现在想让AI能查订单状态。你不需要把整个系统改成MCP只需要在MCP服务端里引用你的业务Service然后包一层Tool方法。关键步骤是Tool方法的返回类型不要直接用Entity对象最好定义一个给AI看的DTO把字段精简成模型真正需要的那些。原因很简单Entity里经常有内部字段、敏感字段、导航属性一旦序列化给模型等于把这些信息全部暴露出去。我在第一版就是这么干的结果AI连内部备注字段都能读到吓得我赶紧改了。实际执行时Tool方法里只需要做三件事接收参数、调用业务Service、返回结果对象。返回类型可以是复杂对象SDK会自动序列化成JSON。这里要注意返回的JSON结构越扁平越好嵌套层级深了AI在解读结果时更容易出错。2.4 远程部署把MCP服务端推到服务器上本地stdio模式搞通之后远程场景会复杂一些。如果是局域网内或者公网环境一般建议走HTTP传输最常用的方式是把MCP服务端挂到ASP.NET Core里用Streamable HTTP或SSE暴露一个/mcp端点。var builder WebApplication.CreateBuilder(args); builder.Services .AddMcpServer() .WithHttpTransport(); var app builder.Build(); app.MapMcp(); app.Run();这个模式下AI客户端通过HTTP POST访问/mcp消息体是JSON-RPC请求。部署到服务器时有几个坑必须注意必须使用HTTPS很多AI客户端对非HTTPS的远程服务端是拒绝连接的尤其涉及浏览器侧调用时。防火墙端口要开放否则客户端一直超时。反向代理如果开启了重写或缓冲要确保不会把SSE流给吞了否则工具调用会卡在响应阶段。注意本地用localhost调试时可能遇到证书问题浏览器或某些客户端会报类似SSL协议错误。这个现象多半是本地开发证书过期或不受信任重新安装一下开发证书就行不一定要上生产证书。3. 客户端接入让AI真正“摸到”你的接口3.1 用现成的MCP客户端连接服务端服务端写完了接下来要让它被AI用起来。最直接的方式是配置一个支持MCP的AI客户端。以Claude Desktop为例配置文件是json把服务端加进去即可{ mcpServers: { dotnet-order-server: { command: dotnet, args: [ run, --project, C:\\Projects\\McpOrderServer\\McpOrderServer.csproj ], cwd: C:\\Projects\\McpOrderServer } } }重启客户端后在对话里发起一句“帮我查一下用户ID为1001的订单状态”正常情况下AI会先去tools/list发现能力再按你的描述填参数调用。这里有一个常见认知误区不是所有对话都会触发工具调用AI只有在判断“这件事需要外部工具”时才会去查工具列表否则它可能直接凭训练知识回答。所以当你发现AI没调用工具时先别急着怀疑服务端换个更明确的指令试试。3.2 不依赖UI用JSON-RPC请求手动验证服务端客户端界面好看但排查问题时不够直接。我更喜欢先用命令行验证服务端到底对不对。stdio模式的验证比较麻烦因为你得模拟父进程去启动它但HTTP模式的验证就很简单直接发JSON-RPC请求即可。以HTTP模式为例先用tools/list确认能力是否被正确暴露curl -X POST http://localhost:5100/mcp \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:1,method:tools/list,params:{}}正常会返回一个tools数组里面包含你定义的GetUserInfoAsync以及从它的参数、特性生成出来的inputSchema。看到这个返回说明服务端的Tool注册没问题。接下来发tools/call测试调用curl -X POST http://localhost:5100/mcp \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:2,method:tools/call,params:{name:GetUserInfoAsync,arguments:{userId:1001}}}如果返回里有content内容和结构化结果那整个链路就算通了。这一步养成习惯能帮你把客户端问题和服务端问题迅速切分开。3.3 写一个最小的C#客户端如果不想依赖第三方客户端也可以用C# SDK自己写一个客户端。这样做的对处是方便做自动化测试也能在集成到自己的Agent时用。using ModelContextProtocol; var clientOptions new McpClientOptions { ServerName my-dotnet-client }; // 以stdio方式连接本地服务端 await using var transport new StdioClientTransport(new StdioClientTransportOptions { Command dotnet, Arguments [run, --project, C:\\Projects\\McpOrderServer] }); await using var client new McpClient(clientOptions, transport); await client.ConnectAsync(CancellationToken.None); var tools await client.ListToolsAsync(CancellationToken.None); foreach (var tool in tools) { Console.WriteLine($发现工具: {tool.Name}); } var result await client.CallToolAsync(GetUserInfoAsync, new Dictionarystring, object? { [userId] 1001 }, CancellationToken.None); Console.WriteLine(result.Content);这里SDK版本不同API可能有细微差异以你引入的NuGet包版本为准但整体流程就是这么三步连接、发现、调用。跑通这个客户端之后你其实就拥有了一个完全由自己控制的“AI接线员”管道。后面如果你想在自己的后台页面里塞一个AI助手也可以复用这套客户端逻辑。3.4 实战场景把MCP服务端接到现有的业务闭环里工具调通只是第一步真正常用的是把它接进业务闭环。我举一个我们团队实际用过的例子运营同事在群里丢一句话“把编号10086的工单状态改为已处理”AI客户端在接收到这句话后自动调用MCP暴露的UpdateTicketStatus工具先查权限、再改状态、最后把新的工单状态返回对话。这个过程中MCP服务端的价值不只是帮你省了一次接口调用而是让人和系统的交互从“填表单”变成了“说需求”。后面如果还想让AI主动读取工单详情、生成日报只需要继续暴露对应的Tool或者Resource即可客户端不用改。4. 核心细节与避坑要点参数描述、并发和权限控制4.1 Tool描述写得越好AI调用越准MCP服务端里最影响AI调用准确率的往往不是代码而是Tool的描述与参数Schema。原因很好理解AI在运行时看不到你的源代码它只能靠Description和ParameterName猜测这个工具是干什么的。我踩过的一个典型坑是一开始把Tool描述写成“处理订单数据”结果AI经常不知道该在什么场景调用它有时候用户问“我的包裹到哪了”它完全没有把这句话和“处理订单数据”关联起来。后来我把描述改成“根据订单编号查询最新物流动态包括运输中、已签收、异常状态”效果立刻不一样了。所以Tool描述里务必写清楚这个工具是干什么的、在什么场景下用、参数是什么格式、有没有默认值。必要时把示例值写进描述里AI会更容易填对参数。4.2 长耗时任务异步、取消与超时兜底MCP的调用可能会很慢尤其是当你把AI的请求接进一个内部接口而内部接口本身要查数据库、调第三方服务时。如果你在Tool方法里用同步阻塞方式会在请求量大时把服务端线程池打满导致后续所有调用排队。正确的做法是所有Tool方法都写成异步并接收CancellationToken你内部调用Service时把这个Token一路传下去。这样当客户端超时取消时服务端也能及时中断不会留下一个还在跑的僵尸任务。另外建议在HttpTransport模式下配置合理的超时时间。不同客户端的超时策略不一样有的30秒没有响应就直接放弃。如果业务确实要跑很久要么优化接口要么把耗时的任务拆成“提交任务”和“查询结果”两个Tool避免长时间占用一次调用。4.3 权限与安全别把整个数据库暴露给AI这是整个MCP落地过程中我认为最不能省的一环。MCP Server一旦被AI客户端接入就等于给一个“看不见的调用者”开了访问通道如果不对能力做收敛风险很大。有几个原则可以参考只暴露必要的Tool不要图省事把整个Service类全部注册上去。身份验证尽量在MCP Server层做比如通过请求头传递API Key但要注意不同的MCP客户端对自定义Header的支持不一样本地stdio模式通常没有Header可用这种情况下就要靠对方进程的身份来限定访问范围。数据的返回要做脱敏。返回DTO里不要包含手机号完整字段、身份证、内部备注等敏感信息。给Tool调用加审计日志记录下来“哪个客户端、在什么时候、调用了哪个工具、传了什么参数”。出了事能追这是底线。注意如果MCP Server要暴露给公网使用强烈建议放在内网环境里通过API网关统一出口而不是直接把/mcp裸奔在公网上。你会少踩很多安全审计的雷。4.4 多Tool场景下的组织方式当你的Tool数量多起来比如十几二十个组织方式会直接影响AI的调用效果。我自己的经验是按业务域拆分成多个类每个类负责一个垂直领域然后使用WithTools ()、WithTools ()分别注册。这样既能保持代码清晰也能让AI在tools/list时看到分组明确的能力清单。Tool的命名也要讲究尽量用名词加动词的组合比如QueryUserInfo、UpdateTicketStatus少用含糊的DoSomething、HandleData。Model在生成调用时会优先选择名字和意图匹配度高的工具。5. 常见问题与排查技巧实录MCP服务端和客户端联调时问题往往出在几个固定的点上。我把实际遇到过的、和周边朋友交流时听到的高频问题整理成一张速查表后面再逐个展开。问题现象大概率原因建议排查方向客户端连不上本地stdio服务dotnet不在PATH里或路径配错手动在终端执行启动命令确认进程能正常拉起发现不了ToolSDK版本与代码写法不匹配检查NuGet版本确认特性是否被正确识别工具调用返回超时服务端阻塞或网络层问题先直接请求服务端点排除业务接口本身慢的情况报SSL协议错误本地证书过期 / 远程没有HTTPS重新安装开发证书或给远程配置合法证书参数传错类型描述和Schema不够清楚给参数加示例和说明尽量用明确的参数名返回内容AI看不懂返回结构太复杂精简DTO加一个人类可读的摘要字段5.1 “客户端一行工具都看不到”的排查思路如果你配置好客户端但AI说没有可用工具我的排查顺序是先确认进程有没有被拉起。很多客户端会写着command是dotnet但dotnet不在客户端所在用户的环境变量PATH里导致进程根本没启动。手动在终端执行一遍命令如果正常启动说明配置路径问题如果不能启动先解决启动问题。然后看服务端有没有成功注册Tool。最简单的方法是加一条启动日志把注册的工具名列出来。SDK通常会提供某种方式获取已注册的Tool集合打印一下就知道是不是注册环节出了问题。排除注册问题后再检查客户端和服务端的版本兼容性老版本客户端可能不支持新协议特性换成匹配的版本就好。5.2 HTTPS证书与SSL协议错误的处理开发阶段最让人头大的就是证书问题。常见的报错文本类似“net::err_ssl_protocol_error”或“SSL certificate problem”原因通常是本地开发证书没装或过期也可能是你用了自签名证书而客户端不信任。本地开发时先执行dotnet dev-certs https --check看看证书状态如果无效就dotnet dev-certs https --trust重新安装。远程部署时不要为了省事继续用自签名证书AI客户端对自签名证书普遍不友好直接用正规证书服务签发的证书成本很低却能帮你少踩一大片坑。5.3 调用超时与进程卡死远程模式下出现工具调用超时先用curl直接请求MCP端点确认它能在超时阈值内返回。如果直接请求也慢说明问题在你的业务Service本身和MCP无关。如果直接请求很快但客户端调用慢重点检查反向代理的缓冲配置SSE响应被缓冲会造成内容长时间不返回。本地stdio模式下出现卡死大概率是程序在等待标准输入或者意外崩溃。这种问题建议先把服务端的日志打到文件里客户端拉起后立刻查看日志输出很多异常在日志里一眼就能看出来。我自己遇到过一种情况Tool方法里用了Console.WriteLine打印调试信息结果stdio模式下这些输出被MCP客户端当成了协议消息来解析直接把连接搞崩了。记住stdio模式下不要往标准输出里写任何非协议内容。5.4 JSON序列化与返回结构不稳定的处理模型调用工具后能不能顺利把结果读明白很大程度取决于服务端返回的JSON结构。如果返回结构里面带有循环引用、DateTime格式不统一、或者是强类型对象序列化后带上了奇怪的属性名AI解读起来就容易出错。我的建议是给返回DTO统一用record类型日期字段序列化成ISO 8601字符串数字字段明确用decimal或int不要用object兜底。这样返回的JSON结构稳定AI从返回内容里提取信息时也不容易翻车。最后再分享一个我自己的小习惯在你把MCP服务端接进真实AI客户端之前先用程序化客户端把每个Tool都调一遍检查返回结构是否稳定。不要嫌麻烦因为AI客户端调用工具时对异常返回的容忍度极低只要一次返回结构不合法它在后续对话里可能就会“忘了”这个工具。把基础调用稳定性搞好后面接再多Tool都只是加Description的事。MCP这套协议目前迭代很快但底层的“服务端定义能力、客户端发现并调用”这一套思路未来很长一段时间内应该都不会变。
返回列表