拓冰建站拓冰建站
首页 / 资讯中心 / 正文

MCP TypeScript SDK v2 架构升级:从资源-工具模型到统一工具模型

1. 从 v1 到 v2一次面向未来的架构重塑如果你最近在捣鼓 AI 应用开发特别是想给 Claude Desktop 或者 Cursor 这类工具加装“外挂”那你大概率绕不开 MCPModel Context Protocol这个东西。简单说它就是个让 AI 模型能安全、可控地调用外部工具和数据的协议。而 TypeScript SDK就是咱们用 JavaScript/TypeScript 来快速搭建这些“外挂”MCP 服务器的脚手架。最近这个 SDK 悄无声息地从 v1 蹦到了 v2版本号跳得不大但里面的变化用“翻天覆地”来形容一点不过分。这可不是修修补补的更新而是一次从底层理念到上层 API 的彻底重构。我花了一周时间把新老版本代码和文档翻了个底朝天也动手把几个自己的项目做了迁移这过程中的体会和踩的坑今天就跟大伙儿好好唠唠。对于正在用或者打算用 MCP 的开发者来说理解这次升级不仅能帮你平滑迁移更能让你看清这个协议未来的发展方向。2. 核心变更一从“资源-工具”模型到统一的“工具”模型在 v1 版本里MCP SDK 的核心概念非常清晰就是两样东西Resources资源和Tools工具。资源代表静态或动态的数据比如一个文件列表、数据库查询结果工具则代表可执行的操作比如读写文件、调用 API。这种设计直观但用久了就会发现边界有些模糊而且客户端处理起来逻辑也不统一。v2 版本做了一个大胆的决定废弃了独立的Resource概念一切皆Tool。刚看到这个变化时我也愣了一下心想“那我怎么提供只读的数据呢” 但深入理解后发现这是个非常聪明的设计。2.1 新模型的设计哲学与实现在新的模型下无论是获取数据还是执行操作都通过定义Tool来完成。一个用来“获取数据”的工具它不产生副作用或者说副作用可控其输出就是数据本身。这实际上是将 v1 中的Resource抽象为了一种特殊的Tool一个只读的、用于查询的“工具”。这样做的好处显而易见概念简化客户端和服务端只需要处理一种核心抽象降低了心智负担。一致性无论是获取数据还是执行命令调用方式、错误处理、权限模型都变得完全一致。灵活性可以更容易地创建那些“混合型”端点比如一个工具它既返回数据又会在后台记录日志轻度副作用。让我们看一个具体的代码对比。假设我们要提供一个“获取系统当前时间”的功能v1 实现方式使用 Resource// server-v1.ts import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; const server new Server( { name: example-server, version: 1.0.0 }, { capabilities: { resources: {} } } ); // 定义一个时间资源 server.setRequestHandler(resources/list, async () { return { resources: [ { uri: example://current-time, mimeType: text/plain, name: Current Time, description: The current system time in ISO format, }, ], }; }); server.setRequestHandler(resources/read, async (request) { if (request.params.uri example://current-time) { return { contents: [ { uri: request.params.uri, mimeType: text/plain, text: new Date().toISOString(), }, ], }; } throw new Error(Resource not found); }); const transport new StdioServerTransport(); await server.connect(transport);在 v1 中客户端需要先list资源再read资源才能拿到时间。v2 实现方式使用 Tool// server-v2.ts import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { z } from zod; const server new Server( { name: example-server, version: 2.0.0 }, { capabilities: { tools: {} } } ); // 定义一个获取时间的工具 server.setRequestHandler(tools/call, async (request) { if (request.params.name get_current_time) { // 输入参数为空用z.object({})表示 const schema z.object({}); schema.parse(request.params.arguments); // 验证参数虽然为空 return { content: [ { type: text, text: The current system time is: ${new Date().toISOString()}, }, ], }; } throw new Error(Tool not found); }); // 在初始化时声明工具列表 server.setRequestHandler(tools/list, async () { return { tools: [ { name: get_current_time, description: Get the current system time in ISO format, inputSchema: { type: object, properties: {}, // 无输入参数 }, }, ], }; }); const transport new StdioServerTransport(); await server.connect(transport);在 v2 中客户端直接call一个名为get_current_time的工具即可。从使用角度看后者更像一个自然的函数调用。注意v2 中Tool的inputSchema必须是一个有效的 JSON Schema 对象。这里使用properties: {}表示无参数。在实际复杂场景中你可以用zod定义复杂的模式然后通过zodToJsonSchema库将其转换为 JSON Schema 填入这是社区推荐的做法能保证类型安全和清晰的文档。2.2 迁移策略与思维转换对于已有 v1 项目的迁移你需要将每一个Resource重新思考为一个Tool。问自己两个问题这个资源是纯粹静态的如一个配置文件模板还是动态生成的如数据库查询客户端是如何使用这个资源的是浏览、引用还是作为某个操作的输入大多数“只读”资源都可以直接转换为一个无参数或参数固定的Tool。例如一个file:///etc/hosts资源可以变成一个read_hosts_file工具。而一些用于导航的“目录”资源可能会变成一个list_directory工具它接受一个path参数。这个转变最大的挑战在于思维模式。在 v1 中你设计的是“数据端点”在 v2 中你设计的是“能力函数”。后者更贴近编程直觉也使得服务器提供的功能更容易被 AI 模型理解和组合调用。3. 核心变更二传输层Transport的重构与灵活性提升v1 SDK 的传输层设计相对简单直接主要提供了StdioServerTransport和SSEServerTransport分别用于标准输入输出和 Server-Sent Events。虽然能用但在错误处理、生命周期管理和自定义扩展方面显得比较笨拙。v2 版本对传输层进行了彻底的重构引入了更抽象、更强大的接口。现在Transport不再是一个具体的类而是一个需要你实现特定方法的对象。这带来了极大的灵活性。3.1 新旧传输层 API 对比v1 的传输层使用方式以 Stdio 为例是“黑盒”的import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; const transport new StdioServerTransport(); await server.connect(transport); // 之后的事情 SDK 内部处理v2 的传输层要求你显式地处理消息循环// transport-v2.ts import { Server } from modelcontextprotocol/sdk/server/index.js; import { Message } from modelcontextprotocol/sdk/message.js; const server new Server(...); // 1. 实现一个简单的 Stdio Transport const stdioTransport { async start() { // 设置 stdin 监听 process.stdin.on(data, async (chunk) { try { const message JSON.parse(chunk.toString()); // 将收到的消息交给服务器处理 const response await server.handleMessage(message); if (response) { // 将服务器的响应写入 stdout process.stdout.write(JSON.stringify(response) \n); } } catch (error) { // 错误处理将错误信息以 MCP 错误格式输出 process.stderr.write(JSON.stringify({ jsonrpc: 2.0, id: message?.id || null, error: { code: -32603, message: error.message } }) \n); } }); }, async close() { // 清理工作 process.stdin.removeAllListeners(data); } }; // 2. 启动服务器并连接传输层 await server.connect(stdioTransport); await stdioTransport.start();可以看到v2 的传输层实现让你完全掌控了数据的读取、解析、传递和写入过程。SDK 核心只关心消息Message的处理逻辑而如何获取和发送消息则由你实现的Transport对象决定。3.2 新传输层带来的可能性与实战案例这种设计解锁了哪些新场景呢自定义协议适配你可以轻松编写一个 Transport让 MCP 服务器通过 WebSocket、MQTT 甚至自定义的 TCP 协议进行通信。不再局限于 Stdio 和 SSE。增强的错误恢复与日志你可以在start方法里加入更精细的重试逻辑、心跳检测或者将所有流入流出的消息都记录到日志文件便于调试。多路复用与负载均衡理论上你可以创建一个 Transport它背后管理着多个物理连接实现请求的负载均衡。这里分享一个我实际项目中的案例我需要一个 MCP 服务器既能通过 Claude Desktop 的 Stdio 调用也能通过一个简单的 HTTP API 被其他系统调用。在 v1 时代这几乎需要写两套代码。而在 v2 中我实现了两个 Transport// multi-transport-server.ts import { Server } from modelcontextprotocol/sdk/server/index.js; import express from express; const server new Server(...); // 服务器逻辑定义 // Transport 1: 传统的 Stdio Transport (简化版) const stdioTransport { async start() { /* ... 同上 ... */ }, async close() { /* ... */ } }; // Transport 2: HTTP POST Transport const app express(); app.use(express.json()); const httpTransport { async start() { app.post(/mcp, async (req, res) { try { const response await server.handleMessage(req.body); res.json(response || { jsonrpc: 2.0, result: null }); } catch (error) { res.status(500).json({ jsonrpc: 2.0, id: req.body.id, error: { code: -32603, message: error.message } }); } }); app.listen(3000, () console.log(HTTP Transport listening on port 3000)); }, async close() { /* 关闭 HTTP 服务器 */ } }; // 主函数同时启动两种传输 async function main() { await server.connect(stdioTransport); await server.connect(httpTransport); // 注意一个 Server 可以连接多个 Transport await stdioTransport.start(); await httpTransport.start(); } main();这个例子清晰地展示了 v2 传输层的威力一个 MCP 服务器实例可以同时连接多个传输层。这意味着你的服务器能力可以同时暴露给不同的客户端协议代码复用率极高。踩坑提醒实现自定义 Transport 时务必严格按照 JSON-RPC 2.0 规范处理消息。特别是消息的id字段必须在响应中原样返回这是实现请求-响应匹配的关键。另外错误处理必须规范返回的 error 对象需要包含code和message字段否则客户端可能无法正确解析。4. 核心变更三消息Message与请求处理Handler的精细化在 v1 中请求处理器的设置相对松散类型安全主要靠开发者自己保证。v2 版本在这方面下了大功夫通过更严格的类型系统和更清晰的 API让编写服务器变得更加可靠。4.1 强类型化的请求处理器v2 为每一个 MCP 协议定义的方法如tools/list,tools/call,notifications/initialized等都提供了专属的请求处理器类型。这在与 TypeScript 配合时能提供完美的类型提示和校验。例如设置一个tools/call的处理器// v2 强类型处理器 server.setRequestHandler(tools/call, async (request) { // 此时request 的类型被自动推断为 CallToolRequest // request.params.name 是 string // request.params.arguments 是 unknown需要你手动校验 const { name, arguments: args } request.params; if (name calculate) { // 使用 zod 进行运行时验证并与 TypeScript 类型同步 const schema z.object({ a: z.number(), b: z.number(), operator: z.enum([, -, *, /]) }); const { a, b, operator } schema.parse(args); // 如果 args 不符合 schema这里会抛出清晰的错误 let result: number; switch (operator) { case : result a b; break; case -: result a - b; break; case *: result a * b; break; case /: result a / b; break; } // 返回值的类型也被严格约束 return { content: [{ type: text, text: Result: ${result} }] }; } throw new Error(Unknown tool: ${name}); });对比 v1你不再需要手动去查找协议文档来确认request对象的结构。IDE 的自动补全和类型检查会在你写代码时就帮你避免许多低级错误比如拼写错误request.param应该是request.params或者访问不存在的属性。4.2 通知Notification处理的显式化MCP 协议包含一些通知Notification比如notifications/initialized客户端初始化完成和notifications/tools/list_changed工具列表变更。在 v1 中对这些通知的处理比较隐晦。v2 版本要求你显式地设置通知处理器这使得服务器的生命周期管理更加清晰。// 处理客户端初始化完成的通知 server.setNotificationHandler(notifications/initialized, async (notification) { console.log(Client ${notification.params.clientInfo?.name} initialized.); // 可以在这里执行一些依赖客户端就绪的逻辑比如推送初始状态 }); // 处理客户端发出的工具列表变更请求这是一个通知服务器无需回复 server.setNotificationHandler(notifications/tools/list_changed, async () { console.log(Client requested a refresh of the tools list.); // 通常服务器不需要做任何事情客户端收到这个通知后会主动重新调用 tools/list });这种显式处理的好处是你能精确地知道服务器在哪个阶段该做什么。例如你可以在initialized通知到达后才允许执行某些需要客户端上下文的高权限工具调用从而实现了简单的“握手”和状态同步机制。4.3 错误处理与消息传递的规范化v2 SDK 内部对错误处理也做了加强。现在在请求处理器中抛出的错误会被 SDK 自动捕获并封装成符合 JSON-RPC 2.0 规范的错误响应。你还可以通过Server的sendError等方法更主动地向客户端发送错误信息。消息的序列化与反序列化也完全由 SDK 接管开发者几乎不需要直接操作 JSON 字符串降低了出错概率。这一切都使得构建一个健壮的、生产就绪的 MCP 服务器变得更加容易。5. 依赖、构建与工具链的现代化升级这次升级不仅仅是 API 的变化也伴随着整个项目基础设施和开发者体验的现代化。5.1 包管理与模块系统v1 SDK 发布时ES 模块ESM和 CommonJSCJS的战争还未结束所以它可能同时支持两种模块系统或者需要通过复杂的构建配置来适配。v2 SDK 则旗帜鲜明地拥抱了现代 JavaScript 生态。首先查看package.json的差异// v1 package.json (可能的部分) { main: ./dist/index.cjs, module: ./dist/index.js, types: ./dist/index.d.ts, exports: { .: { import: ./dist/index.js, require: ./dist/index.cjs } } }// v2 package.json (典型配置) { type: module, // 明确声明为 ESM 包 main: ./dist/index.js, types: ./dist/index.d.ts, exports: { .: { import: ./dist/index.js, types: ./dist/index.d.ts } } }v2 默认使用 ESM。这意味着在你的服务器代码中必须使用import/export语法。如果项目是 CommonJS你需要进行转换或者使用动态import()。这更符合 Node.js 和现代浏览器的未来方向能更好地支持 tree-shaking 等优化。其次依赖更新v2 很可能升级了其内部依赖例如用于 JSON Schema 验证的库、用于类型定义的库等。在迁移时你需要确保你的项目也能兼容这些新版本。一个常见的坑是v2 可能使用了zod的 v4而你的项目还在用 v3这会导致类型不匹配。务必仔细检查package.json中的依赖冲突。5.2 类型定义TypeScript的增强v2 的 TypeScript 类型定义更加完善和精确。除了前面提到的请求处理器类型所有核心接口如ToolDefinition,CallToolResult,Content等都有了更详细的注释和更严格的约束。例如Content类型现在是一个可辨识联合Discriminated Union明确区分text和image等内容类型// v2 中更精确的 Content 类型 type Content | { type: text; text: string; mimeType?: string } | { type: image; data: string; mimeType: string } // ... 可能还有其他类型这迫使你在返回内容时做出明确选择避免了 v1 中可能出现的模糊不清的content结构。5.3 开发与调试体验优化虽然 SDK 本身不提供 CLI 工具但 v2 的架构变化使得结合现代开发工具链更加顺畅。热重载HMR由于传输层解耦你可以更容易地集成像nodemon或ts-node-dev这样的工具。当服务器代码变更时只需重启你的 Transport 逻辑而不必重启整个进程对于 Stdio Transport这仍然需要重启但 HTTP/WebSocket Transport 可以实现更优雅的热更新。测试现在你可以直接对Server实例进行单元测试通过模拟Mock一个Transport对象来发送和接收消息而无需启动真正的进程。这大大提升了测试的便利性和可靠性。调试你可以轻松地在自定义 Transport 的start方法或消息处理流程中加入调试日志精确追踪每一条消息的来龙去脉。6. 迁移指南与实战避坑清单理论说了这么多最后落到实际操作上。如果你手头有一个 v1 的 MCP 服务器项目想要升级到 v2可以遵循以下步骤。我结合自己迁移三个项目的经验总结了一份避坑清单。6.1 逐步迁移路线图第一步依赖与环境准备将package.json中的 SDK 依赖更新为 v2 最新版本npm install modelcontextprotocol/sdklatest。检查并更新相关依赖特别是zod如果用于参数验证和 TypeScript 版本建议 5.0。将你的tsconfig.json中的module设置为NodeNext或ESNext以支持 ESM。第二步重构服务器初始化修改import语句。v2 的导出路径可能略有变化例如import { Server } from modelcontextprotocol/sdk/server/index.js;。创建新的Server实例。注意构造函数参数可能微调请参照最新文档。关键动作删除所有server.setRequestHandler中关于resources/的处理器list,read,subscribe等。第三步将 Resource 转换为 Tool为每一个 v1 的Resource设计一个对应的Tool。思考工具名、描述、输入参数JSON Schema和输出内容。实现tools/list处理器返回所有工具的完整定义列表。实现tools/call处理器根据工具名和参数执行相应逻辑并返回格式正确的CallToolResult。注意输出格式v2 的content字段是一个数组每个元素必须有type属性如text。第四步重写传输层逻辑如果你之前直接使用StdioServerTransport现在需要按照前文示例实现一个兼容的 Transport 对象。如果你有自定义通信方式如 WebSocket这是一个重构和强化的好机会。确保在server.connect(transport)之后调用transport.start()。第五步处理通知与生命周期根据需要添加notifications/initialized等通知的处理器。审查并更新任何与客户端状态同步相关的逻辑。第六步全面测试使用 Claude Desktop 或mcp-cli等客户端工具进行端到端测试。编写或更新单元测试针对每个 Tool 的输入输出进行验证。特别测试错误路径无效参数、不存在的工具、内部异常等。6.2 实战避坑清单content字段格式错误这是迁移中最常见的错误。v2 要求content是数组且每个元素必须有type。忘记加type或者直接返回一个字符串都会导致客户端解析失败。始终使用{ content: [{ type: text, text: ... }] }的格式。JSON Schema 定义错误Tool的inputSchema必须是一个有效的 JSON Schema 对象。如果你用zod生成确保使用了正确的转换库如zod-to-json-schema并导入了正确的版本。一个格式错误的 schema 会导致客户端无法正确显示工具的参数表单。传输层未正确启动调用server.connect(transport)只是建立了关联必须再调用transport.start()来开始监听消息。很多开发者迁移后卡在“服务器没反应”就是因为漏了这一步。ESM 模块导入问题如果你的项目之前是 CommonJS迁移到 ESM 后__dirname,require等将不可用。需要使用import.meta.url和createRequire来替代。同时在package.json中设置type: module。类型定义未更新升级 SDK 后旧的types/...包可能会冲突。建议先删除node_modules和package-lock.json重新安装。如果使用 TypeScript确保tsc --build --clean或重启 IDE 的语言服务器以获取最新的类型提示。客户端兼容性确保你使用的客户端如特定版本的 Claude Desktop支持 MCP 协议的最新特性。有时服务器升级了但客户端还未跟进可能导致部分功能失效。在升级前查阅客户端的更新日志或社区讨论。从 v1 到 v2 的升级是一次向着更灵活、更健壮、更面向未来架构的迈进。它要求开发者付出一些学习和迁移的成本但换来的却是更清晰的抽象、更强大的扩展能力和更愉悦的开发体验。对于新项目毫无疑问应该直接从 v2 开始。对于老项目如果你的服务器逻辑复杂建议规划一个迭代迁移的过程可以先实现一个 v2 的“适配层”逐步将功能迁移过来而不是一次性重写。无论如何理解这次升级背后的设计思想对于掌握 MCP 协议和构建更优秀的 AI 集成应用都是至关重要的一步。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门