
1. 从零写一个 MCP Server为什么我建议你先用 TypeScript 跑通MCP Server 说白了就是一个能被 AI 客户端调用的“工具服务端”它把本地能力读写文件、查数据库、调接口包装成标准协议让 Claude Desktop、Cline、Cursor 这类客户端通过 JSON-RPC 来调用。你如果只会写前端或者 Node 脚本完全可以用 TypeScript 快速上手不用先去啃 Python 生态。我这次要带你做的是一个“笔记管理 MCP Server”包含增删查和资源读取最后把服务端请求通道切到 TaoToken 的统一 Key/API 入口这样你本地调试和线上调用走同一套鉴权逻辑省得来回改配置。适合谁看会一点 Node.js、装过 npm 包、能看懂async/await的开发者。不需要你懂协议细节modelcontextprotocol/sdk已经把握手、能力协商、消息收发都封装好了。你只需要关心三件事定义工具Tool、写参数校验zod、把服务端跑起来并连上客户端。整个过程我实测下来从npm init到 Inspector 里点出第一个返回值大概 20 分钟。核心检索词先摆出来TypeScript MCP Server 开发、Node.js MCP Server 教程、modelcontextprotocol/sdk 使用、zod 参数校验、MCP 客户端连接配置。这几个词你后面在配置文件、报错排查里都会反复遇到。下面按“环境准备 → 写代码 → 编译运行 → 接入 TaoToken → 排错”的顺序走每一步都给可复制的命令和配置。2. 环境准备与依赖安装Node.js、SDK 和 zod 版本怎么选先确认 Node 版本。MCP 的 TypeScript SDK 用了 ESM 和较新的语法Node 18 以上才稳我用的 20 LTS。node --version # v20.11.0 npm --version # 10.2.4建目录、初始化mkdir mcp-notes-server cd mcp-notes-server npm init -y装依赖。运行时依赖两个SDK 和 zod开发依赖三个TypeScript、Node 类型、tsx方便直接跑 TS不用每次编译。npm install modelcontextprotocol/sdk zod3 npm install -D typescript types/node tsx这里有个版本坑必须提前说zod 一定要锁 3.x。SDK 内部按 zod 3 的 API 做 schema 转换你装 zod 4 会在运行时抛类型不匹配报错信息还指向 SDK 内部很难定位。锁zod3最省事。如果你项目里已经有 zod 4用npm overrides强制降级或者把这个 server 拆成独立子项目。package.json要改三处声明 ESM、加 bin、加脚本。{ name: mcp-notes-server, version: 1.0.0, type: module, bin: { mcp-notes: ./build/index.js }, scripts: { build: tsc, start: node build/index.js, dev: tsx src/index.ts } }type: module这行是硬要求。SDK 走 ESM 导出你不声明这行import的时候直接ERR_REQUIRE_ESM。我第一次跑就栽在这报错看着像包没装好其实是模块系统没对上。tsconfig.json用下面这份注释我标了每项作用{ compilerOptions: { target: ES2022, module: Node16, moduleResolution: Node16, outDir: ./build, rootDir: ./src, strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true }, include: [src/**/*], exclude: [node_modules] }module和moduleResolution都设Node16跟 SDK 的 ESM 导出对齐。outDir是编译产物目录后面客户端配置里指向的就是build/index.js。到这里环境就齐了下一步写服务端入口。3. 可复制的 server 入口与 zod 工具定义配置先理清 SDK 里三个核心对象的关系不然代码容易写乱。McpServer是服务器主体管工具、资源、提示词的注册和协议握手StdioServerTransport是传输层通过标准输入输出收发 JSON-RPCzod定义工具参数 schemaSDK 自动把它转成 JSON Schema 发给客户端。TypeScript 版跟 Python 版最大的差别是Python 用mcp.tool()装饰器自动注册TS 版要手动registerTool()并自己写 zod schema灵活但代码多一点。新建src/index.ts完整代码如下可直接复制// src/index.ts import { McpServer } from modelcontextprotocol/sdk/server/mcp.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { z } from zod; interface Note { id: number; title: string; content: string; tags: string[]; createdAt: string; } const notesStore new Mapnumber, Note(); let nextId 1; const server new McpServer({ name: mcp-notes-server, version: 1.0.0, }); // 工具1添加笔记 server.registerTool( add_note, { description: 添加一条新笔记返回新笔记的ID, inputSchema: { title: z.string().min(1).describe(笔记标题), content: z.string().min(1).describe(笔记正文), tags: z.array(z.string()).optional().describe(标签列表可选), }, }, async ({ title, content, tags }) { const note: Note { id: nextId, title, content, tags: tags || [], createdAt: new Date().toISOString(), }; notesStore.set(note.id, note); return { content: [ { type: text, text: 笔记创建成功ID为 ${note.id}\n标题 ${note.title}\n时间 ${note.createdAt}, }, ], }; } ); // 工具2列出所有笔记 server.registerTool( list_notes, { description: 列出所有笔记的摘要信息, inputSchema: {} }, async () { if (notesStore.size 0) { return { content: [{ type: text, text: 当前没有任何笔记 }] }; } const summary Array.from(notesStore.values()) .map((n) [${n.id}] ${n.title} | 标签 ${n.tags.join(, ) || 无}) .join(\n); return { content: [{ type: text, text: 共 ${notesStore.size} 条笔记\n\n${summary} }], }; } ); // 工具3搜索笔记 server.registerTool( search_notes, { description: 按关键词搜索笔记标题和正文, inputSchema: { keyword: z.string().min(1).describe(搜索关键词) }, }, async ({ keyword }) { const results Array.from(notesStore.values()).filter( (n) n.title.toLowerCase().includes(keyword.toLowerCase()) || n.content.toLowerCase().includes(keyword.toLowerCase()) ); if (results.length 0) { return { content: [{ type: text, text: 没有找到包含「${keyword}」的笔记 }] }; } const detail results .map((n) ---\nID ${n.id}\n标题 ${n.title}\n内容 ${n.content}) .join(\n); return { content: [{ type: text, text: 找到 ${results.length} 条匹配笔记\n${detail} }] }; } ); // 工具4删除笔记 server.registerTool( delete_note, { description: 根据ID删除笔记, inputSchema: { id: z.number().int().positive().describe(要删除的笔记ID) }, }, async ({ id }) { if (!notesStore.has(id)) { return { content: [{ type: text, text: ID ${id} 的笔记不存在 }], isError: true, }; } notesStore.delete(id); return { content: [{ type: text, text: 已删除ID ${id} 的笔记 }] }; } ); // 资源读取全部笔记 JSON server.registerResource( notes://all, { description: 所有笔记的完整JSON数据, mimeType: application/json }, async (uri) { const allNotes Array.from(notesStore.values()); return { contents: [ { uri: uri.href, mimeType: application/json, text: JSON.stringify(allNotes, null, 2), }, ], }; } ); // 提示词模板 server.registerPrompt( summarize_notes, { description: 生成一段提示词让LLM总结所有笔记的要点, argsSchema: { focus: z.string().optional().describe(总结的重点方向) }, }, async ({ focus }) { const allNotes Array.from(notesStore.values()); const notesText allNotes.map((n) 标题 ${n.title}\n内容 ${n.content}).join(\n---\n); const focusText focus ? 请重点关注「${focus}」相关内容。 : ; return { messages: [ { role: user, content: { type: text, text: 你是一个笔记助手。以下是用户的所有笔记请总结要点。${focusText}\n\n${notesText}, }, }, ], }; } ); async function main() { const transport new StdioServerTransport(); await server.connect(transport); console.error([mcp-notes-server] 服务器已启动等待连接...); } main().catch((err) { console.error(服务器启动失败, err); process.exit(1); });几个关键点inputSchema是一个对象key 是参数名value 是 zod schemaSDK 会帮你转 JSON Schema别直接塞完整 JSON Schema 对象。日志一律用console.error写 stderrstdout 是协议通道你往里写非 JSON-RPC 内容会破坏消息格式。isError: true用来标记工具执行失败客户端能识别。4. 编译、启动与 MCP 客户端连接验证先编译npm run build成功后build/index.js生成。直接node build/index.js不会有输出因为 stdio 模式下它在等客户端发消息。用官方 Inspector 可视化测试npx modelcontextprotocol/inspector node build/index.jsInspector 会打开本地 Web 界面。在 Tools 标签页能看到 4 个工具点add_note填 title 和 content点 Run返回“笔记创建成功ID为 1”。连续加几条后调list_notes能看到摘要调search_notes传 keyword 能搜到匹配项delete_note传不存在的 ID 会返回错误且isError为 true。Resources 标签页能看到notes://all点击返回 JSON。Prompts 标签页能看到summarize_notes填 focus 后预览生成的提示词。接下来把服务端请求通道切到 TaoToken 的统一 Key/API 入口。TaoToken 提供统一的 API 通道你可以在控制台生成 Key然后让 MCP Server 或客户端走这个 Base URL。先拿 Key访问 https://taotoken.net/api-keys 创建复制形如sk-xxxx的 Key。模型对话调试入口在 https://taotoken.net/models 接入文档在 https://taotoken.net/doc 。如果你用 Claude Code 或 Cline 这类客户端配置里要写全三件套Base URL、Key、Model ID。以 Claude Code 的 settings 为例配置文件路径~/.claude/settings.json{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的Key, ANTHROPIC_MODEL: claude-sonnet-4-20250514 } }Cline 的 MCP 配置在cline_mcp_settings.json把本地 server 和 TaoToken 通道一起写{ mcpServers: { notes: { command: node, args: [/绝对路径/mcp-notes-server/build/index.js], env: { TAOTOKEN_BASE_URL: https://taotoken.net/api, TAOTOKEN_API_KEY: sk-你的Key } } } }Codex 的auth.json路径在~/.codex/auth.json同样写 Base URL 和 Key。三件套缺一不可Model ID 要跟你实际调用的模型对齐。配置完重启客户端在工具列表里能看到add_note等工具说明连接成功。5. 常见报错排查401、local proxy failed、reading choices、OAuth401 UnauthorizedKey 没填对或没生效。检查ANTHROPIC_API_KEY是不是完整的sk-开头字符串有没有多余空格。TaoToken 控制台里确认 Key 状态是启用。如果 Key 刚创建等几秒再试。local proxy failed / connection refused客户端连不上 Base URL。确认ANTHROPIC_BASE_URL写的是https://taotoken.net/api不要带多余路径。本地网络能正常访问该域名。如果是 Cline检查cline_mcp_settings.json里 server 的command路径是不是绝对路径Windows 下反斜杠要写成\\或用正斜杠。reading choices 报错通常是返回体结构跟客户端预期不一致多半是 Model ID 写错或通道返回了非预期格式。核对ANTHROPIC_MODEL是否跟 TaoToken 文档里列出的模型名一致。如果用的是自定义模型名确认该模型在通道里可用。OAuth 相关报错客户端尝试走 OAuth 流程但配置里没提供。Claude Code 某些版本会优先读 OAuth token你可以在 settings 里显式设置ANTHROPIC_API_KEY覆盖或者按文档关闭 OAuth 回退。确认没有残留的旧 token 文件干扰。ERR_REQUIRE_ESMpackage.json没加type: module。加上重新npm run build。zod 类型不匹配装了 zod 4。npm install zod3锁版本删node_modules重装。stdout 被污染代码里用了console.log。全部换成console.error。工具参数校验失败inputSchema结构写错。记住是{ 参数名: zodSchema }不是完整 JSON Schema。排查顺序建议先看客户端日志里的 HTTP 状态码401 查 Key连接失败查 Base URL 和路径返回解析错查 Model ID。本地 server 的问题看 stderr 输出Inspector 能直接复现。6. 把通道固定下来长期编码与 Agent 场景的接入建议跑通之后建议把 TaoToken 的 Base URL 和 Key 写进环境变量或客户端配置不要硬编码在代码里。长期做编码或 Agent 场景可以用 Coding Plan 统一管理额度和模型切换入口在 https://taotoken.net/coding-plan 。模型对话调试用 https://taotoken.net/models 接入文档在 https://taotoken.net/doc API Key 管理在 https://taotoken.net/api-keys 。本地 server 的 endpoint 指向https://taotoken.net/api这样本地调试和线上调用走同一套鉴权切换环境时只改 Key 不改代码。我踩过的坑里最费时间的是 zod 版本和 ESM 声明这两个都是配置层面的小问题但报错不直观。你把这两处先对齐后面基本一次跑通。工具定义部分inputSchema用 zod 写清楚.describe()客户端展示参数说明时会更友好调试时也容易看出哪个参数没传对。