前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
原理篇
面经篇
AI 面试
自检篇
每日一题
  • 综合
    • 综合题型
    • 其他问题
    • 设计模式
    • 思维导图
    • 学习路线
  • 前端基础
    • HTTP
    • 浏览器
    • 计算机基础
  • 进阶学习
    • NPM工作流
    • Docker
    • Canvas
    • Node学习指南
    • 前端综合文章
  • 其他
    • Handbook
    • 职场话题
    • CSS可视化
小程序题库
公众号动态
博客动态
AI 热点
开发者导航
基础篇
进阶篇
高频篇
精选篇
手写篇
原理篇
面经篇
AI 面试
自检篇
每日一题
  • 综合
    • 综合题型
    • 其他问题
    • 设计模式
    • 思维导图
    • 学习路线
  • 前端基础
    • HTTP
    • 浏览器
    • 计算机基础
  • 进阶学习
    • NPM工作流
    • Docker
    • Canvas
    • Node学习指南
    • 前端综合文章
  • 其他
    • Handbook
    • 职场话题
    • CSS可视化
小程序题库
公众号动态
博客动态
AI 热点
开发者导航
返回 AI 情报前线
All News · 全部资讯5134
  • 换用新模型前,先用历史失败用例回归测试
  • 编程Agent能读git log,却读不到你试错的四次失败
  • AI集成层大规模密钥泄露:Composio等平台暴露数千凭证
  • NVIDIA Nemotron 3.5 Lightning:解决Agent执行层成本与能力矛盾
  • AI代理真正学会记住的四层记忆系统
  • 两阶段语义检索 + 结构化引用:游戏答案类 RAG 架构设计
  • 语音转文字 + 模型网关:知识库流水线的 API 设计实践
  • Qwen3.8-27B 开源:家用显卡可跑,可商用
  • 大吞吐量 LLM 工单分类的成本控制五法
  • 阿里开源 OpenSandbox:AI Agent 安全沙箱,两天斩获 1.27 万星
  • AI 记忆不必是数据库:试试 Markdown + Git 方案
  • 用 Spring Boot 做三个 AI 产品后的工程总结
  • AI 功能开发中的信任边界设计原则
  • AI 编程模型评测实战框架:从真实开发任务出发的选型方法
  • 阿里开源 Qwen3.8-27B:编程办公场景超越 Qwen3.7-Plus,推理资源可调控
  • AI生成的Shell命令需像MR一样审查
  • 中国医生用GPT-5.6-Sol 16小时证明22年数学难题
  • Node.js API客户端防御式解析实战:四种结果分而治之
  • AGENTS.md成跨工具标准:Claude/Cursor/Codex统一配置指南
  • 一行属性将ASP.NET Core API暴露为MCP服务器
  • .NET消费MCP服务三动词:Connect/Discover/Call
  • .NET消费MCP服务:应用作为客户端
  • 生产级 AI Agent 可观测性建设实战指南
  • LLM分类器不必用真实类目:基于嵌入的假设分类法
  • 为什么我们的Agent需要请求许可才能无人值守运行
  • Dataverse MCP:Claude Code/Copilot接入企业数据平台
  • Agent 执行可视化:让 AI 思维链路可调试
  • 移动端安全配对本地 AI Agent:短命配对码替代 QR 码内置密钥
  • 生产级 AI Agent 通信安全:加密、身份验证与信任 checklist
  • Apple Silicon 本地跑 TTS:Qwen3-TTS + MLX 实战
  • SpaceX 以600亿美元收购 AI 编程助手 Cursor
  • 免费模型调用前,先做请求分类路由
  • OpenAI上线Ultrafast模式:GPT-5.6 Sol速度提升14倍
  • 52款纯前端开发者工具合集:零后端、零数据泄露
  • AI补丁审查的只读worktree门禁:应用后先跑smoke test再合并
  • 廉价模型接入Agent前,先用免费工具调用测试验证可靠性
  • 新模型发布时,别信 hype 用自己的测试用例验货
  • Meta发布开源模型Glimmer,挑战闭源AI垄断
  • 容器镜像未签名是AI时代的定时炸弹
  • 用三步代码分流测试把 MiniMax H3 hype 变成实证
  • 给 Laravel 应用接入 MCP 协议:让 AI 代理直接查询业务数据
  • RAG+Playwright MCP构建零代码AI测试自动化Agent
  • Claude Code fork与subagent的token开销实测
  • 强模型的加密思考链可被弱模型解密:182组凭证泄露
  • 千种Agent技能:模块化JSON schema如何取代提示词工程
  • AI写的四个不抛异常的bug:无声的线上陷阱
  • 非程序员用 AI 把作品集变成可对话的 MCP 服务器
  • 国产AI API路由器的运行时成本熔断器设计
  • Cloudflare 如何检测并保护 MCP 流量安全
  • 实战EU AI Act第50条:我的WordPress聊天机器人插件合规经历
  • 如何根据任务场景选择LLM:超越基准测试的评估方法论
  • 已加载 51 / 5134
