
最近我连续接了几个 Agent 方向的项目踩完一圈坑之后最大的感受是MCP 已经不只是 Python 圈的玩具了。现在 Claude Desktop、Cursor、Trae、Codex 这些主流 AI 客户端都把 MCP 当成了标配能力Figma、蓝湖、Playwright 这些工具也陆续推出了官方 Server整个生态明显在快速成型。但奇怪的是.NET 社区里关于 MCP 的内容少得可怜搜来搜去基本都是 Python 或 Node 的示例很多 .NET 开发者看完只能干瞪眼觉得自己要么得换技术栈要么就得手写协议。真的没必要。这篇文章就把我在 .NET 里落地 MCP 服务端和客户端的完整过程讲清楚怎么把现有业务方法包装成标准 MCP 工具怎么让 AI 自动发现并调用你已有的 .NET 接口以及传输层和权限层有哪些必须避开的坑。适合正在做 AI Agent 集成、又不想丢掉现有 .NET 技术栈的开发者也适合团队里准备给现有系统接 AI 能力的后端同学。1. MCP 到底是什么为什么 .NET 开发者应该现在上车1.1 一个类比MCP 就是 AI 世界的 USB 接口MCP 全称 Model Context Protocol中文一般叫模型上下文协议。它要解决的事情用一句话概括就是让 AI 应用通过一套统一标准去调用外部工具和数据源而不是每个工具都单独写一套对接逻辑。你可以把它理解成 USB 接口。在 USB 出现之前打印机、鼠标、键盘各自有不同的接口标准电脑厂商得给每个外设适配不同的协议。USB 出现之后所有外设都用同一个口插上就能用。MCP 解决的问题一模一样以前你想让 AI 调用一个内部接口得为这个接口专门写提示词、写函数调用Function Calling的描述、写参数解析逻辑现在你只要在 MCP Server 里把工具暴露出来任何支持 MCP 的 AI 客户端都能直接发现并调用它。MCP 协议建立在 JSON-RPC 2.0 之上核心抽象有三个**Tools工具**是 AI 可以主动调用的函数**Resources资源**是 AI 可以读取的上下文数据**Prompts提示词模板**是可复用的对话引导。绝大多数场景下你最先接触、也是最有价值的就是 Tools——把业务接口变成 AI 可以按需调用的工具函数。这个协议最初由 Anthropic 在 2024 年底开源现在已经是 AI Agent 生态里事实上的通用标准。微软在 2025 年初也加入了进来发布了 .NET 官方的 MCP SDK这意味着 .NET 开发者不需要从零手写协议直接用官方包就能在 ASP.NET Core 项目里挂出 MCP Server。1.2 服务端与客户端两个角色必须分清楚MCP 的架构里有两个角色很多人一开始会搞混。**MCP Server服务端**是能力提供方。它负责声明自己有哪些工具、每个工具的参数结构是什么、执行工具时对应什么逻辑。Server 不主动发起调用它只是把能力挂在那里等着客户端来发现和调用。对你来说一个 Server 可以是一个独立的 ASP.NET Core 进程也可以是你现有 WebAPI 项目里新增的一组端点。**MCP Client客户端**是消费方也就是 AI 应用本身。Claude Desktop、Cursor、Trae、Codex 这些客户端都内置了 MCP Client 能力。你的 Agent 应用如果自研也可以自己写一个 MCP Client。Client 负责启动时去连接 Server拉取工具列表把用户的指令交给大模型判断模型决定要调用哪个工具Client 再把工具名和参数发给 Server 执行。这里有一个很关键的点大模型本身不直接执行 MCP 调用。模型只负责“决定调用哪个工具、生成什么参数”真正把请求发出去、把结果拿回来的是 Client。这也是为什么 MCP 比裸 Function Calling 更安全、更可控——执行层在宿主程序手里不在模型手里。1.3 为什么不直接让 AI 调 HTTP 接口非要走 MCP很多人第一反应是我的接口本来就有 HTTP 暴露为什么不直接让 AI 去调这个问题我一开始也纠结过实际做下来发现至少有四个痛点。第一个痛点是接口发现。AI 不知道你系统里有哪些接口、每个接口要传什么参数。虽然你可以在提示词里写“订单查询接口是 GET /api/orders/{id}”但接口一多就完全不可维护。MCP Server 本身就是一份结构化的工具清单AI 通过 ListTools 就能看到所有可用的能力、参数说明和用途描述。第二个痛点是参数生成的可靠性。模型生成 HTTP 请求时很容易把参数类型搞错比如该传字符串的传了数字、DateOnly 的格式写不对。MCP 里每个工具都有 JSON Schema客户端在调用前就会做一次结构校验模型生成错误参数的概率会明显下降。第三个痛点是权限边界。如果你让 AI 直接访问 HTTP 接口等于把整个 API 表面暴露给了模型模型可能会“创造”出一些你根本没有设计过的调用方式甚至把 DELETE、批量操作这些危险接口翻出来。MCP 则是你主动暴露什么AI 才能用什么相当于做了一个默认拒绝的白名单。第四个痛点是上下文管理。HTTP 调用只返回原始数据但 MCP 的返回结果可以带着结构化类型信息回到模型上下文里模型能更自然地把结果整合进回答中。比如查完订单后模型可以直接说出订单金额和状态而不是只能念一串 JSON。2. 技术选型.NET 生态里做 MCP 的三条路2.1 三条路线对比官方 SDK、自研 SSE、纯 stdio在 .NET 里接 MCP目前有三条技术路线我分别试过体验差异很大。**路线一用微软官方 ModelContextProtocol SDK。**这是首选。官方包提供两个ModelContextProtocol 是核心库包含 Client、Server 抽象和协议实现ModelContextProtocol.AspNetCore 是 ASP.NET Core 集成可以用 MapMcp() 一行代码把 MCP 端点挂到你现有的 Web 应用里。它支持两种传输方式本地进程用 stdio标准输入输出远程部署用 Streamable HTTP底层协议细节都被封装好了。**路线二自研 SSE 端点。**第二种是自己在 ASP.NET Core 里实现 MCP 的 HTTP 传输层比如 SSEServer-Sent Events端点。好处是可控性强、不依赖 SDK 版本坏处是协议细节很多握手、初始化、工具列表、调用响应每一步都要自己处理 JSON-RPC 消息光踩协议坑就能花掉一整天。**路线三纯 stdio 自研协议。**第三种是完全不用 HTTP写一个控制台程序监听 stdin、输出 stdout用 JSON-RPC 消息和客户端通信。Claude Desktop 这类本地客户端典型就是通过 stdio 拉起进程的。如果你不用任何 SDK 自己实现一遍代码量会非常惊人而且极易出错不建议任何人从零开始这么干。三条路线的对比如下方案接入成本协议复杂度适合场景推荐度官方 ModelContextProtocol SDK低几行代码接入内部封装大多数 .NET 项目强烈推荐自研 SSE 端点中高需要处理完整协议细节对 SDK 版本敏感或特殊定制场景不推荐优先纯 stdio 自研很高完全手写 JSON-RPC学习协议原理仅做技术验证我最后选的是官方 SDK这也是目前 .NET 生态里投入产出比最高的选择。虽然 SDK 还在快速迭代API 偶尔会调整但核心模型已经稳定踩过的坑都有人帮你填了。2.2 环境准备.NET 8 起步老项目用桥接方案官方 MCP SDK 要求的是现代 .NET也就是 .NET 8 或更高版本。如果你手里还是 .NET Framework 的老项目比如 4.8SDK 没法直接在老项目里跑起来网上那些“.NET Framework 3.5 安装报错”“4.8 运行库下载”之类的问题本质上跟 MCP 没有关系——老框架连现代依赖都拉不齐更别说跑基于 .NET 8 的协议栈了。遇到老项目我的落地策略是做一个 Sidecar 桥接单独起一个 .NET 8 的 ASP.NET Core 小进程对外暴露 MCP 工具内部用 HttpClient 去调老系统的 HTTP 接口。老项目一行代码不用改新进程负责协议转换和权限控制。实际项目里我们就是把一个 .NET Framework 4.8 时代的订单系统用这种方式接进了 Agent两天就完成了整个联调。工具方面Visual Studio 2022、VS Code 或者 Rider 都可以反正就是 ASP.NET Core 的项目结构。需要安装的 NuGet 包主要是dotnet add package ModelContextProtocol.AspNetCore核心类型都在 ModelContextProtocol 命名空间下。如果你需要一个 .NET 客户端来调试服务端再装一个dotnet add package ModelContextProtocol装完后你就可以在 Program.cs 里开始搭 Server 了。3. 服务端实战把 .NET 业务方法变成 MCP 工具3.1 第一步建一个最小可用的 MCP ServerMCP Server 的最小骨架非常简单本质上就是一个 ASP.NET Core 应用加上一行 MapMcp()。下面是我在实际项目里跑通的代码注册了一个订单服务并把它暴露成 MCP 工具。using ModelContextProtocol; using ModelContextProtocol.AspNetCore; var builder WebApplication.CreateBuilder(args); builder.Services.AddSingletonOrderService(); builder.Services.AddMcpServer() .WithToolsOrderTools(); var app builder.Build(); app.MapMcp(); app.Run();就这么几行你的应用就已经具备 MCP Server 的能力了。默认情况下MapMcp() 会注册一个/mcp端点支持 Streamable HTTP 传输。客户端连接这个端点后就能自动发现工具列表并调用。这里解释一下每一步在干什么AddMcpServer() 是注册 MCP 核心服务WithTools () 是把 OrderTools 这个类里所有标记为工具的方法收集起来生成 JSON Schema 并注册到工具列表。MapMcp() 则是把 MCP 的握手、工具发现、工具调用这些端点映射到 ASP.NET Core 路由上。如果你想验证服务端是不是真的能通可以先启动项目然后找一个支持 MCP 的客户端去连。第一次看到工具列表被 AI 客户端正常发现的时候还是有点小激动的。3.2 第二步把既有 Service 方法包装成 Tool这是整个实战里最核心的一步把业务逻辑变成 AI 可识别的工具。直接用特性标注就好不需要手写 JSON Schema。using ModelContextProtocol; [McpToolType] public class OrderTools { private readonly OrderService _orders; public OrderTools(OrderService orders) { _orders orders; } [McpTool(Description 根据订单号查询订单状态和金额订单号格式如 ORD20250301001)] public async TaskOrderDto GetOrderById(string orderId, CancellationToken cancellationToken) { return await _orders.GetByIdAsync(orderId, cancellationToken); } [McpTool(Description 查询指定客户最近 N 笔订单customerId 为内部客户编号)] public async TaskListOrderDto GetRecentOrders( string customerId, int count 10, CancellationToken cancellationToken default) { return await _orders.GetRecentAsync(customerId, count, cancellationToken); } }两个特性是关键[McpToolType]标记这个类里包含工具[McpTool]标记具体的方法Description 会作为工具的说明提供给大模型。模型就是根据这段文字去判断“用户问的问题应该调用哪个工具”的所以 Description 一定要写得准确、包含参数约束信息比如订单号的格式。这里有一个值得注意的设计我并没有把 OrderService 的所有方法都暴露出去而是只挑了 GetOrderById 和 GetRecentOrders 这两个查询方法。MCP 工具的暴露是白名单制的你需要刻意地、有选择性地暴露能力而不是把整个 Service 直接挂出去。还要说明的是不同版本的 SDK 在特性名称上可能有微调我最初用 0.1 预览版时有些地方还不叫 McpTool而是别的命名后来升级到较新版本才统一。所以代码以你实际安装的 SDK 版本为准但整体的思路是固定的。3.3 第三步参数 Schema 与返回值规范MCP 工具的参数 Schema 由 SDK 根据方法签名自动生成。基础类型string、int、bool、枚举都可以直接推断日期时间类型也能正确映射。但有一个常见坑千万别把大型 DTO 作为工具参数。比如你写一个方法参数是一个整个 OrderQueryRequest 对象里面有十几个字段还嵌套了分页对象SDK 确实能把这个类的复杂结构全都生成到 Schema 里但后果是模型在生成这个参数时极其容易漏字段、填错层次。大模型的参数生成能力有限你给的 Schema 越复杂AI 翻车概率越高。我自己的经验是工具参数尽量扁平化、最小化。一个工具只做一件事参数建议控制在三到五个以内都用基础类型。比如订单查询就拆成 orderId 一个参数最近订单查询就是 customerId count。返回值方面推荐返回一个精简的 DTO序列化成 JSON 后由客户端回传给模型。不要直接把数据库 Entity 抛出去因为 Entity 往往带冗余字段、关联导航属性或者敏感字段比如内部备注、供应商成本价这些信息一旦进了模型上下文就可能被模型“看到”并表达出来。我的做法是为 MCP 工具单独定义返回模型只包含 AI 回答用户问题真正需要的信息。3.4 传输方式stdio 还是 HTTP按场景选MCP Server 建好后你会面临一个选择传输层用 stdio 还是 HTTP。这个选择直接决定了客户端怎么连你。stdio 模式适合本地开发场景。Server 是一个控制台程序Client 通过启动进程的方式拉起它然后通过标准输入输出交换消息。Claude Desktop、Cursor 这类桌面应用默认就是这种连接方式。优点是不需要处理端口、跨域、鉴权缺点是 Server 必须和 Client 在同一台机器上而且进程生命周期由客户端管理。**HTTP 模式Streamable HTTP**适合远程部署场景。Server 是一个可访问的 HTTP 端点Client 通过网络连接。比如你把 Server 部署到测试服务器团队里的多个 Agent 或者在线服务都能连也可能部署到 Docker 容器里。我之前那个订单系统接 Agent就是走这个模式。选择建议很简单调试阶段用 stdio部署阶段用 HTTP。两种模式在代码层面区别不大因为官方 SDK 对传输层做了一层抽象你只需要在客户端配置里换一下连接方式就行。4. 客户端实战让 AI 客户端真正调起来4.1 现成客户端接入Claude Desktop / Cursor / Trae 配置实例服务端跑起来之后下一步就是让 AI 客户端连上去。支持 MCP 的桌面客户端基本都支持通过一个 JSON 配置文件来注册 Server。以 Claude Desktop 为例它的 MCP 配置文件一般叫 claude_desktop_config.json里面用 mcpServers 声明你要连接的 Server。如果是本地 stdio 模式配置大概是这样的{ mcpServers: { order-server: { command: dotnet, args: [run, --project, /data/mcp-servers/OrderServer/OrderServer.csproj] } } }如果 Server 已经部署成 HTTP 服务配置更简单{ mcpServers: { order-server-http: { url: http://localhost:5088/mcp } } }Cursor、Trae 也类似只是配置文件的位置和格式略有区别本质都是告诉客户端“有一个 Server 在这里你连上去”。配置完成后重启客户端正常情况下你会看到工具列表被加载这时可以直接在对话里问“订单 ORD20250301001 现在是什么状态”如果 AI 回答“我来查一下”然后自动调用了你写的 GetOrderById 工具就说明整条链路已经通了。我第一次跑通这个场景的时候的感受是以前写 AI 应用需要自己搭 Agent 框架、写 Function Calling 的注册和路由现在协议层标准化了至少省掉了三成重复工作。4.2 用 C# 写一个 MCP Client如果你不满足于用现成客户端想在自己的 C# Agent 里消费 MCP Server官方 SDK 也提供了 Client 能力。下面是一个最小示例创建一个 stdio 客户端连接 Service 并调用工具。using ModelContextProtocol; using ModelContextProtocol.Client; using var loggerFactory LoggerFactory.Create( b b.AddConsole().SetMinimumLevel(LogLevel.Information)); var client await McpClientFactory.CreateAsync( new McpClientOptions { Transport new StdioClientTransport(new StdioClientTransportOptions { Command dotnet, Arguments [run, --project, /data/mcp-servers/OrderServer/OrderServer.csproj] }), ClientName MyCSharpAgent }, loggerFactory); var tools await client.ListToolsAsync(); Console.WriteLine($可用工具: {string.Join(, , tools.Select(t t.Name))}); var result await client.CallToolAsync( GetOrderById, new Dictionarystring, object? { [orderId] ORD20250301001 }); Console.WriteLine(result);这段代码做的事情就是启动一个本地进程作为 MCP Server通过 stdio 通信列出它暴露的所有工具然后调用其中的 GetOrderById 工具把结果打印出来。如果你要连远程 HTTP Server只需要把 Transport 换成基于 HTTP 的传输配置其余代码基本不用变。这就意味着你可以用一个完全统一的代码框架去对接任意 MCP Server——不管是自己写的订单服务还是别人发布的第三方工具。这种“适配器模式”带来的扩展性就是 MCP 最大的价值。4.3 调用链路的完整视角与安全边界从端到端的视角看一次完整的 MCP 调用是这样流转的用户在 AI 客户端里提问比如“帮我查一下 ORD20250301001 订单状态”。AI 客户端把问题发给大模型模型看到可用工具列表里有 GetOrderById决定调用这个工具。客户端的 MCP Client 把调用请求以 JSON-RPC 消息发给 Server。Server 收到请求反序列化参数执行你写的 C# 业务方法。业务方法返回 DTOServer 把结果序列化成 JSON 返回给 Client。Client 把结果带回给模型上下文模型基于结果生成最终回答。理解这条链路你就知道安全保障要分三层去控模型层通过 Description 和参数 Schema 引导模型只做预期内的事执行层在 MCP 工具方法内部做权限校验、参数校验、操作审计数据层对返回值做脱敏和最小化输出。我在实际环境中严格限定 MCP 工具只能访问查询类接口所有写操作一律走额外的人工审批流程。不要让 AI 直接执行数据库删除、批量修改、资金转账这类操作即便模型“看起来”理解了你说的条件——模型不是人它不会像员工一样掂量操作后果。5. 踩坑记录与排查技巧含安全红线5.1 高频报错速查表我在这套体系上踩了不少坑下面是最常见的几类问题整理成了一张速查表现象大概率原因解决办法客户端连不上 ServerServer 没启动或端口/路径不对确认 /mcp 端点已映射检查地址是否以 /mcp 结尾工具列表是空的工具类没加 [McpToolType]或方法没加 [McpTool]检查特性标注确认类型已通过 WithTools 注册调用工具报参数校验失败客户端传的参数类型与 Schema 不匹配核对方法签名避免复杂嵌套对象尽量用基础类型stdio 模式下进程拉不起来Command 或 Argument 路径不对本地先在终端里手动跑一遍命令确认能正常启动返回结果被模型理解错误返回值里信息过多或过少精简返回 DTO只保留模型回答问题需要的数据HTTP 模式访问不了跨域、防火墙或绑定地址不对检查 CORS 配置、监听地址是否 0.0.0.0生产环境用 TLS排查时我的习惯是先在进程里用 C# Client 手动调一遍把 Server 本身的问题和客户端集成的问题剥离开来。Client 能通说明 Server 没问题Client 不通那就是协议或者配置层面的问题不用拉着大模型一起背锅。5.2 调试三板斧日志、回放、手工调用MCP 是多方协作的协议出问题的时候最怕的就是“黑盒”。我的调试三板斧是日志、回放、手工调用。第一招是把服务端日志打全。在 ASP.NET Core 里配置日志输出到控制台和文件重点记录每次工具请求的原始输入、参数校验结果、执行耗时、返回大小。MCP 调用一旦出问题日志能直接告诉你请求到没到 Server、参数长什么样。第二招是回放工具调用。客户端记录的对话里会包含模型选择的工具和参数你可以把它复制出来用 C# Client 手工重放一次同样的调用看看 Server 返回的到底是什么。如果手工调用也报错那就是 Server 逻辑问题如果手工调用正常那是模型生成参数的问题需要调整 Description 或参数结构。第三招是用 MCP Inspector 做笨办法调试。官方提供过一个调试界面可以手动连接你的 Server、查看工具列表、逐个调用工具、查看原始 JSON-RPC 消息。这个工具且不管它用什么技术栈对你排查 Server 端的问题很有用很多时候能直接看到协议层发来的完整请求和响应。5.3 MCP 服务端的安全红线必须看最后说安全这块比功能本身重要得多。MCP 工具的本质是给 AI 一把调用你系统的钥匙如果钥匙随便配后果很严重。下面几条是我在实际项目里定死的红线。第一条默认白名单暴露。只有你明确标记的工具才会被 AI 发现。任何查不到、不明确的接口一律不让模型摸到。特别是批处理、删除、发送消息、转账这类高影响操作默认不暴露。第二条参数必须校验。AI 生成参数不会像前端表单那样守规矩它可能传超长字符串、负数、不存在的 ID。MCP 工具方法内部要像处理用户输入一样做防御式校验长度、范围、格式一层都不能少。第三条返回值必须脱敏。工具返回的数据会进入大模型上下文而模型生成回答时可能把这些内容原样复述出来。手机号、身份证、内部成本、供应商信息这些敏感数据在 DTO 层面就过滤掉不要让它们有机会进入模型上下文。第四条远程部署用 TLS。HTTP 模式下工具调用就是普通的网络请求明文传输等于把你的业务接口裸奔在网络上。生产环境务必配 HTTPS客户端和服务端之间的认证凭证也要妥善管理。第五条留审计日志。谁在什么时候调了哪个工具、传了什么参数、返回了多少数据这条链路全部记下来。MCP 会放大操作效率也会放大出错影响没有审计日志出了问题连定位都困难。这五条不复杂但每一条背后都有实际翻车案例。我自己就在早期把内部一个批量清理工具暴露了出去AI 在测试时直接把一批预发环境的脏数据清掉了要不是预发环境后果不堪设想。从那以后MCP 工具的白名单和审计日志成了我这里的强制要求。最后分享一个我个人的经验第一次跑通 MCP 链路后别急着把所有业务方法都标记成工具。先挑两三个查询类、低风险的方法跑通全链路把 Schema、权限、日志都调整到位再慢慢扩大范围。另外官方 .NET SDK 还在快速迭代API 偶尔会变项目里最好锁定版本升级前先跑一遍客户端连通性测试。MCP 这套技术本身不复杂复杂的是接入之后的治理和边界控制这两件事想清楚剩下的都是体力活。以后有空我再写一篇关于 MCP Resources 和 Prompts 在 .NET 里的实际用法那两个能力在知识库场景下比 Tools 还好用。