
1. 从零跑通 MCP TypeScript SDK为什么第一个服务端总卡在“连不上”Model Context Protocol简称 MCP是一套开放协议用来规范大语言模型和外部工具、数据源之间的交互方式。你可以把它理解成“AI 世界的 USB-C 接口”不管背后接的是数据库、文件系统还是某个 HTTP API只要按 MCP 的格式暴露出来任何支持 MCP 的客户端都能发现并调用它。而modelcontextprotocol/sdk就是官方提供的 TypeScript 实现让你在 Node.js 里用几十行代码就能写出一个 MCP 服务端。这篇文章面向的是刚接触 MCP、想用 TypeScript SDK 从零搭出第一个服务端的人。我会带你走完初始化项目、定义工具与资源、本地调试的完整路径并且把模型调用的 endpoint 统一改到 TaoToken 的 API 通道上用一把 Key 管理所有模型请求。最后附一次真实的工具调用返回结果帮你确认整条链路是通的。很多人第一次写 MCP 服务端代码照着文档敲完了node server.js也能跑起来但一到客户端调用就报错要么是local proxy failed要么是reading choices之类的字段读取失败。问题往往不在 MCP 协议本身而在于模型请求的出口没有配对——SDK 负责协议层模型调用负责推理层这两层要分别配置。下面我按“先搭骨架、再接通道、再验证”的顺序拆开讲每一步都给可复制的片段。2. TaoToken 前置准备统一 Key 与 API 通道怎么配在写 MCP 服务端之前先把模型调用的出口准备好。TaoToken 提供的是统一的 API 通道你只需要一个 Key就能在 MCP 服务端里调用不同模型不用为每个模型单独维护一套鉴权和 endpoint。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 这个地址不加 UTM 参数直接用于代码里的 Base URL。具体操作分三步。第一步登录后进入控制台在 API Keys 页面创建一个新 Key复制出来先存到本地环境变量里别硬编码进代码。第二步确认你要用的模型 ID比如常见的对话模型或代码模型记下准确的名称后面配置里要用。第三步把 Base URL 和 Key 写进项目的.env文件MCP 服务端启动时读取。这里有个容易踩的坑MCP 服务端本身通过 stdio 和客户端通信它不直接“联网”真正发起模型请求的是你在工具函数里调用的那个 HTTP 客户端。所以 endpoint 要配在工具的执行逻辑里而不是配在 MCP 的 transport 上。我见过有人把 Base URL 填到StdioServerTransport的参数里那当然不生效。环境变量建议这样组织方便本地调试和后续部署切换# .env TAOTOKEN_API_KEYsk-你的Key TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_MODEL你的模型ID然后在代码里用process.env.TAOTOKEN_API_KEY读取。如果你用的是 Node 20 以上可以直接用--env-file.env启动省得装 dotenv。这样一套配置下来MCP 服务端里所有需要模型推理的工具都走同一个通道Key 也只有一份管理起来清爽很多。3. 可复制配置package.json 与 server.ts 完整片段这一节是核心给你能直接抄的配置。先看package.json重点是type: module和依赖版本。MCP SDK 目前对 ESM 支持较好用 CommonJS 容易在导入路径上出问题所以建议直接上 ESM。{ name: mcp-demo-server, version: 1.0.0, type: module, scripts: { build: tsc, start: node --env-file.env dist/server.js, dev: tsx --env-file.env src/server.ts }, dependencies: { modelcontextprotocol/sdk: ^1.0.0, zod: ^3.23.8 }, devDependencies: { typescript: ^5.5.0, tsx: ^4.16.0, types/node: ^20.14.0 } }tsconfig.json保持最小配置即可target设成ES2022module设成NodeNextmoduleResolution也设NodeNext这样 SDK 的子路径导入比如modelcontextprotocol/sdk/server/mcp.js才能正确解析。接下来是src/server.ts。我把它拆成三块初始化服务器、注册工具和资源、启动 stdio 传输。工具里我放了一个真正会调用 TaoToken 通道的例子这样你能看到 endpoint 是怎么接进去的。import { McpServer } from modelcontextprotocol/sdk/server/mcp.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { z } from zod; const server new McpServer({ name: taotoken-demo-server, version: 1.0.0, }); // 资源暴露一份静态配置客户端可读取 server.resource( server-info, config://server-info, async (uri) ({ contents: [ { uri: uri.href, text: JSON.stringify({ baseUrl: process.env.TAOTOKEN_BASE_URL, model: process.env.TAOTOKEN_MODEL, }), }, ], }) ); // 工具调用 TaoToken 通道做一次对话补全 server.tool( ask_model, 向模型提问并返回回答, { prompt: z.string().describe(要发送给模型的问题), }, async ({ prompt }) { const res await fetch(${process.env.TAOTOKEN_BASE_URL}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${process.env.TAOTOKEN_API_KEY}, }, body: JSON.stringify({ model: process.env.TAOTOKEN_MODEL, messages: [{ role: user, content: prompt }], }), }); if (!res.ok) { const errText await res.text(); throw new Error(模型请求失败: ${res.status} ${errText}); } const data await res.json(); const answer data.choices?.[0]?.message?.content ?? 无返回内容; return { content: [{ type: text, text: answer }], }; } ); const transport new StdioServerTransport(); await server.connect(transport); console.error(MCP server 已启动等待客户端连接);注意几个细节。第一日志用console.error而不是console.log因为 stdio 传输下 stdout 被协议占用往 stdout 打日志会污染消息流客户端解析就会失败。第二工具返回结构必须是{ content: [...] }这是 MCP 规定的格式少一层都会让客户端读不到。第三fetch是 Node 18 内置的不用额外装 axios。如果你用的是 Claude Code 这类客户端配置里要写全三件套Base URL、Key、Model ID。以settings.json为例MCP 服务端注册片段大概是这样{ mcpServers: { taotoken-demo: { command: node, args: [--env-file.env, dist/server.js], env: { TAOTOKEN_API_KEY: sk-你的Key, TAOTOKEN_BASE_URL: https://taotoken.net/api, TAOTOKEN_MODEL: 你的模型ID } } } }这样客户端启动时会拉起你的服务端进程环境变量也一并注入工具调用时就能拿到正确的通道配置。4. 验证请求一次工具调用返回结果的完整动作配置写完了怎么确认链路真的通最直接的办法是写一个最小客户端主动调用ask_model工具看返回内容。下面这段src/client.ts用 SDK 的客户端连到刚才的服务端发一次调用。import { Client } from modelcontextprotocol/sdk/client/index.js; import { StdioClientTransport } from modelcontextprotocol/sdk/client/stdio.js; const transport new StdioClientTransport({ command: node, args: [--env-file.env, dist/server.js], }); const client new Client({ name: demo-client, version: 1.0.0 }); await client.connect(transport); const tools await client.listTools(); console.log(可用工具:, tools.tools.map((t) t.name)); const result await client.callTool({ name: ask_model, arguments: { prompt: 用一句话解释什么是 MCP }, }); console.log(工具返回:, JSON.stringify(result, null, 2)); await client.close();先npm run build编译再node --env-file.env src/client.ts用 tsx 直接跑也行。如果一切正常你会看到类似这样的输出{ content: [ { type: text, text: MCP 是一套让大模型与外部工具、数据源按统一格式交互的开放协议。 } ] }看到content数组里有text字段就说明三件事都成了MCP 协议层握手成功、工具被正确注册和发现、TaoToken 通道的模型请求返回了有效结果。如果这一步卡住先别急着改代码按下一节的报错对照表逐条排查。5. 本篇常见错排查401、local proxy failed 与 reading choices实际调试时报错信息往往比代码本身更值得读。我把几个高频错误和对应原因列出来你对着改就行。401 Unauthorized模型请求返回 401基本是 Key 没读到或者格式不对。检查.env里TAOTOKEN_API_KEY是否带了Bearer前缀重复拼接代码里已经写了Bearer ${key}环境变量里就不要再带。另外确认启动命令带了--env-file.env否则process.env里是空的请求头会变成Bearer undefined。local proxy failed / ECONNREFUSED这类错误通常出现在客户端拉起服务端进程时。检查args里的路径是不是相对当前工作目录dist/server.js是否真的编译出来了。如果服务端启动就崩客户端会报连接失败。可以先把服务端单独跑一遍node --env-file.env dist/server.js看有没有抛异常。Cannot read properties of undefined (reading choices)这个报错说明data.choices是 undefined也就是返回体结构和你预期的不一样。常见原因是 Base URL 拼错了比如写成了https://taotoken.net/api/v1/chat/completions而实际路径重复了/v1或者模型 ID 填错导致服务端返回了错误对象。打印一下res.status和原始文本就能定位。OAuth / 鉴权相关报错如果你在客户端配置里同时写了 OAuth 和 API Key可能互相干扰。MCP 服务端走 stdio 时不需要 OAuth把客户端里多余的鉴权字段去掉只保留env里的 Key 即可。工具调用返回空 content检查工具函数有没有return以及返回结构是不是{ content: [{ type: text, text: ... }] }。少写return或者返回裸字符串客户端都会读不到。排查顺序建议从外到内先确认服务端能独立启动再确认客户端能连上最后确认模型请求返回 200。每层都打印关键变量比盲改代码快得多。6. 把 MCP 服务端接进日常开发流跑通第一个服务端之后你可以把它当成模板往里加更多工具。比如加一个读本地文件的工具、一个查数据库的工具或者一个调用内部 API 的工具。每个工具都是独立的server.tool(...)注册块输入用 zod 定义 schema执行逻辑里该走 TaoToken 通道的走通道该走本地逻辑的走本地逻辑。如果你打算长期做编码类或 Agent 类项目可以考虑用 Coding Plan 来管理模型调用额度把 Key 和通道统一在一处省得每个项目单独配。需要看模型实际对话效果时模型对话页面可以直接试要管理 Key 就去 API Keys 页面接入细节查接入文档。这几个入口配合起来从调试到上线基本够用。我自己的习惯是新工具先在本地用客户端脚本调一次确认返回结构对了再注册到正式配置里。这样每次只验证一个变量出问题也好定位。MCP 的生态还在快速演进SDK 的 API 偶尔会有调整遇到导入路径报错时先去看一眼node_modules/modelcontextprotocol/sdk里的实际目录结构比对着旧文档猜要靠谱。