8.0
热点
AI SCORE
技术实践2026-08-14 22:59

.NET消费MCP服务三动词:Connect/Discover/Call

dev.to · AI#.NET#MCP#Agent
Editor brief · 编辑速览

.NET应用作为MCP客户端通过stdio或HTTP连接服务器,手动编写的60行Agent循环可压缩为三个动词,附Aurora Coffee Co.完整示例。

文章思维导图
Knowledge map
拖拽缩放
Full translation

完整中文译文

协议的客户单侧

一篇讲服务器的文章说的是暴露能力。讲客户端的文章是它的镜像,只需要三个动词:

连接——通过某种传输层。要么把服务器作为子进程启动(stdio),要么指向一个 URL(HTTP)。

发现——询问服务器有什么。工具及其输入 schema 作为数据到达,没有什么是被编译进代码里的。

调用——通过名称和一组参数调用工具,并接收返回的内容。

值得在第三步停下来,因为它和 tool use 那篇文章看到的安全边界是同一枚硬币的反面。之前,你的代码拥有执行权,Claude 只能发起请求。现在发起请求的是你,服务器拥有执行权。你发送一个名称和参数;实际执行什么完全由服务器决定。

发现这一步是将它与简单调用 API 客户端区分开来的关键。你没有一个用 GetOrderStatus(string) 生成的代理类。你有一个在运行时到达的工具列表,代码必须对此感到舒适。

连接 Aurora 服务器

启动一个控制台应用并添加 SDK。如果想要最小依赖,客户端在 ModelContextProtocol.Core 里;如果同一个应用还要托管服务器,则用完整的 ModelContextProtocol 包:

dotnet new console -o AuroraCoffee.Support
cd AuroraCoffee.Support
dotnet add package ModelContextProtocol.Core

对于本地服务器,stdio 传输层将它作为子进程启动——正是之前 Claude Desktop 用那个 JSON 配置文件替你做的事,只不过现在由你来启动它:

using ModelContextProtocol.Client;

var transport = new StdioClientTransport(new StdioClientTransportOptions
{
    Name = "aurora-coffee",
    Command = "dotnet",
    Arguments = ["run", "--project", "../AuroraCoffee.Mcp"],
});

await using var client = await McpClient.CreateAsync(transport);

这就是全部的连接代码。McpClient.CreateAsync 启动进程、完成协议握手,并将一个可用的客户端交到你手上。注意那个 await using——客户端拥有一个子进程,不释放它就会留下一个孤儿 dotnet 进程运行着。别问我要烧掉多少散兵才能把这个教训刻进脑子里。

调用前先发现

现在这一步是人们容易跳过去、然后花一个小时调试的部分。在写任何 CallToolAsync 之前先打印工具列表:

foreach (var tool in await client.ListToolsAsync())
{
    Console.WriteLine($"{tool.Name} — {tool.Description}");
}

原因不是为了走过场。SDK 从服务器端的方法名派生工具名称,所以协议暴露的确切字符串可能和你写的 C# 标识符不一样。发现才是契约;服务器端的 C# 源代码是实现细节。打印列表,复制真实的名称,然后据此写调用。用猜的名字会在运行时得到一个错误,而且这次没有编译器来救你。

调用工具

手握真实名称后,调用它就是一个名称加一个参数字典:

using ModelContextProtocol.Protocol;

var result = await client.CallToolAsync(
    "get_order_status",
    new Dictionary<string, object?> { ["orderId"] = "A-1001" },
    cancellationToken: CancellationToken.None);

Console.WriteLine(result.Content.OfType<TextContentBlock>().First().Text);
Pedido A-1001: shipped, ETA 2026-08-03.

Content 是一个块列表,不是一个字符串,因为工具可以同时返回文本、图片或多个块。用 OfType<TextContentBlock>() 过滤是诚实读取它的方式——如果你对不返回文本的工具做 .First(),那会抛异常,所以当你不控制服务器时用 FirstOrDefault()。

注意你没有写的那些东西:没有 JSON Schema,没有 HTTP plumbing,没有消息帧。也没有引用服务器项目。Order 和 OrdersStore 在这个应用里不存在——唯一的契约就是那根线。

奖励:把工具交给 Claude

以上都是客户端手工处理工具。真正有意思的版本是让模型决定调用哪个——这正是我们在 tool use 文章里手工写循环所做的事。

McpClientTool 继承自 Microsoft.Extensions.AI 的 AIFunction。这唯一的一个继承就是全部的诀窍:从 MCP 服务器发现的工具直接进入任何 IChatClient 作为可调用函数,不需要任何适配器代码。

dotnet add package Anthropic
dotnet add package Microsoft.Extensions.AI
using Anthropic;
using Microsoft.Extensions.AI;

