使用 .NET C SDK 构建 MCP Server Tools:ModelContextProtocol 2.x 工具原语完整实战指南

发布时间:2026/9/13 15:12:49
使用 .NET C SDK 构建 MCP Server Tools:ModelContextProtocol 2.x 工具原语完整实战指南 使用 .NET C# SDK 构建 MCP Server ToolsModelContextProtocol 2.x 工具原语完整实战指南【免费下载链接】awesome-copilotCommunity-contributed instructions, agents, skills, and configurations to help you make the most of GitHub Copilot.项目地址: https://gitcode.com/GitHub_Trending/aw/awesome-copilot导读本文是 awesome-copilot 仓库中 dotnet-mcp-builder 技能集的核心参考文档《Tools》的深度展开。它面向希望在 C#/.NET 中为 Model Context ProtocolMCP服务器编写高质量工具Tool的开发者你将掌握基于[McpServerToolType]/[McpServerTool]属性声明工具、通过WithToolsFromAssembly()/WithToolsT()完成注册、控制 JSON Schema 生成、处理异步与依赖注入、设计返回值与内容块ContentBlock、正确区分工具级错误与协议级错误并规避最常见的实现陷阱。文中所有代码示例均基于官方ModelContextProtocol2.x NuGet 包对应 2026-07-28 协议修订且与同仓库的 transport-stdio.md、transport-http.md、testing.md 等姊妹文档相互印证。1. MCP 工具原语一句话心智模型在 MCP 协议中工具Tool是 LLM 可以直接调用的函数。LLM 看到的是工具名、描述和 JSON Schema 形式的参数定义真正执行的是你写的 C# 方法。在 .NET C# SDK 中工具就是某个标有[McpServerToolType]的类上的普通方法每个方法再标[McpServerTool]。SDK 会根据方法签名与[Description]属性自动生成对应的 JSON Schema——你不需要手写 schema 文件。依据 SKILL.md 的决策树凡是「新增/修改工具」的任务都应加载 references/tool-primitive.md即本文所基于的原始文档同时该技能明确指出[Description]是 LLM 选择与构造调用时的唯一依据描述含糊是工具没人用的头号原因。一个最小可用的工具using System.ComponentModel; using ModelContextProtocol.Server; [McpServerToolType] public class WeatherTools { // Static 或 instance 均可 —— instance 方法可为所在类获得 DI 注入 [McpServerTool, Description(Returns the current weather for a city.)] public static string GetWeather( [Description(City name, e.g. Brussels)] string city, [Description(Units: celsius or fahrenheit)] string units celsius) { return ${city}: 18°{units[0]}; } }注册工具二选一.WithToolsFromAssembly() // 发现调用程序集内所有 [McpServerToolType] 类 .WithToolsWeatherTools() // 显式指定单个工具类关键细节LLM 看到的工具名是GetWeatherPascalCase 不会自动转换为 snake_case——除非显式设置Name否则「你写什么就是什么」。这一点在参考文档 tool-primitive.md 中有明确强调。2. 工具解剖从方法签名到 JSON SchemaSDK 生成 schema 的过程完全自动化方法名、参数名与类型、[Description]属性、参数的默认值全部会被翻译成inputSchema。理解这条链路就能预测 LLM 视角下你的工具长什么样。源码要素生成的 Schema 影响方法名GetWeather工具名不做大小写转换参数string citytype: string的必填属性参数string units celsius可选属性附默认值参数[Description(...)]属性描述LLM 理解参数含义的关键方法[Description(...)]工具描述LLM 决定何时调用它的依据行为提示Behavior Hints[McpServerTool( Name get_weather, // 覆盖工具名 Title Get current weather, // 人类可读的显示名 Destructive false, // 提示该工具会不可逆地修改状态 Idempotent true, // 提示相同参数 ⇒ 相同结果 OpenWorld true, // 提示与外部系统交互 ReadOnly true // 提示不修改任何状态 )] [Description(Returns the current weather for a city.)] public static string GetWeather(...) { ... }重要Destructive、Idempotent、OpenWorld、ReadOnly这四项行为提示仅具建议性advisory——客户端用它来决定诸如「自动批准auto-approval」之类的策略不会改变服务端运行行为依据 tool-primitive.md。合理标注这些提示可以让 Claude Desktop、VS Code 等宿主在安全策略上对工具更友好。3. 异步、取消令牌与依赖注入真实世界的工具几乎都是异步 IO 型查数据库、调外部 API、读文件系统。C# SDK 为这些场景提供了开箱即用的支持。[McpServerTool, Description(Fetches the latest commits for a repo.)] public async TaskIEnumerableCommit GetCommits( string owner, string repo, IGitHubClient github, // 从 DI 注入 CancellationToken cancellationToken) // 由 SDK 注入 { return await github.GetCommitsAsync(owner, repo, cancellationToken); }被 SDK 特殊识别的参数类型不会出现在 schema 中SDK 会识别并特殊处理以下参数类型——它们不进入工具 schemaIMcpServer/McpServer—— 当前服务器实例用于ElicitAsync、SampleAsync、RequestRootsAsync、发送通知等注意sampling/roots 在 2.x 中已被标记[Obsolete]详见第 7 节CancellationToken—— 从 JSON-RPC 请求传播而来客户端取消请求时触发RequestContextCallToolRequestParams—— 完整请求上下文可读取请求元信息例如 server-features.md 中进度通知所需的ctx.Params.Meta?.ProgressTokenIServiceProvider—— 请求作用域的 service provider任何可被 DI 解析、且 SDK 能识别为非原始负载的类型如上面的IGitHubClient。除此之外的一切参数都被当作 JSON-RPC 参数进入 schema。这也意味着如果你的 DI 服务名字恰好叫string之类需要小心命名冲突复杂类型参数会通过System.Text.Json绑定这也是 SKILL.md 故障排查清单第 4 条「参数未绑定」的根因——参数名必须与 JSON-RPCarguments的键一致。关于依赖注入的工具类工具类本身也参与 DI 生命周期将工具类声明为实例类非 static其构造函数即可注入任意已注册服务包括ILoggerT。这在 server-features.md 的日志示例中得到了体现——工具类通过构造函数注入ILoggerWeatherTools在方法内记录业务日志而无需向 stdout 输出。4. 返回值设计LLM 到底能看到什么SDK 会把方法的返回值序列化成合适的内容块content blocks返回给客户端。这是工具设计中最容易被忽视、却直接影响 LLM 使用效果的部分。返回值类型与 LLM 视角对照表返回类型LLM 看到的内容string单个文本内容块int、bool、double等字符串化后放入文本内容块任意 DTOrecord/class序列化为 JSON 的文本内容块 面向支持客户端的结构化内容IEnumerableTDTO 集合JSON 数组ContentBlock/ImageContentBlock/AudioContentBlock/EmbeddedResourceBlock原样返回该单个块IEnumerableContentBlock按顺序返回多个内容块CallToolResult完全控制——可设置Content、StructuredContent、IsError4.1 返回结构化数据让 LLM 能直接操作结果public record Forecast(string City, double TempC, string Conditions); [McpServerTool, Description(Returns a 3-day forecast.)] public static Forecast[] GetForecast(string city) new[] { new Forecast(city, 18.0, sunny), new Forecast(city, 16.5, cloudy), new Forecast(city, 14.2, rain), };SDK 会把数组同时输出为 JSON 文本块兼容旧客户端和structuredContent面向新客户端并从Forecastrecord 推断输出 schema。这样 LLM 无需解析自由文本即可直接读取字段。v2 行为变更务必注意在 2.x 中非对象结果会以原始structuredContent值输出——例如返回72会得到structuredContent: 72而 1.x 会包装成{ result: 72 }。读取结构化输出的客户端应当遵循对外公布的输出 schema。另外如果你手写Tool定义而不是用属性inputSchema在 2.x 反序列化时是必填的——空的{}也足够依据 tool-primitive.md。4.2 返回图片与音频二进制内容通过专用内容块返回宿主可直接渲染[McpServerTool, Description(Generates a chart and returns it as a PNG.)] public static ImageContentBlock RenderChart(string title) { byte[] png Renderer.Render(title); return ImageContentBlock.FromBytes(png, image/png); } [McpServerTool, Description(Synthesises speech.)] public static AudioContentBlock Speak(string text) { byte[] wav Tts.Synthesize(text); return AudioContentBlock.FromBytes(wav, audio/wav); }4.3 混合多个内容块一个工具可以按顺序返回多个块——比如「图表 说明文字」[McpServerTool, Description(Returns the chart and a caption.)] public static IEnumerableContentBlock RenderAnnotatedChart(string title) { byte[] png Renderer.Render(title); return new ContentBlock[] { new TextContentBlock { Text $Chart for: {title} }, ImageContentBlock.FromBytes(png, image/png), new TextContentBlock { Text Generated at DateTime.UtcNow.ToString(u) } }; }4.4 返回嵌入式资源EmbeddedResourceBlock当工具结果本身是用户可能想复用的文档时用嵌入式资源块最合适——宿主可以决定如何渲染它比如展示为可保存的文件而不是把原始字节塞进模型上下文[McpServerTool, Description(Looks up a contract.)] public static EmbeddedResourceBlock GetContract(string id) { return new EmbeddedResourceBlock { Resource new TextResourceContents { Uri $contracts://{id}, MimeType text/markdown, Text LoadContract(id) } }; }性能提醒返回数兆字节 JSON 的工具会快速耗尽模型的上下文窗口见第 6 节陷阱清单。对于二进制大对象tool-primitive.md 明确建议返回EmbeddedResourceBlock让宿主决定渲染方式而不是让 LLM 去消化原始数据。5. 错误处理工具级错误与协议级错误错误处理是工具设计中防守的关键。SDK 区分两种错误语义选择错误会直接影响 LLM 能否从失败中恢复。5.1 工具级错误LLM 可读并可从中恢复抛出任意异常即可——SDK 会捕获它并返回一个IsError true的CallToolResult异常消息放入文本块[McpServerTool, Description(Divides a by b.)] public static double Divide(double a, double b) { if (b 0) throw new ArgumentException(Cannot divide by zero.); return a / b; }也可以显式构建结果以便向 LLM 提供更详细的错误解释[McpServerTool, Description(…)] public static CallToolResult Foo(...) { return new CallToolResult { IsError true, Content [new TextContentBlock { Text Detailed error explanation for the LLM. }] }; }5.2 协议级错误调用在 LLM 看到结果之前就被拒绝对「坏参数」这类调用方问题使用McpException或带显式错误码的McpProtocolException[McpServerTool, Description(…)] public static string Process(string input) { if (string.IsNullOrWhiteSpace(input)) throw new McpProtocolException(Missing required input, McpErrorCode.InvalidParams); return $Processed: {input}; }选择启发式来自 tool-primitive.md如果 LLM 应该换参数重试就抛普通异常让它拿到工具级错误如果调用本身畸形、LLM 无法修复例如缺失必填输入就抛McpProtocolException。5.3 错误处理与测试的呼应仓库中的 testing.md 展示了如何在进程内验证这些错误语义使用StreamServerTransport/StreamClientTransport把真实的 server 与 client 接在同一进程内然后断言result.IsError与文本内容——这正是验证工具级错误对 LLM 可见的可执行方式。6. 运行时通知工具列表变更如果你的工具会在运行时动态出现或消失例如插件被加载、用户登录/登出应当主动通知客户端刷新工具列表await server.SendNotificationAsync( NotificationMethods.ToolListChangedNotification, new ToolListChangedNotificationParams(), cancellationToken);前提这需要有状态传输——STDIO 或有状态的 HTTPStreamable。在 2.x 默认的无状态 HTTPStateless true见 transport-http.md上推送类通知没有可用通道动态工具列表通常只适用于 STDIO 或显式关闭 Stateless 的部署。类似地进度通知spinner 文本也遵循先检查客户端是否传了progressToken再发送的约定详见 server-features.md。7. 与 v2 / 2026-07-28 协议修订的兼容性要点dotnet-mcp-builder 技能面向stable 2.x与2026-07-28 协议修订工具开发必须了解几处关键变化详见 SKILL.md 的 Cardinal Rules固定稳定版包不要用 previewModelContextProtocol/ModelContextProtocol.AspNetCore/ModelContextProtocol.Core应使用最新 2.x参考文档 packages.md 建议新建项目默认 .NET 10。roots / sampling / MCP-channel logging 已弃用SDK 2.x 将其标记[Obsolete]编译警告MCP9005。如果你的工具通过注入的IMcpServer调用SampleAsync/RequestRootsAsync请规划迁移新设计优先使用多轮InputRequiredException模式与ILogger日志。工具内的中段提问改用多轮模式在 2026-07-28 的 HTTP 上不存在 server→client 的elicitation/create请求ElicitAsync在无状态 HTTP 上直接抛异常当前协议下的做法是从工具中抛出InputRequiredException构建InputRequest.ForElicitation(...)在客户端重试的调用里读取context.Params.InputResponses详见 elicitation.md。STDIO 则始终支持ElicitAsync。结构化工具体验2.x 支持结构化内容块与输出 schema 推断非对象结果的structuredContent输出规则已变化见第 4.1 节。8. 常见陷阱清单参考文档 tool-primitive.md 明确列出四大高频坑结合技能文档与测试文档可进一步归纳为忘记给类加[McpServerToolType]仅有方法级[McpServerTool]不会被WithToolsFromAssembly()发现。这也与 SKILL.md 故障清单第 3 条「工具不出现」一致——检查类级属性与注册行是否都在。描述含糊[Description(Gets data)]会让 LLM 靠猜。花一句话说清楚工具做什么、何时调用、返回什么。用 MCP Inspector 可以像 LLM 一样渲染并检查工具描述见 testing.md。大负载返回数兆 JSON 的工具会吞噬模型上下文。应裁剪或分页二进制大对象用EmbeddedResourceBlock交给宿主渲染。隐藏错误把failed当字符串返回SDK 会把它当成成功结果。要么抛异常要么显式设置IsError true。向 stdout 写东西STDIO 场景stdout 是 JSON-RPC 通道。工具内任何Console.WriteLine都会破坏协议详见 transport-stdio.md 的 stdout/stderr 陷阱日志一律走ILoggerLogToStandardErrorThreshold。9. 验证工具从进程内测试到端到端联调工具写完后仓库的 testing.md 给出了三层验证路径均可直接用于工具开发MCP Inspector交互式npx modelcontextprotocol/inspector dotnet run --project ./MyMcpServer启动后即可列表/调用工具、查看 schema最适合检查[Description]在 LLM 视角下的可读性进程内集成测试推荐用McpServer.CreateStreamServerTransport与McpClient.CreateAsync在同一进程接线调用client.ListToolsAsync()/tool.CallAsync(...)断言工具暴露行为——不需要子进程、不需要网络、不需要 Node/Dockerdotnet test即可在任何 CI 上运行端到端联调dotnet publish后接入 Claude Desktop / VS Code配置方式见 transport-stdio.md从聊天中真实触发工具。其中「工具不出现」类问题的最快定位法在测试中调用client.ListToolsAsync()并打印工具名——如果工具不在列表里注册[McpServerToolType]或.WithTools...()一定有问题。10. 小结在 C#/.NET 中构建 MCP 工具本质上是「写好一个普通方法 用属性声明协议语义」[McpServerToolType][McpServerTool]声明、[Description]喂给 LLM、方法签名驱动 JSON Schema、返回值映射内容块、异常决定错误语义。在 2.x / 2026-07-28 协议下还需时刻留意无状态 HTTP 默认值、弃用的 sampling/roots/logging 能力以及多轮input_required模式。进一步阅读本技能集的其他参考文档覆盖了 Prompt 原语、Resource 原语、客户端开发、STDIO 传输、Streamable HTTP 传输 与 测试它们与本文共同构成完整的 .NET MCP 服务端开发知识体系。【免费下载链接】awesome-copilotCommunity-contributed instructions, agents, skills, and configurations to help you make the most of GitHub Copilot.项目地址: https://gitcode.com/GitHub_Trending/aw/awesome-copilot创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询