news 2026/9/23 8:08:55

.NET 后端如何通过 MCP 协议让 AI 安全调用你的业务接口

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
.NET 后端如何通过 MCP 协议让 AI 安全调用你的业务接口

让 AI 直接调你的 .NET 接口——这句话放到一年前,可能还会被当成“大模型幻觉吹出来的需求”。但现在你再提,业内已经有一个非常具体的协议在支撑了,就是 MCP(Model Context Protocol,模型上下文协议)。作为 .NET 后端工程师,我第一次接触 MCP 的时候,脑子里冒出来的第一个问题是:“我辛苦写的接口,凭什么要让 AI 来猜着调?”后来把协议和服务端、客户端都跑通之后,我才发现,这个问题的核心不是“接口能不能被调”,而是“AI 客户端怎么知道你的接口存在,知道了之后又怎么按契约来调”。

这篇文章我会用一套完整的 .NET 落地实践来讲清楚 MCP 服务端和客户端怎么实现。内容包括协议原理、服务端封装、客户端调用、实战排查,所有代码都在 .NET 8 环境下验证过。想给现有系统开一个“AI 通道”,又不想为每家模型厂商单独写适配层的同学,可以直接照着做。

1. 项目背景与核心思路

1.1 MCP 到底在解决什么问题

先回到那个最朴素的问题:AI 模型本身不会主动去调你的 API,它只会“生成文本”。但 AI 应用一旦要落地到业务场景,比如查库存、发工单、调订单状态,就必须让它能操作真实系统的数据和方法。

在 MCP 出现之前,常见做法有以下两种:

  • 把接口文档丢进 system prompt,让模型自己“猜”应该怎么构造 HTTP 请求;
  • 为每个接口单独实现一套 Function Calling 适配层,把参数、schema、调用逻辑揉在一起。

第一种方式很不稳定,模型经常把参数名写错,接口返回值一变化 prompt 就失灵。第二种方式倒是稳,但你每接一个模型供应商,或者每次加一个接口,都要重新写一套适配代码。时间一长,工程里到处都是“half-function-calling”的死代码。

MCP 的思路本质上和数据库驱动接口很类似:定一套统一的协议,客户端和服务端各说各话,但都遵守同一个“接口描述语言”。服务端把工具、资源、提示词暴露出来,客户端拿到这份描述后自动发现、自动调用。对 .NET 开发者来说,就是你写一个标准 ASP.NET Core 服务,把现有业务方法包装成 MCP 工具,然后各种支持 MCP 的 AI 客户端就能直接拉起来用。

1.2 这套方案的适用场景

我做了几个测试场景之后,觉得最适合用 MCP 落地的,是下面这几类:

  • 内部业务系统接入 AI 助手:比如 OA 系统里的请假查余额、CRM 里的客户查询,AI 助手通过 MCP 直接读写内部接口,不用绕一层中间服务。
  • 给大模型工具链增加操作能力:像 Playwright MCP、Figma MCP、Blender MCP 这些生态里的名字已经非常响,本质都是把某个垂直领域的能力封装成标准工具集。
  • 团队内部共享 AI Agent 技能:后端维护一份 MCP Server,前端、测试、产品都能通过各自的 AI 客户端去调用同一组接口,避免了“到处写死接口地址”的混乱。

对我而言,最大的收益不是“AI 能调我的接口”,而是“接口契约变成了标准化产物”。以前给模型写 function schema,每个模型一套;现在只需要维护一个 MCP Server,协议栈统一,客户端随便换。

2. 协议原理与整体架构

2.1 三个核心抽象先搞清楚

MCP 协议里最核心的三个抽象概念,如果理解不透,后面写代码会反复返工。

  • Tools(工具):这是 AI 客户端真正会“执行”的东西。每个工具对应一个可调用方法,有名字、有描述、有参数 schema。AI 会根据用户输入判断要不要调这个工具、传什么参数。工具执行结果可以返回给模型继续生成回答。
  • Resources(资源):暴露给 AI 客户端读取的数据源,相当于“只读数据接口”。比如数据库里的配置表、文件内容、日志片段。AI 客户端可以主动读取资源内容作为上下文补充。
  • Prompts(提示词模板):预置在服务端的一组提示词模板,客户端可以通过模板名获取。这个适合把复用的指令、few-shot 示例封装在服务端,避免客户端每次都要拼一大串提示词。

