资讯详情

MCP TypeScript SDK选型:先搞清协议边界再动手

📅 2026/9/10 7:25:56 | 华诺云谱 👁 阅读
MCP TypeScript SDK选型:先搞清协议边界再动手
最近连续接了好几个 MCP 相关的项目从给 Cursor 挂数据库工具到把 Figma 设计稿接进 AI 生成流程大家几乎都会问同一个问题MCP 的 TypeScript SDK 到底应该选哪个帮人选型前我都会先反问一句——你先把 MCP 协议边界搞清楚了吗其实很多示例能跑通原因不是代码有多好而是刚好绕开了边界问题。这篇文章我把边界和选型放在一起讲适合刚接触 MCP、准备用 TypeScript 写 Server 或 Client 的开发者也适合那些已经在跑示例但一改就挂的人。1. 为什么“先看协议边界”比“先跑示例”更重要1.1 示例跑通的假象我不止一次见到这种情况照着仓库里的 README 把 MCP server 跑起来了在桌面客户端里也能看到工具列表但只要一改动输入参数结构或者从调用工具变成订阅资源整条链路立刻断掉。原因很直接你看的示例通常只覆盖了最顺滑的那条路径而 MCP 真正的复杂度都藏在协议边界里。MCP 的全称是 Model Context Protocol说白了就是让 AI 应用和外部工具、数据源对话的一套标准协议。它定义了 Client 和 Server 两个角色也定义了消息格式、传输方式和能力清单。TypeScript SDK 的价值在于把这些协议概念映射成了类型和类但如果不懂边界你写出来的代码表面在调用 SDK实际在绕过协议这种代码最不稳定。边界意识是排障的第一层直觉。你遇到“工具调不到”“请求超时”“内容被截断”这类问题时第一反应不应该是改代码而是先问我是不是越过了哪条边界1.2 MCP 协议边界究竟有哪几层以我自己的理解MCP 协议里的边界至少可以拆成四层。第一层是角色边界。Client 负责发起请求Server 负责响应请求角色不能互换。你在写 TypeScript SDK 代码时如果用错构造函数或者把 Client 的 API 用在 Server 上编译可能不报错但协议层会直接拒绝。第二层是传输边界。MCP 目前常见的传输方式有两种基于标准输入输出的 stdio和基于 HTTP 的流式传输。stdio 适用于本地子进程场景也就是桌面应用启动一个 Node 进程来跑 ServerHTTP 流式传输则适用 Server 在远端、Client 通过网络访问的场景。这两者的边界非常硬你不能在 stdio Transport 上跑 HTTP 的鉴权逻辑也不能指望 HTTP Transport 下 Server 的进程生命周期跟随 Client 自动结束。第三层是能力边界。MCP 定义了三种核心能力工具tools、资源resources和提示词prompts。工具是让模型执行动作的资源是给模型读取上下文的提示词是模板化输入流程的。它们各有各的消息类型、注册方式和返回格式混在一起用就会踩坑。第四层是生命周期边界。从 Client 发起 initialize 握手到 Server 返回协议版本和能力列表再到后续的工具调用最后是关闭连接。每一步都有状态约束你不能在初始化完成之前调用工具也不该在关闭之后继续发消息。1.3 协议版本与会话协商边界还有一个容易被忽略的维度就是协议版本。MCP 的规范还在快速演进今天你用的协议版本可能半年后就不推荐了。Server 在 initialize 阶段会返回自己支持的 protocolVersionClient 会判断这个版本是否能接受。如果两边版本差太多连接会被直接拒绝表现就是工具列表迟迟不出现。所以你先看协议边界其实是在看三样东西消息往哪个方向走、消息能带什么能力、两边是否对同一个协议版本有共识。这三样都清楚了SDK 的选型自然会变得很具体。2. MCP TypeScript SDK 选型我踩过的坑和推荐2.1 官方 modelcontextprotocol/sdk值得作为默认选项目前 TypeScript 生态里最值得作为默认选项的就是官方维护的 modelcontextprotocol/sdk。我的建议是除非有非常明确的自研理由否则不要绕过它。这个 SDK 有几个特点让它特别适合 MCP 开发者。第一它同时提供了 Server 和 Client 的高层封装McpServer类让你可以用声明式的方式注册工具、资源和提示词不需要手写 JSON-RPC 消息。第二它的传输层是插件化的支持StdioServerTransport、SSEClientTransport、StreamableHTTPServerTransport切换传输方式时核心业务代码可以基本不变。第三它的 TypeScript 类型非常完整从CallToolResult到ListResourcesResult几乎每个协议消息都有对应类型。我在用它做服务端时遇到最多的问题不是 SDK 功能不够而是文档版本跟实际安装版本对不上。npm 上的包更新频繁有时候 API 会加一个可选参数有时候会改导出路径。所以我建议你安装之后先打开node_modules/modelcontextprotocol/sdk/dist/esm/下的类型定义文件以你实际的版本为准。2.2 社区 SDK 与封装库怎么判断值不值得用除了官方 SDK社区里也有不少 MCP 相关的 TypeScript 库有纯协议的实现也有一些针对特定工具的封装比 MCP 客户端抽象、SSE 辅助库等。判断这些库值不值得用我一般会看三点。第一是它跟官方协议的同步速度。MCP 规范调整频繁如果一个库半年没发版那它很可能已经落后于当前协议版本。第二是类型覆盖率。好的库会导出完整的请求响应类型不是用any一把梭。第三是传输层抽象。有些库只支持 stdio有些只支持 HTTP你要看它是否覆盖了你真正的部署目标。社区封装里的确有很好用的比如针对特定场景做的高层封装能让你的业务代码更简洁。但如果你的目标是长期维护一个生产级 MCP 服务我更推荐用官方 SDK 做协议层自己包一层业务逻辑而不是把命运绑在一个小维护团队身上。2.3 我的推荐组合官方 SDK 做协议业务代码自己包一层这里直接给出我目前的推荐组合用官方modelcontextprotocol/sdk负责协议和传输在它外面自己写一个薄薄的业务适配层专门负责参数校验、错误映射和日志转换。为什么要自己包一层因为 MCP 协议目前还在演进阶段你今天写的自定义工具逻辑不应该和协议细节绑死。比如你在 Server 内部要调用一个数据库那么数据库连接、SQL 校验这些应该放在 adapter 里McpServer上注册的工具函数只做参数接收和结果返回。这样协议升级时你要改的只有 adapter 和传输层数据库逻辑一行都不用动。如果你打算写 Client我的思路也类似。官方 SDK 的 Client 提供listTools、callTool等基础方法但你可以封装一个领域客户端比如DatabaseClient或DesignClient对外暴露业务方法内部再拼装协议请求。这样上层业务不需要关心 MCP 细节。3. 在跑示例之前先确认协议版本与传输方式3.1 MCP 协议版本如何影响你的 SDK 选择我见过有人下载了半年前的示例代码直接npm install后又说 SDK 报错查了半天发现是协议版本不匹配。MCP 规范在 2024 年底到 2025 年之间经历了好几轮调整尤其是传输层从早期的 SSE 到现在的 Streamable HTTPAPI 变化非常大。所以你在安装 SDK 时第一件事是先确认你希望支持的 protocolVersion。一般来说新项目直接用当前最新版本就行但如果你要兼容现有客户端比如某个桌面应用只支持某个特定版本那就要仔细看 SDK 的 release notes。好在官方 SDK 在initialize请求里会自动带上它支持的版本列表你用 SDK 时很少需要手动做版本判断但懂这个机制还是能帮你减少认知盲区。另外你在选择“先跑示例”的示例仓库时也一定要看它的发布日期。如果示例是几个月前的最好先看它的 package.json 里 SDK 版本再对照当前最新版本决定是否升级。不要盲目npm install最新版依赖破坏的坑往往比功能缺失的坑更难查。3.2 标准输入stdio和 HTTP 流式传输怎么选这是选型里最关键的一条边界。如果你的 MCP Server 要被本地桌面应用调用比如 Claude Desktop、Cursor以及各种本地 MCP 客户端那最常见的选择就是 stdio。它的特点是启动快、生命周期随父进程走不需要监听端口也不需要考虑跨域和鉴权。如果你的 MCP Server 要部署到远端让多个 Client 通过网络访问那就应该用 Streamable HTTP。这种模式需要你管理服务地址、身份验证、会话生命周期复杂度更高但换来了分布式能力。我自己的经验是比赛或内部工具优先用 stdio 快速验证一旦要考虑多人使用或远程调用再迁移到 HTTP。迁移的核心是替换 Transport而不是重写业务逻辑这也是我推荐官方 SDK 的原因它把这个抽象做得比较干净。3.3 一个最小示例的真实搭建过程这里我演示一个我实际用过的快速上手流程。假设你有一个全新的 Node.js 项目安装了 TypeScript。先安装依赖npm install modelcontextprotocol/sdk npm install -D typescript tsx然后创建src/server.ts写一个最简单的工具import { McpServer } from modelcontextprotocol/sdk/server/mcp.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; const server new McpServer({ name: demo-server, version: 0.1.0, }); server.registerTool( greet, { title: Greet user, description: 生成一个简单的问候语。, inputSchema: { type: object, properties: { name: { type: string, description: 要问候的名字 }, }, required: [name], }, }, async ({ name }) { return { content: [ { type: text, text: 你好${name} }, ], }; } ); const transport new StdioServerTransport(); await server.connect(transport);这段代码里registerTool的第一个参数是工具名第二个参数里的inputSchema是 JSON Schema第三个参数是执行函数。执行函数的返回值必须是 MCP 协议认可的CallToolResult结构也就是{ content: [{ type: text, text: ... }] }不能直接返回一个字符串或对象。这个边界很关键我在这里栽过跟头。把 TypeScript 编译到dist后你可以用 MCP Inspector 来连接测试。输入npx modelcontextprotocol/inspector node dist/server.js然后在浏览器里打开调试面板看看工具是否正常展示、调用是否返回预期结果。4. 从最小示例到能用的工具必须处理的边界细节4.1 工具tools的输入输出边界工具是 MCP 里最常用的能力但也是边界错误的高发区。很多人在注册工具时只写了inputSchema却没考虑到参数如何在协议消息中传递。比如你定义了一个inputSchema要求name是字符串但客户端传入null如果你的执行函数没有做运行时校验返回的报错信息可能非常难懂。我的习惯是在执行函数开头先用一层手工校验。虽然 MCP 有 schema 校验但服务端最好还是自己挡一道特别是面对不可信客户端时。校验不通过时不能用throw乱抛要返回一个标准错误结构或者调用McpServer提供的错误处理逻辑。协议层面要求结构化错误而不是任意异常。返回内容的结构同样有边界。MCP 的content支持多种类型最常用的是text类型还可以返回image、audio等资源类型。如果只是简单文本就用{ type: text, text: xxx }千万不要把一个对象直接塞进text。客户端拿到后是否能解析完全由这个结构决定。4.2 资源resources和提示词prompts不是摆设很多人以为 MCP 里只有工具这其实是对协议边界的误解。资源用于暴露可读取的数据比如文件内容、数据库查询结果、网页快照客户端可以主动读取这些资源来补充上下文。提示词则是预设的对话模板用于规范 AI 的使用方式。这三个能力不仅应用场景不同在 SDK 里的注册方法也不同。官方 SDK 用server.registerTool注册工具用server.registerResource注册资源用server.registerPrompt注册提示词。如果你把本该是资源的数据设计成工具客户端必须显式调用才能拿到那上下文注入就会变得很别扭。判断标准很简单如果数据是只读的、持续的、需要被拉取的优先考虑资源如果是一次性动作比如发送邮件、更新数据库才考虑工具。4.3 错误处理与进度通知协议边界还包括错误类型。MCP 的错误返回通常遵循 JSON-RPC 错误码规范常见的有解析错误、请求无效、方法不存在、内部错误。你在 SDK 里跑业务逻辑时如果数据库连接失败应该把错误映射到合适的错误码而不是随便抛一个字符串。一些复杂工具需要执行很长时间这时候 MCP 还支持进度通知。详情可以参考协议文档里progress相关字段。如果你忽略了进度通知客户端可能因为长期没有响应而超时。我自己跑数据批量处理工具时就因为在工具里没有发送进度通知被客户端判定为超时过好几次。进度通知不仅是用户交互优化也是避免断连的工程手段。4.4 日志与调试不要在 stdio 上 console.log这条建议看起来像常识但我依然见过不少人在StdioServerTransport模式下用console.log打印调试信息。后果非常严重console.log会把内容写到标准输出流而 MCP 的 stdio 传输恰恰是用标准输出发送协议消息的。日志一旦混入客户端解析 JSON-RPC 消息就会失败表现为工具列表加载不出来或者连接直接断开。正确的调试方式是用console.error或者专门的日志文件。在 stdio 模式下标准错误流是客户端不会解析的你可以放心输出调试信息。你也可以使用 MCP 框架里的 Logger 能力为不同模块设置日志级别。如果使用 HTTP 传输则可以直接用服务端应用日志中间件那就自由很多。5. 实用排查清单与生产建议5.1 连接不上、工具不显示的排查顺序如果你配置好 MCP Server 后客户端里看不到工具先别急着怀疑 SDK。我的排查顺序是这样的首先确认进程是否真的启动了。在 stdio 模式下你能在任务管理器或ps里看到对应的 Node 进程吗如果进程一闪而过多半是初始化阶段就抛异常了。其次确认initialize是否成功。MCP 的第一步是握手任何版本不兼容都会在这一步暴露。可以看服务端日志如果收到initialize请求但返回错误就去看返回的错误码。再次确认传输层是否匹配。客户端配置的是 stdio但你的 Server 启动的是 HTTP 监听那当然连不上。这个听起来低级但真的经常发生。最后用 MCP Inspector 独立测试。如果你用 Inspector 能连上用目标客户端连不上那问题大概率在客户端配置比如命令路径写错、环境变量没设置。5.2 进程生命周期Server 如何优雅退出MCP Server 在 stdio 模式下生命周期大多由父进程控制。父进程关闭时子进程接收到关闭信号你的 Server 应该清理资源后退出。不要写那种“无法退出”的死循环也不要因为某个工具调用还在进行就阻塞退出。我建议在服务端监听SIGTERM和SIGINT在回调里关闭数据库连接、停止定时任务然后调用server.close()或让进程自然退出。这能避免很多诡异问题比如客户端重启后旧进程还在占用端口或文件锁。HTTP 模式下的生命周期又不一样。每个连接可能有会话状态也可能是无状态的。你需要根据协议要求实现会话管理和超时清理否则连接数一多内存就涨上去了。5.3 当我把它接到 Cursor、Figma、数据库、游戏引擎时边界发生了什么变化MCP 最常见的落地场景就是把某个已有系统的能力暴露给 AI。比如把设计稿工具接入 MCP让 AI 能读取图层结构、生成前端代码把数据库接入 MCP让 AI 能查询表结构、执行只读 SQL把游戏引擎接进来让 AI 控制场景对象。这些场景里协议本身没有变变的只是 Server 内部要适配的具体业务。你在设计工具软件里调 SDK暴露的其实是该工具开放的插件 API你在数据库上包一层 MCP那就要考虑权限收敛不能让模型拥有执行任意 SQL 的能力。协议边界在这里就变成了真正的系统边界MCP Server 是连接 AI 和外部系统的“接线盒”接线盒两侧的权限、数据格式、异常处理都必须仔细设计。5.4 面向生产的 TypeScript 工程建议如果你准备把 MCP Server 部署到生产环境我有几个额外建议。别只在本地用tsx跑建议用tsc编译到dist再用 Node 直接运行避免运行时依赖 TypeScript 编译器。做好类型定义和 schema 的一致性检查可以用zod这类库在运行时做参数解析。环境变量统一管理不要把数据库密码直接写在 MCP 配置命令里。还要做好可观测性。简单场景下把请求日志写到文件或者 stdout 的 JSON 格式里复杂场景下接入完整的 APM 链路确保每次工具调用的耗时和结果都可追踪。AI 应用出问题时常常不是协议的问题而是你不知道模型在哪个环节拿错了数据日志是你唯一的线索。最后说一个我自己的习惯每次拿到新版本的 MCP SDK我会先用 MCP Inspector 把官方示例里的全部能力清单看一遍再对照 release notes 看有没有破坏性变更。工具链迭代快的时候经验不如流程可靠先把协议边界和市场共识摸清楚代码反而写得顺手很多。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。