1. 从 UI 到多 MCP Server为什么鉴权会先崩在本地把 UI、MCP Client、MCP Server 串起来时最容易卡住的不是工具调用逻辑而是「多个 Server 各自要鉴权」这件事。前端 UI 发一个请求MCP Client 要同时连mcp、mcp1、mcp2好几个 Server每个 Server 背后可能又各自去调模型或外部 API。如果每个 Server 都塞一份独立的 Key配置会迅速失控改一个 Key 要翻五六个文件本地调试时还容易把 Key 提交进 Git。我试过最省事的做法是让 MCP Client 只认一个统一入口把模型侧的鉴权收敛到一处Server 只负责暴露 tools。这样 UI 侧只需要关心「连了哪几个 Server、每个 Server 有哪些工具」而不用关心每个 Server 用什么 Key 去调模型。这篇就按这个思路交付一份可复制的 MCP Client 多 Server 配置骨架并用 TaoToken 的统一 Key 打通鉴权与路由最后逐条验证连通性。适合谁看正在用 Node/TypeScript 写 MCP Client、UI 侧要同时挂多个 MCP Server、并且被多份 Key 配置折磨过的同学。核心检索词先摆出来——MCP Client 多 Server 配置、MCP Server 链接、TaoToken 统一 Key 接入这三个就是全文主线。2. TaoToken 前置统一 Key 与 API 通道怎么准备TaoToken 在这里扮演的角色是「模型侧的统一入口」。MCP Server 本身不直接持有各家模型的 Key而是通过一个兼容 OpenAI 风格的 API 通道去请求模型鉴权用同一把 Key。这样多 Server 场景下你只需要维护一份 KeyServer 侧只改baseURL和apiKey两个字段。先拿到统一 Key。打开官网注册后进入控制台在 API Keys 页面创建一把 Key控制台入口https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAPI Keys 页面https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite创建后把 Key 写进本地.env不要硬编码进代码# .env TAOTOKEN_API_KEYsk-你的统一Key TAOTOKEN_BASE_URLhttps://taotoken.net/api注意TAOTOKEN_BASE_URL用https://taotoken.net/api不要带任何查询参数。Key 只放.env.gitignore里加上.env。如果你还没确认模型通道是否通可以先用模型对话页面手动发一条消息验证模型对话https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite这一步的意义是在写 MCP Client 之前先确认「统一 Key API 通道」这条链路本身是活的。链路不通后面多 Server 配置再对也白搭。3. 可复制配置MCP Client 多 Server 骨架MCP Client 的核心任务是遍历 Server 配置数组为每个 Server 建一个 Client 实例连上后拉取 tools并给每个 tool 打上serverName标记这样调用时才知道该把请求路由回哪个 Server。下面这份骨架可以直接改路径用。先看 Server 配置数组这是多 Server 的入口// mcp-servers.config.ts export interface McpServerConfig { name: string; args: string; } export const MCP_SERVERS: McpServerConfig[] [ { name: mcp, args: /Users/you/work/mcpserver/mcp/build/index.js, }, { name: mcp1, args: /Users/you/work/mcpserver/mcp1/build/index.js, }, { name: mcp2, args: /Users/you/work/mcpserver/mcp2/build/index.js, }, ];然后是 Client 侧的多 Server 连接逻辑。关键点是Promise.all并发连接以及用数组下标作为serverName索引把 tool 和 Server 绑定// mcp-client.ts import { Client } from modelcontextprotocol/sdk/client/index.js; import { StdioClientTransport } from modelcontextprotocol/sdk/client/stdio.js; import { MCP_SERVERS } from ./mcp-servers.config; interface ToolDefinition { name: string; description: string; parameters: Recordstring, any; serverName: number; execute: (args: any) Promisestring; } class MCPClient { private mcpServer: Client[] []; private tools: ToolDefinition[] []; async connectToServer() { const res await Promise.all( MCP_SERVERS.map((item, index) { this.mcpServer.push( new Client({ name: mcp-client- item.name, version: 1.0.0 }) ); const mcp this.mcpServer[index]; const command item.args.endsWith(.py) ? process.platform win32 ? python : python3 : process.execPath; const transport new StdioClientTransport({ command, args: [item.args], }); mcp.connect(transport); return mcp.listTools(); }) ); const nestedArr res.map(({ tools }, index) tools.map((item) ({ ...item, serverName: index })) ); const toolsArr nestedArr.flat(); this.tools toolsArr.map((t) ({ name: t.name, description: t.description || , parameters: t.inputSchema, serverName: t.serverName, execute: async (args: any) { const result await this.mcpServer[t.serverName].callTool({ name: t.name, arguments: args, }); return result.content as string; }, })); console.log( 已连接 MCP Server工具, this.tools.map((t) ${t.name}${t.serverName}).join(, ) ); } }模型调用侧统一走 TaoToken 通道Key 从环境变量读// model-call.ts import axios from axios; import dotenv from dotenv; dotenv.config(); const API_KEY process.env.TAOTOKEN_API_KEY; const BASE_URL process.env.TAOTOKEN_BASE_URL; if (!API_KEY) throw new Error(请在 .env 中配置 TAOTOKEN_API_KEY); export async function callModel(payload: any) { const { data } await axios.post( ${BASE_URL}/v1/chat/completions, { ...payload, stream: false }, { headers: { Content-Type: application/json, Authorization: Bearer ${API_KEY}, }, } ); return data; }这样 Server 侧完全不需要知道模型 Key它只暴露 tools模型鉴权全部收敛到 MCP Client 这一层。多 Server 场景下你改 Key 只改.env一处。4. 验证请求确认多 Server 真的连上了配置写完先别急着接 UI。分三步验证每步都有明确的成功信号。第一步单独跑 Client 的连接逻辑看工具列表是否带上了serverNamenpx ts-node mcp-client.ts期望输出类似已连接 MCP Server工具 read_file0, write_file0, query_db1, fetch_url2如果只看到一个 Server 的工具说明MCP_SERVERS数组里其他条目路径不对或者对应 Server 的build/index.js没编译出来。第二步验证工具路由。手动触发一个属于mcp1的工具确认请求被路由到正确的 Server 实例const tool client.tools.find((t) t.name query_db); console.log(该工具归属 Server 索引, tool?.serverName); // 期望 1 const result await tool?.execute({ sql: select 1 }); console.log(工具返回, result);第三步把 UI 侧接上。用一个最小的 HTTP Server 暴露/api/hello让前端通过 query 传参进来// server.ts import http from http; import url from url; const server http.createServer(async (req: any, res: any) { const parsedUrl url.parse(req.url, true); const name parsedUrl.query.name; res.setHeader(Access-Control-Allow-Origin, *); res.setHeader(Content-Type, application/json; charsetutf-8); if (req.method GET parsedUrl.pathname /api/hello) { const ans await mcpClient.processQuery(name as string); res.end(JSON.stringify({ message: ans })); } else { res.statusCode 404; res.end(JSON.stringify({ error: 接口未找到 })); } }); server.listen(3002, () { console.log(本地服务器启动成功http://127.0.0.1:3002); });浏览器或 curl 访问http://127.0.0.1:3002/api/hello?name帮我查一下数据库如果返回里包含工具调用后的结果并且控制台打印出「已获取到两个仓库的参数」「开始调用」就说明 UI → MCP Client → 多 MCP Server 这条链路通了。提示验证阶段建议把每个 Server 的listTools结果单独打印一次确认工具名没有跨 Server 重名。重名会导致find命中错误的 Server。5. 本篇常见错排查错误一Error: spawn ... ENOENT。这是StdioClientTransport找不到启动命令。检查command字段.js文件用process.execPath即 Node 可执行文件.py文件在 Windows 用python、macOS/Linux 用python3。路径里有空格时不要手动加引号SDK 会处理。错误二只连上一个 Server其他静默失败。Promise.all里某个mcp.connect抛错会整体 reject但如果你没 catch可能只看到部分工具。给每个连接加独立日志MCP_SERVERS.map(async (item, index) { try { // connect 逻辑 console.log([Server ${index}] ${item.name} 连接成功); } catch (e) { console.error([Server ${index}] ${item.name} 连接失败, e); } });错误三工具调用返回undefined。多半是serverName索引错位。this.mcpServer.push的顺序必须和MCP_SERVERS数组顺序一致否则this.mcpServer[t.serverName]会指向错误的 Client。用index显式赋值不要依赖 push 的隐式顺序。错误四模型侧 401。检查.env里TAOTOKEN_API_KEY是否有多余空格BASE_URL是否是https://taotoken.net/api。如果 Key 刚创建确认没有复制到前后空白字符。接入细节可对照接入文档接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite错误五UI 跨域被拦。本地调试时Access-Control-Allow-Origin设成*即可但生产环境要收敛到具体域名。另外Content-Type要显式设成application/json; charsetutf-8否则前端JSON.parse可能拿到乱码。6. 长期编码与 Agent 场景的接入建议如果你只是临时验证多 Server 连通性上面这套骨架够用了。但如果要把 MCP Client 长期挂在 UI 后面跑尤其是做 coding agent 这类需要反复调用工具的场景建议把模型通道换成更稳定的方案避免每次调试都手动管 Key。长期编码和 Agent 场景可以走 Coding Plan它把模型调用额度统一管理MCP Client 侧仍然只认一个入口Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewriteClaude Code 这类工具接入时Anthropic 兼容通道的配置方式可以参考ClaudeCodeAnthropichttps://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite回到多 Server 本身一个实用技巧是给每个 Server 的 tools 加一层命名空间前缀比如mcp1__query_db这样即使两个 Server 有同名工具模型侧也不会混淆find时用前缀直接定位 Server比用数组下标更抗改动。配置数组里加个prefix字段连接时拼到 tool name 上即可改动量很小但能省掉后面很多排查时间。