MCP TypeScript SDK 请求体大小限制全面解析:Streamable HTTP 全入口 4 MiB 上限与 413 防护机制
【免费下载链接】typescript-sdkThe official TypeScript SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/GitHub_Trending/ty/typescript-sdk
导读
本文基于@modelcontextprotocol/typescript-sdk仓库中.changeset/request-body-size-limit.md变更记录,系统讲解 SDK 为 Streamable HTTP 服务端引入的请求体大小限制机制:从WebStandardStreamableHTTPServerTransport到createMcpHandler、toNodeHandler、createMcpHonoApp等所有 SDK 自有的 body 读取路径,如今统一在解析任何内容之前以默认4 MiB上限拦截超大请求并回答413 Payload Too Large,同时配套引入 JSON-RPC 批量消息数量上限(100 条)与 Host/Origin 校验前置的硬化改造。读完本文,你将掌握每个入口的maxRequestBodySize配置方法、readRequestBody有界读取器的实现原理、parsedBody逃逸通道的边界,以及 Node 适配器双层上限的调优要点。
一、背景:为什么需要统一的请求体大小限制
MCP(Model Context Protocol)服务端通过 Streamable HTTP 接收客户端 POST 上来的 JSON-RPC 消息。在过去,SDK 的各个 HTTP 入口对请求体大小缺乏一致的约束:
- 一个超大的 POST body 会被完整读入内存后再尝试 JSON 解析,攻击者或故障客户端可以借此耗尽服务器内存(DoS 风险);
- 不同入口(web-standard 传输层、
createMcpHandler入口、Node/Hono 适配器)各自为政,行为不统一,运维难以预期; - 旧版 SSE 传输早已存在读取上限,而 Streamable HTTP 路径却没有对齐。
本次变更的目标十分明确:让所有 SDK 自有的 body 读取都在"解析之前"就受限于统一大小,超限直接回答413 Payload Too Large,不浪费任何解析与调度资源。核心实现位于 packages/server/src/server/requestBody.ts,常量与工具函数则从@modelcontextprotocol/server包公开导出(见 packages/server/src/index.ts)。
二、统一上限:DEFAULT_MAX_REQUEST_BODY_SIZE= 4 MiB
在 requestBody.ts 中定义了两个核心常量:
/** Default upper bound, in bytes, on a request body read by the HTTP entry points (4 MiB). */ export const DEFAULT_MAX_REQUEST_BODY_SIZE = 4 * 1024 * 1024; /** Upper bound on the number of messages accepted in one JSON-RPC batch array. */ export const MAX_BATCH_SIZE = 100;DEFAULT_MAX_REQUEST_BODY_SIZE:4 * 1024 * 1024字节,即4 MiB。这个值并非凭空而来——它正是旧版 SSE 传输早已使用的限制,本次变更让 Streamable HTTP 各入口与其对齐。Express 适配器与 stdio 传输此前也已各自对读取设限,因此全 SDK 的 HTTP 读取路径现在处于同一防护水位。MAX_BATCH_SIZE:单次 JSON-RPC 批量(batch)数组最多接受100 条消息,超过则整体拒绝,详见下文第六节。
上限可通过新的maxRequestBodySize选项(单位:字节,默认即DEFAULT_MAX_REQUEST_BODY_SIZE)配置,覆盖以下全部入口:
| 入口 | 选项挂载点 | 源码位置 |
|---|---|---|
WebStandardStreamableHTTPServerTransport(含基于它构建的 Node transport) | WebStandardStreamableHTTPServerTransportOptions.maxRequestBodySize | streamableHttp.ts |
createMcpHandler(转发到其无状态 legacy 分支) | CreateMcpHandlerOptions.maxRequestBodySize | createMcpHandler.ts |
isLegacyRequest/legacyStatelessFallback | 相同的选项,保证判定与处理用同一把尺子 | createMcpHandler.ts |
createMcpHonoApp(JSON 预解析) | CreateMcpHonoAppOptions.maxRequestBodySize | hono.ts |
toNodeHandler/toWebRequest(Node 适配器) | ToNodeHandlerOptions/ToWebRequestOptions | toNodeHandler.ts |
选项值的校验由resolveMaxRequestBodySize完成(requestBody.ts):省略时回落默认值;传入的值必须为正的有限数值,否则在配置期即抛出RangeError(maxRequestBodySize must be a positive number of bytes),杜绝运行时才暴露的配置错误。
三、核心实现:readRequestBody有界读取器
maxRequestBodySize之所以能在"解析之前"生效,靠的是导出的有界读取器readRequestBody(requestBody.ts):
export async function readRequestBody( request: Request, maxBytes: number = DEFAULT_MAX_REQUEST_BODY_SIZE ): Promise<{ tooLarge: true } | { tooLarge: false; text: string }> { if (Number(request.headers.get('content-length')) > maxBytes) { return { tooLarge: true }; } if (request.body === null) { return { tooLarge: false, text: '' }; } const reader = request.body.getReader(); const decoder = new TextDecoder(); let received = 0; let text = ''; try { for (;;) { const { done, value } = await reader.read(); if (done) break; received += value.byteLength; if (received > maxBytes) { return { tooLarge: true }; } text += decoder.decode(value, { stream: true }); } } finally { reader.releaseLock(); } return { tooLarge: false, text: text + decoder.decode() }; }其设计要点有三:
Content-Length预检查(零读取拒绝):若请求头声明的Content-Length已经超过上限,一个字节都不读,直接返回tooLarge: true。这是最高效的拦截路径。- 流式累积检查(边读边断):对于没有可靠
Content-Length(或使用 chunked transfer)的请求,通过ReadableStream的getReader()逐块读取,累计字节数一旦超过上限立即返回tooLarge,不会等到把整个 body 读完。流式读取失败(网络中断等)会原样向上传播。 - 释放锁与 UTF-8 安全解码:
finally中保证reader.releaseLock();文本用TextDecoder流式解码并在末尾 flush,避免多字节字符在块边界被截断损坏。
该函数作为readRequestBody从@modelcontextprotocol/server导出(index.ts),其定位是"像isJsonContentType一样"的基础构件,供需要自行预解析 body 的适配器作者复用,从而让第三方适配器也能获得与 SDK 一致的有界读取语义。
超限后的统一响应由requestBodyTooLargeMessage生成(requestBody.ts):
Payload Too Large: Request body must not exceed {maxBytes} bytes对应响应为 HTTP413,JSON-RPC error code-32000,且在 body 被解析、任何服务器实例被创建之前就已返回。
四、各入口的行为与配置详解
4.1WebStandardStreamableHTTPServerTransport(及 Node transport)
这是核心的 Web 标准 Streamable HTTP 传输层,可在任意支持 Web 标准的运行时运行(Node.js 18+、Cloudflare Workers、Deno、Bun 等)。Node 环境下的NodeStreamableHTTPServerTransport正是对其的封装(其 JSDoc 中明确说明 "wraps this transport"),因此同样继承了上限。
在构造函数中(streamableHttp.ts),maxRequestBodySize经resolveMaxRequestBodySize解析后存入实例。handlePostRequest的处理流程(streamableHttp.ts)为:
- 校验
Accept头与Content-Type(415 检查); - 若调用方未提供
parsedBody,则调用readRequestBody(req, this._maxRequestBodySize); tooLarge时通过createJsonErrorResponse返回413,并触发onerror报告;- 通过后才
JSON.parse,然后做 batch 数量检查与JSONRPCMessageSchema校验。
有状态/无状态会话语义、SSE 流式响应等既有行为均不受影响——限制只在"读 body"这一步之前生效。
4.2createMcpHandler/isLegacyRequest/legacyStatelessFallback
createMcpHandler是服务 2026-07-28 协议修订版并默认回落到 2025 时代无状态服务的 HTTP 入口。它的maxRequestBodySize同时作用于两条腿:
- 现代分支(modern leg):请求分类步骤(
classifyEntryRequest,createMcpHandler.ts)读取 body 时使用同一上限,读取结果tooLarge会返回{ step: 'body-too-large' },随后入口直接以413/-32000回答(createMcpHandler.ts),不创建任何服务器实例、不进入分类阶梯; - 无状态 legacy 分支(legacy leg):
createLegacyStatelessFallback构造的 per-request transport 会收到同样的maxRequestBodySize并透传给WebStandardStreamableHTTPServerTransport(createMcpHandler.ts),保证两个时代的服务对超大请求行为一致。
isLegacyRequest判定函数同样接受maxRequestBodySize选项(createMcpHandler.ts),并用它驱动自己的分类读取,因此判定与处理永远在同一把尺子下——从源码看,它就是createMcpHandler内部分类步骤的导出形态,二者共用classifyEntryRequest,不会出现"判定为可处理、实际却被 413 拒绝"的分歧。对于 body 超限的请求,isLegacyRequest会将其报告为非 legacy(返回false),于是它被路由到现代 handler,由现代路径回答413。legacyStatelessFallback同样提供LegacyStatelessFallbackOptions.maxRequestBodySize(createMcpHandler.ts)。
4.3toNodeHandler/toWebRequest:RequestBodyTooLargeError
Node 适配器toNodeHandler将 web-standard 的{ fetch, close, notify, bus }handler 适配为 Node 的(req, res, parsedBody?)形态。当没有传入预解析 body时,适配器需要自行从 Node 流读取 body 并转换为 web-standardRequest,这一步由toWebRequest完成(toNodeHandler.ts)。现在,当读取过程中 body 超过上限时:
toWebRequestreject 一个错误,其name为'RequestBodyTooLargeError'、status为413(toNodeHandler.ts);toNodeHandler捕获该错误后回答413,响应体为 JSON-RPC 错误-32000,并额外携带connection: close头——注释说明这是为了让 HTTP/1.1 服务器在回答后直接关闭 socket,而不是挂起一个请求流只被读了一半的连接(toNodeHandler.ts)。
手动调用toWebRequest的调用方(例如自组装isLegacyRequest路由时)需要自行处理该 rejection:要么 catch 后返回 413,要么直接传入已解析的 body(parsedBody)绕开读取。这一点在 toWebRequest 的 JSDoc 中有明确说明。
4.4createMcpHonoApp与createMcpExpressApp:Host/Origin 校验前置
两个框架入口还伴随一项顺序性硬化:Host/Origin 校验现在先于 JSON body 解析器运行。
- Hono:
createMcpHonoApp依次注册 DNS rebinding 防护(hostHeaderValidation/localhostHostValidation)→ Origin 校验 → JSON body 解析中间件(hono.ts)。body 解析中间件对Content-Type为application/json的请求从 clone 上调用readRequestBody(上限即maxRequestBodySize,默认 4 MiB),超限回答413,并把解析结果存入c.set('parsedBody', ...)供 MCP 适配器使用。 - Express:
createMcpExpressApp同样先注册 Host/Origin 校验,再app.use(express.json(...))(express.ts),其jsonLimit选项直接透传给 Express 的express.json({ limit }),默认即 Express 内置的'100kb'。
由此带来的可观察行为变化:来自不允许的 Host 或 Origin、且携带无效 JSON body 的请求,现在回答403而非400,并且其 body 根本不会被读取——校验失败即短路,解析器永远没有机会接触 payload。
五、双层上限的调优要点:Node 适配器
使用toNodeHandler时存在两层上限,需要特别注意(ToNodeHandlerOptions JSDoc 明确提示):
- 适配器层(
toNodeHandler的maxRequestBodySize):在toWebRequest从 Node 流缓冲 body 时应用; - handler 层(
createMcpHandler的maxRequestBodySize):在 web-standardfetch内部读取时应用。
适配器的 bound 先于 handler 的 bound 生效——请求必须先通过适配器的读取,才可能到达 handler。因此若想调大上限,两层必须同时提高,否则无论 handler 层设置多大,适配器层仍会以 4 MiB(或你设置的更小值)先行拦截。
六、JSON-RPC 批量消息上限:100 条,400/-32600
与 body 大小限制配套,SDK 在所有入口统一施加了 batch 数量约束:MAX_BATCH_SIZE = 100。在WebStandardStreamableHTTPServerTransport.handlePostRequest中(streamableHttp.ts):
if (Array.isArray(rawMessage) && rawMessage.length > MAX_BATCH_SIZE) { this.onerror?.(new Error(`Invalid Request: Batch must not exceed ${MAX_BATCH_SIZE} messages`)); return this.createJsonErrorResponse(400, -32_600, `Invalid Request: Batch must not exceed ${MAX_BATCH_SIZE} messages`); }- 超过 100 条消息的 batch 被整体回答
400/-32600(Invalid Request); - 其中任何一条都不会被调度执行(检查发生在消息分发之前);
- 该约束不区分是否传入
parsedBody:即使调用方预解析了 body 从而跳过 SDK 的 body 读取与大小限制,batch 数量上限依然生效——这是本文第三节"parsedBody逃逸通道"之外的唯一例外,它是无条件的。
七、parsedBody逃逸通道:何时跳过限制
HandleRequestOptions.parsedBody(streamableHttp.ts)与McpHandlerRequestOptions.parsedBody(createMcpHandler.ts)是 SDK 为"上游中间件已经解析好 body"的场景预留的通道:
- 当调用方传入
parsedBody时,SDK不再自行读取请求体,因此maxRequestBodySize的大小限制不会作用于该路径——这一般是合理的,因为 body 已经在你的 body-parser(如express.json()、Hono 内置解析)阶段被有界处理过了; - 但如第六节所述,batch 数量上限在任何情况下都适用。
典型用法(来自toNodeHandler的 JSDoc 示例,toNodeHandler.ts):
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));八、测试与验证路径
本次变更在仓库中配有完整测试,可据此验证各入口行为:
- packages/server/test/server/streamableHttp.test.ts:验证 transport 层 413 响应、batch 限制与
parsedBody通道; - packages/server/test/server/createMcpHandler.test.ts:验证入口层 body 超限返回 413、
isLegacyRequest分类行为; - packages/middleware/node/test/toNodeHandler.test.ts:验证
RequestBodyTooLargeError与toNodeHandler的 413 回答; - packages/middleware/hono/test/hono.test.ts:验证 Hono 应用 JSON 预解析的 413 与 Host/Origin 前置。
相关使用文档可进一步参考 docs/serving/http.md、docs/serving/express.md、docs/serving/hono.md 与 docs/serving/web-standard.md。
九、迁移与配置速查
- 默认值:
maxRequestBodySize缺省即 4 MiB(DEFAULT_MAX_REQUEST_BODY_SIZE),与旧版 SSE 传输的历史限制对齐;该常量从@modelcontextprotocol/server导出。 - 单位与校验:一律为字节数;非正数或非有限值在配置期抛
RangeError。 - Node 双层:
toNodeHandler与createMcpHandler的上限都要配置,适配器层先生效。 parsedBody:传入后 SDK 不读 body、不套大小限制;batch 的 100 条上限依旧。- Host/Origin 前置:Hono 与 Express 应用中,不允许的 Host/Origin 请求在 body 解析前即被 403 拒绝。
- 行为一致性:
createMcpHandler、isLegacyRequest、legacyStatelessFallback共享同一分类代码路径与同一上限,判定与处理不会互相矛盾。
总体而言,这次变更把"请求体大小防护"从各传输层零散的隐式约束,收敛为 SDK 所有 HTTP 入口默认统一、可在配置期显式调整、并在解析前短路拒绝的一等公民能力,配合 batch 数量上限与 Host/Origin 前置校验,显著降低了 MCP 服务端暴露在公网时遭受超大 payload 攻击与解析资源浪费的风险面。
【免费下载链接】typescript-sdkThe official TypeScript SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/GitHub_Trending/ty/typescript-sdk
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考