MCP Client 配 qwen-max 时,Key 和 Endpoint 被锁死在 DashScope(https://dashscope.aliyuncs.com/compatible-mode/v1),想换模型或统一管理就得改代码里的配置。TaoToken 提供了另一条路:去 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 创建 Key,再把 ChatClient 的 Endpoint 改成 https://taotoken.net/api,WebApi、Swagger、kernel.ImportPluginFromOpenApiAsync、WithTools 这些 MCP Server 构建步骤完全不动。我把原文的 Microsoft.Extensions.AI MCP Client 演示改了一遍,结论是,“MCP Server 原样保留,Client 里只改模型侧配置”这句话在 TaoToken 上真的成立。MCP 本身只管工具协议,WebApi 的 OpenAPI 文档被 MCP Server 包装成 Tools 后,模型侧走哪个供应商是 Client 的事。所以“MCP Client 不走 DashScope,改走 TaoToken 行不行”的答案,不在于什么深度改造,而在于 Endpoint 与 Key 的替换是否干净。
1. MCP Client 换供应商:真正要改的代码段在哪
1.1 ChatClient 构造参数绑定 DashScope
原文的 Microsoft.Extensions.AI 演示里,MCP Client 的模型连接是这样写的:
string apiKey = "sk-****"; var chatClient = new ChatClient( "qwen-max-2025-01-25", new ApiKeyCredential(apiKey), new OpenAIClientOptions { Endpoint = new Uri("https://dashscope.aliyuncs.com/compatible-mode/v1") }).AsIChatClient();第三个参数是 OpenAI 兼容接口的 Endpoint。它由供应商决定,不由模型决定,所以供应商一换,这个 URI 就必须跟着换。qwen-max-2025-01-25是 DashScope 侧的模型快照命名,切到另一个通道后,同样可能不存在。Key、模型 ID、Endpoint 三者在原代码里是绑在一起的,这就是“锁死”。
1.2 MCP Server 与模型侧配置其实是两层
MCP Server 只是一个中介。WebApi 提供 Swagger 文档,MCP Server 项目通过ImportPluginFromOpenApiAsync读取文档,再用WithTools(kernel.Plugins)把每个接口包装成McpServerTool。MCP Client 通过ListToolsAsync拿到的是一个AIFunction集合。模型层只是在使用这个集合时才知道“有哪些 Tools 可以调用”。架构上,供应商切换只影响 ChatClient 的构造,不影响 MCP Server 和 WebApi。这个分层是 MCP 协议带来的价值,也是 TaoToken 能“只改模型侧配置”的前提。
2. 申请 TaoToken Key,模型 ID 看模型广场
2.1 注册并在控制台创建 YOUR_API_KEY
准备阶段要做的事,和原文在 DashScope 上做的事一样:打开 TaoToken 注册并登录,进入控制台 API Keys 页面创建一个新 Key。创建后复制下来,这就是代码里的YOUR_API_KEY。原文里string apiKey = "sk-****"的位置,不要保留原来的 DashScope 密钥。TaoToken 会给一把统一的 Key,代码里只需要记住一个 Endpoint。如果你把配置放在环境变量里,运行 Client 的控制台窗口要确认环境变量没有被覆盖,否则 Endpoint 可能仍然指向旧的 DashScope 地址。
2.2 模型 ID 别照抄 qwen-max-2025-01-25
原文选的模型是qwen-max-2025-01-25。TaoToken 模型广场列出的 ID 未必和 DashScope 完全一致,切换前打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 的模型广场,找到你开通的模型,复制它显示的 ID。我测试时仍然用的是 qwen 系列,因为中文 Function Calling 效果比较好,但具体到哪个版本,以广场当时列表为准。同时注意模型是否标注支持 Function Calling;MCP 工具调用依赖模型侧的这项能力,模型 ID 正确但不支持工具调用,对话阶段也不会触发 GetCurrentWeather。
下面这个对照表可以帮你快速定位差异:
| 配置项 | DashScope 写法 | TaoToken 写法 |
|---|---|---|
| API Key | DashScope 控制台创建的 sk-** | YOUR_API_KEY |
| Endpoint | https://dashscope.aliyuncs.com/compatible-mode/v1 | https://taotoken.net/api |
| 模型 ID | qwen-max-2025-01-25 | 以模型广场列表为准 |
3. 原样保留 MCP Server:WebApi 到 OpenAPI 的转接
3.1 WebApi 提供 Swagger 元数据
MCP Server 的数据源是 WebApi 的 OpenAPI 文档。WeatherController 里有三个 Action:GetCurrentDate返回当前日期,GetLocation查询当前 IP 所在城市,GetCurrentWeather根据省份、城市、日期返回天气。它们只是普通 HTTP 接口,不依赖任何 MCP 包:
[ApiController] [Route("api/[controller]/[action]")] public class WeatherController(IHttpClientFactory httpClientFactory) : ControllerBase { [HttpGet] public string GetCurrentDate() => DateTime.Now.ToString("MM/dd"); [HttpGet] public async Task<IpInfo> GetLocation() { var client = httpClientFactory.CreateClient(); var ipData = await client.GetFromJsonAsync<IpData>("https://ipinfo.io/json"); return IpTool.Search(ipData!.ip); } [HttpGet] public async Task<string> GetCurrentWeather(string region, string city, string currentDate) { var client = httpClientFactory.CreateClient(); var weatherRoot = await client.GetFromJsonAsync<WeatherRoot>( $"https://cn.apihz.cn/api/tianqi/tqybmoji15.php?id=88888888&key=88888888&sheng={region}&place={city}"); var today = weatherRoot!.data!.FirstOrDefault(i => i.week2 == currentDate); return $"{today!.week2} {today.week1},天气{today.wea1}转{today.wea2}。最高气温{today.wendu1}摄氏度,最低气温{today.wendu2}摄氏度。"; } }这里的 id=88888888 和 key=88888888 是原文示例的测试参数,实际使用时换成天气服务商给你的密钥。只要 Swagger JSON 能正常输出,MCP Server 就能读到每个 Action 的 URL、参数类型和返回描述。
3.2 MCP Server 项目引用与 OpenAPI 加载
MCP Server 项目需要引用 Semantic Kernel 的 OpenAPI 插件和 ModelContextProtocol,客户端通过 Stdio 启动这个 exe 时,不需要额外暴露端口。项目里涉及的包如下:
<ItemGroup> <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> <PackageReference Include="Microsoft.SemanticKernel.Plugins.OpenApi" Version="1.47.0" /> <PackageReference Include="ModelContextProtocol" Version="0.1.0-preview.11" /> </ItemGroup>加载 OpenAPI 的代码是ImportPluginFromOpenApiAsync。它接受 URI 或本地文件路径,读取 Swagger 元数据后,把每个 HTTP 接口变成一个KernelFunction。这个函数被触发时,底层会发起真实的 HTTP 请求到原来的 WebApi 地址。原文的 URI 是http://localhost:5021/swagger/v1/swagger.json,你自己运行时换成你的 WebApi 实际端口:
#pragma warning disable SKEXP0040 await kernel.ImportPluginFromOpenApiAsync( pluginName: "city_date_weather", uri: new Uri("http://localhost:5021/swagger/v1/swagger.json"), executionParameters: new OpenApiFunctionExecutionParameters { EnablePayloadNamespacing = true }); #pragma warning restore SKEXP0040EnablePayloadNamespacing表示给请求参数加上命名空间前缀,避免多个接口的参数名冲突。这一步和模型供应商无关,不需要为 TaoToken 做任何调整。
3.3 WithTools 扩展方法让 Plugin 变成 McpServerTool
为了让 Plugin 能被 MCP 标准协议识别,需要把KernelPluginCollection里的每个KernelFunction转成McpServerTool。原文的自定义扩展方法遍历所有 Plugin 和 Function,调用function.AsAIFunction(),再用McpServerTool.Create创建实例。它本质上是把 Semantic Kernel 的 Function 包装成AIFunction,而AIFunction可以序列化成 MCP 客户端能读取的工具描述:
public static class McpServerBuilderExtensions { public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, KernelPluginCollection plugins) { foreach (var plugin in plugins) { foreach (var function in plugin) { builder.Services.AddSingleton(services => McpServerTool.Create(function.AsAIFunction())); } } return builder; } }这段代码和模型供应商也没有关系,保持不变即可。
4. Microsoft.Extensions.AI 里替换 Endpoint 与 Key
4.1 ChatClient 的 Endpoint 改成 https://taotoken.net/api
MCP Client 项目才是这次切换的重点。原文的ChatClient构造来自Microsoft.Extensions.AI.OpenAI包,它接收模型 ID、API Key 和 OpenAI 兼容的 Endpoint。切到 TaoToken 后,三个值按 TaoToken 的信息替换:
apiKey换成在 TaoToken 创建的 Key;Endpoint换成https://taotoken.net/api,注意末尾没有/v1;- 模型 ID 以模型广场的列表为准。
客户端项目的包引用如下:
<ItemGroup> <PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.4.3-preview.1.25230.7" /> <PackageReference Include="ModelContextProtocol" Version="0.1.0-preview.12" /> </ItemGroup>改造后的完整 Client 代码:
using System.Text; using Microsoft.Extensions.AI; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol.Transport; await using IMcpClient mcpClient = await McpClientFactory.CreateAsync( new StdioClientTransport(new() { Name = "city_date_weather", Command = @"..\..\..\..\McpServerDemo\bin\Debug\net9.0\McpServerDemo.exe" })); var tools = await mcpClient.ListToolsAsync(); foreach (AIFunction tool in tools) { Console.WriteLine($"Tool Name: {tool.Name}"); Console.WriteLine($"Tool Description: {tool.Description}"); Console.WriteLine(); } // 从 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 创建 string apiKey = "YOUR_API_KEY"; // 模型 ID 以模型广场列表为准,例如 qwen-max string modelId = "qwen-max"; var chatClient = new ChatClient( modelId, new ApiKeyCredential(apiKey), new OpenAIClientOptions { Endpoint = new Uri("https://taotoken.net/api") }).AsIChatClient(); IChatClient client = new ChatClientBuilder(chatClient) .UseFunctionInvocation() .Build(); ChatOptions chatOptions = new() { Tools = [.. tools] }; List<Microsoft.Extensions.AI.ChatMessage> chatList = []; Console.Write("User:"); while (Console.ReadLine() is { } question && question != "exists") { chatList.Add(new Microsoft.Extensions.AI.ChatMessage(ChatRole.User, question)); Console.Write("Assistant:"); var responseText = new StringBuilder(); await foreach (var update in client.GetStreamingResponseAsync(chatList, chatOptions)) { if (string.IsNullOrWhiteSpace(update.Text)) continue; responseText.Append(update.Text); Console.Write(update.Text); } chatList.Add(new Microsoft.Extensions.AI.ChatMessage(ChatRole.Assistant, responseText.ToString())); Console.WriteLine(); Console.Write("User:"); }这段代码和 DashScope 版本唯一的差异,就是apiKey、modelId和Endpoint。Command路径如果本地目录不同,改成你编译出的 McpServerDemo.exe 的实际位置。UseFunctionInvocation()必须加,它把对话流里的 FunctionCall 请求自动衔接上;不加的话,即使 Tools 已经传进ChatOptions,模型也不会执行工具调用。
4.2 Semantic Kernel 的 AddOpenAIChatCompletion 同步替换
原文另外给了 Semantic Kernel 版本,同样只需要换两个值。AddOpenAIChatCompletion注册模型时,httpClient.BaseAddress就是 OpenAI 兼容接口地址。切换后:
using Microsoft.SemanticKernel; await using IMcpClient mcpClient = await McpClientFactory.CreateAsync( new StdioClientTransport(new() { Name = "city_date_weather", Command = @"..\..\..\..\McpServerDemo\bin\Debug\net9.0\McpServerDemo.exe" })); var tools = await mcpClient.ListToolsAsync(); using HttpClient httpClient = new() { BaseAddress = new Uri("https://taotoken.net/api") }; IKernelBuilder kernelBuilder = Kernel.CreateBuilder(); kernelBuilder.AddOpenAIChatCompletion( "qwen-max", // 以 TaoToken 模型广场为准 "YOUR_API_KEY", httpClient: httpClient); kernelBuilder.Plugins.AddFromFunctions( "weather", tools.Select(aiFunction => aiFunction.AsKernelFunction())); Kernel kernel = kernelBuilder.Build();AddOpenAIChatCompletion第一个参数不要沿用 DashScope 的qwen-max-2025-01-25日期版本。日期快照在另一个通道不一定存在,先用qwen-max跑通,再到模型广场换成你要的精确版本。旧代码如果在 BaseAddress 后面加过/v1,这次要删掉,否则部分 SDK 会拼出双斜杠路径。
5. 验证 ListToolsAsync 与 GetCurrentWeather 是否走通
5.1 先跑 WebApi,再启动 McpServer,最后连 Client
MCP Server 用的是 Stdio 传输层,McpClient 会直接启动 McpServerDemo.exe。运行 Client 之前,保证三件事:WebApi 项目已经启动,Swagger JSON 可以在浏览器访问;McpServerDemo 项目已经编译成 exe;Client 控制台项目里Command指向这个 exe。运行 Client 后,程序首先打印工具列表,你应该能看到GetCurrentWeather、GetCurrentDate、GetLocation三个工具的名字和描述。工具列表能打出来,说明 MCP Server 原来的构建逻辑没有因为切换供应商而失效。
5.2 多轮对话里触发天气工具
接着输入类似“北京今天天气如何”的话。模型支持 Function Calling 并且Tools集合正确传入时,模型会返回一个 FunctionCall,UseFunctionInvocation自动调用GetCurrentWeather,把结果再送回去生成最终回复。测试里GetCurrentWeather的入参由模型生成,比如 region=北京、city=北京、currentDate=07/24,和 DashScope 通道下的行为一致。这说明不只是“能连上”TaoToken,而是 MCP 工具调用链路真的走通了。
5.3 到 TaoToken 控制台核对本次调用
对话结束后,打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 查看用量。如果刚才那轮对话出现在调用记录里,说明请求确实经过了 TaoToken 通道;如果记录是空的,检查 ChatClient 是否还在读环境变量里的旧 Endpoint,或者项目里有没有第二个OpenAIClientOptions覆盖了当前配置。用量页的时间点精确到分钟,只要时间对得上,就能确认这次调用走的是哪条通道。这一步是切换后最直接的确认方式,比抓日志省事。
6. 切换后遇到的 401、404 与工具未触发
6.1 401:Key 没生效或复制到了旧 key
如果控制台打印 401,先确认代码里的apiKey不是YOUR_API_KEY占位符,很多人复制示例代码时,把占位符也当成真 Key 填进去了。其次检查 Key 是否复制完整,TaoToken 控制台生成的 Key 如果复制时少了一位,通常也会在请求阶段报 401。还有一种情况是项目里某个 HttpClient 中间件改写了 Authorization 头,把ApiKeyCredential生成的认证信息覆盖了。这种问题与切换供应商无关,可以换一把新 Key 逐个环节测试。
6.2 404:模型 ID 沿用了旧日期后缀
报错信息里如果出现model not found,基本是模型 ID 的问题。DashScope 的qwen-max-2025-01-25不代表 TaoToken 也有同样后缀,而且日期快照类 ID 在另一个通道上还容易因为版本下架而失效。去模型广场复制当前列表里显示的 ID,不要凭记忆填带日期的版本号。如果你确实想用 qwen 系列,先试qwen-max或qwen-plus这类稳定名,跑通后再根据广场上的精确 ID 微调。
6.3 UseFunctionInvocation 未开启导致工具没触发
Tools 列表能打印,但对话时 AI 只是普通回答,没有触发天气接口,检查两处:UseFunctionInvocation()是否在ChatClientBuilder上调用;ChatOptions.Tools是否真的把ListToolsAsync的结果传进去了。这两个缺一个,模型就算支持 Function Calling,也不会去调用 MCP 工具。另一个可能是指定的模型不支持工具调用,换一个模型广场里标注支持 Tool Use 的型号再试。
6.4 跑通后去控制台对一下这次调用
切换不是改完配置就结束,要回到用量页确认一次真实的请求记录。现在你可以用同一把 Key 在 TaoToken 模型对话 里发一条消息,验证模型 ID 和 Endpoint 都没问题;需要长期写代码的话,再看 Coding Plan 是否适合你的用量;Key 在 控制台 API Keys 管理;Claude Code 环境变量对照见 接入文档。确认这次调用已经被记上账,再批量迁移到其他项目不迟。