实际项目中,Tools 用的最多,Resources 适合做 RAG 的前置条件,Prompts 看起来简单但很少有人真正设计好。真正落地时,我的建议是先从 Tools 起步,跑通主链路后再设计 Resources 和 Prompts。

2.2 消息在 AI 与 .NET 方法之间怎么流转

MCP 的底层传输协议是 JSON-RPC 2.0,之上定义了初始化、工具列表、工具调用等消息类型。流程可以用一句大白话描述:客户端先和服务端握手,然后问“你有哪些工具”,拿到工具列表后,根据用户问题选择一个工具并传入参数,服务端执行完把结果返回,客户端再把结果交给大模型继续生成。

具体到 .NET 服务端,一次完整调用长这样:

  1. AI 客户端向http://localhost:5000/mcp发起连接;
  2. 客户端发送initialize请求,确认协议版本和能力;
  3. 服务端返回 server 信息和 capabilities;
  4. 客户端发送tools/list,服务端返回所有注册工具的 JSON Schema 描述;
  5. 客户端根据用户意图发送tools/call,里面带上工具名和参数字典;
  6. 服务端匹配到 C# 方法,执行后把结构化结果返回。

这整个交互对使用者来说几乎是透明的。你在客户端里自然地说“帮我查一下北京今天的天气”,背后就是上面这些步骤,只是客户端帮你隐藏了。

2.3 为什么不是“直接把 API 文档丢给 AI”

很多人第一反应是:既然 AI 能读文档,那我直接给它 Swagger JSON 不就行了?理论上可以,但两个致命问题马上会出现:

  • 调用语义不明确:Swagger 描述的是 HTTP 端点,AI 需要理解 RESTful 语义、路径参数、请求头、鉴权方式,每一步都有可能产生歧义。
  • 操作边界不可控:AI 拿到全部 API 文档后,理论上可以调用所有端点。你不想让它 DELETE 掉生产库吧?MCP Server 注册工具的时候,可以明确只暴露允许调用的方法,把操作面收得很小。

所以 MCP 不是简单“接口文档的翻版”,它更像是一个面向 AI 场景的、限制操作面的、契约化方法注册中心。这个定位决定了它在架构上和普通 Web API 有本质区别。

3. 服务端落地:把 .NET 接口封装成 MCP Server

3.1 初始化一个最小 MCP 服务端

我用的是官方 Model Context Protocol 仓库下的 C# SDK,NuGet 包当前常用的是ModelContextProtocol.AspNetCoreModelContextProtocol.Server,不同预览版命名可能略有差异,但整体思路通用。

先建一个空的 ASP.NET Core Web API 项目,目标框架选 .NET 8。Program.cs 里的最小配置如下:

using ModelContextProtocol.AspNetCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddMcpServer(); builder.Services.AddMcpToolsFromAssembly(); var app = builder.Build(); app.MapMcp("/mcp"); app.Run();

这段代码做了三件事:

  • AddMcpServer()把 MCP Server 的核心服务注入容器;
  • AddMcpToolsFromAssembly()扫描当前程序集里带有McpServerTool标记的工具类;
  • MapMcp("/mcp")把 SSE 协议的监听端点暴露到/mcp路径。

启动后用浏览器直接访问https://localhost:7001/mcp会看到连接挂起,这是正常的,因为 MCP 是长连接协议,不是普通 REST 接口直接返回 JSON。

3.2 把现有接口方法注册成工具

假设我有个订单查询服务,最核心的方法是“根据单号查订单状态”。改成 MCP 工具最省事的做法,是直接在方法上加特性:

