news 2026/9/15 18:23:55

MCP TypeScript SDK v2 中的 `@modelcontextprotocol/node`:Web 标准优先的 Node.js 适配层演进全解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MCP TypeScript SDK v2 中的 `@modelcontextprotocol/node`:Web 标准优先的 Node.js 适配层演进全解

MCP TypeScript SDK v2 中的@modelcontextprotocol/node:Web 标准优先的 Node.js 适配层演进全解

【免费下载链接】typescript-sdkThe official TypeScript SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/GitHub_Trending/ty/typescript-sdk

@modelcontextprotocol/node是 Model Context Protocol(MCP)TypeScript SDK 官方仓库中面向 Node.js 运行时的一层薄适配器,其职责是把 Web 标准形态的 MCP Server 处理器桥接到 Node 生态的IncomingMessage/ServerResponse(以及 Express、Fastify、node:http等框架)。本文基于该包在 v2 里程碑的完整变更记录(见 packages/middleware/node/CHANGELOG.md),并对照同目录下的源码与测试,逐条拆解 v2 的核心重构:处理器形态从 Node 专属转向 Web 标准优先、新增toNodeHandler/toWebRequest适配函数、Host/Origin 头校验守卫、Content-Type 严格校验,以及双格式构建与依赖治理。读完本文,你将能理解并实操从 v1 迁移到 v2 时 Node 侧代码需要做的全部改动,并能用手写node:http服务安全地托管 MCP 端点。

包定位:为 MCP Server SDK 提供 Node.js 集成层

在 packages/middleware/node/README.md 中,该包被明确描述为@modelcontextprotocol/server的 "thin Node.js integration layer":它提供能与 Node 的IncomingMessage/ServerResponse直接协作的 Streamable HTTP 传输层,让 MCP Server 可以运行在 Node 的标准 HTTP 服务器之上;而面向 Cloudflare Workers、Deno、Bun 等 Web 标准运行时,则应当直接使用@modelcontextprotocol/server导出的WebStandardStreamableHTTPServerTransport

从 packages/middleware/node/package.json 可以看到该包的关键形态:

  • engines.node要求>=20
  • 依赖仅声明@hono/node-server(用于 Node HTTP ↔ Web Standard 转换),@modelcontextprotocol/serverhono为 peerDependencies,其中hono被标记为optional(见下文依赖治理小节);
  • 导出映射同时提供importrequire两个条件(ESM 与 CJS 双格式)。

包的核心导出(见 packages/middleware/node/src/index.ts)包括:

  • NodeStreamableHTTPServerTransport与类型别名StreamableHTTPServerTransportOptions
  • toNodeHandler(handler, opts?):把 Web 标准的{ fetch }处理器适配成 Node 的(req, res, parsedBody?)处理器;
  • hostHeaderValidation/localhostHostValidationoriginValidation/localhostOriginValidation:面向手写node:http服务器的请求守卫;
  • toWebRequest(req, parsedBody?, opts?):NodeIncomingMessage→ Web 标准Request的转换函数;
  • 结构类型NodeIncomingMessageLikeNodeServerResponseLike及选项类型ToNodeHandlerOptionsToWebRequestOptions

v2 重构主线:处理器形态全面转向 Web 标准优先

createMcpHandler返回{ fetch, close, notify, bus }

