ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

使用Avalonia/C#构建一个简易的跨平台MCP客户端:接入TaoToken统一Key通道

使用Avalonia/C#构建一个简易的跨平台MCP客户端:接入TaoToken统一Key通道 1. 从零搭建跨平台 MCP 客户端为什么选 Avalonia C# 这条路MCP 客户端说白了就是一个能跟 MCP 服务器对话的桌面程序它负责把模型返回的工具调用请求转发给对应的 MCP 服务器再把执行结果塞回对话上下文。适合谁适合已经会用 C# 写点小工具、想让 AI 真正操作本地文件、数据库、网页抓取的开发者。Avalonia 在这里的价值是同一套代码在 Windows、Linux、macOS 上都能跑不用为每个平台单独维护 UI 层。我这次要做的 MCP-Studio 结构很朴素一个主窗口左侧是对话区右侧是 MCP 服务器与工具列表底层用ModelContextProtocol官方 C# SDK 管理服务器连接用HttpClient走 OpenAI 兼容协议调用模型。模型通道这块我统一走 TaoToken 的 Key这样切换模型时只改一个 Model ID不用到处翻配置文件。整个项目分四块ChatModelSettings.json存模型接入参数mcp_settings.json存 MCP 服务器启动命令McpService负责连接与工具枚举ChatService负责把工具定义转成 OpenAI function calling 格式并发请求。下面按顺序把每一步拆开你照着敲就能跑起来。先明确一个概念MCP 服务器本质是一个通过 stdio 或 SSE 通信的进程它对外暴露若干 tool。客户端启动时读取配置用stdio方式拉起进程调用list_tools拿到工具清单再把这些工具转成模型能识别的 function schema。模型决定调用哪个工具后客户端执行call_tool把结果作为tool角色消息回传。理解这条链路后面配置就不会迷路。2. TaoToken 统一 Key 通道前置准备Base URL、Key 与 Model ID 三件套在写代码之前先把模型通道准备好。TaoToken 提供 OpenAI 兼容接口所以任何支持自定义 Base URL 的 C# 客户端都能接。你需要准备三样东西Base URL、API Key、Model ID。Base URL 填https://taotoken.net/api注意这里不带任何多余路径SDK 会自动拼/v1/chat/completions。API Key 在控制台的 API Keys 页面创建建议单独建一个给桌面客户端用方便随时吊销。Model ID 这块要留意MCP 场景必须选支持 function calling / tool use 的模型否则模型不会返回tool_calls字段你的工具列表就是摆设。我实测下来带工具调用能力的模型在返回结构里会有明确的tool_calls数组而不支持的模型只会给你一段自然语言。选模型时先在模型对话页面手动发一条带工具的请求验证一下确认能返回结构化调用再写进配置。创建 Key 的入口在控制台路径是 API Keys 页面。点新建复制出来的字符串只显示一次务必先存到密码管理器。如果你打算长期跑编码类 Agent可以顺带了解 Coding Plan它更适合高频调用场景只是偶尔测试的话按量计费的 Key 就够了。把这三件套写进ChatModelSettings.json结构如下。注意ApiKey不要提交到 Git.gitignore里加上这个文件名仓库里只留.example版本。{ BaseUrl: https://taotoken.net/api, ApiKey: sk-你的Key, ModelId: 你的模型ID, MaxTokens: 4096, Temperature: 0.7 }这里有个容易踩的点Base URL 结尾不要加/v1。很多教程会让你填https://xxx/v1但 OpenAI .NET SDK 默认会在 BaseUrl 后追加/v1/chat/completions你再加一层就变成/v1/v1/...直接 404。我试过在HttpClient里手动拼路径结果和 SDK 的默认行为打架最后统一交给 SDK 处理最省心。3. 可复制配置项目结构、MCP 服务器清单与客户端接入代码先把项目骨架搭出来。用dotnet new avalonia.mvvm创建工程然后加两个 NuGet 包ModelContextProtocol和OpenAI。前者管 MCP 连接后者管模型调用。项目结构建议这样分MCP-Studio/ ├── Models/ │ ├── ChatModelSettings.cs │ └── McpServerConfig.cs ├── Services/ │ ├── McpService.cs │ └── ChatService.cs ├── ViewModels/ │ └── MainViewModel.cs ├── Views/ │ └── MainWindow.axaml ├── ChatModelSettings.json └── mcp_settings.jsonmcp_settings.json是 MCP 服务器的启动清单每个条目包含命令、参数和环境变量。下面是我实际用的三个服务器配置你可以直接复制{ mcpServers: { duckduckgo: { command: uvx, args: [duckduckgo-mcp-server] }, fetch: { command: uvx, args: [mcp-server-fetch] }, sqlite: { command: uvx, args: [mcp-server-sqlite, --db-path, ./data/products.db] } } }uvx是 Python 的 uv 工具链提供的命令能直接拉起 PyPI 上的 MCP 服务器包不用手动建虚拟环境。Windows 上如果提示找不到uvx把 uv 的安装目录加进 PATH 即可。Linux 和 macOS 一般装完 uv 就能直接用。接下来是McpService的核心逻辑。它读取上面的 JSON为每个服务器创建一个McpClient用 stdio 传输拉起进程然后调用ListToolsAsync拿工具清单using ModelContextProtocol.Client; using ModelContextProtocol.Protocol.Transport; public class McpService { private readonly Dictionarystring, McpClient _clients new(); public async Task ConnectAllAsync(string configPath) { var json await File.ReadAllTextAsync(configPath); var config JsonSerializer.DeserializeMcpConfigRoot(json)!; foreach (var (name, server) in config.McpServers) { var transport new StdioClientTransport(new StdioClientTransportOptions { Command server.Command, Arguments server.Args, Name name }); var client await McpClientFactory.CreateAsync(transport); _clients[name] client; } } public async TaskListMcpClientTool GetAllToolsAsync() { var all new ListMcpClientTool(); foreach (var client in _clients.Values) { var tools await client.ListToolsAsync(); all.AddRange(tools); } return all; } }ChatService负责把 MCP 工具转成 OpenAI 的ChatTool格式然后发请求。关键转换在McpClientTool到ChatTool的映射工具名和参数 schema 直接透传public async Taskstring SendAsync(string userInput, ListMcpClientTool mcpTools) { var tools mcpTools.Select(t ChatTool.CreateFunctionTool( t.Name, t.Description, BinaryData.FromString(t.JsonSchema.GetRawText()) )).ToList(); var options new ChatCompletionOptions { Tools { } }; foreach (var tool in tools) options.Tools.Add(tool); var messages new ListChatMessage { new SystemChatMessage(你是一个可以调用工具的助手。), new UserChatMessage(userInput) }; var client new ChatClient( _settings.ModelId, new ApiKeyCredential(_settings.ApiKey), new OpenAIClientOptions { Endpoint new Uri(_settings.BaseUrl) }); var response await client.CompleteChatAsync(messages, options); return response.Value.Content[0].Text; }注意Endpoint只填到https://taotoken.net/apiSDK 会自己补全路径。如果你用的是OpenAIClient而不是ChatClient行为一致。工具 schema 直接来自 MCP 服务器的inputSchema不用手写这是 MCP 协议省事的地方。4. 启动验证与请求回显检查确认工具列表和模型调用都通了代码写完先别急着接模型分两步验证。第一步验证 MCP 连接启动程序后在 MCPSettings 页面应该能看到三个服务器各自的工具列表。duckduckgo 会暴露搜索工具fetch 暴露网页抓取工具sqlite 暴露查询和写入工具。如果某个服务器下面空白说明进程没拉起来去看输出窗口的 stderr。第二步验证模型通道。在对话区输入一句会触发工具调用的话比如「帮我搜索一下 Avalonia 最新版本」。如果模型支持 function calling你会看到请求里带上了tools字段返回的finish_reason是tool_calls然后客户端执行搜索工具把结果回传模型再生成最终回答。整个过程在日志里能看到两次请求第一次返回工具调用第二次返回自然语言总结。请求回显这块建议在ChatService里加一行日志把原始响应 JSON 打出来var raw response.GetRawResponse(); Console.WriteLine(raw.Content.ToString());这样你能清楚看到choices[0].message.tool_calls的结构。如果这个字段是空的而content里有大段文字说明模型没走工具调用要么是模型不支持要么是工具 schema 没传对。我踩过的坑是JsonSchema序列化时多包了一层导致模型看不懂参数定义后来直接GetRawText()透传就正常了。验证 sqlite 工具时先准备一个products.db建一张products表塞几条带保质期的数据。然后问「获取 products 表中所有保质期大于 30 天的商品信息」。模型会生成一条 SQL通过 sqlite MCP 服务器执行结果回显在对话区。中文显示如果乱码检查数据库连接的编码设置跟 MCP 客户端本身无关。5. 本篇常见错排查401、local proxy failed 与 reading choices 报错第一个高频错误是 401 Unauthorized。报错原文通常是OpenAI.Authentication.UnauthorizedException或响应体里invalid_api_key。原因就两个Key 复制时带了空格或者 Key 被吊销。检查ChatModelSettings.json里ApiKey字段首尾有没有空白字符用Trim()处理一下。如果确认 Key 没问题去控制台看这个 Key 的状态是否正常。第二个是local proxy failed或连接超时。这个报错一般出现在 MCP 服务器进程启动阶段不是模型通道的问题。常见原因是uvx不在 PATH 里或者服务器包下载失败。解决办法在终端手动执行uvx duckduckgo-mcp-server看能不能正常启动。如果卡在下载检查网络如果提示命令不存在重新安装 uv 并确认 PATH。注意 MCP 服务器是本地进程跟模型 API 的网络是两回事别混在一起排查。第三个是reading choices或Cannot read properties of undefined (reading choices)。这个报错说明响应体结构跟预期不符通常是 Base URL 配错了。比如你填了https://taotoken.net/api/v1SDK 再拼一次/v1/chat/completions请求打到了不存在的路径返回的是 HTML 错误页而不是 JSON。把 Base URL 改回https://taotoken.net/api即可。另外确认ModelId拼写正确模型名写错有时也会返回非标准结构。第四个是 OAuth 相关报错比如OAuth token exchange failed。MCP 协议支持 OAuth 认证的远程服务器但本篇用的都是 stdio 本地服务器不涉及 OAuth。如果你在配置里误加了url字段而不是command客户端会尝试走 SSE OAuth 流程自然失败。检查mcp_settings.json本地服务器只用command和args不要写url。排查顺序建议先看 MCP 工具列表是否为空再看模型请求是否返回tool_calls最后看工具执行结果是否回传。三段链路分开定位比一股脑看日志快得多。6. 继续扩展把 MCP 客户端用起来与后续接入建议跑通之后你可以往mcp_settings.json里继续加服务器。社区里常见的还有文件系统服务器、Git 服务器、时间服务器加进去就能让模型操作对应资源。每加一个重启程序在 MCPSettings 页面确认工具出现即可。工具越多模型的选择空间越大但也要注意上下文长度工具定义本身会占 token。模型通道这边如果你后面要换模型只改ChatModelSettings.json里的ModelIdBase URL 和 Key 都不用动。想验证新模型是否支持工具调用直接去模型对话页面发一条带工具的测试请求看返回结构里有没有tool_calls。确认支持了再写进配置省得在客户端里反复调试。长期跑编码类 Agent 的话可以考虑 Coding Plan它在高频调用场景下更划算。接入文档里有完整的参数说明和示例遇到协议层面的疑问可以先翻文档。API Keys 页面用来管理你的 Key建议按用途分开建桌面客户端一个、脚本一个方便审计和吊销。最后提醒一句MCP 服务器能操作本地文件和数据库权限不小。生产库千万别直接挂上去测试用单独的数据库文件。工具调用结果回传给模型时注意别把敏感字段带进去。这些边界想清楚再放手让 AI 干活。
返回列表