MCPModel Context Protocol是2025年AI工程圈最绕不开的热词。如果你最近在做Agent相关项目大概率已经发现MCP把“工具怎么暴露给AI”这件事彻底标准化了。而.NET这一端最有组合价值的就是Semantic KernelSK。这篇文章不聊虚的概念就聊我自己在一个实际项目里用.NET SK搭MCP能力层的完整方案架构怎么拆、代码怎么写、坑在哪、上线后要注意什么。如果你正打算在公司内部做一个统一的AI工具接入层或者想把现有.NET服务变成MCP Server暴露给各种AI客户端这篇应该能帮你少踩不少雷。1. 先想明白MCP能力层解决什么问题1.1 工具接入的“战国时代”在MCP还没普及的时候我做AI相关项目最头疼的就是“适配”。给OpenAI写function calling要一套JSON Schema给Anthropic要换一种tool写法给自家内部Agent框架又要来一遍工具定义。一个查询天气的小功能硬生生适配了三四个客户端代码里全是协议转换逻辑。这就像你出门要带一堆充电线——每个设备一种接口虽然都能充但就是烦。MCP解决的正是这个问题。它把“AI应用如何发现工具、描述工具、调用工具”这个流程标准化了。无论你是Claude、Cursor、还是某个自研的Agent平台只要支持MCP就能通过一套协议连接到同一个工具服务。工具方只需要实现一次MCP服务端所有支持MCP的客户端都能复用。放在企业内部这其实是给AI能力建立了一个标准化的接入层。协议层面MCP的核心交互非常简洁initialize客户端和服务端握手确认协议版本和能力。tools/list客户端获取服务端暴露的工具清单工具名、描述、参数Schema。tools/call客户端发起一次具体的工具调用。底层消息基于JSON-RPC 2.0服务器可以走stdio和HTTP两种传输。理解这几个端点就够了MCP并没有对你的业务代码做任何侵入它只是在你和AI客户端之间加了一层“翻译官”。1.2 什么是“能力层”这里说的“能力层”不等同于一个API网关也不只是一个工具集合。它的关键点在于能力层是AI可以理解和调用的能力集合。传统API接口暴露出来之后调用方自己决定调哪个、怎么拼参数。能力层不太一样它考虑的是让AI自己“发现”并“编排”这些能力。比如一个用户问“查一下上周北京上海的销售数据做成汇总发我”这其实不是一次API调用能解决的而是一连串动作查数据、做汇总、触发推送。能力层背后需要有AI编排逻辑而不只是API路由。.NET生态里用SK来做编排是顺理成章的选择。SK提供了Kernel、Plugins、Function Calling、Prompt模板这些基础设施把“AI怎么理解意图、怎么决定调用哪些能力”这一层封装了起来。MCP则负责把SK编排好的能力用标准协议暴露给所有AI客户端。1.3 为什么是.NET SK有人可能会问搞AI不是Python的天下吗确实Python在模型训练和POC阶段优势明显但落地到企业系统.NET的生态成熟度不容小觑。特别是已经有大量.NET存量系统、已经有规范的服务治理体系的企业用.NET做MCP能力层可以直接和现有系统打通没有跨语言的鸿沟。SK对.NET开发者也比较友好它把Prompt拼接、函数调用循环、工具Schema生成这些复杂逻辑都封装了写起来很像普通的依赖注入加服务调用。我在实际项目里体会最深的一点是SK彻底消灭了自己维护Function Calling循环的脏活而MCP解决了多方接入的适配问题。这两件事单独拎出来都只是效率提升合在一起才真正形成了一套可以对外输出、对内统一管理的能力层。2. 整体架构SK与MCP怎么分工2.1 三层模型我落地时候把一个MCP能力层拆成了三层层级职责核心组件接入层对外暴露MCP协议端点接收工具调用MCP Serverstdio / HTTP传输编排层理解用户意图、规划工具调用链SK Kernel、Planner、上下文内存执行层实际访问数据库、外部API、文件系统SK Plugins / 自定义工具集这样分的好处是每层可以独立演进。接入层只关心协议不关心业务逻辑执行层只关心“干活”不关心谁来调用它编排层居中负责“翻译”。万一以后MCP协议要升级或者SK要被换成别的Agent框架动一层不动另两层改动成本就小很多。2.2 两种典型的集成模式在实际落地时我见过也用过两种模式你需要先根据场景选好模式一工具直通模式。把SK里的每个插件函数直接映射为MCP工具。客户端通过MCP调用工具时实际执行的是SK插件的逻辑。这种模式适合“能力明确、工具边界清晰”的场景AI客户端直接调用单个工具即可不涉及复杂编排。模式二智能编排模式。MCP只暴露一个或少量“Agent入口”工具客户端把用户问题抛给这个入口由SK在服务端完成工具调用链。这种模式适合复杂任务把AI能力从客户端收回到服务端便于统一管理、统一运营养护。两种模式可以混用。我最终线上环境的方案是对外提供少量编排入口同时把所有可独立调用的原子工具也暴露出来让外部Agent既有“自由调用”的能力又有“交给服务端代理”的选择。2.3 传输方式怎么选MCP协议支持stdio和HTTP两种传输这直接影响部署形态stdio进程内启动子进程通过标准输入输出通信。适合本地工具包比如给某个IDE插件配一个本地MCP工具构建一个控制台应用即可。优点是免部署、免鉴权缺点是只能服务单机上的客户端。HTTPMCP Server部署为HTTP服务多个客户端共享访问可以实现鉴权、监控、限流。适合企业级能力层。我在项目里基本只用HTTP传输因为能力层一定会有多个上游调用方不止一个AI客户端。HTTP模式能复用已有运维设施负载均衡、API网关、日志采集这比把MCP塞进客户端进程里要省心得多。提示如果你只是给个人开发机上的Claude Desktop配工具stdio没问题。但在团队协作场景里强烈建议走HTTP不然每个开发都要本地起一个服务维护成本立刻失控。3. 服务端落地用SK插件直接暴露MCP工具3.1 项目与依赖先打开NuGet给项目加上这几个包Microsoft.SemanticKernel核心SDK提供Kernel、Plugins、ChatCompletion抽象。ModelContextProtocol.AspNetCoreMCP服务端的ASP.NET Core集成HTTP传输。ModelContextProtocolMCP核心SDK一般会被上一个包自动带出来。SK对.NET 8和.NET 9支持得都很好自己项目里用的是.NET 8 LTS版本稳。有一点要提醒SK迭代速度非常快包版本之间偶尔会有破坏性变更你照着某个教程写代码时如果编译不过先看包版本对不对。不要升级依赖时无脑升最高版最好锁定一个经过验证的版本组合。3.2 最小服务端骨架用ASP.NET Core创建一个空Web项目Program.cs里最核心的代码就这么几行var builder WebApplication.CreateBuilder(args); // 注册MCP服务端走HTTP传输 builder.Services.AddMcpServer() .WithHttpTransport(); var app builder.Build(); // 把MCP端点映射到 /mcp app.MapMcp(/mcp); app.Run();就这么简单一个满足MCP协议的服务端骨架就出来了。客户端连接/mcp就能通过握手、ListTools拿到能力列表。这个骨架本身没有任何业务真正的重点是接下来怎么把SK接进来。3.3 定义SK插件并暴露为MCP工具在这个骨架上加SK。先定义一个插件类public sealed class WeatherPlugin { [KernelFunction(get_weather)] [Description(获取指定城市的实时天气信息)] public async Taskstring GetWeatherAsync( [Description(城市名称如北京、上海)] string city, [Description(日期格式yyyy-MM-dd默认当天)] string? date null) { // 实际逻辑调用天气API、查第三方服务或走缓存 return await WeatherApi.GetAsync(city, date ?? DateTime.Now.ToString(yyyy-MM-dd)); } }关键地方在[KernelFunction]和[Description]。SK会根据这两个特性生成工具描述和参数SchemaMCP Server在收到客户端tools/list请求时把这些SK插件转成MCP的Tool定义返回。描述写得好不好直接决定AI能不能正确调用。我总结的通用写法是函数描述动作 对象 适用场景比如“获取指定城市的实时天气信息”。参数描述说清格式和边界比如“日期格式yyyy-MM-dd默认当天”。避免模糊词比如“获取信息”这种太空泛的描述会让模型拿不准适用场景。接下来在Program.cs里注册并挂载var builder WebApplication.CreateBuilder(args); var kernel builder.Services.AddKernel(); kernel.Plugins.AddFromTypeWeatherPlugin(); builder.Services.AddMcpServer() .WithHttpTransport() .WithTools(kernel.Plugins.SelectMany(plugin plugin.Select(f McpServerTool.Create(f))));这段代码做的事情是遍历Kernel中的每个插件把每个函数转成MCP Server能识别的工具。客户端工具列表里就会出现get_weather调用时直接执行对应插件函数和AI客户端原生的工具调用体验完全一致。注意AddFromType会把公有方法中带[KernelFunction]的成员暴露出去。如果你的类里有一些公开但不想暴露给AI的方法务必不要加[KernelFunction]特性。血泪教训我一开始把类里一个内部工具方法也标了特性结果客户端工具列表多了一个完全没有业务价值的内部函数还差点被模型误调用。3.4 让MCP工具走SK的调用通道上面这种直接映射的方式工具执行时并没有经过SK的完整链路。如果工具逻辑不复杂没问题但如果你希望工具的调用被完整记录、被上下文感知或者想在工具执行前做统一预处理就需要让MCP的工具调用进入SK的调用链。我的做法是加一个“编排工具”public sealed class AgentTools { private readonly Kernel _kernel; public AgentTools(Kernel kernel) { _kernel kernel; } [KernelFunction(ask_agent)] [Description(把任务交给AI Agent统一处理适合需要多步骤协作的复杂请求)] public async Taskstring AskAsync( [Description(用户问题或指令文本)] string prompt, CancellationToken ct) { var result await _kernel.InvokePromptAsync(prompt, cancellationToken: ct); return result.ToString(); } }这样客户端可以调具体的原子工具也可以只调ask_agent把复杂决策交给SK。工具直通模式和智能编排模式就打通了。3.5 工具注册时为什么不做业务判断很多人会问在将SK插件转换成MCP工具的时候能不能做点前置判断比如某些工具只对某些客户端开放理论上可以。MCP协议本身不管理权限但服务端代码可以在tools/list返回时做过滤。我的建议是先把工具全量注册把权限判断放到上层网关或授权中间件里去不要在SK插件的转换层写业务分支。因为SK插件的核心价值是复用如果转换层混入权限逻辑插件换一个场景就要改代码失去了能力层该有的独立性。权限是横切面用中间件和拦截器处理更干净。4. 客户端闭环SK消费MCP与外部接入4.1 用SK连接外部MCP Server不只是对外暴露SK也可以作为MCP客户端去消费其他MCP Server的能力。这在做聚合能力层时非常实用自己的能力层需要调用另一个部门提供的MCP工具比如统一的用户查询工具。代码很简单// 创建MCP客户端 var mcpClient await McpClient.CreateAsync( new McpClientOptions { ClientName InternalCapabilityLayer, ClientVersion 1.0.0 }, new McpClientTransportOptions { TransportType http, BaseUri new Uri(https://internal-service/mcp) }); // 获取远端MCP工具 var mcpTools await mcpClient.GetToolsAsync(); // 注入到SK内核 var kernel Kernel.CreateBuilder().Build(); kernel.Plugins.AddFromFunctions(remote-mcp, mcpTools);AddFromFunctions是SK里把外部函数当作本地插件来用的方式。SK在收到请求后可以把“查询用户”这种任务分发给远端的MCP工具处理本地无需重复实现。能力层就变成了“联邦式”的这在大型组织里是特别实用的架构能力。4.2 外部AI客户端接入服务端和客户端都打通了外部AI客户端接入就顺理成章了。不管你是给Claude Desktop配MCP还是给Cursor配MCP原理都一样配置一个MCP Server地址客户端启动时自动调用tools/list获取工具列表用户通过对话触发工具调用。这就是MCP最大的价值你不用为每个AI客户端单独写一套工具适配能力层只要实现一次MCP协议所有客户端自动获得这些能力。一个典型的配置文件片段长这样{ mcpServers: { internal-capability-layer: { type: http, url: https://capability.internal.example.com/mcp } } }4.3 一条完整请求链路长什么样以“智能编排模式”为例一次真实调用长这样用户在AI客户端输入“帮我查一下北京今天天气适不适合出行”。客户端通过MCP协议调用能力层的ask_agent工具带上用户原文。SK Kernel收到Prompt内部通过Function Calling决策需要调用get_weather。SK调用天气插件拿到结果后把结果拼入上下文并生成最终回复。回复内容通过MCP返回给AI客户端客户端展示给用户。决策在SK做执行在插件上做MCP只是一个“快递通道”。如果说MCP是交通规则SK就是司机工具集合是目的地。三者各司其职链路才清晰可查。5. 生产级实战三个值得落地的场景5.1 企业内部知识库检索场景很常见把内部Wiki、知识库、制度文档变成AI可调用的能力层。用户问“年假怎么申请”AI自动调用检索工具从知识库中寻找相关文档再生成回答。工具设计关键点用向量检索做语义召回用BM25做关键词召回两路结果做RRF融合。检索工具返回“内容片段 来源链接 更新时间”让模型有据可依。塞给模型的不是全文而是截取的前N段高相关片段并标注来源方便用户校验。这个场景里我犯过错误最开始把整个文档内容都塞给模型Token消耗爆炸上下文被无关内容污染回答还变差了。后来改成先检索再按相关性截取片段最后把引用来源一起给模型。效果立刻稳定很多。提示词和工具返回值设计往往比模型选型更能影响体验。5.2 数据库查询Agent场景让AI安全地查询业务库。用户问“上个月华东区订单总额是多少”AI通过工具获取表结构信息、生成查询、执行并返回结果。这个场景看起来简单实际上风险最大。关键策略工具只暴露“只读查询”能力SQL执行连接一个只读账号权限细化到库表。尽量不让模型自由生成SQL而是从一组预置查询模板里选择模板没有覆盖的需求走人工审核。所有查询日志全量记录用于事后审计。你在技术上再强权限和审计永远是第一位的。模型自由发挥的边界要划清楚尤其是在数据库操作这种不可逆场景里。5.3 工单系统自动处理场景客服收到一个问题AI判断是否需要创建工单、更新状态、指派负责人。这涉及多个工具的组合调用正好用SK的编排能力。把“创建工单”“查询工单状态”“更新工单优先级”拆成独立插件。SK根据对话内容自动决定调用哪些工具以及调用顺序。MCP对外只暴露一个入口比如handle_service_request。外部AI客户端只需要调这一个工具剩下的内部编排全部由SK处理。这样做还有一个额外好处工单系统相关的复杂Prompt模板、工具调用规则都收敛在服务端客户端只负责触发。以后运营想调整流程不用去改每个客户端配置。6. 踩坑记录与排查实录6.1 SK生成的工具描述被“截断”现象工具调用时模型偶尔会误判参数。后来发现MCP返回的工具描述和SK里写的不一致很多描述被客户端截断了。排查MCP协议对工具描述字段本身没有严格长度限制但部分AI客户端对描述长度有限制。解决方案是控制描述长度不写废话、把边界条件放在参数描述里而不是函数描述里。我习惯把函数描述控制在30字以内参数描述控制在20字以内效果明显提升。6.2 工具多到一定程度模型就“选择困难”了现象能力层挂了30多个工具AI开始频繁选错工具或重复调用。排查AI客户端在选择工具时会把所有工具Schema都塞给模型。工具过多、Schema过长会严重影响模型判断质量。方案是给工具按领域分组或者按路由前缀暴露不同MCP Server端点让每个端点的工具数量控制在10个以内。比如你有一个订单工具组和一个用户工具组不要把一个Server里塞30个工具而是拆成/mcp/order和/mcp/user两个端点外部Agent按场景接入。工具虽多但对模型来说每次可选项是有限的。6.3 参数类型映射问题现象SK里定义的int参数MCP返回的JSON Schema类型是integer但AI客户端可能把参数值传成字符串。排查MCP SDK的JSON Schema转换对C#类型比较严格但模型本身对数值类型处理不总是可靠。所以我一般在工具入口做预校验和类型转换兜底。比如在插件函数入口处不直接假设参数已经是int而是先做一层Parse失败则返回明确错误信息让模型自己纠正。诚实的工具错误信息比模型瞎猜要有效得多。常见问题触发场景推荐处理方式参数类型不对模型把int当string传工具入口做类型转换兜底枚举值越界传了不在预期内的状态值参数描述中写死可选值函数内校验可选参数缺省客户端不传可空参数参数定义提供默认值6.4 工具调用超时与幂等MCP工具调用默认可能等待较长时间SK编排和模型生成又叠加了延迟。我的做法是给每个工具设置明确的超时阈值并让工具调用尽量幂等——重试多次不会产生副作用。一个简单的超时计算公式工具超时 模型决策时长 外部API最大等待时长 缓冲时间。比如模型决策按15秒算外部API最大等待10秒那超时设到30秒比较合理。如果设太短AI在工具执行过程中就返回异常设太长一次故障会把上游调用方全部拖垮。尤其写操作类工具必须提供一次性的request_id。比如创建工单、发起审批这类操作重试时如果没有request_id很容易重复创建这是生产事故级别的问题。6.5 鉴权与网络边界MCP协议本身不携带认证逻辑服务端和客户端之间也没有默认的安全握手。部署在企业内网能力层时我把MCP Server放在API网管之后由网关做认证鉴权、限流和审计。框架层虽然MCP SDK会校验一些头部但业务层的鉴权还是得自己做。一个原则MCP Endpoint要像普通API Endpoint一样受网络边界保护不要裸奔。至少要加一层Bearer Token校验再配合网关做来源IP白名单。MCP协议解的是工具互通不是安全框架这个边界务必要清楚。6.6 并发时模型上下文膨胀现象性能测试时多个并发请求打到ask_agent后服务端内存和Token开销快速增长。排查SK在编排时会维护上下文历史如果每个请求都保留完整历史并发一高内存立刻报警。解决方案是给每个请求独立的Kernel实例并在任务结束后及时释放上下文长度根据业务需要做截断必要时用Token计数器做预算管理。我自己用的是“请求级Kernel”模式每个MCP调用进来创建一个新的Kernel用完即销毁。虽然会多一点点初始化开销但隔离性极好不会有上下文串扰的问题。7. 一点个人体会把这套能力层做完我自己的感受是MCP解决了工具互通的一部分问题但真正的复杂度还在编排层和执行层。工具标准化只是起点AI能不能稳定理解业务语义、工具执行是否能做到可控可观测这两件事比协议本身难得多。如果有人问我什么时候适合搞一个.NET SK的MCP能力层我的回答是公司内部有多个AI客户端要共用同一批工具值得建已经有成熟.NET系统值得用.NET去接如果只是给单项目接工具调用直接用SK本地调用就够了不要为了MCP而MCP协议层的附加成本也是成本。最后再分享一个小技巧把能力层的工具命名和描述当成接口规范来管理这件事比写代码更值得投入时间。工具描述的质量直接决定AI调用的准确率你花半小时把描述写清楚后面能省下无数次模型误调用带来的排查时间。我自己已经把这套命名规范写进了团队的开发约定里后端同学提交的每个工具函数都要经过一段“AI视角描述评审”效果比想象中还要好。