v2 变更记录(PR #2286)中最具架构意义的一步是:createMcpHandler现在返回仅面向 Web 标准的处理器——一个{ fetch, close, notify, bus }形状的对象,这也是 Workers / Bun / Deno 环境中export default所期待的形状。原先鸭子类型化的.node(req, res, parsedBody?)接口被移除,Node 框架(Express、Fastify、裸node:http)统一通过新的toNodeHandler包装一次来完成适配。

也就是说,v2 之后 MCP 处理器的唯一事实源是 Web 标准的 fetch 面;Node 侧不再各自实现一套请求语义,而是共享同一套转换逻辑。这消除了两套入口在行为上的分叉,也让未来支持更多运行时(Workers、Bun、Deno)成为可能。

toNodeHandler:Node 请求处理器的唯一适配入口

toNodeHandler的实现位于 packages/middleware/node/src/toNodeHandler.ts,其职责链如下:

  1. 把 Node 请求转换为 Web 标准Request(内部调用toWebRequest);
  2. 调用handler.fetch(request, { authInfo, parsedBody })
  3. 把返回的Response写回res,且对 SSE 流式响应的**写回压(backpressure)**进行妥善处理。

典型用法:

import { createMcpHandler } from '@modelcontextprotocol/server'; import { toNodeHandler } from '@modelcontextprotocol/node'; const handler = createMcpHandler(factory); app.all('/mcp', toNodeHandler(handler)); // 或当 body parser 已消费流时: const node = toNodeHandler(handler); app.all('/mcp', (req, res) => void node(req, res, req.body));

ToNodeHandlerOptions支持两个选项:

  • onerror?: (error: Error) => void:当请求转换或handler.fetch本身抛出异常、适配器即将写回500之前调用,用于恢复被移除的.node面原先具备的可观测性;即使onerror自身抛错,也不影响 500 响应(源码中有对应保证)。
  • maxRequestBodySize?: number:当未提供parsedBody时,适配器从 Node 流缓冲请求体的字节上限,默认4194304(4 MiB,来自DEFAULT_MAX_REQUEST_BODY_SIZE)。超过上限会直接以413响应拒绝,不会调用handler.fetch,且带connection: close头以便 HTTP/1.1 服务端在回答后关闭连接(避免残留未读完的请求流导致空闲连接)。需要注意的是该上限应配合处理器自身的maxRequestBodySize一起调高,适配器这一层先生效。非正数会抛出RangeError(对应 toNodeHandler.test.ts 中的maxRequestBodySize测试)。

适配器还处理了几个易踩的边界:

  • Express 以中间件挂载时会传入(req, res, next),函数类型的第三参被识别为next不会被当作 parsed body;
  • req.auth(上游认证中间件附着的已验证信息)会作为处理器的 pass-throughauthInfo转发,测试 toNodeHandler.test.ts 中的whoami用例验证了clientId能透传到处理器上下文;
  • 写回压处理:当res.write(chunk)返回false(缓冲已满)时,适配器会等待drain事件后再拉取下一个 chunk;同时把 abort 信号与 drain 等待做竞速,保证客户端中途断开时(close事件、或入口时res.destroyed已为真)不会永久挂起——toNodeHandler.test.ts中用两个专门的用例("does not park on backpressure when the client closes" 与 "response was already destroyed before the adapter listened")验证了这一行为。

toWebRequest:Node 请求 → Web 标准Request的独立转换

toWebRequest是把toNodeHandler内部完成的IncomingMessage → Request转换独立导出,方便手写组合场景,例如用它在裸(req, res)处理器里喂给isLegacyRequest()做路由分流:

const probe = await toWebRequest(req, req.body); await ((await isLegacyRequest(probe)) ? legacy(req, res) : modern(req, res, req.body));

其转换细节(toNodeHandler.ts)包括:

  • method 缺失时回退为GET并大写;Host 头缺失时回退到 HTTP/2 的:authority伪头,再回退到localhost
  • 复制请求头时跳过 HTTP/2 伪头(Headers会拒绝以:开头的名称)与undefined值,数组值逐个append
  • 请求体以文本承载(MCP 请求体是 JSON),未提供parsedBody时从 Node 流读满到maxRequestBodySize(超出即抛出name'RequestBodyTooLargeError'status413的错误);提供parsedBody时(例如express.json()已消费流)则重新序列化并重写content-length、删除content-encoding/transfer-encoding,确保后续读取request.json()/text()的消费者仍能拿到正文;
  • 支持options.signal,把构造出的Request与客户端断连绑定,行为与toNodeHandler内部一致。

NodeIncomingMessageLikeNodeServerResponseLike是鸭子类型化(结构化)的 Node 请求/响应形状——该模块刻意保持结构类型,避免任何node:导入,从而让适配逻辑保持运行时无关性。v2 中这两个类型从@modelcontextprotocol/server迁入@modelcontextprotocol/node

手写node:http的安全守卫:Host 与 Origin 校验

node:http没有中间件链,因此 v2 为它单独提供了请求守卫函数(源码见 packages/middleware/node/src/middleware/hostHeaderValidation.ts 与 packages/middleware/node/src/middleware/originValidation.ts):

  • hostHeaderValidation(allowedHostnames)/localhostHostValidation():针对 DNS rebinding 的 Host 头校验(端口无关)。localhostHostValidation()只放行localhost127.0.0.1[::1](IPv6 需带方括号)。
  • originValidation(allowedOriginHostnames)/localhostOriginValidation():Origin 头校验。不带 Origin 头的请求直接放行(非浏览器 MCP 客户端不受影响);携带的 Origin 不被允许、或无法解析(包括不透明的nullorigin)则以403拒绝。

守卫的返回值语义很关键:返回false它已经自行以403写回了 JSON-RPC 错误响应,调用方不得再继续处理该请求。这是 README 与 JSDoc 反复强调的约定,也是组合使用时的前提:

import { createServer } from 'node:http'; import { localhostHostValidation, localhostOriginValidation, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; import { McpServer } from '@modelcontextprotocol/server'; const server = new McpServer({ name: 'my-server', version: '1.0.0' }); const validateHost = localhostHostValidation(); const validateOrigin = localhostOriginValidation(); createServer(async (req, res) => { if (!validateHost(req, res) || !validateOrigin(req, res)) return; const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined }); await server.connect(transport); await transport.handleRequest(req, res); }).listen(3000, '127.0.0.1');