using ModelContextProtocol.Server; public class OrderTools { [McpServerTool("query_order_status")] public static async Task<string> QueryOrderStatus( [McpServerToolParameter(Description = "订单号,必填")] string orderNo, CancellationToken cancellationToken) { // 这里调用你现有的业务服务或仓储层 var order = await orderService.GetByNoAsync(orderNo, cancellationToken); if (order == null) { return "未找到该订单"; } return $"订单 {order.OrderNo} 当前状态:{order.Status},更新时间:{order.UpdatedAt:yyyy-MM-dd HH:mm:ss}"; } }

这里有几个设计要点:

  • 方法描述一定要写清楚:AI 客户端会拿方法描述和参数描述来理解你的工具。描述越具体,模型选择工具的成功率越高。我踩过最痛的一次坑就是参数只写了一个“id”,结果模型把用户输入的客户编号、商品 ID、订单号全往上塞。
  • 返回格式尽量是自然语言或简单 JSON:让模型可以直接引用并继续生成回答。返回一堆堆砌字段的对象,模型虽然能读,但生成效率会下降。
  • CancellationToken 直接透传:调用超时或者用户中途取消时,服务端能及时释放资源。

如果你的业务方法散落在多个 service 类里,不想改动它们,也可以写一个包装类,在包装类里加特性,内部调用现有方法。这样既不会污染业务层,又能把 MCP 工具集中管理。

3.3 工具注册的两种方式对比

除了扫描程序集,SDK 通常还支持在服务注册时手动注册工具。两种方式各有场景。

注册方式优点适合场景
程序集扫描改一个类加特性就能自动暴露,扩展快初创项目、内部小工具、快速验证
手动注册注册逻辑集中,可以加条件判断大项目、有权限控制、动态判断工具是否可用

手动注册的伪代码大致长这样:

builder.Services .AddMcpServer() .AddMcpTool("query_order_status", async (McpServerToolContext ctx, string orderNo) => { // 动态处理逻辑 });

个人建议:接近生产环境时不要过度依赖自动扫描,最好加一层“工具注册表”,明确哪些类、哪些方法可以对外暴露。不然同事在业务类里随手加个特性,这个接口就被 AI 看到了,风险面太大。

3.4 服务端鉴权与访问控制

MCP 服务端和普通 Web API 一样,必须考虑“谁在调用”。尤其是在公网部署时,不能让任何人连上你的/mcp端点就随便执行工具。

我在服务端里做了两层防护:

  • 第一层,路由级别加鉴权中间件,校验调用方的 Token;
  • 第二层,工具方法内部做业务级权限判断,根据调用方身份决定能不能执行某个具体操作。
app.Use(async (context, next) => { if (context.Request.Path.StartsWithSegments("/mcp")) { var token = context.Request.Headers["Authorization"].FirstOrDefault(); if (!IsValidToken(token)) { context.Response.StatusCode = StatusCodes.Status401Unauthorized; return; } } await next(); });

注意,这里的 Token 校验只是示例。生产环境可以考虑接入 OAuth2 Client Credentials,或者其他内部统一认证方案。核心原则是:MCP Server 不是“内网免登”的借口,它只是传输和契约层,安全体系一点不能省。

4. 客户端落地:让 AI 真正调用 .NET 接口

4.1 用现成 AI 客户端配置连接

服务端跑起来之后,第一步可以先用现成的 MCP 客户端验证连通性。以常见的桌面 AI 客户端为例,配置文件里的 mcpServers 部分可以这样写:

{ "mcpServers": { "net-order-mcp": { "url": "http://localhost:5000/mcp" } } }

有些客户端支持transport: "sse"的显式指定,有些则默认stdio,需要留意一下。如果是本地开发,也可以用 stdio 方式启动dotnet命令来加载 Server 程序集。两种传输方式没有绝对优劣,SSE 适合远程部署,stdio 适合本机快速联调。

配置好之后,在客户端里重启会话,应该能在工具列表里看到刚才注册的query_order_status。然后你直接对 AI 说:

帮我查一下订单号 20250101001 的状态。

如果一切正常,AI 会自己选择query_order_status工具,填入订单号参数,返回服务端结果,然后再组织成自然语言回答你。

4.2 在 .NET 程序里自己实现 MCP 客户端

如果是要把 MCP 调用能力集成进自己的 .NET 应用里,需要以 SDK 方式调用客户端。最小实现大概是下面这样:

using ModelContextProtocol.Client; var client = await McpClientFactory.CreateAsync( new McpClientOptions { ServerName = "net-order-mcp", ProtocolVersion = "2024-11-05" }, new McpTransportOptions { TransportType = "sse", ConnectionString = "http://localhost:5000/mcp" }); var tools = await client.ListToolsAsync(); Console.WriteLine($"服务端暴露了 {tools.Count} 个工具"); foreach (var tool in tools) { Console.WriteLine($"- {tool.Name}: {tool.Description}"); } var callResult = await client.CallToolAsync( "query_order_status", new Dictionary<string, object?> { ["orderNo"] = "20250101001" } ); Console.WriteLine(string.Join("\n", callResult.Content.Select(c => c.Text)));

这里CallToolAsync最后一个参数就是方法参数名到值的映射。工具名称和参数名必须和服务端暴露的完全一致,大小写也不能错。

4.3 端到端验证一次完整调用

我把完整流程跑通过一次,日志记录大概是这个手感:

  • 09:00:01.000 服务端启动,监听 /mcp
  • 09:00:05.220 客户端连接到 /mcp,完成 initialize 握手
  • 09:00:05.310 客户端请求 tools/list,拿到 1 个工具
  • 09:00:08.442 客户端请求 tools/call,工具名 query_order_status,参数 orderNo=20250101001
  • 09:00:08.460 服务端执行订单查询,数据库命中,返回“已发货”
  • 09:00:08.501 客户端拿到结果,交给模型组织回答

如果这四个阶段都在日志里清晰地出现,说明整条链路是通的。哪一步少了,就按后面的排查清单去定位。

4.4 客户端集成时的上下文设计

自己写客户端时,最容易忽略的是“上下文传递”问题。比如用户在系统里选了“当前登录用户是张三”,AI 在调用订单接口时,客户端需要在tools/call里自动带上用户上下文参数,而不是让模型凭感觉输入。

我的做法是:在客户端封装一层McpContextAdapter,把用户身份、租户号、traceId 统一塞进参数字典。这样服务端拿到的不只是用户的一句话,而是完整业务上下文。

public async Task<string> SafeCallToolAsync(string toolName, string userInput) { var args = new Dictionary<string, object?> { ["orderNo"] = ExtractOrderNo(userInput), ["operator"] = _currentUser.Id, ["tenant"] = _currentUser.TenantId }; var result = await _client.CallToolAsync(toolName, args); return string.Join("\n", result.Content.Select(c => c.Text)); }

这种设计能让 AI 调用业务接口时,始终带着操作人、租户等关键信息,避免出现越权查询。

5. 常见问题与排查技巧

5.1 连接失败:TLS 证书和端口问题

本地联调时最经典的报错就是长这样:

net::ERR_SSL_PROTOCOL_ERROR

或者:

failed to start claude's workspace request error: net::ERR_CONNECTION_TIMED

第一个我遇到过很多次,基本都是因为 ASP.NET Core 开发证书没有被客户端信任。我的建议是本地调试时直接用 HTTP 而不是 HTTPS,MCP 是内部协议,链路里一般有网关层做 TLS 终止,不需要每个 Server 各自折腾证书。

dotnet run --urls http://localhost:5000

第二个ERR_CONNECTION_TIMED多半是端口被防火墙拦了,或者服务端没监听预期的端口。先用curl http://localhost:5000/mcp看端口通不通,再排查客户端配置的地址是否一致。

5.2 SSE 流式响应中断

还有一个使用本地服务时经常撞见的坑:

net::ERR_INCOMPLETE_CHUNKED_ENCODING 200 (OK)

这个错误表面看是 HTTP 返回 200,但响应体被截断了。在 MCP 场景里,多半是 SSE 连接没有正确维持,或者有反向代理在中间缓存了响应。SSE 是长连接,不能走普通 CDN 或响应缓冲型代理,需要在 Nginx 里关掉 proxy_buffering,同时配置长超时。

location /mcp { proxy_pass http://localhost:5000; proxy_buffering off; proxy_read_timeout 3600s; proxy_set_header Connection ''; proxy_http_version 1.1; }

如果还是断,看看服务端有没有配置心跳或者保活消息。很多 MCP Server 库会周期发注释行保持连接,如果你的代理把这行吞了,连接也可能被判定为超时断开。

5.3 工具能找到,但调用参数对不上

这个属于“逻辑正常但结果奇怪”的经典问题。AI 客户端明明拉到了工具列表,但tools/call传进来的参数总是缺一个或者类型不对。

第一反应先看服务端日志,把收到的参数原样打出来。很多时候是参数名大小写不一致,比如服务端定义orderNo,客户端根据模型理解传了ordernumber,匹配不上。

第二反应是检查参数描述是不是太模糊。我给orderNo加的描述是“订单号,必填,来自订单系统,通常以数字开头”,模型的命中率立刻提高了。描述越具体,模型越不容易自由发挥。

5.4 工具执行超时

MCP 工具内部如果调了外部 HTTP 服务或数据库,建议设置合理的超时时间,同时把 CancellationToken 传下去。默认行为经常会让你等满 100 秒才报错,体验很差。

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var order = await orderService.GetByNoAsync(orderNo, cts.Token);

超时之外,还要注意工具不要执行太重的事务操作。AI 调用工具的频率可能远高于人工操作,一旦出现大量消耗资源的查询,数据库容易直接被打满。生产环境建议在 MCP Server 前加限流,或者对工具按等级区分最大并发。

5.5 排查速查表

症状可能原因优先排查动作
客户端连不上 /mcp端口监听错误、TLS 证书不受信用 HTTP 启动,curl 验证端口
initialize 不成功协议版本不匹配、无 TLS检查 SDK 版本,查看服务端日志
tools/list 返回空工具类没被扫描、特性没写检查 AddMcpToolsFromAssembly 和类访问级别
tools/call 报参数错误参数名/类型不匹配打印服务端收到参数,对比工具 schema
执行结果丢失代理缓冲、SSE 超时关代理缓冲,调长超时
调用超时业务方法太慢、无 CancellationToken加超时控制,给工具分类限流

5.6 开发调试的三个小技巧

最后分享三个实际干活时的操作习惯:

  • 开详细日志:开发阶段把 MCP SDK 的日志级别调到 Debug,能看到完整 JSON-RPC 消息体,比盲猜快太多。
  • 用 OpenAI 函数调用格式做对比:如果发现 MCP 工具调用不稳定,可以把同一个服务方法封装成 OpenAI 的 function schema 做 A/B 对比,看是不是模型理解问题。
  • 做最小复现:遇到诡异问题,先建一个只有单个工具的空服务端,排除业务逻辑干扰。很多问题最后证明根本不在业务代码里,而是协议层配置问题。

6. 一些落地后的真实体会

跑完整个 MCP 服务端和客户端链路之后,最大的感受是:MCP 的价值不在于让 AI “学会”调接口,而在于让接口描述这件事彻底标准化。对 .NET 开发者来说,这意味着你可以像写普通 Web API 一样,把业务能力开放给所有 AI 客户端,不再需要针对每个模型单独做适配。

如果项目刚起步,我建议先做一个最简单的工具,跑通整条链路,再逐步把鉴权、限流、上下文传递等工程化能力补上去。MCP 的生态还在快速演进,趁着现在把基础实践刷一遍,后面不管是接入本地客户端还是自研 Agent,都会轻松很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 8:05:58

MATLAB实现0-9数字语音识别系统:从原理到工程实践

1. 项目概述&#xff1a;基于MATLAB的0-9数字语音识别系统这个MATLAB语音识别项目实现了一个能识别数字0-9的完整解决方案&#xff0c;特别适合需要快速入门语音处理的开发者。系统包含GUI界面、完整注释和项目报告三大部分&#xff0c;我实际测试下来识别准确率能达到85%以上&…

作者头像 李华
网站建设 2026/9/23 8:05:38

正向代理与反向代理:一文讲透区别,以及为什么代理IP是正向代理

在日常开发中&#xff0c;“代理”这个词出现的频率相当高。Nginx 配负载均衡时叫反向代理&#xff0c;写爬虫配 IP 时叫正向代理。但真要让人用一两句话说清楚两者的区别&#xff0c;不少人还是会含糊。本文从代理的本质出发&#xff0c;把正向代理和反向代理的工作机制、适用…

作者头像 李华
网站建设 2026/9/23 8:04:22

Jetson AGX Orin六路同步采集:从硬件支持到工程落地的全栈解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/23 8:03:16

复杂UI组件重构:逻辑表达式与原子化构建实战

复杂 UI 组件重构&#xff0c;一直是个让人又爱又恨的话题。爱的是重构完之后代码清爽、扩展自如的那股爽劲&#xff0c;恨的是重构过程中牵一发而动全身&#xff0c;稍不留神就把原本还能跑的业务逻辑改得面目全非。我最近正好完成了一个规则配置类复杂组件的重构&#xff0c;…

作者头像 李华
网站建设 2026/9/23 8:02:39

业务战略可视化工具:动态评估与决策优化

1. 项目概述"VTC战略与落地②&#xff1a;战略规划层的革命——一张图看清所有业务的生死位置"这个标题揭示了企业战略管理领域的一个关键痛点&#xff1a;如何直观、清晰地评估各项业务在整体战略布局中的位置和价值。作为从业15年的战略咨询顾问&#xff0c;我深知…

作者头像 李华