摘要使用TypeScript和Node.js开发MCP Server的完整教程涵盖modelcontextprotocol/sdk安装、工具定义、传输配置和调试技巧适合前端和Node开发者进入MCP生态。TypeScript快速上手用Node.js写你的第一个MCP Server我第一次写MCP Server用的是Python因为官方文档的入门示例就是Python版。后来项目组要求统一技术栈我得用TypeScript重写一遍结果发现TypeScript SDK的API设计跟Python版差别还挺大踩了不少坑。这篇文章把我用modelcontextprotocol/sdk从头搭建一个完整MCP Server的过程记录下来包含Tools、Resources、Prompts三种能力的完整实现最后跟Python版做个横向对比。环境准备我用的环境是Node.js 20 LTS建议你也用18以上版本。先确认一下版本。node--version# 我的输出是 v20.11.0npm--version# 我的输出是 10.2.4建一个新项目目录初始化npm。mkdirmcp-notes-servercdmcp-notes-servernpminit-y接下来安装三个核心依赖。第一个是MCP官方TypeScript SDK第二个是zod用来做参数校验第三个是开发依赖里的TypeScript和类型声明。# 安装运行时依赖npminstallmodelcontextprotocol/sdk zod3# 安装开发依赖npminstall-Dtypes/node typescript这里有个坑我踩过。zod一定要装3.x版本别装4.x。我当初装了最新的zod 4结果SDK内部用的还是zod 3的API运行时直接报类型不匹配。锁死zod3最稳妥。配置TypeScript项目根目录下创建tsconfig.json。我把官方推荐的配置贴出来逐行加上注释说明。{compilerOptions:{target:ES2022,// 编译目标ES2022支持top-level awaitmodule:Node16,// 模块系统用Node16匹配SDK的ESM导出moduleResolution:Node16,// 模块解析策略跟module保持一致outDir:./build,// 编译输出目录rootDir:./src,// 源码根目录strict:true,// 开启严格类型检查esModuleInterop:true,// 允许CommonJS和ESM混用skipLibCheck:true,// 跳过第三方库类型检查加快编译forceConsistentCasingInFileNames:true// 强制文件名大小写一致},include:[src/**/*],exclude:[node_modules]}然后改package.json加两行配置。{name:mcp-notes-server,version:1.0.0,type:module,// 必须声明ESM模块bin:{mcp-notes:./build/index.js},scripts:{build:tsc,start:node build/index.js}}type: module这行很关键。MCP SDK用的是ESM导出如果你的package.json没声明这行import的时候会报ERR_REQUIRE_ESM错误。我第一次跑就栽在这。理解SDK的核心对象写代码之前先理清TypeScript SDK里三个核心对象的关系。McpServer是服务器主体负责管理所有Tools、Resources、Prompts的注册和协议握手。StdioServerTransport是传输层负责通过标准输入输出收发JSON-RPC消息。zod用来定义工具参数的schemaSDK会自动把它转成JSON Schema发给客户端。TypeScript SDK跟Python SDK最大的区别在API风格。Python版用装饰器mcp.tool()自动注册TypeScript版要用server.registerTool()手动注册参数schema要自己用zod构建。灵活度更高但代码量也多一点。完整代码下面是完整的MCP Server代码。我实现了一个笔记管理服务包含4个工具、1个资源和1个prompt模板。所有笔记存在内存数组里足够演示用。新建src/index.ts文件。// src/index.ts// MCP笔记管理服务器 - 完整可运行示例// 导入MCP SDK的核心模块// McpServer 负责协议握手和能力注册import{McpServer}frommodelcontextprotocol/sdk/server/mcp.js;// StdioServerTransport 负责通过stdin/stdout收发消息import{StdioServerTransport}frommodelcontextprotocol/sdk/server/stdio.js;// zod 用于定义工具参数的类型校验schemaimport{z}fromzod;// 定义笔记的数据结构interfaceNote{id:number;// 笔记唯一IDtitle:string;// 标题content:string;// 正文内容tags:string[];// 标签列表createdAt:string;// 创建时间ISO字符串}// 内存存储实际项目可换成数据库// 这里用Map方便按ID查找constnotesStorenewMapnumber,Note();// 自增ID计数器letnextId1;// 创建服务器实例// name和version会在initialize握手时发给客户端constservernewMcpServer({name:mcp-notes-server,version:1.0.0,});// 工具注册 // 工具1 添加笔记// registerTool接收三个参数 工具名 配置对象 回调函数server.registerTool(add_note,{description:添加一条新笔记返回新笔记的ID,// inputSchema用zod定义每个参数的类型和描述// SDK会自动把zod schema转成JSON SchemainputSchema:{title:z.string().min(1).describe(笔记标题),content:z.string().min(1).describe(笔记正文),tags:z.array(z.string()).optional().describe(标签列表可选),},},async({title,content,tags}){// 构造笔记对象constnote:Note{id:nextId,title,content,tags:tags||[],createdAt:newDate().toISOString(),};// 存入内存notesStore.set(note.id,note);// 返回结果content数组里可以放多个文本块return{content:[{type:text,text:笔记创建成功ID为${note.id}\n标题${note.title}\n时间${note.createdAt},},],};});// 工具2 列出所有笔记server.registerTool(list_notes,{description:列出所有笔记的摘要信息,inputSchema:{},// 无参数时传空对象},async(){if(notesStore.size0){return{content:[{type:text,text:当前没有任何笔记}],};}// 拼接所有笔记的摘要constsummaryArray.from(notesStore.values()).map((n)[${n.id}]${n.title}| 标签${n.tags.join(, )||无}|${n.createdAt}).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}){// 遍历所有笔记在标题和正文中搜索关键词constresultsArray.from(notesStore.values()).filter((n)n.title.toLowerCase().includes(keyword.toLowerCase())||n.content.toLowerCase().includes(keyword.toLowerCase()));if(results.length0){return{content:[{type:text,text:没有找到包含「${keyword}」的笔记}],};}// 返回匹配到的笔记详情constdetailresults.map((n)---\nID${n.id}\n标题${n.title}\n内容${n.content}\n标签${n.tags.join(, )||无}).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}的笔记不存在}],// MCP支持isError字段标记工具执行失败isError:true,};}notesStore.delete(id);return{content:[{type:text,text:已删除ID${id}的笔记}],};});// 资源注册 // 资源允许客户端直接读取数据// 这里注册一个URI为 notes://all 的资源server.registerResource(notes://all,{description:所有笔记的完整JSON数据,mimeType:application/json},async(uri){// 把所有笔记转成JSON字符串返回constallNotesArray.from(notesStore.values());return{contents:[{uri:uri.href,mimeType:application/json,text:JSON.stringify(allNotes,null,2),},],};});// Prompt注册 // Prompt是预定义的模板客户端可以填参数后调用server.registerPrompt(summarize_notes,{description:生成一段提示词让LLM总结所有笔记的要点,// 定义模板参数argsSchema:{focus:z.string().optional().describe(总结的重点方向比如技术或产品),},},async({focus}){// 收集所有笔记内容constallNotesArray.from(notesStore.values());constnotesTextallNotes.map((n)标题${n.title}\n内容${n.content}).join(\n---\n);// 构造提示词文本constfocusTextfocus?请重点关注「${focus}」相关内容。:;constpromptText你是一个笔记助手。以下是用户的所有笔记请总结要点并给出建议。${focusText}\n\n${notesText};// 返回消息数组角色设为userreturn{messages:[{role:user,content:{type:text,text:promptText},},],};});// 启动服务器 asyncfunctionmain(){// 创建stdio传输层实例consttransportnewStdioServerTransport();// 连接传输层开始监听stdin上的消息awaitserver.connect(transport);// 这行日志输出到stderr不会干扰协议消息// stdout只能发JSON-RPC消息不能随便打印console.error([mcp-notes-server] 服务器已启动等待连接...);}main().catch((err){console.error(服务器启动失败,err);process.exit(1);});编译和运行代码写完了先编译TypeScript。npmrun build编译成功后会在build/目录下生成index.js。直接运行的话不会有输出因为stdio模式下服务器在等客户端发消息。正确的方式是用MCP Inspector或者配置到Claude Desktop里测试。用MCP Inspector测试的命令如下。npx modelcontextprotocol/inspectornodebuild/index.js运行后Inspector会打开一个本地Web界面你可以可视化地调用工具、查看资源、测试prompt。与Python版的对比我同一个笔记服务用Python也写过一版这里做个横向对比。对比维度TypeScript版Python版包管理npm依赖锁定在package-lock.jsonuv/pip依赖在pyproject.tomlSDK导入从mcp.js和stdio.js分别导入from mcp.server.fastmcp import FastMCP工具注册server.registerTool手动传schemamcp.tool装饰器自动从类型注解生成参数校验用zod定义schema编译时检查用Python类型注解运行时检查传输层StdioServerTransport类mcp.run(transport“stdio”)一行搞定类型安全编译期类型检查IDE提示完善运行时类型检查靠mypy补充启动速度编译后启动快几十毫秒解释执行首次启动稍慢部署体积需要node_modules可用esbuild打包需要虚拟环境可用pyinstaller打包Python版用装饰器的写法确实简洁。加一个工具只要写个函数加个装饰器就行参数类型从函数签名自动推断。TypeScript版要手动写zod schema代码量多出将近一倍。但TypeScript的优势在类型安全。zod schema同时服务于运行时校验和编译期类型推导改参数类型时IDE会立刻标出所有不匹配的地方。Python版改了函数签名调用方不一定能及时发现。我的建议是这样。如果你做的是快速原型或者数据处理类的serverPython版更顺手。如果你做的是要长期维护、多人协作的生产级serverTypeScript版的类型安全能帮你省很多调试时间。效果验证用Inspector连上服务器后我在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参数后能预览生成的提示词。常见问题与避坑第一个坑zod版本不匹配。装了zod 4.x会导致SDK内部类型报错。锁死zod3就能避免。如果你项目里已经有zod 4建议用npm overrides强制降级或者把MCP server独立成一个子项目。第二个坑忘了加type: module。package.json里没声明ESMimport SDK时直接报ERR_REQUIRE_ESM。这是最常见的新手错误报错信息还不直观容易让人以为是SDK安装有问题。第三个坑用了console.log打印日志。stdio模式下stdout是协议通道你往里面写任何非JSON-RPC的内容都会破坏消息格式客户端会报解析错误。所有日志一律用console.error写到stderr。第四个坑registerTool的参数结构。TypeScript SDK的inputSchema是一个对象key是参数名value是zod schema。这跟直接传一个完整JSON Schema对象不一样。我一开始按JSON Schema的格式传结果参数校验全乱了。记住SDK会帮你把zod转成JSON Schema你只需要按参数名列出zod定义。第五个坑Windows路径反斜杠。在Claude Desktop的配置文件里写Windows路径JSON要求双反斜杠\\或者直接用正斜杠/。我就因为路径少写了一个反斜杠Claude死活连不上server排查了半小时。小结用TypeScript写MCP Server的完整流程就是建项目、装SDK和zod、配tsconfig和package.json、用McpServer注册Tools/Resources/Prompts、接上StdioServerTransport启动。跟Python版相比TypeScript版代码量更大但类型安全更强适合长期维护的生产项目。核心要记住三点锁死zod3版本、声明ESM模块、日志只写stderr。相关推荐5分钟跑通你的第一个MCP ServerPython版TypeScript MCP SDK进阶高级特性与最佳实践Python MCP SDK入门FastMCP快速开发