IChatClient chatClient = new AnthropicClient()   // reads ANTHROPIC_API_KEY
    .AsIChatClient("claude-opus-4-8")
    .AsBuilder()
    .UseFunctionInvocation()
    .Build();

var tools = await client.ListToolsAsync();

var response = await chatClient.GetResponseAsync(
    "¿El pedido A-1001 está enviado, y todavía te quedan ETH-250 en stock?",
    new ChatOptions { Tools = [.. tools] });

Console.WriteLine(response.Text);
El pedido A-1001 ya fue enviado y está en camino para llegar el 2026-08-03. Y sí —
los granos de Etiopía (ETH-250) están en stock, con 42 unidades disponibles.

读一下这个输出,然后再回头看我们手工写的那个循环。同样的问题,同样的两次工具调用,同样的回答——只不过 while (true)、消息列表的 bookkeeping、块的重建和"原样返回 assistant turn"这个陷阱都消失了。UseFunctionInvocation() 就是替你跑那个循环的中间件:Claude 请求一个工具,它调用对应的 AIFunction,返回结果,然后重复直到有了最终回答。

工具本身运行在一个完全独立的进程里,由一个从未听说过这个应用的人写的。这一点值得静下心来好好体会。

连接远程服务器

当服务器是本地子进程时用 stdio。对于共享服务器——也就是前一篇末尾那个 ASP.NET Core 的 HTTP 版本——换一个传输层,其他不变:

var transport = new HttpClientTransport(new HttpClientTransportOptions
{
    Endpoint = new Uri("https://tools.auroracoffee.example/mcp"),
    TransportMode = HttpTransportMode.StreamableHttp,
});

await using var client = await McpClient.CreateAsync(transport);

从这往后 ListToolsAsync、CallToolAsync 和 IChatClient 的接线方式完全相同——唯一注意到差异的就是传输层。TransportMode 默认是 AutoDetect,它先尝试 Streamable HTTP,降级到遗留 SSE,所以如果你不知道另一端支持什么可以省略它。当你确实知道时明确写出来;自动检测要付出一次往返的代价。

关于前一篇以来的一些变化

如果你在之前那篇服务器文章的基础上构建,值得注意一下:C# 的 SDK 在 2026 年 7 月 28 日发布了 v2.0,实现了 2026-07-28 的规范修订版——这是 MCP 发布以来最大的一次协议变更。服务器端的属性([McpServerToolType]、[McpServerTool])和上面的客户端 API 保持不变,但有两样东西变了:

HTTP 传输层默认变成无状态(stateless)的了。HttpServerTransportOptions.Stateless 现在默认为 true,initialize 握手消失了,Mcp-Session-Id 头也没了。对在负载均衡器后面扩展共享服务器来说很棒;如果你假设了有会话,这就是一个行为变更。

服务器发起的请求(sampling、roots)在 MCP9005 诊断下被废弃,在无状态模式下会抛异常。交互式流程改成了 Multi Round-Trip Requests。

如果你的服务器做的是简单的请求/响应工具——大多数服务器都是,包括 Aurora——你不会注意到任何东西。如果依赖了会话,在升级前先读一下迁移说明。

什么时候写客户端——什么时候不写

当你的应用需要它之外的运行时能力时,写一个 MCP 客户端:平台团队维护的服务器、第三方无法控制的服务器、或者多个应用共享的自己的服务器。收益是工具列表可以在服务器端变化而无需你这边重新部署——出现一个新工具,ListToolsAsync 就返回它,如果有模型在驾驶,它就开始使用它而你一行代码都不用发。

当工具仅仅是……你自己的方法、在你自己的应用里、被你自己的模型循环调用时,跳过客户端。为了调用一个本可以直接调用的函数而经过一个协议和进程边界,完全是建筑学上的作秀。在那种场景下 tool use 的方式更简单更快,一直都是。

一般规则:工具跨越你不控制的边界时用 MCP 客户端;不跨越边界时在进程内用 tool use。

写一个客户端只需要三个动词——通过某种传输层连接,发现有什么,然后通过名称调用。其余的都是 SDK 的事。

契约是发现,不是你服务器端的 C# 代码——打印 ListToolsAsync() 并用它返回的名称。编译器无法捕获一个猜出来的工具名称。

McpClientTool 就是一个 AIFunction——唯一这个继承关系就让 MCP 工具进入任何 IChatClient,而 UseFunctionInvocation() 替代了完整的手工代理循环。

传输层是唯一因远程而改变的东西——本地子进程用 stdio,共享服务器用 HttpClientTransport;发现和调用的代码完全相同。

v2.0 改成了默认无状态——HTTP 服务器不再做 initialize 握手,sampling 和 roots 废弃了。简单工具服务器不受影响;依赖会话的需要检查一下。

Original source

本文由 AI 翻译整理自 dev.to · AI,原文版权归原作者所有。

阅读英文原文
上一篇
一行属性将ASP.NET Core API暴露为MCP服务器
下一篇
.NET消费MCP服务:应用作为客户端