news 2026/9/16 3:41:08

MCP Client 不走 DashScope,改走 TaoToken 行不行?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MCP Client 不走 DashScope,改走 TaoToken 行不行?

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 KeyDashScope 控制台创建的 sk-**YOUR_API_KEY
Endpointhttps://dashscope.aliyuncs.com/compatible-mode/v1https://taotoken.net/api
模型 IDqwen-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 SKEXP0040

EnablePayloadNamespacing表示给请求参数加上命名空间前缀,避免多个接口的参数名冲突。这一步和模型供应商无关,不需要为 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 版本唯一的差异,就是apiKeymodelIdEndpointCommand路径如果本地目录不同,改成你编译出的 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 后,程序首先打印工具列表,你应该能看到GetCurrentWeatherGetCurrentDateGetLocation三个工具的名字和描述。工具列表能打出来,说明 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-maxqwen-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 环境变量对照见 接入文档。确认这次调用已经被记上账,再批量迁移到其他项目不迟。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 3:40:36

Linux设备驱动开发实战:从设备树到OLED驱动全链路

1. 为什么今天还在啃《Linux设备驱动开发》这本“硬核砖头”&#xff1f;我第一次翻开《Linux设备驱动开发详解》那本书时&#xff0c;手边正插着一块刚焊歪的STM32开发板&#xff0c;串口打印出来的全是乱码&#xff0c;dmesg里刷着一长串i2c i2c-0: Failed to register devic…

作者头像 李华
网站建设 2026/9/16 3:40:35

Termexo v0.9.0:Antigravity引擎与语义级Diff导航实战指南

1. 项目概述&#xff1a;Termexo v0.9.0 到底带来了什么实质性变化&#xff1f;Termexo v0.9.0 这个版本更新标题里藏着三个关键信号&#xff1a;Antigravity 加入工作台、CLI 安装流程重构、Diff 导航能力升级。这不是一次常规的补丁更新&#xff0c;而是 Termexo 从“代码编辑…

作者头像 李华
网站建设 2026/9/16 3:39:09

Claude 3.7出海报实操:从提示词到成品的完整指南

最近真被Claude 3.7出海报出图这事给惊到了。以前要做一张能看的海报&#xff0c;要么自己开PS套模板&#xff0c;要么去Midjourney写一堆描述词碰运气&#xff0c;要么花钱找在线海报工具&#xff0c;折腾半天出来的东西还总是差点意思。现在用Claude 3.7&#xff0c;基本流程…

作者头像 李华
网站建设 2026/9/16 3:38:17

企业微信SCRM的AI能力分水岭:底层架构如何决定智能化上限

我上季度把市面上叫得出名字的企业微信SCRM几乎都跑了一遍&#xff0c;不是看官网宣传&#xff0c;而是真的开账号、接企微、灌测试数据&#xff0c;按销售跟单和客服接待的场景从头打到尾。先说结论&#xff1a;2026年选SCRM&#xff0c;别只盯着客户群发、渠道活码这些传统功…

作者头像 李华
网站建设 2026/9/16 3:37:10

小学英语资源第二辑:从囤资源到用资源的完整学习路径

小学英语资源合集&#xff08;第二辑&#xff09;上次整理完第一辑之后&#xff0c;后台留言区就一直没消停过&#xff0c;问得最多的几类问题是&#xff1a;资源太多孩子根本用不过来怎么办、听力材料到底怎么分级才不挫伤积极性、自然拼读和分级阅读到底先搞哪个。这些问题其…

作者头像 李华
网站建设 2026/9/16 3:36:09

Apple CarPlay认证全解析:从iAP2到MFi的避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华