该用法与框架适配器(createMcpExpressAppcreateMcpHonoAppcreateMcpFastifyApp)默认施加的保护一致——后者在 v2 中默认对 localhost 类绑定启用 Origin 校验,并新增allowedOrigins工厂选项;原先的 "0.0.0.0 且无白名单" 警告保持不变。守卫内部复用了@modelcontextprotocol/server提供的框架无关助手(validateHostHeaderlocalhostAllowedHostnamesvalidateOriginHeaderlocalhostAllowedOriginsoriginValidationResponse)。校验测试见 packages/middleware/node/test/validation.test.ts,覆盖了拒绝恶意 Host/Origin、放行允许值、放行无 Origin 请求、拒绝畸形 Origin(null)四类情形,被拒响应均为403+application/json+ JSON-RPC-32000错误。

Content-Type 严格化:415 与isJsonContentType

v2(PR #2441)修复了一个安全与合规层面的隐患:此前对Content-Type的判断是子串匹配——text/plain; a=application/json这类值会因包含application/json子串而通过检查;大小写变体则被错误拒绝;而 2026-07-28 修订入口此前甚至不检查Content-Type。修复后:

  • POST 请求的Content-Type媒体类型不是application/json时,一律以415 Unsupported Media Type拒绝(缺失或非 JSON 头都返回 415);
  • 头部解析采用真正的媒体类型解析而非子串匹配,允许参数(如application/json; charset=utf-8),连application/json;这种参数段残缺的畸形值也能继续工作;
  • SDK 客户端始终发送正确头部,不受影响。

新导出的isJsonContentType(header)助手位于 packages/core-internal/src/shared/mediaType.ts,实现上先走 RFC 9110 的content-type包解析提取 media type essence(WHATWG MIME Sniffing 标准中type/subtype裸对),解析失败时回退到按第一个;切分、去除参数段的兜底逻辑——这与浏览器及多数 HTTP 栈对畸形参数段的宽松行为一致;若参数尾含逗号(拼接的重复头)则判定为无 essence。该助手面向 transport 与框架适配器作者导出:任何组合使用classifyInboundRequestPerRequestHTTPServerTransport等导出积木的自定义入口,都必须自行应用它;Hono 适配器的 JSON 体预解析与客户端的响应分发现在也复用同一套解析媒体类型比较(见 packages/middleware/hono/src/hono.ts)。

双格式构建与包形态:ESM + CJS 并存

v2(PR #2405)为所有包同时产出 ESM 与 CommonJS 构建:

  • 每个包通过 tsdown 的format: ['esm', 'cjs']同时生成.mjs/.d.mts.cjs/.d.cts
  • exports映射新增require条件,使require('@modelcontextprotocol/…')在 CommonJS 消费方可用;
  • 输出扩展名在整个仓库统一规范化(例如@modelcontextprotocol/core.js/.d.ts迁移到.mjs/.d.mts),公开导入路径保持不变

在 packages/middleware/node/package.json 中可以实际看到这一形态:"."导出下import条件指向dist/index.mjs+dist/index.d.mtsrequire条件指向dist/index.cjs+dist/index.d.cts,同时保留maintypes顶层字段。

围绕类型解析还有三处配套修复:

  • 声明文件自洽(PR #2394):修复skipLibCheck: false编译场景——打包后的.d.mts不再残留无法解析的URIComponent引用(ajv 的发布类型从fast-uri导入它,dts 打包器无法链接其 export-assigned 命名空间,现通过 dts-only 路径映射内联),也不再从未声明依赖导入json-schema-typed(通过dts.resolve内联);@modelcontextprotocol/node@modelcontextprotocol/server同时清理了指向从未发布子路径的过期typesVersions条目。
  • types顶层字段(PR #1898):为使用遗留moduleResolution: "node"的消费方补充顶层types字段(client/server 在子路径导出上加typesVersions),exports映射仍是nodenext/bundler解析的事实源。
  • TypeScript ≥ 6.0 注意:由于发布声明引用了Buffer,包 README 提示此类用户需要在 tsconfig 中配置"types": ["node"]

依赖治理与兼容性细节

  • honopeer 依赖改为 optional(PR #1896)@modelcontextprotocol/node只在运行时用到@hono/node-servergetRequestListener(Node HTTP ↔ Web Standard 转换),并不需要hono框架本体,因此消费方不再需要安装hono即可使用NodeStreamableHTTPServerTransport。需要注意@hono/node-server自身仍把hono声明为硬性 peer,包管理器可能因此告警——这是上游行为,对仅使用getRequestListener的场景无害。
  • overrideGlobalObjects: false(PR #1410):通过向getRequestListener()传入overrideGlobalObjects: false,阻止 Hono 覆盖全局Response对象,修复与 Next.js 等其响应类继承原生Response的框架的兼容性。该选项在 packages/middleware/node/src/streamableHttp.ts 中构造 request listener 的两处均已使用。
  • setSupportedProtocolVersions转发(PR #2286):此前服务器在supportedProtocolVersions选项中声明的协议版本永远到不了 Node 适配器的MCP-Protocol-Version头校验,导致一直按默认版本列表校验。修复后NodeStreamableHTTPServerTransport会把该方法转发给被包装的 Web 标准 transport,使版本校验真正生效。
  • 包管理器统一为 pnpm(PR #1534):移除 npm 引用,仓库内全部使用 pnpm(参见 packages/middleware/node/package.json 的prepack等脚本,以及仓库根 pnpm-workspace.yaml)。
  • 废弃 API 清理(PR #1419):移除废弃的.tool.prompt.resource方法签名。
  • tsdown 导出解析修复(PR #1840):修复 tsdown 在导出解析上的问题,保证产物与exports映射一致。

NodeStreamableHTTPServerTransport:薄封装背后的能力

packages/middleware/node/src/streamableHttp.ts 展示了一个关键设计:NodeStreamableHTTPServerTransport只是对WebStandardStreamableHTTPServerTransport的薄包装,通过@hono/node-servergetRequestListener完成 Node HTTP 与 Web Standard 之间的转换,并正确支持 SSE 流式输出。

  • 有状态模式:sessionIdGenerator生成会话 ID 并写入响应头;初始化响应始终带会话 ID;无效会话 ID 返回404 Not Found;非初始化请求缺失会话 ID 返回400 Bad Request;状态(连接、消息历史)保存在内存中。
  • 无状态模式:sessionIdGenerator: undefined,任何响应都不带会话 ID,也不做会话校验。
  • 与 Express 结合时,若express.json()已消费请求流,应把解析结果作为第三参传入transport.handleRequest(req, res, req.body),避免重复读流——packages/middleware/node/src/streamableHttp.examples.ts 中提供了有状态、无状态、Express 预解析三种可直接对照的示例。
  • closeSSEStream(requestId)closeStandaloneSSEStream()可在长任务期间主动关闭 SSE 流,促使客户端按 priming event 中指定的重试间隔重连,从而实现轮询式行为。

升级路径:从 v1 到 v2

v2 的首次 beta 发布(PR #2402)即支持 MCP 2026-07-28 规范修订。变更记录明确指向两份迁移指南:

  • docs/migration/upgrade-to-v2.md:从 v1 升级到 v2 的总指南;
  • docs/migration/support-2026-07-28.md:采用 2026-07-28 修订的指南。

对 Node 侧消费者,结合上文可将迁移要点归纳为:把依赖的createMcpHandler处理器通过toNodeHandler包装后再挂载到 Express/Fastify/node:http;手写node:http场景需自行组合 Host/Origin 守卫(或绑定 loopback);已用express.json()的场景以parsedBody第三参传入以复用预解析结果;确认Content-Type严格化(非 JSON 的 POST 将得到 415)不影响既有客户端——SDK 客户端始终发送正确头部;若消费方是 CommonJS 或旧moduleResolution,v2 的双格式构建与types/typesVersions修复已覆盖相应解析路径。

验证与测试覆盖

上述行为并非仅停留在变更记录中,包内测试提供了可执行的印证:

  • packages/middleware/node/test/toNodeHandler.test.ts:覆盖请求流读取、预解析 body 优先、Expressnext识别、req.auth透传、HTTP/2 伪头跳过、写回压节流与断连不挂起、500 JSON-RPC 错误回退、onerror上报(含其自身抛错时仍返回 500)、413 超限拒绝(含Content-Length预判与流式超限两条路径)以及maxRequestBodySize边界。
  • packages/middleware/node/test/validation.test.ts:Host/Origin 守卫的放行与拒绝矩阵。
  • packages/middleware/node/test/streamableHttp.test.ts 与 packages/middleware/node/test/toWebRequest.test.ts:分别验证传输层与请求转换的端到端行为。

对希望深入源码的读者,核心实现集中在 packages/middleware/node/src/toNodeHandler.ts(适配与转换)、packages/middleware/node/src/streamableHttp.ts(传输层包装)、packages/middleware/node/src/middleware/(守卫),以及底层的 packages/core-internal/src/shared/mediaType.ts(媒体类型解析)。

【免费下载链接】typescript-sdkThe official TypeScript SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/GitHub_Trending/ty/typescript-sdk

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 18:21:51

用MySQL和ODBC构建Cadence CIS统一元器件库管理系统

搞硬件设计的兄弟应该都有这种体会:原理图库和PCB封装库要是乱起来,那真是灾难。一个项目里同一个电阻,有人用R0603,有人用RESC1608,还有人直接画个矩形框当电阻用,到了做BOM的时候,采购拿着Exc…

作者头像 李华
网站建设 2026/9/15 18:20:14

Flutter与HarmonyOS跨端日期格式化解决方案

1. 跨端开发中的日期格式化痛点在Flutter与HarmonyOS 6.0的混合开发场景下,日期格式化这个看似简单的功能却暗藏玄机。我最近在开发一个便签类应用时,就遇到了这样的典型问题:当同一条数据需要在Android、iOS和HarmonyOS三端显示时&#xff0…

作者头像 李华
网站建设 2026/9/15 18:19:38

Windows虚拟内存设置指南:页面文件原理与16G/32G配置实操

干这行十几年,被同事喊去救急的场景里,出现频率最高的不是服务器宕机,而是 Windows 突然弹一句“系统虚拟内存太低”。机型五花八门,处理流程倒是出奇一致:先怀疑物理内存不够用,加一条内存条,然…

作者头像 李华