@modelcontextprotocol/[email protected]:新增OAuth scope挑战
@modelcontextprotocol/[email protected]
推荐理由
本次更新包含破坏性变更:强制要求 MCP-Protocol-Version 请求头且默认限制请求体大小。使用自定义客户端或代理的开发者必须升级 SDK 或调整代码以适配新规范,否则将遭遇连接失败或请求被拒。
Minor Changes
次要更改
- #1624 6032170 Thanks @SamMorrowDrums! - Add request-time OAuth scope challenges for tools, resources, resource templates,
- and prompts. Each primitive's scopeChallenge callback receives the parsed
- request and verified authentication info, then either continues or returns the
- exact scope set for an insufficient_scope response. requireScopes provides a
- small helper for static all-of checks.
- createMcpHandler and Streamable HTTP transports return HTTP 403 with an
- insufficient_scope challenge before handler execution or SSE setup. The
- preflight is active whenever a registered primitive carries a scopeChallenge
- callback — there is no handler- or transport-level configuration. The
- challenge's WWW-Authenticate header is built by the same formatter as the
- bearer-auth 401/403 answers, and its resource_metadata parameter is derived
- from the verified AuthInfo: requireBearerAuth / verifyBearerToken now
- stamp their configured resourceMetadataUrl onto the AuthInfo they return
- (new optional AuthInfo.resourceMetadataUrl field), with a fallback to the
- well-known location for an HTTP(S) RFC 8707 resource identifier; the
- parameter is omitted when neither is available.
- #1624 6032170 感谢 @SamMorrowDrums!- 为工具、资源、资源模板以及提示词添加请求时的 OAuth 范围挑战。
- 每个原语(primitive)的 scopeChallenge 回调接收已解析的请求和已验证的身份认证信息,然后继续执行或返回用于 insufficient_scope 响应的确切范围集。requireScopes 提供了一个小型辅助函数,用于静态的全量检查。
- request and verified authentication info, then either continues or returns the
- exact scope set for an insufficient_scope response. requireScopes provides a
- small helper for static all-of checks.
- createMcpHandler 和 Streamable HTTP 传输在处理器执行或 SSE 设置之前,返回带有 insufficient_scope 挑战的 HTTP 403 状态码。
- insufficient_scope challenge before handler execution or SSE setup. The
- 每当注册的某个原语携带 scopeChallenge 回调时,预检(preflight)即处于激活状态——无需处理器或传输级别的配置。
- callback — there is no handler- or transport-level configuration. The
- 挑战的 WWW-Authenticate 标头由与 bearer-auth 401/403 响应相同的格式化程序构建,其 resource_metadata 参数源自已验证的 AuthInfo:requireBearerAuth / verifyBearerToken 现在
- 将其配置的 resourceMetadataUrl 附加到它们返回的 AuthInfo 上
- (新增可选的 AuthInfo.resourceMetadataUrl 字段),并回退至 HTTP(S) RFC 8707 资源标识符的 well-known 位置;当两者均不可用时,省略该参数。
- stamp their configured resourceMetadataUrl onto the AuthInfo they return
- (new optional AuthInfo.resourceMetadataUrl field), with a fallback to the
- well-known location for an HTTP(S) RFC 8707 resource identifier; the
- parameter is omitted when neither is available.
Patch Changes
补丁更改
- #2726 6fa4227 Thanks @LuckTerence! - SdkError and SdkHttpError accept standard ErrorOptions as an optional fourth constructor argument and forward it to Error, so a wrapped error is reachable through the standard Error.cause chain. Version-negotiation probe failures (SdkErrorCode.EraNegotiationFailed) now use it: the underlying TypeError: fetch failed and the DNS or socket error beneath it surface via error.cause, so pino, Sentry, and util.inspect render ENOTFOUND / ECONNREFUSED / ETIMEDOUT instead of stopping at the SdkError (#2657). The previous error.data.cause slot is still populated for compatibility but is deprecated and slated for removal; read error.cause instead.
- #2654 03842cd Thanks @pshah19! - Treat request id 0 as a real id. Two guards tested a RequestId for truthiness, so the legal JSON-RPC ids 0 and '' were read as absent. Id 0 is not a corner case: the outbound request counter is zero-based, so it is the first id every peer assigns, which on the server→client leg is the first sampling/createMessage, elicitation/create, or roots/list a server sends.
- notifications/cancelled carrying id 0 was ignored, and the in-flight handler ran to completion with its AbortSignal never fired.
- A notification sent with relatedRequestId: 0 wrongly passed the debounce gate (for methods opted into debouncedNotificationMethods). Because the pending set is keyed by method alone, a second such notification in the same tick was silently dropped rather than sent.
- Absent is now the only value that means "no id".
- #2668 3e90449 Thanks @KKonstantinov! - Stop sending notifications/cancelled for the initialize handshake. The spec is explicit that a client MUST NOT attempt to cancel its initialize request, but the outbound cancel path fired for any in-flight request: aborting the AbortSignal passed to connect(), or letting the handshake hit its timeout, put a forbidden cancellation on the wire naming the initialize request id.
- The local behaviour is unchanged — the caller's promise still rejects with the same abort/timeout error, and connect() still tears the connection down. Only the wire notification is suppressed. Every other method keeps the existing cancellation path.
- #2698 7b781ed Thanks @maxisbey! - Read Streamable HTTP request bodies with a size limit. Every SDK-owned body read —
- WebStandardStreamableHTTPServerTransport (and the Node transport built on it),
- createMcpHandler, toNodeHandler, and createMcpHonoApp's JSON pre-parse — now stops at
- 4 MiB by default (the limit the legacy SSE transport already uses; the Express adapter and stdio
- bound their reads too) and answers 413 Payload Too Large before anything is parsed.
- toWebRequest (when it reads the Node stream itself) now rejects once the body exceeds the
- limit with an error whose name is 'RequestBodyTooLargeError' and status is 413, and
- toNodeHandler answers that with 413; hand-wired callers of toWebRequest should handle the
- rejection or pass a pre-parsed body, and isLegacyRequest reports such a request as non-legacy
- so the modern handler answers it. JSON-RPC batch arrays are limited to 100 messages; a longer
- batch is answered 400 / -32600 and none of it is dispatched.
- The limit is configurable with a new maxRequestBodySize option (bytes, default
- DEFAULT_MAX_REQUEST_BODY_SIZE = 4 MiB, exported from @modelcontextprotocol/server) on
- WebStandardStreamableHTTPServerTransportOptions, CreateMcpHandlerOptions (forwarded to its
- stateless legacy leg; isLegacyRequest and legacyStatelessFallback take the same option),
- CreateMcpHonoAppOptions, and ToNodeHandlerOptions / ToWebRequestOptions (the adapter's
- bound applies before the handler's, so raise both). The bounded reader is exported as
- readRequestBody for adapter authors. Hosts that pre-parse the body and pass it as
- parsedBody skip the SDK's read and its size limit entirely; the batch bound applies either way.
- createMcpHonoApp and createMcpExpressApp now run their Host/Origin validation before the
- JSON body parser, so a request from a disallowed Host or Origin with an invalid JSON body is
- answered 403 rather than 400, and its body is not read.
- #2590 75dc7ea Thanks @davidpavlovschi! - Reject a modern (2026-07-28) POST that omits the required MCP-Protocol-Version header.
- createMcpHandler accepted a request whose body carried a valid per-request _meta
- envelope but whose MCP-Protocol-Version header was absent: the request was classified
- modern, dispatched, and answered 200 — tool handlers ran. Only the mismatch case
- (header present, disagreeing with the body) was rejected, so of the standard headers
- SEP-2243 requires on a modern POST, presence was enforced for Mcp-Method (and for
- Mcp-Name on the methods that mirror params.name / params.uri) but not for
- MCP-Protocol-Version.
- Such a request is now refused with 400 Bad Request and JSON-RPC -32020
- (HeaderMismatch), matching the shape the sibling missing-header cells already emit and
- echoing the request id — per the Streamable HTTP spec, which requires the header on every
- POST and lists a missing required standard header as a HeaderMismatch failure. The
- spec's allowance to treat a header-less request as 2025-03-26 is available only to a
- server that also serves pre-2025-06-18 clients, and permits routing it to legacy
- handling — never serving it as 2026-07-28; under legacy: 'reject' the requirement is
- unconditional.
- Era classification is deliberately unchanged and stays body-primary: a proxy that strips
- the header still must not change the era, so such a request is still classified modern
- and is refused one rung later, at standard-header-validation — the same rung that
- already answers a missing Mcp-Method. Legacy-era traffic is untouched, notifications
- are unaffected, body-less GET / DELETE session operations are method-routed before
- any header validation, and stdio serving (which has no HTTP headers) is not involved.
- Clients built with this SDK always send the header, so no first-party client is affected;
- hand-rolled clients that omitted it must add it.
- #2726 6fa4227 感谢 @LuckTerence!- SdkError 和 SdkHttpError 接受标准的 ErrorOptions 作为第四个可选构造函数参数,并将其转发给 Error,因此可以通过标准的 Error.cause 链访问被包装的错误。版本协商探测失败(SdkErrorCode.EraNegotiationFailed)现在使用它:底层的 TypeError: fetch failed 及其下的 DNS 或套接字错误通过 error.cause 暴露出来,因此 pino、Sentry 和 util.inspect 会渲染 ENOTFOUND / ECONNREFUSED / ETIMEDOUT,而不是停在 SdkError 处(#2657)。之前的 error.data.cause 槽位仍为兼容而填充,但已被弃用并计划移除;请改用 error.cause。
- #2654 03842cd 感谢 @pshah19!- 将请求 ID 0 视为真实 ID。两个守卫对 RequestId 进行了真值测试,导致合法的 JSON-RPC ID 0 和 '' 被视为缺失。ID 0 并非边缘情况:出站请求计数器从零开始,因此它是每个对端分配的第一个 ID,这在服务器→客户端链路中是服务器发送的第一个 sampling/createMessage、elicitation/create 或 roots/list。
- 携带 ID 0 的 notifications/cancelled 被忽略,且正在处理的处理器运行至完成,其 AbortSignal 从未触发。
- 带有 relatedRequestId: 0 的通知错误地通过了防抖动门控(针对选择加入 debouncedNotificationMethods 的方法)。由于待处理集合仅以方法为键,同一时间片内的第二个此类通知被静默丢弃,而非发送。
- Absent 现在是唯一表示“无 ID”的值。
- #2668 3e90449 感谢 @KKonstantinov!- 停止在初始化握手期间发送 notifications/cancelled。规范明确规定客户端不得尝试取消自己的初始化请求,但出站取消路径会对任何进行中的请求触发:向 connect() 传递的 AbortSignal 中止,或让握手超时,都会在链路上放置一个命名了初始化请求 ID 的非法取消操作。
- 本地行为保持不变——调用者的 promise 仍会以相同的中止/超时错误拒绝,且 connect() 仍会拆除连接。仅抑制链路通知。其他所有方法保留现有的取消路径。
- #2698 7b781ed 感谢 @maxisbey!- 使用大小限制读取可流式传输 HTTP 请求体。每个由 SDK 拥有的请求体读取——
- WebStandardStreamableHTTPServerTransport(以及基于它构建的 Node 传输层),
- createMcpHandler、toNodeHandler 和 createMcpHonoApp 的 JSON 预解析——现在默认在
- 4 MiB 处停止(这是旧版 SSE 传输层已使用的限制;Express 适配器和 stdio
- 也限制了它们的读取)并在解析任何内容之前返回 413 Payload Too Large。
- toWebRequest(当它直接读取 Node 流时)一旦请求体超过
- 限制便会拒绝,错误名称为 'RequestBodyTooLargeError',状态码为 413,并且
- toNodeHandler 对此返回 413;手动连接 toWebRequest 调用者应处理该
- 拒绝或传递预解析的请求体,而 isLegacyRequest 会将此类请求报告为非旧版
- 以便现代处理器处理它。JSON-RPC 批处理数组限制为 100 条消息;更长的
- 批处理将收到 400 / -32600 响应,且其中没有任何内容会被分发。
- 该限制可通过新的 maxRequestBodySize 选项进行配置(字节,默认
- DEFAULT_MAX_REQUEST_BODY_SIZE = 4 MiB,从 @modelcontextprotocol/server 导出)在
- WebStandardStreamableHTTPServerTransportOptions、CreateMcpHandlerOptions(转发给其
- 无状态旧版分支;isLegacyRequest 和 legacyStatelessFallback 采用相同选项)、
- CreateMcpHonoAppOptions 以及 ToNodeHandlerOptions / ToWebRequestOptions(适配器的
- 限制先于处理器的限制生效,因此两者都应提高)。有界读取器作为
- 供适配器作者使用的 readRequestBody。预解析请求体并将其作为
- parsedBody 传递的主机完全跳过 SDK 的读取及其大小限制;无论哪种情况,批处理边界均适用。
- createMcpHonoApp 和 createMcpExpressApp 现在在
- JSON 请求体解析器之前运行主机/来源验证,因此来自不允许的主机或来源且 JSON 请求体无效的请求将
- 返回 403 而非 400,并且其请求体不会被读取。
- #2590 75dc7ea 感谢 @davidpavlovschi!- 拒绝缺少必需 MCP-Protocol-Version 标头的现代(2026-07-28)POST 请求。
- createMcpHandler 接受了一个请求体携带有效逐请求 _meta
- 信封但其 MCP-Protocol-Version 标头缺失的请求:该请求被分类为
- 现代版本,已分发并返回 200 —— 工具处理器已运行。仅不匹配的情况
- (标头存在但与请求体不一致)被拒绝,因此在标准标头中
- SEP-2243 要求在现代 POST 上强制存在 Mcp-Method(以及对于
- 镜像 params.name / params.uri 的方法上的 Mcp-Name),但不强制存在
- MCP-Protocol-Version。
- 此类请求现在以 400 Bad Request 和 JSON-RPC -32020
- (HeaderMismatch)被拒绝,这与兄弟缺失标头单元格已经发出的形状相匹配,并
- 回显请求 id —— 根据 Streamable HTTP 规范,该规范要求每个
- POST 都包含此标头,并将缺失的必需标准标头列为 HeaderMismatch 失败。允许
- 规范允许将无标头请求视为 2025-03-26 仅适用于同时服务于
- 2025-06-18 之前客户端的服务器,并允许将其路由到遗留
- 处理流程 —— 绝不应将其作为 2026-07-28 提供服务;在遗留模式下:'reject' 时该要求是
- 无条件的。
- 时代分类故意保持不变且仍以请求体为主:剥离
- 标头的代理仍不得更改时代,因此此类请求仍被分类为现代
- 并在稍后的标准标头验证层级被拒绝 —— 同一层级即
- 已经解决了缺失的 Mcp-Method 问题。遗留时代的流量不受影响,通知
- 不受影响,无主体的 GET / DELETE 会话操作在
- 任何头部验证之前进行方法路由,而 stdio 服务(没有 HTTP 头部)不涉及其中。
- 使用此 SDK 构建的客户端始终发送该头部,因此没有第一方客户端受到影响;
- 手动编写的省略了该头部的客户端必须添加它。
更进一步:量化金融体系
看懂新闻只是起点——沿量化金融路径,把它变成能交付的工程能力