各位 .NET 的同行们不知道你们最近有没有一种感觉AI 的火烧得越来越旺但大多停留在“聊天”、“写周报”、“改文案”这种层面。真正想把 AI 落到自己的业务里让它直接操作我们苦心经营多年的后端系统似乎总隔着一层膜。我以前也这么觉得直到我认真研究了 MCPModel 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 ClientClient 再发给 Server。Server 执行完业务逻辑把结果原样返回Claude 生成最终回答。整个过程对用户完全透明。对我们这种后端开发者来说大部分时候的重点是写好 Server 那半边但如果你打算做一个“自己的 AI 应用”那 Client 半边也得心里有数。.NET 生态里官方提供了ModelContextProtocol包一个包同时覆盖 Server 和 Client这套封装确实省掉了很多底层 JSON-RPC 的折腾。2.3 为什么传输层要区分 stdio、SSE 和 Streamable HTTPMCP 支持三种传输方式但用哪个完全看场景stdio客户端启动一个子进程来运行 Server两者通过标准输入输出通信。适合本地开发的 AI 工具比如 Claude Desktop 配置一个本地 MCP Server启动快、无需网络。SSEServer-Sent Events服务端通过 HTTP 单向推送事件给客户端适合处理流式响应和事件通知。Streamable HTTPMCP 最新推荐的传输模式客户端可以用普通的 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 Taskstring 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 ServerMapMcp()把 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 Taskstring Ask(string question, CancellationToken ct) { var response await chatClient.GetResponseAsync(question, cancellationToken: ct); return response.Text; } }如果你要走这条路记得在服务注册里加上 ChatClientbuilder.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 ServerClaude 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 streamWindows 能直接查看%APPDATA%\Claude\logs\下的日志文件。绝大多数连接失败都能在日志里找到原因最常见的就是 URL 写错、服务没启动、端口被占用。4.2 用对话触发工具调用验证“语义路由”是否生效一切正常后你可以在 Claude 的对话框里输入“请帮我查一下订单 SO-2025-0001 的状态。”Claude 做了什么它会解析这句话识别出“查订单状态”的意图发现有一个叫QueryOrderStatus的工具匹配这个意图于是请求 MCP Client 调用这个工具传入参数orderNumberSO-2025-0001。MCP Server 执行真实业务逻辑把结果返回给 ClientClaude 再把结果整理成自然语言回复你。这个过程中有个真正值得关注的点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 Taskstring QueryInventory( [Description(商品的唯一编码例如 SKU12345)] string sku, CancellationToken cancellationToken) { var result await _inventoryService.GetStockAsync(sku, cancellationToken); return System.Text.Json.JsonSerializer.Serialize(result); } }然后注册的时候使用AddMcpServerToolT()而不是从程序集扫描这样能用 DI 把服务注入进来builder.Services.AddMcpServer() .AddMcpServerToolInventoryMcpTools() .AddMcpServerToolOrderTools();5.2 把“读操作”和“写操作”分开注册按权限暴露MCP 工具一旦暴露给 AIAI 就相当于有了一把能操作系统的钥匙。我强烈建议在架构上把“读工具”和“写工具”拆开。比如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-cliPython 生态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提交任务返回任务 IDGetExportResult查询任务结果配合异步轮询问题彻底解决了。6.5 性能优化尽量让工具“轻”MCP 工具调用会经过完整的 AI 链路用户提问 → 模型推理 → 生成工具调用请求 → 网络传输 → 服务端执行 → 结果返回 → 模型总结。任何一步慢了用户体感都会很差。这里说的性能优化不是让你去调数据库索引而是从产品设计上尽量轻量化查询类工具的返回数据量不要太大。AI 对超长文本的处理能力有限传回一千条明细远不如传回聚合统计。给工具加缓存。同一个 SKU 的库存查询五分钟内返回相同结果也不是不可以没必要每次都打到数据库。能分页就分页。如果用户问“最近订单”给前 20 条就够了配合一个“加载更多”工具体验比一次性返回 500 条好得多。最后分享一个小技巧关于工具描述我后来发现了一个特别管用的写法在描述里写出“这个工具适合什么场景、不适合什么场景”。例如“当用户询问查询订单状态时使用。如果用户希望修改订单信息请使用 UpdateOrder 工具不要使用本工具。”这种“排除法”描述能大幅降低 AI 的错误调用率。原因是模型在工具选择时不仅仅根据关键词匹配还会语义理解描述中的约束条件。你告诉它“不适合做什么”它就不会在类似意图上误用。这个方法在我接入五个工具以上时效果尤为明显。你的工具越多越建议在描述里把工具边界写清楚。MCP 在 .NET 里的生态还在快速迭代但核心架构已经稳定。把现有 API 包装成 MCP 工具这件事属于前期投入小、后续回报高的改造。一旦你接入了第一套工具后面再接入新的工具、新的模型、新的业务方都会变得异常顺滑。希望这篇文章能帮你迈过最初那道坎。
