
最近好几个做后端的同事私信问我同一个问题MCPModel Context Protocol到底是什么为什么那么多人突然开始聊MCP Server我用几天时间把官方文档翻了一遍又用.NET 8写了个最小可运行的示例在MCP Inspector里跑通了工具调用。这篇文章就是把这次实验的完整过程写出来包括协议握手细节、stdio和SSE两种传输方式的差异、以及我在实测中踩过的几个坑。想快速搞清楚MCP并动手跑一遍的.NET开发者可以直接照着做。1. MCP是什么它解决的是AI应用接数据和调工具的标准化问题1.1 从function calling说起为什么会有MCP在大模型应用落地之前想让AI去查天气、查数据库、调用某个内部API最常见的做法是function calling。每个应用自己定义一套函数清单把函数描述和参数Schema传给模型模型生成一个调用指令再由应用代码去执行。这套模式能跑但有一个很别扭的地方集成关系是一对一的。A应用接了自己的天气服务B应用想复用这个能力得重新写一遍适配代码同一个模型服务商换个接入方式就要重新调参。如果企业里有几十个系统和AI平台相连每个连接都是一份定制开发维护成本相当高。MCP的设计思路和function calling完全不同它把AI应用和工具/数据源之间的通信抽象成一层独立的协议。AI应用那边不再针对每个数据源写适配而是作为MCP Client连接任意MCP Server工具提供方也不再为每个AI平台定制接口只要实现一个MCP Server所有支持MCP的客户端都能用。这就是MCP的定位它不是又一个AI框架而是一套开放协议。任何一方只要遵守协议就能在AI生态里即插即用。1.2 MCP的三个核心原语Prompts、Resources、ToolsMCP协议定义了三种基本能力理解这三种原语基本就理解了MCP的使用场景Prompts提示词模板服务端预先定义好的可复用提示词。比如一个代码审查的MCP Server可以提供一个审查这个PR的提示词模板客户端调用后直接得到结构化的评审提示。Resources资源可以被读取的数据内容比如文件内容、数据库记录、API响应适合给模型提供上下文的场景。资源以URI形式标识客户端请求时服务端返回数据。Tools工具可被模型调用的函数比如查询天气创建工单执行SQL。这是目前最常用、讨论最多的部分也是我这次实验的重点。打个不太严谨但好记的比方如果把MCP Server比作一个工具箱Resources是箱子里可以翻阅的资料Tools是箱子里的电动工具Prompts是箱盖内侧贴的操作说明。1.3 传输层stdio和HTTPSSEMCP的通信消息统一走JSON-RPC 2.0但底层传输方式有两种stdio标准输入输出Client启动一个Server子进程通过标准输入输出双向传递JSON消息。适合本地开发、单机工具特别适合Client拉起一个隔离进程的场景。HTTP SSEServer-Sent EventsServer作为HTTP服务Client通过HTTP请求发送消息通过SSE连接接收服务端推送。适合跨机器、多客户端、需要持续运行的场景。在实际开发中这两种模式我会同时使用本地调试和自动化测试走stdio部署给多个客户端或远程使用时走SSE。2. 为什么用.NET写MCP Server生态现状与开发环境准备2.1 MCP是语言中立的协议服务端用什么语言都行MCP和语言没有绑定关系Server端完全可以用C#、Go、Python、Java等任意语言实现。关键是正确实现JSON-RPC消息格式和MCP方法定义。那为什么我特意选了.NET理由有四个第一微软官方直接参与了MCP生态建设提供了ModelContextProtocol系列的官方NuGet包。在.NET 8/9环境下创建Server、注册工具、配置stdio或SSE传输都有现成的API不用自己解析JSON-RPC报文。第二国内大量政企项目和后端系统跑在.NET技术上这些系统有数据库、内部API、文件服务、消息队列等大量私有能力。用.NET实现MCP Server是让这些存量系统接入AI生态成本最低的路径。第三C#的强类型和代码提示在做工具参数Schema映射时有天然优势工具定义写在代码里反射扫描就能生成协议层需要的JSON Schema减少手工维护。第四如果你想做的是MCP Client端嵌入自己的应用微软同样提供客户端SDK.NET技术栈能同时覆盖服务端和客户端不用引两套语言。2.2 身边已经能看到的MCP Server生态在开始写代码之前可以先感受一下MCP落地现状。互联网圈里讨论热度比较高的几个方向设计协同工具比如蓝湖、Figma都提供了MCP ServerAI可以直接读取设计稿结构、导出切图标注安全测试工具比如Burpsuite提供MCP接入让AI辅助分析HTTP流量3D创作工具Blender有MCP Server可以让模型操作建模软件的交互自动化测试框架Playwright也支持MCP方式AI可以驱动浏览器执行操作这些例子说明MCP不是概念炒作而是工具厂商正在主动接入的事实标准。协议本身很年轻迭代很快但方向已经比较明确了。2.3 开发环境准备清单项目版本说明.NET SDK8.0或9.0我用的8.0长期支持版本IDE / 编辑器Visual Studio 2022 / VS Code二选一VS Code需要C#扩展Node.js18可选运行MCP Inspector调试客户端NuGet包Microsoft.ModelContextProtocol预览版微软官方MCP SDK当前是预发布状态如果你的机器上已经有了Visual Studio 2022开发体验会更好一些用VS Code的话装个C# Dev Kit插件也够用。Node.js不是必须的但没有它MCP Inspector跑不起来而Inspector是目前调试MCP Server最方便的工具建议顺手装上。提示微软的MCP SDK还在预览版阶段API和包名可能随版本变化。下面示例代码保证可以跑通但如果你安装的包版本更新遇到编译报错时优先看IntelliSense给出的新API签名。3. 手把手写一个可运行的MCP Server从控制台程序开始3.1 创建项目并安装NuGet包我先建了一个控制台项目路径尽量用英文避免中文目录在部分工具链里出问题。dotnet new console -n McpDemoServer cd McpDemoServer dotnet add package ModelContextProtocol --prerelease装包的时候务必加上--prerelease当前MCP SDK没有正式版不加这个参数很可能提示找不到包。安装后我习惯看一眼项目文件确认包版本ItemGroup PackageReference IncludeModelContextProtocol Version0.1.1 / /ItemGroup不同时间点装到的版本号可能不同这都正常。关键是包里能引用到ModelContextProtocol.Server命名空间。3.2 写一个带工具的MCP Server我设计了一个极小但完整的场景给模型提供一个查询城市实时天气的工具。因为不建议演示代码直接请求真实的外部API容易出现网络超时、接口变化等干扰我在Server里返回模拟数据这样整个实验可以被快速复现。完整代码如下using ModelContextProtocol.Server; using ModelContextProtocol; using System.ComponentModel; var server McpServerBuilder.Create() .WithStdioTransport() .AddToolsFromAssembly(typeof(Program).Assembly) .Build(); await server.RunAsync(); [McpServerToolType] public class WeatherTool { [McpServerTool(Description 查询指定城市的实时天气情况。城市名称使用中文例如杭州、上海、广州。)] public static string GetWeather([Description(要查询天气的城市名称中文例如杭州)] string city) { if (string.IsNullOrWhiteSpace(city)) { return 城市名称不能为空; } // 演示用模拟数据实际项目中替换为真实天气API或数据库查询 var fakeData new Dictionarystring, string { { 杭州, 多云气温26℃湿度60%东风3级 }, { 上海, 小雨气温24℃湿度85%东南风2级 }, { 广州, 晴气温31℃湿度50%南风2级 } }; if (fakeData.TryGetValue(city, out var weather)) { return ${city}天气{weather}; } return $暂未收录{city}的天气数据; } }这段代码的要点有几个McpServerBuilder.Create()创建Server构建器WithStdioTransport()告诉SDK使用标准输入输出传输。AddToolsFromAssembly(typeof(Program).Assembly)通过反射扫描当前程序集中所有标记了McpServerToolType特性的类把它们注册为MCP工具。这是典型的约定式注册后面新增工具类不需要改注册代码。工具方法必须是public static每个参数上的Description特性最终会映射成协议层JSON Schema里的description字段这是大模型理解参数含义的关键信息来源我会在第4节专门说。准备好之后从命令行启动Server它会等待客户端通过标准输入连接dotnet run --project McpDemoServer/McpDemoServer.csproj程序启动后没有任何输出这是正常的JSON-RPC消息全部走标准输入输出。3.3 用MCP Inspector做可视化调试MCP官方提供了Inspector工具可以在网页里启动MCP Server、查看工具列表、手动调用工具观察返回结果。npx modelcontextprotocol/inspector dotnet run --project ./McpDemoServer/McpDemoServer.csproj命令执行后Inspector会在浏览器打开一个管理页面。操作流程是在左侧看到Smart Connect或Transport配置区配置Transport Type为STDIOCommand为dotnet run --project ./McpDemoServer/McpDemoServer.csproj。点击连接等待状态变为已连接。选择Tools标签页能看到weather_getWeather工具名默认是类型名_方法名格式。展开工具详情能看到完整的参数Schema然后手动输入城市名称并调用。我在这个页面里测试了杭州上海不知道什么城市三种输入返回结果都符合预期。这说明我的Server在协议层面的工具注册和调用链路是通的。3.4 给首次接触SDK的朋友的提醒如果你装到的包版本比我新运行示例时可能遇到两类问题McpServerBuilder被挪到别的命名空间或改名为McpServerFactory之类的结构IntelliSense会提示搜索一下当前包里的API即可。工具注册方式从AddToolsFromAssembly变成AddTools或显式注册同样以当前版本的扩展方法列表为准。SDK版本迭代期我养成了一个习惯每次升级包版本之后先跑一遍已有的最小示例再决定要不要改代码。MCP这种预览阶段协议API变动是常态不是你的问题。4. 拆解MCP的JSON-RPC握手机制调试时值得看懂的底层细节直接说结论虽然官方SDK封装了协议细节但如果完全不懂底层消息流遇到连接失败、工具注册不上、参数乱传这类问题时会无从下手。我在调试过程中手动输出过客户端发来的原始JSON下面把关键交互过程展示出来。4.1 握手阶段initialize请求与响应客户端连接Server后发出的第一条消息是initialize请求作用是协商协议版本和能力{ jsonrpc: 2.0, id: 1, method: initialize, params: { protocolVersion: 2025-03-26, capabilities: {}, clientInfo: { name: mcp-inspector } } }Server响应时返回自己支持的协议版本、Server能力列表和Server信息{ jsonrpc: 2.0, id: 1, result: { protocolVersion: 2025-03-26, capabilities: { tools: {} }, serverInfo: { name: McpDemoServer, version: 1.0.0 } } }如果客户端请求的协议版本服务端不支持服务端会返回自己支持的最高版本客户端再决定是否降级。在实际集成中如果两端版本协商不通过连接会在这一步直接失败所以看到failed during initialize这类报错时先看两端协议版本。握手完成后客户端发送notifications/initialized通知表示初始化阶段结束可以开始业务消息了。4.2 工具发现tools/list初始化之后客户端为了知道这个服务器到底能干什么会发送tools/list{ jsonrpc: 2.0, id: 2, method: tools/list, params: {} }Server返回工具列表每个工具包含name、description、inputSchema三项。我上面的天气工具在协议层展开后大概长这样{ tools: [ { name: weather_getWeather, description: 查询指定城市的实时天气情况。城市名称使用中文例如杭州、上海、广州。, inputSchema: { type: object, properties: { city: { type: string, description: 要查询天气的城市名称中文例如杭州 } }, required: [city] } } ] }4.3 工具调用tools/call当模型决定调用天气工具时客户端发送tools/call携带工具名和参数{ jsonrpc: 2.0, id: 3, method: tools/call, params: { name: weather_getWeather, arguments: { city: 杭州 } } }Server执行方法后返回{ jsonrpc: 2.0, id: 3, result: { content: [ { type: text, text: 杭州天气多云气温26℃湿度60%东风3级 } ] } }我在看这些报文时最大的感受是MCP协议本身不复杂整个生命周期非常清晰就是初始化 - 发现能力 - 调用工具三步。真正容易出问题的不是协议本身而是工具定义层不严谨。4.4 踩坑复盘参数Schema写不好模型真的会乱调我在调试时故意做了一个对比实验去掉工具方法参数上的Description特性只在方法上留一句查询天气。结果在Inspector里手动调用没什么区别因为参数是人填的。但换到实际AI客户端时模型会猜测city应该传什么值甚至可能把杭州传给location之类的其他字段导致报错。工具描述写得模糊模型会用模糊的参数去调用这是MCP Server开发里最常见也最隐蔽的问题。所以写工具时我给自己定了几条规矩方法Description写清楚这个工具做什么、在何种场景下调用。每个参数Description写清楚格式、示例值、边界约束。能设置枚举约束的参数尽量写死不留给模型发挥空间。参数尽量用简单类型string、number、boolean避免嵌套对象嵌套越深模型出错概率越高。4.5 异常处理也要走协议不能直接抛异常还有一个容易忽略的点Server里发生业务异常时不能直接抛异常让进程崩溃应当捕获异常并返回符合JSON-RPC格式的错误对象例如{ jsonrpc: 2.0, id: 3, error: { code: -32001, message: Internal error: 天气服务超时 } }这样客户端能感知到工具调用失败并把结果反馈给模型。进程崩溃或者输出一段非JSON文本到标准输出会直接破坏stdio通道里的消息流导致客户端悬挂超时。5. 从本地到远程把MCP Server部署成ASP.NET Core SSE服务5.1 什么时候需要SSE远程模式stdio模式适合本地开发因为它默认就是Client拉起进程、进程跟着Client生命周期走。但实际使用时我们更可能希望Server独立部署在服务器上多个客户端比如几个同事的AI工具同时连上来调用。这时需要HTTP SSE模式HTTP提供请求响应通道SSE提供服务端主动推送的通道。协议消息结构不变变的只是传输层。5.2 一个最小改造示例把控制台项目改成ASP.NET Core项目主要改动是换传输类型。我用的是微软官方SDK目前推荐的写法using ModelContextProtocol.AspNetCore; using ModelContextProtocol.Server; using System.ComponentModel; var builder WebApplication.CreateBuilder(args); builder.Services.AddMcpServer() .WithHttpTransport() .AddToolsFromAssembly(typeof(Program).Assembly); var app builder.Build(); app.MapMcp(/mcp); app.Run(); [McpServerToolType] public class WeatherTool { [McpServerTool(Description 查询指定城市的实时天气情况。城市名称使用中文例如杭州、上海、广州。)] public static string GetWeather([Description(要查询天气的城市名称中文例如杭州)] string city) { return ${city}天气模拟数据晴26℃; } }创建项目的命令dotnet new web -n McpDemoServerHttp cd McpDemoServerHttp dotnet add package ModelContextProtocol.AspNetCore --prerelease启动后MCP服务监听在/mcp路径。客户端连接时的URL就是http://localhost:5000/mcp。如果你的SDK版本对应的API变化较大核心思路不变注册MCP服务时用HTTP传输然后映射一个端点路径。我建议你在SDK的GitHub仓库里拉最新的示例工程对比参考。5.3 部署SSE模式时容易出现的三个问题SSE模式在本地调试时一切正常一到实际部署就容易在这三个地方出岔子第一是CORS跨域。如果MCP客户端运行在浏览器比如Web版AI工具、Inspector页面而你的Server在另一个域名下必须配置允许的跨域来源。我的配置是builder.Services.AddCors(options { options.AddPolicy(AllowMcpClients, policy { policy.WithOrigins(http://localhost:3000) .AllowAnyHeader() .AllowAnyMethod(); }); }); app.UseCors(AllowMcpClients);实际环境里按客户端的来源严格配置不建议图省事用AllowAnyOrigin。第二是反向代理下的SSE连接保持。我在一台Linux服务器上通过Nginx反代到ASP.NET Core服务客户端总是连不上。排查后发现是Nginx默认缓冲了响应体SSE这种需要实时输出的事件流被缓冲后无法及时发送。需要在Nginx配置里关闭缓冲proxy_buffering off; proxy_cache off;如果用的是其他反向代理同样要确认它对长连接和流式响应的支持方式。第三是并发与超时设置。MCP客户端打开后通常保持长连接如果服务端在网关层面设置了很短的响应超时时间连接会被无故断开。同时要注意SSE模式下Server处理工具调用是异步的工具里如果有耗时操作需要在代码层控制并发量避免被大量调用打满线程池。我给WeatherTool加耗时模拟时发现一个问题不加并发限制时连续调用5次以上请求排队时间明显上升。MCP Server虽然协议上是轻量的但真实使用时承载的是并发工具调用建议给工具执行做好超时控制和资源限制别让AI把后端打死。6. 跑通示例之后我踩过的坑和值得继续折腾的方向6.1 实测中最影响体验的几个问题整个示例从开始写到跑通耗时比我预期长卡点不在业务代码而在环境小问题上。简单记录一下包版本不一致导致的编译失败第一次装的是0.1.x早期版本WithStdioTransport方法还没有暴露换成较新版本后就好了。后来我把NuGet包的版本号固定下来了避免不相关的小改动影响调试。端口被占用跑HTTP模式时5000端口被别的进程占用换成其他端口重跑即可。Inspector里看不到工具大多数时候是进程启动失败或者项目目录有编译错误先在命令行单独跑一次dotnet run确认能否正常启动再做网络连接。中文路径问题中文目录名在部分SDK和调试工具里出现过异常工程路径尽量全英文。6.2 日志是最好的调试手段MCP Server调试时最难的是看不到客户端那边发生了什么。我的做法是在Server里记录每一帧收到的原始JSON消息以及响应返回的内容。使用官方SDK时可以在构建器阶段注册日志Provider或者直接自己实现一个消息处理中间件把initialize、tools/list、tools/call三类关键消息打到控制台或日志文件里。有一次工具调用失败客户端报tool execution failed我就是在日志里看到方法内部抛了空引用异常才定位到是参数映射的问题。没有日志这类问题基本只能靠猜。6.3 安全你的工具暴露给AI后AI会做你没想到的事这是我在写MCP Server时最想强调的一点。MCP工具对客户端来说就是一个可调用的执行接口大模型只要理解了工具描述就会在合理的场景下调用它。如果工具内部没有做权限校验和参数校验会出现几种情况模型可能按用户指令把恶意文本作为参数传入比如查询XX的天气里夹带SQL语句内部工具暴露给外部客户端后原本的权限边界消失一次对话里模型可能反复调用同一工具产生大量请求和费用我给自己定下的MCP Server安全底线是所有工具入口做参数白名单校验工具执行前做身份认证和授权校验执行过程加审计日志敏感操作加二次确认或只读限制。目前MCP协议本身没有内建安全机制安全责任完全在Server实现方。6.4 后续值得尝试的扩展方向这次实验跑通的是一个最小闭环实际项目完全可以从这里继续扩展把工具执行改为连接真实数据库在工具方法里执行参数化SQL返回查询结果给模型把工具注册改成动态注册运行时从配置中心读取工具定义不需要改代码接企业内部API网关让AI助手可以查工单、查审批、操作业务系统在客户端侧做一个ASP.NET Core的MCP Client让你的应用去调用别人开放的MCP Server如果要做知识库类场景重点研究Resources原语采用类似文件读取/搜索的模式我的建议是先从单个工具的真实业务场景开始把一个Tool做到能用、安全、有日志再慢慢扩。最后分享一个个人体会MCP现在生态还很年轻协议版本、SDK API都在快速变化。玩这种新东西最靠谱的做法是在官方仓库里维护一个最小示例工程每次协议或SDK更新后跑一遍确认自己的代码还能编译。踩过几次版本坑之后你会发现适应它比学它更省力。