MCP这个词,近半年在AI圈子里几乎快被说烂了。但真正让我决定上手去搞的,是团队里一个真实需求:产品经理说,能不能让AI自动帮运营同事查订单、改状态、拉报表,而不是每次都在对话里贴一份Excel给我?于是我开始研究怎么把手上现成的.NET接口,直接变成AI能调用的能力。
我所说的这个“能力”,指的就是MCP(Model 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在你内部知识库里检索资料,你可以把知识库包装成Resource,AI在回答之前会先读取相关内容。这种东西如果自己实现,要考虑协议格式、传输层、鉴权,工作量不小;用MCP就省事了。从协议设计角度来看,MCP更像是一个“AI时代的接口网关”,它不是替你做业务逻辑,而是把业务能力以模型能理解的方式暴露出去。
2. 服务端先行:用.NET把自己的接口改造成MCP Server
2.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 Task<UserInfo> 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() .WithTools<QueryTools>(); 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 Dictionary<string, 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里或路径配错 | 手动在终端执行启动命令,确认进程能正常拉起 |
| 发现不了Tool | SDK版本与代码写法不匹配 | 检查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这套协议目前迭代很快,但底层的“服务端定义能力、客户端发现并调用”这一套思路,未来很长一段时间内应该都不会变。