各位 .NET 的同行们,不知道你们最近有没有一种感觉:AI 的火烧得越来越旺,但大多停留在“聊天”、“写周报”、“改文案”这种层面。真正想把 AI 落到自己的业务里,让它直接操作我们苦心经营多年的后端系统,似乎总隔着一层膜。我以前也这么觉得,直到我认真研究了 MCP(Model Context Protocol,模型上下文协议),并且在 .NET 项目里把它跑通之后,才发现“让 AI 调你自己的接口”这件事,没有想象中那么玄乎,反而比传统方式干净利落得多。
这篇文章,我想从一个 .NET 后端开发者的视角,完整聊聊我把 MCP 服务端和客户端落地到实际项目里的全过程。不是照搬文档,而是把我查资料时的困惑、选型时的纠结、写代码踩过的坑、以及最终跑通那一刻的爽快感都写出来。如果你手里有一套 .NET API,想让自己的 AI 助手能安全、规范地直接调用它们,这篇文章应该能给你一套可以直接照抄的作业。
1. 一个 .NET 老接口的“AI 化”难题到底卡在哪
先说个场景。我手上维护着一套订单系统,Web API 是标准的 ASP.NET Core 项目,里面有查询订单、创建订单、修改状态、统计报表这一堆接口。系统用得好好的,直到业务方提了个需求:能不能让 AI 直接帮我们查订单数据?比如在群里问一句“昨天华东区的订单量是多少”,AI 直接给出答案。
第一个冒出来的念头是开一个接口给它调。可问题来了——AI 怎么知道该调哪个接口?带着什么参数?参数格式是什么?返回结果怎么解释?传统做法是让 AI 看 Swagger 文档,或者自己写一堆 Function Calling 的 JSON Schema 描述。我试过,功能确实能跑,但维护成本高得离谱。每加一个接口,就得给每个模型(GPT、Claude、文心)单独写一套 function 描述,还得手动测试描述和真实接口逻辑是否对得上。模型升级了、协议变了、字段描述写差了,行为就飘忽不定。
这就引出了 MCP 存在的意义。MCP 本质上解决的是“AI 和外部系统对话需要一套统一标准”的问题。它做的事情,在你第一次接触时会觉得“这不就是 RPC 吗”,但真正用起来,你会发现它定位的层级比 RPC 更高——它定义了“如何把 AI Agent 能够使用的工具、资源、能力暴露出来”,而不仅仅是“如何调一个远程过程”。
打个比方,你把 AI 想象成一个新入职的实习生。传统方式是丢给他一本 API 文档,说“你自己看着办”;Function Calling 则是你提前猜好实习生可能要干什么,每一步都给他在键盘上贴一个便利贴。MCP 做的事情,更像是给这个实习生一张合格的门禁卡,让他可以走进公司,根据墙上的指引牌找到各个办公室,然后按照门口写清楚的操作说明去使用里面的机器。
这套标准对 .NET 开发者尤其重要。因为 .NET 在后端领域非常成熟,我们沉淀了大量业务逻辑和 API。MCP 给了一个程度刚刚好的“管装接口”方案:既不用像 Function Calling 那样为每个模型写一遍工具描述,也不会像直接开一个裸端口给 AI 调用那么危险。数据模型、认证方式、调用边界,都可以通过代码清清楚楚地管控起来。
2. MCP 的构成拆解:Server、Client 和协议层各自扮演什么角色
如果你去翻 MCP 的官方文档,会发现它一直在强调三个概念:Server、Client、Protocol。我一开始以为这就是普通的 C/S 架构,后来实际操作了才明白,这里的 Client 和 Server 不是我们平时理解的那种“前端调后端”。
2.1 MCP Server 不是“被调的接口”,而是“能力的翻译官”
在 MCP 里,Server 是能力提供方,但这个“能力”不一定等于一个 HTTP 接口。它可以是一个工具(Tool),让 AI 执行某个动作;可以是一个资源(Resource),让 AI 读取某段数据;也可以是一组提示词(Prompt),引导 AI 以某种方式完成任务。最常用、也最容易落地的,是 Tool。
一个 MCP Server 的职责是:把你的业务能力包装成标准的“工具”,然后告诉任何连接的 Client:“我这里有这些工具,定义如下,你可以按标准格式调用我。”它本身可以是一个独立进程,也可以嵌入到现有 Web 应用里。进程间通信走的是 JSON-RPC 2.0,传输层可以是 stdio(标准输入输出),也可以是 SSE 或 Streamable HTTP。
这跟我们熟悉的“服务端接口”有本质区别。传统的服务端接口是被动等人调;MCP Server 更像是主动“递交简历”——它在启动时或连接时,会把自己的工具清单和数据模型发给 Client,让 Client 知道自己手里有什么牌。
2.2 MCP Client 是“AI 大脑”和“工具集”之间的接线员
MCP Client 这个角色,很多人容易搞混。它并不是用户直接操作的 App,而是嵌在 AI 应用里的一个模块,负责维护与 Server 的连接、接收工具清单、把 AI 的意图转化成具体的工具调用请求、再把结果返回给 AI。
以 Claude Desktop 为例,它在启动时就去连接配置好的 MCP Server,把服务器提供的工具全部加载进来。用户和 Claude 聊天时,Claude 的模型会根据对话内容自主决定“我该调用哪个工具”,这个请求交给 MCP Client,Client 再发给 Server。Server 执行完业务逻辑,把结果原样返回,Claude 生成最终回答。整个过程对用户完全透明。
对我们这种后端开发者来说,大部分时候的重点是写好 Server 那半边;但如果你打算做一个“自己的 AI 应用”,那 Client 半边也得心里有数。.NET 生态里,官方提供了ModelContextProtocol包,一个包同时覆盖 Server 和 Client,这套封装确实省掉了很多底层 JSON-RPC 的折腾。
2.3 为什么传输层要区分 stdio、SSE 和 Streamable HTTP
MCP 支持三种传输方式,但用哪个完全看场景:
- stdio:客户端启动一个子进程来运行 Server,两者通过标准输入输出通信。适合本地开发的 AI 工具,比如 Claude Desktop 配置一个本地 MCP Server,启动快、无需网络。
- SSE(Server-Sent Events):服务端通过 HTTP 单向推送事件给客户端,适合处理流式响应和事件通知。
- Streamable HTTP:MCP 最新推荐的传输模式,客户端可以用普通的 HTTP POST 发请求,服务端既能响应普通 JSON,也能推送流式事件。更适合跨网络、跨进程的正式部署。
我在实际落地时优先选择了 Streamable HTTP,因为我要把现有 ASP.NET Core 项目的 HTTP 链路直接用起来,不需要额外起一个独立进程。但如果你只是本机调试,stdio 会更简单,没有端口和安全组的概念,直接一个配置项就搞定。
2.4 对比:用 MCP 和写 Function Calling 的体验差异
这里必须给还没入坑的朋友提个醒,MCP 和 Function Calling 不是替代关系,而是“标准”和“实现”的关系。Function Calling 是模型层面的能力——模型说“我下一步想执行一个动作,参数是这些”;MCP 是应用层面的协议——把这个“动作”如何描述、如何传输、如何安全执行给标准化了。
之前的经验是,直接把业务接口映射到 Function Calling 的参数上时,参数校验、错误重试、鉴权逻辑全都得自己写,而且换一个模型就要重新适配。现在通过 MCP Server 暴露工具,Claude、GPT、甚至一些国内模型,只要是按照 MCP 协议实现的 Client,都可以直接使用同一套工具,不需要针对每家改代码。这种“一次包装,处处可用”的感觉,在你手里有多个模型需要接入的时候,价值会特别明显。
3. 用 C# 写一个 MCP Server:从 NuGet 包到第一个可调用工具
接下来进入正题。如果你已经把环境准备好了——需要一个支持 .NET 8 或更高版本的 SDK、一个顺手代码编辑器,外加一个 AI 客户端(我用的是 Claude Desktop 做验证),那我们开始动手。
3.1 先理解官方包的分工再动手
官方包有两个,职责不同,别搞混:
ModelContextProtocol:核心包,包含协议实现、类型定义、Server/Client 的基类,不依赖特定框架。ModelContextProtocol.AspNetCore:给 ASP.NET Core 用的集成包,提供了把 MCP Server 挂到现有 Web 应用里的扩展方法,支持 Streamable HTTP。
另外如果你要用 MCP 直接对接 AI 模型,可能还需要一些Microsoft.Extensions.AI系列的包。不过我第一次做的时候没急着接模型,而是先用一个独立的 AI 客户端(Claude Desktop)来验证 Server,这样能最快看到效果。等确认 Server 没问题,再去写自定义 Client。
安装命令很简单:
dotnet add package ModelContextProtocol dotnet add package ModelContextProtocol.AspNetCore如果需要在 Server 端调用 AI 大模型(比如让工具执行过程中自动调用 LLM),再装:
dotnet add package Microsoft.Extensions.AI.OpenAI安装时留意一下版本,建议直接装最新的稳定版。我踩过一个坑:早期预览版的 API 命名和正式版有较大出入,网上一搜全是旧教程,代码对标不上,浪费了不少时间。
3.2 定义你的第一个 MCP 工具
我拿一个最简单的业务场景举例:查询订单状态。
MCP 工具本质上是一个普通方法,加上[McpServerTool]特性标记,方法的参数和返回值会自动进行 JSON 序列化。官方封装的类型系统会基于方法签名生成工具描述,AI 模型会自动理解参数含义。不过,为了让模型更好地理解参数,最好给每个参数加清晰的描述,这个描述会通过协议完整地传给 Client。
来看代码:
using ModelContextProtocol; public class OrderTools { [McpServerTool(Name = "QueryOrderStatus", Description = "根据订单号查询订单当前状态")] public static async Task<string> QueryOrderStatus( [Description("订单号,例如:SO-2025-0001")] string orderNumber, CancellationToken cancellationToken) { // 这里调用你真实的业务服务,查数据库也好,调内部服务也行 await Task.Delay(100, cancellationToken); return $"订单 {orderNumber} 当前状态:已发货,物流单号 SF1234567890"; } }注意几点:
- 我用的返回类型是
string,因为想简化演示。实际项目里可以直接返回自定义类型的 JSON 序列化结果,MCP 会自动把复杂对象序列化成 JSON 字符串,AI 模型再根据描述去理解。 CancellationToken参数会被框架自动识别为请求取消信号,不需要 AI 传值。- 工具命名不要带中文、空格和特殊符号,建议采用 PascalCase 或 snake_case;描述信息务必写清楚“工具是干嘛的”、“参数应该怎么填”,AI 模型的工具选择能力极大依赖于这段描述的质量。
- 工具类可以加
[McpServerToolType]特性,也可以不加,框架会自动扫描程序集里带[McpServerTool]的公开静态方法。如果想用实例方法,需要把它注册到 DI 容器里。
3.3 注册 MCP Server 到现有 ASP.NET Core 项目
现在把这个工具挂到一个 ASP.NET Core Web API 项目上。我的做法是新建一个空的 Web API 项目(也可以直接在老项目里加),然后在Program.cs里配置:
using ModelContextProtocol.AspNetCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddMcpServer(options => { options.ServerInfo = new() { Name = "OrderSystemMCP", Version = "1.0.0" }; }) .AddMcpServerToolFromAssembly(typeof(OrderTools).Assembly); var app = builder.Build(); // 将 MCP Server 挂载到 /mcp 路径,使用 Streamable HTTP 传输 app.MapMcp(); app.Run();就这么简单。AddMcpServer注册核心服务,AddMcpServerToolFromAssembly扫描程序集里所有工具并注册到 MCP Server,MapMcp()把 MCP 端点映射到应用的/mcp路径。启动项目后,一个基于 Streamable HTTP 的 MCP Server 就跑起来了。
调用方式就是标准的 HTTP POST,请求体是 JSON-RPC 格式。你可以直接用 Postman 或者 curl 验证:
curl -X POST http://localhost:5000/mcp \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }'正常返回会列出你注册的所有工具,包括工具名称、描述、参数 Schema。如果这一步能看到OrderQuery,说明 MCP Server 已经能正常工作。
这里有个细节值得展开:为什么用MapMcp()而不是MapPost("/mcp")然后自己解析 JSON-RPC?因为在ModelContextProtocol.AspNetCore内部,框架已经帮你处理了 JSON-RPC 2.0 协议的各种边界情况,包括错误码、请求 ID 配对、流式响应协商等。自己写解析器的话,各种协议细节很容易漏。比如 JSON-RPC 要求,Client 发来的请求如果不含id,那它就是一个通知,服务端不需要回复;如果含id,即使处理出错也要返回带相同id的错误响应。这些细节直接写业务代码的人根本不会注意到,但协议实现错了 Client 就会静默失败,排查起来非常痛苦。
3.4 投影:让工具能够调用 AI 模型(可选但要懂)
接 AI 模型这部分,官方推荐的路径是先把依赖注入进来,然后把IChatClient传给工具。比如:
public class QaTools(IChatClient chatClient) { [McpServerTool(Name = "AskModelAboutPolicy", Description = "询问 AI 关于业务政策的回答")] public async Task<string> Ask(string question, CancellationToken ct) { var response = await chatClient.GetResponseAsync(question, cancellationToken: ct); return response.Text; } }如果你要走这条路,记得在服务注册里加上 ChatClient:
builder.Services.AddChatClient(new OpenAIClient(new ApiKeyCredential("your-key"))) .UseOpenAI("gpt-4o-mini");这个功能适合什么场景呢?比如你的 MCP Server 管理的工具很多,某些工具内部需要做数据判断和文本总结。这时候让 Server 内部嵌套一个 LLM 调用,可以把最终输出整理得更好读。但是要小心延迟和费用,不是所有工具里都应该塞一个模型进去。我在生产环境里只有两个工具用了内嵌 LLM,其他都是纯逻辑处理,响应速度才能控制在几百毫秒内。
4. 客户端接入实战:先把 Claude Desktop 调通,再谈扩展
服务端就绪之后,就需要一个客户端来验证了。我最先用的是 Claude Desktop,因为它的配置方式最直观,帮我确认了协议链路是通的。这个阶段不要急于写代码做自定义 Client,先把现成的 Client 跑通,再来理解客户端的工作原理会轻松很多。
4.1 配置 Claude Desktop 连接本地 MCP Server
Claude Desktop 通过配置文件来管理 MCP Server。不同操作系统路径不同,我的 Windows 上是%APPDATA%\Claude\claude_desktop_config.json。Linux、macOS 的路径分别是~/.config/Claude/claude_desktop_config.json和~/Library/Application Support/Claude/claude_desktop_config.json。
因为我的 MCP Server 是跑在 ASP.NET Core 里的,监听地址是http://localhost:5000/mcp,传输方式设为 Streamable HTTP,配置如下:
{ "mcpServers": { "order-system": { "command": "cmd", "args": ["/c", "npx", "mcp-remote", "http://localhost:5000/mcp"] } } }等一下,这里为什么要通过mcp-remote转发?因为 Claude Desktop 到目前版本为主,原生配置里的url字段对 Streamable HTTP 传输的支持还不够完整,反倒是command方式更稳定。mcp-remote是一个桥接工具,它通过 stdio 和 Claude Desktop 通信,然后把请求转发到指定的 HTTP 端点。简单理解就是,它的作用是让本地 stdio 的 Client 能够连接远程 HTTP 的 Server。
如果你的 ASP.NET Core 项目跑在别的电脑上,配置里直接换 IP 地址即可:
{ "mcpServers": { "order-system": { "command": "cmd", "args": ["/c", "npx", "mcp-remote", "http://192.168.1.100:5000/mcp"] } } }配置完后重启 Claude Desktop。如果你看到界面右上角出现了一个带插头的小图标,点开会列出已连接的工具列表,就说明 Server 被成功发现了。没出来的话,注意看一下 Claude 的日志:macOS 用log stream,Windows 能直接查看%APPDATA%\Claude\logs\下的日志文件。绝大多数连接失败都能在日志里找到原因,最常见的就是 URL 写错、服务没启动、端口被占用。
4.2 用对话触发工具调用:验证“语义路由”是否生效
一切正常后,你可以在 Claude 的对话框里输入:
“请帮我查一下订单 SO-2025-0001 的状态。”
Claude 做了什么?它会解析这句话,识别出“查订单状态”的意图,发现有一个叫QueryOrderStatus的工具匹配这个意图,于是请求 MCP Client 调用这个工具,传入参数orderNumber=SO-2025-0001。MCP Server 执行真实业务逻辑,把结果返回给 Client,Claude 再把结果整理成自然语言回复你。
这个过程中有个真正值得关注的点:Claude 是从工具描述和参数描述里“学”到了怎么调用接口,而不是靠硬编码。你的描述写得越清晰,AI 的调度准确率就越高。我在最初测试时,工具描述写得很简短,只写了“查询订单状态”五个字,结果 Claude 有时候会把参数传错;后来我把参数描述改成“订单号,格式为 SO-年份-四位流水号”,准确率立刻上来了。这个经验建议你尽早采纳。
4.3 写一个最小 .NET MCP Client:把 AI 能力嵌入自己的产品
验证完 Claude Desktop 之后,可以尝试在 .NET 产品里自己写一个 Client。这个 Client 要做到两件事:连上 MCP Server 拿工具列表,再通过IChatClient把用户问题交给大模型,由大模型决定调用哪个工具。
代码骨架大概长这样:
// 1. 创建 MCP 客户端连接 await using var mcpClient = await McpClientFactory.CreateAsync( new McpClientOptions { ClientInfo = new() { Name = "MyApp", Version = "1.0.0" } }, new HttpClientTransport( new HttpClient { BaseAddress = new Uri("http://localhost:5000/mcp") } )); // 2. 把 MCP 工具加载为 AI 函数 var functionInvocationClient = new FunctionInvokingChatClient(innerChatClient); await foreach (var tool in mcpClient.ListToolsAsync()) { // 把 MCP 工具映射成 AI 函数 // 实际中需要按 FunctionInvokingChatClient 的要求构造 AIFunction } // 3. 让模型基于用户输入自主选择工具 var response = await functionInvocationClient.GetResponseAsync("昨天华东区订单量多少?", cancellationToken: ct);严格来说,FunctionInvokingChatClient内部如何接收工具定义,要看你用的Microsoft.Extensions.AI版本。如果版本更新了 API,直接查官方示例就好。核心思路没有变:MCP Client 作为工具提供方,模型作为决策方。
这里我建议先不要急着做高深功能。把“列表”和“调用”这两个动作跑通,你就算完全理解了 MCP 的闭环。后续再逐步加上会话保持、多 Server 聚合、权限过滤等高级能力,那也是水到渠成的事。
5. 落地案例:把既有 ASP.NET Core 业务接口包进 MCP 工具集
前面讲的都是概念和最小实现,这一节拿一个更接近真实业务的项目做拆解,方便你对号入座。假设我有一个仓储管理系统的 Web API,里面有几个核心接口:
- 查询库存:
GetInventory(string sku) - 创建出库单:
CreateOutboundOrder(string sku, int quantity, string warehouse) - 查询最近入库记录:
GetRecentInboundRecords(string sku, int days) - 修改库存预警阈值:
UpdateSafetyStock(string sku, int threshold)
这些接口已经被现有系统调用得很稳定。现在要给 AI 用,但不能让 AI 直接访问底层数据库,也不能让 AI 绕过现有权限体系。于是我做了四步改造:
5.1 用仓储层封装,而不是把 Controller 直接暴露
很多人第一反应是把 Controller 里的方法拿来加[McpServerTool]完事。我不建议这样。Controller 的方法签名往往包含HttpRequest、ClaimsPrincipal、CancellationToken、DTO 等和协议报文强相关的东西,直接暴露给 MCP 会让工具描述变得混乱,而且 Controller 里的模型绑定逻辑和 MCP 的 JSON 绑定逻辑很可能冲突。
我的做法是新建一个InventoryMcpTools静态类,内部注入领域服务接口,然后把真实的业务参数梳理成简单类型。例如:
public class InventoryMcpTools { private readonly IInventoryService _inventoryService; public InventoryMcpTools(IInventoryService inventoryService) { _inventoryService = inventoryService; } [McpServerTool(Name = "QueryInventory", Description = "根据 SKU 查询商品当前库存数量,包括可用库存和在途库存")] public async Task<string> QueryInventory( [Description("商品的唯一编码,例如 SKU12345")] string sku, CancellationToken cancellationToken) { var result = await _inventoryService.GetStockAsync(sku, cancellationToken); return System.Text.Json.JsonSerializer.Serialize(result); } }然后注册的时候使用AddMcpServerTool<T>()而不是从程序集扫描,这样能用 DI 把服务注入进来:
builder.Services.AddMcpServer() .AddMcpServerTool<InventoryMcpTools>() .AddMcpServerTool<OrderTools>();5.2 把“读操作”和“写操作”分开注册,按权限暴露
MCP 工具一旦暴露给 AI,AI 就相当于有了一把能操作系统的钥匙。我强烈建议在架构上把“读工具”和“写工具”拆开。比如:
InventoryQueryTools:只包含查询类工具,给所有 AI 助理使用。InventoryWriteTools:包含创建、修改类工具,限制在内部高权限 AI 应用里使用。
这样你就可以在不同入口用不同的 Server 配置。比如内部助手连的 MCP Server 是带写权限的,对客服的 AI 只暴露查询类工具。因为工具是按类注册的,这个隔离做起来很干净。
5.3 给工具补充“参数校验”和“业务提示”,提高 AI 调用成功率
工具的参数校验和普通接口的参数校验不一样。普通接口的调用方是前端,参数错误会立刻有报错;但 AI 调用时,参数错了它自己不一定知道,甚至会不断尝试导致循环请求。所以参数校验逻辑要做到两点:
- 必须失败时抛异常,让 Client 拿到明确的错误信息;
- 错误信息要写得足够“口语化”,这样模型能读懂错误原因并尝试修正。
比如:
if (quantity <= 0 || quantity > 10000) { throw new McpException("出库数量必须大于 0 且小于等于 10000,请检查后重试"); }如果抛的是普通业务异常,协议层可能把它包装成-32603内部错误,模型看到的提示就没有那么明确。用 MCP 框架带的McpException,错误文本能更完整地传给模型,让模型有针对性地调整参数。
5.4 历史接口和 MCP 工具并行运行,渐进式替换
这个改造不需要停掉现有服务。MCP Server 只是挂在同一个 ASP.NET Core 进程里的一个额外端点,原有 Controller 照常工作。这样带来的好处是:你可以先让一个团队试用 AI 调用,没问题再推广;也可以在 AI 调用出问题时立刻回退,不影响原有系统稳定性。
我落地时就是把 MCP 端点挂在一个新的子路径/mcp-internal,通过反向代理只允许内网访问,对外完全不可见。这样即使 MCP Server 有什么安全问题,也不会暴露到公网。
6. 我在生产化过程中踩过的坑和沉淀的“安全底线”
这个章节算是最值钱的部分了。MCP Server 接入看似简单,但真到了生产环境,各种细节都开始冒头。我把踩过的坑按类别整理出来,希望能帮你少走弯路。
6.1 调试 MCP 服务端:没有客户端也能自测的路子
有一种情况最让人抓狂:代码写好了,但 Claude Desktop 就是连不上,日志里只有一句 Connection failed。这时候先把 Claude Desktop 晾在一边,用纯 HTTP 的方式测。
MCP 的 Streamable HTTP 传输,本质还是 JSON-RPC。你可以先用 Postman 测tools/list,看返回是否符合协议。如果这步都不对,问题一定在 Server 端,别去折腾客户端配置。
再进一步,MCP 官方还提供了一个 CLI 工具mcp-cli(Python 生态,pip install mcp-cli)。它能以交互模式连接任意 MCP Server,直接模拟客户端调用工具。这比 Claude Desktop 更适合日常调试,因为控制台信息要详细得多。装好后运行:
mcp-cli --transport http http://localhost:5000/mcp然后就能在交互式终端里执行tools/list、tools/call了。这个工具能看到原始 JSON-RPC 请求响应,对判断协议问题非常有帮助。我一遇到问题就会先开 mcp-cli 验证,确认 Server 无问题后再回来看客户端配置。
6.2 安全问题:认证、授权、审计一个都不能少
MCP Server 暴露的本质上是一组可调用的业务方法。如果这些方法背后牵扯到订单、资金、客户数据,安全要求的级别和你自己写一个公网 API 完全一样。但很多人第一次做 MCP 时容易忽略这一点,觉得“反正只有我自己能连”。这句话在本地调试时成立,一旦部署到服务器、连上公司内网,就不成立了。
我沉淀下来的安全底线有这几条,供参考:
- 内网部署优先:MCP Server 端点不要直接暴露公网。如果一定要提供远程访问,走公司已有的反向代理和身份网关。
- 认证机制不能省:Streamable HTTP 支持在
Authorization头里传 Bearer Token。在 ASP.NET Core 里加认证中间件是一件很成熟的事情,别因为麻烦就跳过。 - 工具级授权:在
[McpServerTool]方法内部,先判断当前调用者有没有权限执行这个工具。可以从 DI 里拿ClaimsPrincipal,也可以用更简单的自定义上下文。 - 记录审计日志:对写操作类工具,务必记录调用方、调用时间、入参、出参。出了问题有迹可查,这是我对线上系统的基本要求。
- 限流保护:AI 一旦开始调用,可能瞬间发起大量请求。最好在 MCP 端点前加一层限流,避免业务系统被拖垮。
6.3 工具设计:命名规范、描述质量和参数类型
工具描述是 AI 的“使用说明书”,写得好不好,直接决定 AI 的调用准确率。我的经验是多花点时间在描述上,甚至值得专门写一段代码把描述统一管理起来。具体来说:
- 工具名用
Verb + Noun结构,比如QueryInventory、CreateReturnOrder,不要只写Inventory这种名词。 - 参数全部使用简单类型,
string、long、int、double、bool。尽量不要使用复杂的嵌套对象作为工具参数,因为 AI 生成嵌套 JSON 的正确率远低于扁平键值对。 - 描述里避免歧义。比如“订单号”就有可能是订单 ID、订单编号、外部单号,必须在描述里说明清楚。
- 对返回结果做归一化处理。如果工具返回的是一个大 JSON,模型在总结时可能会把不需要的字段也带出来;与其让它自己挑,不如在工具内部就把结果格式化成精简后的字符串。
6.4 超时、取消与重试:别让一次调用拖垮整个连接
AI 调用工具时,如果工具体验很慢,模型可能会按自己的策略超时或者重试。所以在工具内部主动处理超时和取消,比依赖外部配置更可靠。
在 .NET 里,拿到CancellationToken后,把它透传给所有数据库查询和下游 HTTP 调用。如果某个工具要做长任务,就不要同步等待,而是设计成“提交任务 + 返回任务 ID + 通过另一个工具查询状态”的异步模式。这样既不会阻塞连接,也符合 AI 对话的习惯——模型可以先答应你“我正在处理”,再通过查询工具获取结果。
我最初写过一个导出报表工具,执行时间可能要两三分钟。直接同步调用时,MCP 连接已经被撑到超时,工具返回失败。后来改成两步式:CreateExportJob提交任务返回任务 ID,GetExportResult查询任务结果,配合异步轮询,问题彻底解决了。
6.5 性能优化:尽量让工具“轻”
MCP 工具调用会经过完整的 AI 链路:用户提问 → 模型推理 → 生成工具调用请求 → 网络传输 → 服务端执行 → 结果返回 → 模型总结。任何一步慢了,用户体感都会很差。这里说的性能优化,不是让你去调数据库索引,而是从产品设计上尽量轻量化:
- 查询类工具的返回数据量不要太大。AI 对超长文本的处理能力有限,传回一千条明细远不如传回聚合统计。
- 给工具加缓存。同一个 SKU 的库存查询,五分钟内返回相同结果也不是不可以,没必要每次都打到数据库。
- 能分页就分页。如果用户问“最近订单”,给前 20 条就够了,配合一个“加载更多”工具,体验比一次性返回 500 条好得多。
最后分享一个小技巧
关于工具描述,我后来发现了一个特别管用的写法:在描述里写出“这个工具适合什么场景、不适合什么场景”。例如:
“当用户询问查询订单状态时使用。如果用户希望修改订单信息,请使用 UpdateOrder 工具,不要使用本工具。”
这种“排除法”描述能大幅降低 AI 的错误调用率。原因是模型在工具选择时,不仅仅根据关键词匹配,还会语义理解描述中的约束条件。你告诉它“不适合做什么”,它就不会在类似意图上误用。这个方法在我接入五个工具以上时,效果尤为明显。你的工具越多,越建议在描述里把工具边界写清楚。
MCP 在 .NET 里的生态还在快速迭代,但核心架构已经稳定。把现有 API 包装成 MCP 工具这件事,属于前期投入小、后续回报高的改造。一旦你接入了第一套工具,后面再接入新的工具、新的模型、新的业务方,都会变得异常顺滑。希望这篇文章能帮你迈过最初那道坎。