
1. 先从一次AI 不会查库存的现场说起1.1 一个再常见不过的痛点LLM 只聊天不办事几个月前团队想给内部运营做一个自然语言查订单的助手场景很朴素运营同事在对话框里输入华东区上周有多少未发货订单AI 能直接查出数据而不是只丢回来一段你可以执行以下 SQL。当时我们第一版让 ChatGPT 接入了公司知识库效果确实不错但一碰到实时数据就露馅——模型训练数据里根本没有你数据库里的订单它只能靠猜猜错了还一本正经地给结论。为什么会这样因为大模型本质上是个离线大脑它擅长推理和生成但没有主动访问外部系统的能力。你想让它查订单、改状态、发通知就得把手伸给它。最原始的做法是把接口包装成 Function Calling让模型学会调用预定义的函数。但我很快发现Function Calling 的方案在不同模型、不同平台之间各搞一套OpenAI 有 OpenAI 的格式、Claude 有 Claude 的格式自己写 Agent 还得维护一堆胶水代码。团队里 .NET 服务一大堆我不想每个模型接入都重写一遍工具协议。正好那段时间 MCPModel Context Protocol开始火起来我研究了两天之后确定这玩意儿就是解决这个问题的。用一句话说MCP 是一种开放协议让 AI 应用通过标准化的方式发现并调用外部工具、读取资源、使用提示词模板。对 .NET 开发者来说最吸引人的地方在于——你不用把现有业务逻辑改造成某种特定 AI 平台的格式只要实现一个 MCP 服务端把现有接口暴露成工具任何支持 MCP 的客户端都能直接调。1.2 MCP 那份「AI 世界的 USB-C 接口」协议我第一次接触 MCP 时脑子里最先冒出来的类比是 USB-C。早年出门要带好几根线Lightning、Micro-USB、Type-A 各管各的后来一个 USB-C 口能充电、传数据、接显示器关键不是它长得统一而是协议统一了。MCP 在 AI 生态里干的是类似的事它定义了一套标准的 JSON-RPC 2.0 消息格式统一了AI 应用如何发现能力、如何调用能力、如何接收结果。具体来说MCP 有三个核心抽象Tools工具相当于给 AI 暴露的可调用函数。服务端声明工具名字、描述、输入参数的 JSON SchemaAI 根据这些信息决定要不要调用、传什么参数。这是本项目里最核心的部分我们的订单查询、状态更新都走工具通道。Resources资源类似于向外提供上下文数据不让 AI 执行操作只让它读取。比如把订单状态枚举、仓库地址表、业务术语字典暴露成资源模型在回答前可以主动加载这些信息。Prompts提示词模板可复用的对话模板比如订单客服模式售后处理流程用户或客户端可以一键把模板注入当前会话。这三个抽象里日常落地用得最多的是 Tools。Resources 和 Prompts 更像锦上添花但理解它们能帮你设计出更完整的能力面。标准协议规定的消息方法也很直白服务端要支持tools/list、tools/call、resources/list、resources/read、prompts/list这些方法客户端通过这些方法和服务端对话。1.3 Host、Client、Server 三层关系彻底理清MCP 的架构分成三层初次接触的人容易把 Client 和 Host 搞混。我习惯这样理解Host是用户直接面对的应用比如 Claude Desktop、IDE 插件、自研的 Web Agent。它是发起方负责把模型、界面、会话组织起来。Client是 Host 内部负责跟 MCP Server 通信的连接器。一个 Host 里可以同时存在多个 Client分别连接不同的 Server。Server是能力提供方负责实现具体工具、资源、提示词通过传输层stdio 或 HTTP跟 Client 通信。打个比方Host 是老板Client 是秘书Server 是各业务部门的接口人。老板要查订单不需要自己认识每个业务系统只需要让秘书去对接接口人把需求翻译成对方能听懂的话再把结果拿回来。后面我们在 .NET 里的代码也是一样Host 负责接用户输入Client 负责遵守 MCP 协议Server 负责实际查询数据库返回结果。这三层清楚了之后我们再看代码就顺了。下面我从服务端选型开始把整个落地过程拆开讲。2. 服务端方案选型stdio 还是 HTTP为什么我押注 Streamable HTTP2.1 两种传输方式的取舍MCP 服务端最常见的两种传输方式是 stdio 和 HTTP。stdio 的意思是客户端启动一个本地的子进程比如dotnet McpOrderServer.dll通过标准输入输出跟这个进程交换 JSON-RPC 消息。很多本地 AI 工具默认用这种方式好处是简单、没有网络暴露风险坏处是进程生命周期跟着客户端走每个连接一个进程不适合远程部署、负载均衡和多租户。HTTP 传输则让 MCP Server 成为一个真正的网络服务客户端通过 Web 请求连接。早期 MCP 的 HTTP 传输基于 SSEServer-Sent Events写起来绕调试也费劲后来协议演进出了 Streamable HTTP把请求响应统一成标准的 JSON-RPC over HTTP既可以单次请求直接返回结果也支持流式返回明显比 SSE 那版清爽。我在项目里最终选择了 Streamable HTTP原因很直接我们已有 .NET Web API 基础设施、鉴权网关、日志链路把 MCP Server 挂成一个 HTTP 端点天然能复用这些能力。客户端不只是内网的工具还有可能从运营的前端页面发起请求HTTP 方式是唯一现实的选择。团队不想在用户电脑上装 .NET 运行时stdio 方式要求每个客户端都能启动本地进程这个运维成本对内部业务系统来说不可接受。2.2 选型结论与 C# SDK 现状确定走 HTTP 后我翻了 .NET 生态的方案。MCP 的 C# SDK 目前由微软官方维护NuGet 包名是ModelContextProtocol系列其中ModelContextProtocol.AspNetCore可以直接跟 ASP.NET Core 集成几行代码就能把工具集合暴露成标准 MCP 端点。版本迭代比较快API 名字在不同小版本之间可能会有微调我下面给的代码基于我本机安装的 1.x 稳定版你如果拿到的是更新版本安装时以 NuGet 最新版为准核心思路不会变。除了官方 SDK社区也有一些基于 Source Generator 的封装但我的建议是优先用官方包。原因一是长期维护有保障二是我们团队已经有 ASP.NET Core 的技术栈直接用官方扩展方法最省事三是从ModelContextProtocol这包里能看到协议层、服务端、客户端三套命名空间后续想自己写 Host 也不用另起炉灶。2.3 项目结构规划我按一个真实的订单查询服务来规划项目结构读者可以直接照搬McpOrderServer/ ├── McpOrderServer.csproj ├── Program.cs ├── Services/ │ ├── IOrderService.cs │ └── OrderService.cs └── Tools/ └── OrderTool.csServices放业务逻辑Tools放 MCP 工具定义。这样拆的好处是业务代码和工具暴露层分开将来不接 AI 了也不影响现有接口。下面的落地步骤都是基于这个结构。3. 把现有 .NET 接口包装成 MCP 工具从订单服务到工具暴露的完整步骤3.1 创建服务端项目并接入依赖注入先用模板建一个空的 ASP.NET Core Web 项目dotnet new web -n McpOrderServer cd McpOrderServer dotnet add package ModelContextProtocol.AspNetCore然后用一个极简的Program.cs把 MCP 服务端挂到 HTTP 管道上using McpOrderServer.Services; using McpOrderServer.Tools; var builder WebApplication.CreateBuilder(args); builder.Services.AddScopedOrderService(); builder.Services.AddMcpServer() .WithHttpTransport(); builder.Services.AddSingletonMcpServerTool(sp { var orderService sp.GetRequiredServiceOrderService(); return McpServerTool.FromMethod(orderService.GetOrderById); }); var app builder.Build(); app.MapMcpServer(); app.Run();这段代码的核心动作有两个AddMcpServer().WithHttpTransport()把 MCP 协议处理管道注册进依赖注入容器McpServerTool.FromMethod把一个普通 .NET 方法包装成 MCP 工具。app.MapMcpServer()则是把协议端点映射到 HTTP 路径默认路径是/mcp也就是客户端连接地址就是http://localhost:5000/mcp。我用FromMethod显式注册是因为它最直观、最不容易出问题。SDK 也支持通过程序集扫描自动注册所有带特性的工具类团队人少、工具数量可控时显式注册反而更清楚每个工具对应的业务服务是怎么来的一眼就能看明白。3.2 编写业务服务与工具类让 AI 按订单号查订单现在写一个最典型的业务服务。假设我们有一个订单服务原本是给 Web API 用的namespace McpOrderServer.Services; public record OrderDto( string OrderId, string Status, string CustomerName, string ShippingAddress, DateTime CreatedAt, string? TrackingNumber); public interface IOrderService { TaskOrderDto? GetOrderByIdAsync(string orderId, CancellationToken ct); } public sealed class OrderService : IOrderService { public TaskOrderDto? GetOrderByIdAsync(string orderId, CancellationToken ct) { // 这里简化成模拟数据实际上通常查数据库或调用内部服务 var result new OrderDto( OrderId: orderId, Status: 已出库, CustomerName: 张三, ShippingAddress: 上海市浦东新区, CreatedAt: DateTime.Now.AddDays(-2), TrackingNumber: SF1234567890); return Task.FromResultOrderDto?(result); } }然后定义一个OrderTool类把GetOrderByIdAsync暴露成 MCP 工具using ModelContextProtocol; using System.ComponentModel; using System.Text.Json; using System.Threading; namespace McpOrderServer.Tools; public sealed class OrderTool(IOrderService orderService) { private readonly IOrderService _orderService orderService; [McpServerTool(get_order_by_id, Description 根据业务订单号查询订单详情。 订单号格式为 SO 加日期加流水号例如 SO20250401-008。 返回结果包含订单状态、客户姓名、收货地址、创建时间和物流单号。 如果订单不存在返回 null。 )] public async Taskstring GetOrderById( [Description(业务订单号格式如 SO20250401-008)] string orderId, CancellationToken ct) { var order await _orderService.GetOrderByIdAsync(orderId, ct); return JsonSerializer.Serialize(order); } }这里有几个关键点方法的返回类型我统一设计成Taskstring内部把OrderDto序列化成 JSON。MCP 工具调用结束后返回给 AI 的本质上是一段文本内容直接序列化成字符串对模型最友好模型拿到 JSON 再做后续总结。[McpServerTool]特性里的get_order_by_id是工具名AI 最终会在函数调用列表里看到这个名字。描述内容很关键我后面章节会专门说怎么写描述。方法的第一个参数orderId会被 SDK 自动转换成 JSON Schema 中的string类型[Description]用来补充参数说明模型会根据它来提取参数。CancellationToken ct参数 SDK 会特殊处理不会当成工具入参而是用于请求取消长时间查询场景下必须带上。写完OrderTool后更新Program.cs里的注册方式builder.Services.AddScopedOrderTool(); builder.Services.AddSingletonMcpServerTool(sp { var tool sp.GetRequiredServiceOrderTool(); return McpServerTool.FromMethod(tool, nameof(OrderTool.GetOrderById)); });注意这里OrderTool注册成Scoped因为它的构造函数依赖了同样是Scoped的OrderService而OrderService内部如果用了 EF Core 的DbContext就绝对不能把整条链提升为Singleton。这是 .NET 阶段最常见的生命周期坑后面我会专门展开。3.3 注册多个工具把工具集合交给 MCP Server实际项目当然不止一个查订单接口。假设我们再加一个按状态统计订单数量的工具写进同一个OrderTool类[McpServerTool(count_orders_by_status, Description 按订单状态统计订单数量。 状态支持待支付、已出库、已签收、已取消。 返回结果是一个 JSON 字符串包含每个状态对应的订单数量。 )] public async Taskstring CountOrdersByStatus( [Description(订单状态取值为待支付、已出库、已签收、已取消)] string status, CancellationToken ct) { var count await _orderService.CountByStatusAsync(status, ct); return JsonSerializer.Serialize(new { status, count }); }在Program.cs里再注册一个McpServerTool实例即可builder.Services.AddSingletonMcpServerTool(sp { var tool sp.GetRequiredServiceOrderTool(); return McpServerTool.FromMethod(tool, nameof(OrderTool.CountOrdersByStatus)); });如果工具数量膨胀到几十个这种显式注册会有点啰嗦。那时可以改用程序集扫描builder.Services.AddMcpServer() .WithHttpTransport() .WithToolsFromAssembly(typeof(Program).Assembly);扫描式注册的代价是工具的实例化和依赖注入规则变得隐晦我建议一开始先用显式注册把工具和服务的对应关系理清楚等数量真多到影响维护再说。3.4 用 MCP Inspector 做第一轮联调服务端代码写完第一件事不是接客户端而是用官方 Inspector 做联调。启动方式很简单dotnet run --project McpOrderServer --urls http://localhost:5000 npx modelcontextprotocol/inspectorInspector 启动后会打开一个本地 Web 页面在连接配置里选择 HTTP输入http://localhost:5000/mcp就能看到服务端暴露的工具列表。点进get_order_by_id可以手工填参数、直接调用返回结果会原样展示。这一步能很快暴露服务端的问题比如工具没注册上、参数 schema 生成不对、返回内容格式异常等等。我特别建议在接入 AI 之前先用 Inspector 把每个工具都手工调用一遍。因为后面让大模型自动调工具时一旦参数不对排查链路会拖得很长。工具自己先跑通后续的问题就主要集中在提示词和描述上而不是服务端本身。3.5 我踩过的序列化大坑参数描述和类型映射说一个实际踩过的问题。最初我把参数类型设计成一个复杂的OrderQuery对象public record OrderQuery(string? Status, DateTime? Start, DateTime? End, int Page 1);SDK 确实会把这个类自动转成 JSON Schema 的object类型模型也能看到里面的字段。但问题出在模型理解上——它面对一个复杂对象时经常搞不清哪些字段必填、哪些可选、日期格式是什么。我们测试时模型把日期填成了2025年4月1日服务端一解析直接炸。后来我吸取教训拆解成两个原则工具参数尽量用简单类型。比如日期范围就拆成startDate和endDate两个字符串参数并在描述里写明yyyy-MM-dd格式模型几乎不会填错。能用描述说清的就别指望模型猜。比如状态字段写取值为待支付、已出库、已签收、已取消模型会老老实实从这组值里选而不是自由发挥。另外提醒一点DateTime类型在 JSON Schema 里默认生成的是 ISO 8601 字符串格式说明但很多模型对ISO 8601的具体含义并不敏感。所以凡是涉及日期、时间、枚举、编码规则都建议在参数描述里给出明确示例。模型是模仿高手你给它一个格式如 xxx的例子它照做的概率远高于你给它一套规范。4. 客户端接入从 Claude Desktop 配置文件到自研 Agent 的工具调用4.1 用官方 C# SDK 写一个最小 MCP 客户端服务端准备好之后客户端接入是另一大块。先从最纯粹的 C# 客户端开始方便你理解 MCP 通信的完整链路。新建一个控制台项目dotnet new console -n McpOrderClient cd McpOrderClient dotnet add package ModelContextProtocol最小客户端代码如下using ModelContextProtocol; using ModelContextProtocol.Client; var httpClient new HttpClient { BaseAddress new Uri(http://localhost:5000) }; var transport new McpClientTransport(httpClient) { ServerEndpoint new Uri(http://localhost:5000/mcp) }; await using var client new McpClient(transport); await client.ConnectAsync(); var toolResult await client.ListToolsAsync(); foreach (var tool in toolResult.Tools) { Console.WriteLine($工具名{tool.Name}); Console.WriteLine($描述{tool.Description}); } var callResult await client.CallToolAsync( get_order_by_id, new Dictionarystring, object? { [orderId] SO20250401-008 }); foreach (var content in callResult.Content) { if (content is TextContent text) { Console.WriteLine(text.Text); } }这段代码做完三件事连接 MCP Server、拉取工具列表、调用指定工具并读取文本结果。McpClient内部已经实现了tools/list、tools/call这套 JSON-RPC 交互你不需要手动拼请求。这个客户端也可以直接嵌入到自研 Agent 后端里作为连接外部工具的统一入口。4.2 把服务端接入 Claude Desktop / 其他 Host如果你的目标是快点看到效果可以直接拿现成的 Host 做验证。Claude Desktop、Codex、Trae 这类工具都支持配置外部 MCP Server。以 Claude Desktop 为例配置文件路径通常在%APPDATA%\Claude\claude_desktop_config.json{ mcpServers: { order-server: { command: dotnet, args: [/path/to/McpOrderServer.dll], transport: stdio } } }这种配置方式要求 MCP Server 以 stdio 方式启动所以服务端要额外支持 stdio 传输。我们的服务端目前配的是 HTTP不过官方 C# SDK 可以同时挂多个传输端点在Program.cs里加builder.Services.AddMcpServer() .WithHttpTransport() .WithStdioTransport();这样同一个服务端既能为远程 HTTP 客户端服务也能被本地桌面工具拉起。如果你手里的 Host 对远程 HTTP 支持得比较好有些也能直接配置url字段具体看各家的配置约定。实际上在我落地时Claude Desktop 这类工具更适合作为演示如何连接 MCP Server的验证环境真要集成到公司业务里我还是建议自研客户端。因为内部业务流程涉及权限、审批、审计这些逻辑需要嵌到自己的 Agent 应用里而不是放在第三方桌面软件中。4.3 在自研 Agent 中把 MCP 工具转成模型可用的 function自研 Agent 的常见套路是用户问题进 LLM → LLM 决定调用某个函数 → 后端执行函数 → 把结果回传给 LLM → LLM 给出最终回答。MCP 在这里的定位是函数执行层也就是后端拿到函数名和参数后不用自己写一堆业务调用直接转给 MCP Client 完成执行。但这里有一个衔接问题大多数 LLM 平台的 function calling 格式跟 MCP 的tools/list返回格式并不完全一致。我在项目里写了一个小适配层把 MCP 工具转成 OpenAI 风格的 functionvar openAiTools new Listobject(); foreach (var tool in mcpToolList.Tools) { openAiTools.Add(new { type function, function new { name tool.Name, description tool.Description, parameters tool.InputSchema, } }); }这样 LLM 就能看到全部可调用工具。模型返回一个 function call 后我的代码再调用 MCP Clientvar result await mcpClient.CallToolAsync( functionCall.Name, JsonSerializer.DeserializeDictionarystring, object?(functionCall.Arguments)!);一套标准的工具调用闭环就串起来了。整个链路里具体业务查询仍然发生在 MCP Server 端Agent 只负责模型决定调用 → 转交 MCP 执行 → 结果回传。这也是为什么我说 MCP 对 AI 应用开发的价值是解耦——业务团队维护 MCP 服务端Agent 团队只做协议对接双方不用互相绑定。4.4 联网调用与内网发布场景的注意点接入自研 Agent 之后还要考虑部署拓扑。我们的服务端跑在内网Agent 后端也在内网只要网络通就没问题。但如果客户端在公网、服务端在内网就不能直接连通了。此时有几个常见方案给 MCP Server 所在的 ASP.NET Core 服务加一层反向代理用 TLS 对外暴露客户端通过https://your-domain.com/mcp连接。在网关层做租户或 IP 白名单限制避免任意公网客户端都能调用。如果场景允许也可以把 MCP Server 部署到 DMZ 区网络层做单向访问控制。HTTP 传输方式在这种拓扑下是唯一合理的选择因为 stdio 要求进程在客户端本机启动完全没法跨网络。这一点在我选定 Streamable HTTP 时就已经确定后面部署时果然省了很多事。5. 上线前绕不开的四个坑描述、超时、安全与生命周期5.1 工具描述写得像废话AI 就会选错工具MCP 工具是给模型看的不是给程序员看的。模型不会读你的 C# 代码它只能看工具名、描述、参数 schema。所以描述文本的质量直接决定了工具被调用的准确率。我见过最典型的反面教材是查询订单这种描述等于没写。模型只知道有个工具叫查询订单但不知道它查询的是内部订单号还是外部销售单号、不知道返回什么字段、不知道适合什么场景遇到订单相关的用户问题可能把这个工具当成万能接口硬调甚至选错成另一个同类工具。改进后的描述长这样根据业务订单号查询订单详情仅支持以 SO 开头的业务订单订单号格式如 SO20250401-008。返回订单状态、客户姓名、收货地址、创建时间、物流单号。该工具适合回答我的订单到哪了订单什么时候发货等问题。如果订单不存在返回 null 字符串。写描述的心法可以总结成三个要素这个工具做什么一句话说清功能边界避免模型把不相干问题也套到这个工具上。参数怎么填明确格式、取值范围、示例让模型提取参数时有据可依。返回什么、适合什么问题给出典型的用户问题类型模型会把它和当前对话匹配。写描述跟写接口文档很像只不过读者不是程序员而是大模型。你描述得越具体模型表现得越聪明这比换更强的模型还管用。5.2 长查询接口容易超时超时、取消与异步进度MCP 客户端调用工具时底层走的是 HTTP 或 stdio 通道任何一层超时都会导致调用失败。我们第一次接报表查询工具时用户问题统计最近三个月各仓发货量让模型去调一个聚合接口结果跑了三十多秒客户端直接超时返回给模型的是一个错误信息模型没办法只能跟用户说查询失败。解决思路有三个层次调大超时时间HttpClient.Timeout或者 MCP 客户端配置里的超时参数可以调大。但这只是治标几十秒以上的查询终究不靠谱。把长任务拆成两个工具一个工具负责提交任务立刻返回taskId另一个工具负责查询任务结果。模型先调用提交任务接口再根据返回的taskId轮询结果。这个模式跟异步任务队列一模一样能彻底绕开单次请求超时问题。利用 MCP 的进度通知机制如果 SDK 支持服务端可以在工具执行过程中发送进度通知客户端把进度展示给用户。这种方式体验最好但实现成本稍高适合查询时间确实长的场景。我后来把长报表查询统一改成了提交任务 轮询结果两个工具稳定运行至今。这类工具在描述里一定要说明调用后返回 taskId请继续调用查询任务结果工具直到状态为完成模型会自动走完两步流程。5.3 认证与数据安全MCP 不是免密通道很多人第一次把工具暴露出来时脑子里默认这是内部服务不用鉴权。我必须要提醒MCP Server 一旦走 HTTP 暴露它就是一个普通的 Web 服务任何能访问到这个地址的客户端都能拿到工具列表并调用工具。如果你的工具里有查订单、改状态、访问客户信息这类能力不鉴权就是裸奔。我给项目加的是一套组合拳JWT Bearer 认证在Program.cs里启用 ASP.NET Core 自带的认证给MapMcpServer()加RequireAuthorization()。客户端调用前先通过统一登录接口拿 token再在请求头里带上Authorization: Bearer token。字段白名单工具方法内部对返回结果再做一次脱敏或过滤比如不返回完整手机号、不返回内部备注。这层不能只依赖数据库查询防止未来工具扩展时漏掉。审计日志在OrderTool里记录每次工具调用的参数、调用者身份、执行结果摘要。后续如果出现数据泄露或者误操作能追溯到具体会话。限流按调用者或 IP 做限流防止有人把工具当成免费接口疯狂调用。MCP 工具往往比普通接口更危险因为它是被大模型自动调用并发频率可能远高于人工操作。你可以在工具类外面包一层中间件或者直接在方法开头做检查。原则是MCP 工具暴露的是能力不是权限能力可以被模型自动触发权限必须由你的代码严格把关。5.4 并发场景下的 DbContext 生命周期与连接泄露这是 .NET 项目最容易踩的坑我单独拿出来说。一开始为了省事我试图把所有东西都注册成 Singleton包括OrderService结果一压并发就报错DbContext 不能同时被多个线程使用。原因不难理解。EF Core 的DbContext是轻量工作单元不是线程安全的官方推荐的生命周期是Scoped。当OrderService被提升为 Singleton 时里面注入的DbContext就变成了共享对象多个工具调用并发进来必然打架。正确做法是让整个依赖链保持Scopedbuilder.Services.AddScopedOrderService(); builder.Services.AddScopedOrderTool();然后注册 MCP 工具时利用IServiceProvider来解析工具实例而不是直接new一个builder.Services.AddSingletonMcpServerTool(sp { var scopeFactory sp.GetRequiredServiceIServiceScopeFactory(); var tool scopeFactory.CreateScope().ServiceProvider.GetRequiredServiceOrderTool(); return McpServerTool.FromMethod(tool, nameof(OrderTool.GetOrderById)); });如果工具类里没有DbContext这类 scoped 依赖也可以放心注册成 Singleton。但这个判断得由你自己做凡是在工具方法里访问了数据库、Redis、外部 HttpClient都要注意相应客户端的生命周期。另外客户端侧要用await using释放McpClient防止连接和内部缓冲区泄漏。HTTP 传输模式下每个客户端实例持有一个HttpClient频繁创建客户端却不释放最后会吃满连接池。我的做法是Agent 后端启动时创建单一 MCP Client整个进程生命周期内复用只在应用关闭时统一释放。6. 端到端实测用户一句话AI 真的把订单数据拿回来了6.1 实测链路所有代码跑通后我在测试环境走了一遍完整链路。前端是自研的 Web Agent 对话框用户输入查一下订单 SO20250401-008 现在到哪个环节了中间链路依次是用户问题发到 Agent 后端调用 LLM。LLM 根据系统提示和工具列表选择调用get_order_by_id工具参数自动提取为orderId SO20250401-008。Agent 后端把函数调用转给 MCP Client。MCP Client 通过 Streamable HTTP 请求http://localhost:5000/mcp。MCP Server 收到tools/call请求路由到OrderTool.GetOrderById。OrderService返回模拟订单数据工具方法把它序列化成 JSON 字符串。MCP Server 把 JSON 字符串作为工具结果返回给 Agent。Agent 把结果回传给 LLMLLM 生成最终回答订单 SO20250401-008 当前状态为已出库物流单号是 SF1234567890预计 2 天内送达。需要我帮你查询其他订单吗整个流程大概 3 到 5 秒主要耗时在模型推理上真正查数据库的时间几乎可以忽略。跟我最初设想的完全一致业务代码一行没改只是多加了一层工具包装和协议通道AI 就能访问实时数据了。6.2 后续扩展空间这套架构跑通后可以扩展的方向其实不少。我列一列我实际调研过的几个点供你参考接入更多工具订单查询、库存查询、客户信息查询、运费试算、售后单创建都可以按同样的模式暴露成 MCP 工具。每个工具一个方法业务逻辑继续留在Service层。用 Resources 暴露业务字典把订单状态枚举、发货方式、仓库列表做成 Resources模型在回答前可以先读取这些上下文减少对工具调用的依赖。比如用户问有哪些订单状态模型直接读资源就能回答不需要调数据库。用 Prompts 固化场景把订单客服物流催办等场景做成提示词模板用户一键进入对应模式模型会用更贴合业务的话术回答问题。监控与可观测性给 MCP Server 接入 OpenTelemetry记录工具调用频率、平均耗时、错误率。模型自动调用工具的场景比人工操作更难排查问题必须有日志链路追踪。6.3 一点个人经验最后分享一个我实际沉淀下来的小技巧MCP 工具不是越多越好而是越说明书化越好。我见过不少人一口气注册了几十个工具模型反而选择困难经常选错。我的建议是先只暴露最核心的五六个工具把描述写到满意为止再逐步增加。每次新增工具后用 MCP Inspector 手动验证一遍同时在 Agent 里跑几条典型用户问题看模型是否能准确选中新工具。另外工具命名上尽量用动词 业务对象的组合比如get_order_by_id、cancel_order、query_shipping_route不要用execute、do_something这种抽象名字。模型对英文命令式命名更敏感描述是中文还是英文其实影响不大工具名尽量统一用英文。这套方案落地到现在团队里十几个旧接口接进 Agent 的节奏基本稳定在半天一个。真正花时间的不是协议对接而是把工具描述和参数边界想清楚。只要这一步做扎实MCP 的威力会远超你的预期。