
1. 为什么 .NET 项目接 LLM 总卡在第一步如果你正在用 C# 写业务系统最近又被要求「加个 AI 能力」大概率会遇到一个很具体的尴尬模型 API 能调通但一旦要把「工具调用」这件事做规范代码就开始散架。每个模型厂商的 function calling 格式不一样工具描述写在 prompt 里、参数解析靠字符串匹配、多轮上下文自己拼数组最后维护成本比业务代码还高。MCPModel Context Protocol就是来解决这个问题的。它把「LLM 怎么发现工具、怎么传参、怎么拿结果」定义成一套 JSON-RPC 规范工具方只要按协议暴露能力模型侧只要按协议调用两边解耦。对 .NET 开发者来说MCP C# SDK 让你可以用熟悉的 Host、DI、特性标注来写工具而不是手搓 JSON。这篇要解决的具体场景是在一个 .NET 控制台或 Web 项目里用 MCP C# SDK 搭一个能调用 LLM 的最小链路并且把模型通道统一到 TaoToken 的 Key 上。为什么强调统一 Key因为实际项目里你往往要试好几个模型如果每个模型都单独配 endpoint、单独管密钥环境变量会爆炸。TaoToken 提供的是 OpenAI 兼容的 API 通道一个 Key、一个 Base URL就能切换不同模型MCP 客户端侧只需要改 Model ID。适合谁看有 C# 基础、用过dotnet add package、知道什么是环境变量但没系统接过 MCP 的人。跟着做完你会得到一个能跑通的 MCP 服务端 客户端 一次真实对话请求并且知道 401、local proxy failed 这类报错怎么定位。先说清楚边界MCP C# SDK 目前仍在演进包名和 API 可能随版本变化本文以ModelContextProtocol包和Microsoft.Extensions.AI的集成为主线代码可直接复制但请以你还原出来的版本为准。下面从环境准备开始。2. TaoToken 统一 Key 与 MCP C# SDK 环境准备2.1 先拿 Key再谈代码MCP 客户端最终要调用 LLM所以第一步是把模型通道准备好。TaoToken 的接入信息很固定Base URLhttps://taotoken.net/apiAPI Key在控制台创建形如sk-...Model ID按你需要的模型填比如对话类、代码类各有对应 ID创建 Key 的入口在控制台登录后进 API Keys 页面新建即可。这里有个习惯建议不要把 Key 写进appsettings.json提交到 Git用环境变量或 user-secrets。后面配置片段我会两种都给。2.2 创建项目并装包新建一个控制台项目或者在你现有项目里加dotnet new console -n McpLlmDemo cd McpLlmDemo dotnet add package ModelContextProtocol dotnet add package Microsoft.Extensions.AI dotnet add package Microsoft.Extensions.AI.OpenAI dotnet add package Microsoft.Extensions.HostingModelContextProtocol是 MCP 的 C# 实现Microsoft.Extensions.AI提供统一的IChatClient抽象Microsoft.Extensions.AI.OpenAI负责把 OpenAI 兼容的客户端接进来。TaoToken 是 OpenAI 兼容通道所以走这个包最省事。装完后确认一下版本不同版本 API 差异较大dotnet list package2.3 环境变量怎么设Windows PowerShell$env:TAOTOKEN_API_KEYsk-你的key $env:TAOTOKEN_BASE_URLhttps://taotoken.net/apimacOS / Linuxexport TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api如果你用 user-secrets推荐给 Web 项目dotnet user-secrets init dotnet user-secrets set TaoToken:ApiKey sk-你的key dotnet user-secrets set TaoToken:BaseUrl https://taotoken.net/api2.4 appsettings 配置片段如果你更习惯配置文件可以这样写。注意 Key 用占位符真实值从环境变量注入{ TaoToken: { BaseUrl: https://taotoken.net/api, ApiKey: , ModelId: 你的模型ID }, Logging: { LogLevel: { Default: Information } } }然后在代码里用builder.Configuration读取Key 为空时回退到环境变量。这样本地开发和部署都能兼顾。2.5 为什么 MCP 服务端和客户端要分开理解很多人第一次看 MCP 会懵到底谁调谁简单说MCP 服务端是「工具提供方」它暴露[McpServerTool]标注的方法MCP 客户端是「工具使用方」它连接服务端、拿到工具列表、把工具描述交给 LLMLLM 决定调哪个工具后客户端再通过协议把调用转发给服务端。LLM 本身不直接连服务端中间这层协议由 SDK 处理。理解了这个分工后面的代码就不会乱。下一节先写服务端工具再写客户端初始化。3. 可复制的 MCP 服务端与客户端配置代码3.1 服务端用特性标注暴露工具新建DocumentTools.cs写一个最简单的工具比如「统计文本字数」和「生成摘要占位」。真实项目里你可以换成查数据库、调内部 APIusing System.ComponentModel; using ModelContextProtocol.Server; [McpServerToolType] public static class DocumentTools { [McpServerTool, Description(统计输入文本的字符数)] public static int CountCharacters( [Description(待统计的文本)] string text) { return text?.Length ?? 0; } [McpServerTool, Description(对输入文本生成一句话摘要)] public static string Summarize( [Description(待摘要的文本)] string text) { if (string.IsNullOrWhiteSpace(text)) return 空文本; var trimmed text.Length 50 ? text.Substring(0, 50) ... : text; return $摘要{trimmed}; } }[McpServerToolType]标记这个类里有工具[McpServerTool]标记具体方法Description会作为工具描述传给 LLM。参数上的Description同样重要LLM 靠它理解每个参数含义。3.2 服务端宿主注册 MCP 与传输方式在Program.cs里配置 Hostusing Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using ModelContextProtocol.Server; var builder Host.CreateApplicationBuilder(args); builder.Services .AddMcpServer() .WithStdioServerTransport() .WithToolsFromAssembly(); await builder.Build().RunAsync();这里用WithStdioServerTransport()适合本地进程间通信调试最方便。如果你要做 Web 场景可以换成 SSE 传输但本文主线是本地跑通stdio 足够。3.3 客户端把 TaoToken 接进 IChatClient新建LlmClientFactory.cs负责构建聊天客户端using Microsoft.Extensions.AI; using OpenAI; using System.ClientModel; public static class LlmClientFactory { public static IChatClient Create() { var apiKey Environment.GetEnvironmentVariable(TAOTOKEN_API_KEY) ?? throw new InvalidOperationException(缺少 TAOTOKEN_API_KEY); var baseUrl Environment.GetEnvironmentVariable(TAOTOKEN_BASE_URL) ?? https://taotoken.net/api; var modelId Environment.GetEnvironmentVariable(TAOTOKEN_MODEL_ID) ?? 你的模型ID; var openAiClient new OpenAIClient( new ApiKeyCredential(apiKey), new OpenAIClientOptions { Endpoint new Uri(baseUrl) }); return new ChatClientBuilder(openAiClient.GetChatClient(modelId)) .UseFunctionInvocation() .Build(); } }关键点有三个Endpoint指向 TaoToken 的 Base URLApiKeyCredential用你的 KeyUseFunctionInvocation()让客户端具备自动调用工具的能力。Model ID 从环境变量读方便切换。3.4 客户端连接 MCP 服务端并注册工具MCP 客户端需要知道服务端在哪。stdio 模式下客户端启动服务端进程并通过标准输入输出通信using ModelContextProtocol.Client; using Microsoft.Extensions.AI; var transport new StdioClientTransport(new StdioClientTransportOptions { Command dotnet, Arguments new[] { run, --project, ../McpServer } }); var mcpClient await McpClientFactory.CreateAsync(transport); var tools await mcpClient.ListToolsAsync(); var chatClient LlmClientFactory.Create(); var chatOptions new ChatOptions { Tools tools.Select(t (AITool)t).ToList() };ListToolsAsync()拿到的工具列表直接转成AITool塞进ChatOptionsLLM 就能看到这些工具。这一步是 MCP 和 LLM 真正打通的地方。3.5 一次完整对话请求var messages new ListChatMessage { new(ChatRole.User, 请统计这句话的字符数MCP C# SDK 集成 LLM 实战) }; var response await chatClient.GetResponseAsync(messages, chatOptions); Console.WriteLine(response.Text);如果一切正常LLM 会决定调用CountCharacters客户端自动执行并把结果回传最终输出类似「这句话共有 24 个字符」。你可以在CountCharacters里打断点确认工具真的被调用了。3.6 配置对照表配置项值说明Base URLhttps://taotoken.net/apiOpenAI 兼容通道API Keysk-...控制台创建Model ID按需填写决定用哪个模型传输方式stdio / SSE本地调试用 stdio工具注册WithToolsFromAssembly()自动扫描特性4. 验证请求是否真的走通了4.1 先用 curl 验证 Key 和通道在写 C# 之前先用 curl 确认 TaoToken 通道可用能排除掉一半问题curl https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -d { model: 你的模型ID, messages: [{role: user, content: 只回复 OK}] }返回里如果有choices数组且content是OK说明 Key、Base URL、Model ID 三者都对。这一步过了再排查 C# 代码就有方向。4.2 跑 C# 客户端看工具调用回到项目运行客户端dotnet run --project McpClient预期输出是模型对字符数的回答。如果模型没有调用工具而是直接瞎猜通常是工具描述不够清楚或者ChatOptions.Tools没传进去。可以在ListToolsAsync()后打印工具数量确认Console.WriteLine($发现工具数量{tools.Count}); foreach (var t in tools) Console.WriteLine($- {t.Name}: {t.Description});4.3 观察 MCP 通信日志stdio 模式下服务端的标准输出被协议占用所以调试信息要写到标准错误。你可以在服务端加Console.Error.WriteLine($[MCP] 工具被调用CountCharacters);客户端运行时就能在控制台看到这行确认协议链路是通的。4.4 成功结果的判断标准一次成功的集成应该同时满足curl 能拿到choices返回C# 客户端打印出工具数量大于 0模型回答里包含工具执行的真实结果而不是编造服务端标准错误里能看到工具被调用的日志四条都满足说明 MCP C# SDK 集成 LLM 的链路完整跑通。缺哪条就按下一节的报错对照排查。5. 常见报错排查401、local proxy failed 与 choices 解析5.1 401 Unauthorized最常见。原因通常是 Key 没读到、Key 失效、或者 Base URL 写错导致请求打到了别的地址。排查顺序先echo $TAOTOKEN_API_KEY确认环境变量在当前终端可见再确认Endpoint是https://taotoken.net/api而不是带/v1的完整路径OpenAI SDK 会自己拼/v1/chat/completions你多写一层就 404 或 401最后用 curl 复测。如果 curl 通、C# 不通就是代码里读 Key 的逻辑有问题检查Environment.GetEnvironmentVariable是否在正确的进程环境里执行。5.2 local proxy failed这个报错通常出现在客户端尝试连接服务端时。stdio 模式下StdioClientTransportOptions.Command和Arguments必须能正确启动服务端进程。常见坑Command写dotnet但当前目录找不到项目Arguments里的--project路径是相对路径而工作目录不对服务端启动失败但异常被吞掉解决办法是把Command换成服务端编译后的 dll 绝对路径或者用dotnet run --project时确保路径从客户端工作目录能解析到。也可以先在终端手动执行一遍服务端启动命令确认它能跑起来。5.3 reading choices 相关解析错误如果报错里出现reading choices或反序列化失败说明返回的 JSON 结构和 SDK 预期不一致。可能原因Base URL 指向了一个返回 HTML 的地址比如少了/api打到了官网首页模型 ID 不存在服务端返回了错误结构用了不兼容的 SDK 版本先用 curl 看原始返回确认是标准 OpenAI 格式。如果 curl 返回的是{error: ...}那就是模型 ID 或权限问题不是解析问题。5.4 OAuth 与鉴权混淆有些 MCP 示例会配 OAuth但本文场景是 API Key 鉴权不需要 OAuth。如果你看到OAuth相关报错检查是不是误引入了需要 OAuth 的传输方式或服务端配置。TaoToken 走的是 Bearer TokenHeader 形如Authorization: Bearer sk-...和 OAuth 是两套东西。5.5 工具没被调用模型直接回答而不调工具排查三点工具描述是否清晰、ChatOptions.Tools是否真的传了、模型是否支持 function calling。可以先用一个极简工具比如无参数返回固定字符串测试排除参数描述干扰。5.6 报错对照表报错关键词大概率原因处理方向401 UnauthorizedKey 缺失或 Base URL 错误检查环境变量与 Endpointlocal proxy failed服务端进程启动失败检查 Command 与路径reading choices返回非 OpenAI 格式curl 看原始响应OAuth鉴权方式混淆改用 Bearer Token工具未调用Tools 未传或描述不清打印工具列表确认6. 把这条链路用起来下一步怎么走跑通之后你手里其实有了一个可扩展的骨架。工具侧你只要继续加[McpServerTool]方法就能把数据库查询、内部 API、文件处理都暴露给 LLM客户端侧换 Model ID 就能切换模型不用改工具代码。这就是 MCP 的价值工具和模型解耦。如果你打算长期在项目里用建议把 Key 管理收敛到配置中心客户端和服务端分开部署时注意传输方式的选择。本地开发用 stdio 最省事跨机器就考虑 SSE 或 WebSocket。另外工具方法里不要直接连生产库加一层服务封装权限和审计都好做。需要继续深入的话接入文档里有更完整的参数说明和示例模型对话页面可以直接验证不同 Model ID 的效果长期做编码类 Agent 的话可以看 Coding Plan 的额度方案。先把本文这条最小链路跑通再往上叠能力比一上来就搭大框架稳得多。