MCP TypeScript SDK 于 2026 年 9 月 28 日发布 1.31.0:给存储的 OAuth 凭据加 issuer 归属绑定,ClientCredentialsProvider 等三个提供者不传 expectedIssuer 会打弃用警告,前置部署 Forward Deployed Engineer(FDE)升级前要给存储放行新字段
MCP TypeScript SDK 于 2026 年 9 月 28 日发布 1.31.0:存储的 OAuth 凭据新增 issuer 归属绑定。
这条改动落在企业侧 MCP 连接器的授权交付这一段。前置部署 Forward Deployed Engineer(FDE)把 MCP 服务器 MCP server 接进客户环境时,授权服务器 authorization server 的地址是客户给的,凭据当初注册在哪一个授权服务器上,就是部署期必须钉死的配置项。
改动来自官方仓库 modelcontextprotocol/typescript-sdk 的合并请求 2888,标题是给存储的 OAuth 凭据绑定签发它的授权服务器。npm 上包的正式名是 @modelcontextprotocol/sdk,latest 标签已指向 1.31.0,上一个版本 1.30.1 发布于 2026 年 9 月 23 日,1.31.0 发布于 2026 年 9 月 28 日 18:59:36 UTC。
这篇只写 1.30.1 与 1.31.0 之间在 src/client/auth.ts、src/client/auth-extensions.ts、src/shared/auth.ts 三个文件上的逐字差异,以及升级、回退两条路径上具体该敲什么,不讨论该不该自建 MCP 网关这类架构选择。
这条改动的现场意义集中在一句话上。前置部署 Forward Deployed Engineer(FDE)在客户环境里最关心的两件事,是这次升级会不会让线上已存的凭据突然失效、以及出问题时能不能安全退回上一个版本。
一、改动前的行为:读回来的凭据不问归属,谁广播就发给谁
1.30.1 的 auth() 在拿到 provider.clientInformation() 与 provider.tokens() 之后直接使用,不检查这些值当初是在哪一个授权服务器上注册或签发的。而 MCP 客户端要访问的授权服务器地址来自服务器端广播的 OAuth 元数据,也就是说,换一个 MCP 服务器、或者同一个 MCP 服务器改了元数据指向,客户端就会把上一次注册得到的 client_id 与 client_secret、以及 refresh_token 直接发给新的地址。
1.31.0 的做法是在 auth() 里加一个绑定键,源码写的是 const issuer = String(authorizationServerUrl),取值就是 discovery 实际使用的那个授权服务器 URL。写回存储时所有对象都被戳上 issuer,读回时先做一次比对。
比对逻辑在 src/client/auth.ts 新增的函数 issuersMatch() 里。它先把两个值各过一遍 new URL() 归一化,所以 scheme、host 与默认端口的写法差异不影响判定;然后做一次 === 比较,并额外容忍任意一侧多一个尾部斜杠。源码注释写明了为什么要容忍斜杠:String(new URL(...)) 的结果带尾斜杠,而授权服务器广播出来的值往往不带。
第二个新增函数是 discardIfIssuerMismatch()。它把存储里读回来的值与 issuer 比对,一致就原样返回;不一致就返回 undefined,调用方于是完全按「什么都没存」来处理,重新走注册或授权流程。
第三个新增函数是 boundElsewhereError(),它构造的报错文本是 OAuth client information is bound to authorization server 加上原服务器地址、再接 and is not presented to 加上新服务器地址,末尾是可操作提示 Clear the stored client information, or correct 加 expectedIssuer,if the authorization server has moved.。走到这个分支的条件在源码里写得很明确:存储里有 client information、但它绑在别的授权服务器上,同时 provider 没有实现 saveClientInformation 或者没有 redirectUrl,也就是无法靠重新注册来重建。
二、三个预注册凭据提供者新增 expectedIssuer,不传会打弃用警告
1.31.0 里多了一个构造参数 expectedIssuer,涉及 ClientCredentialsProvider、PrivateKeyJwtProvider 与 StaticPrivateKeyJwtProvider 三个类,对应的选项类型是 ClientCredentialsProviderOptions、PrivateKeyJwtProviderOptions 与 StaticPrivateKeyJwtProviderOptions。
不传 expectedIssuer 不再算正常用法。新函数 checkedExpectedIssuer() 会在构造阶段打印一条弃用警告,原文是 [mcp-sdk] Omitting expectedIssuer is deprecated. Without it, the MCP server decides which authorization server receives this client's credentials; pass your authorization server's issuer URL as expectedIssuer so they are only sent there.
传了非字符串或者空串则直接抛错,报错文本是 expectedIssuer must be the authorization server's issuer URL。源码用两个 TypeScript 重载把两种用法分开:constructor(options: ClientCredentialsProviderOptions & { expectedIssuer: string }) 是推荐路径,constructor(options: ClientCredentialsProviderOptions) 单独带一条 @deprecated 注释。
这三个类共用的文档注释把语义写清了:expectedIssuer 是这些凭据注册时所用的授权服务器 URL;设置之后 auth() 只会把它们交给该服务器,一旦 MCP 服务器广播出别的 URL 就抛错,判定时容忍一个尾部斜杠的差异。
三、存储格式多一个 issuer 字段,解析授权服务器响应反过来要 omit 掉它
字段定义在 src/shared/auth.ts。OAuthTokens 与 OAuthClientInformation 两个 zod schema 各新增一个字段:issuer: z.string().optional().catch(undefined)。源码注释写明它不属于线上传输格式,原文是 Not part of the wire format,它是客户端的 auth() 在写入存储之前加上、在读取时用来比对的。
catch(undefined) 这个写法的实际效果是:从存储里读回来的 issuer 只要不是字符串就会被吞成 undefined,不会让整个对象的校验直接失败。这条容错是给那些把存储内容原样回吐的 provider 留的空间。
反方向也被处理了。授权服务器返回的响应里没有 issuer 字段,所以解析响应改用两个派生 schema:TokenResponseSchema = OAuthTokensSchema.omit({ issuer: true }),RegistrationResponseSchema = OAuthClientInformationFullSchema.omit({ issuer: true })。受影响的两处解析点是授权码换令牌与动态客户端注册 dynamic client registration 的响应。
官方 release 页的升级说明只给了一条迁移动作,原文是 Storage that rejects unknown fields needs to allow it.,意思是存储层如果拒绝未知字段,就要放行它。这条针对的是把凭据存进 Postgres 表、Redis 哈希或者用 strict 模式 zod schema 校验读回值的那类实现。
这一条容错决定了存量数据能不能平滑过渡。前置部署 FDE 在客户现场做凭据迁移时,把 issuer 塞进去是纯追加动作,不需要回填历史值,也不需要停机重授权。
四、升级前存下来的凭据怎么走:先放行一次,再在首次成功后绑定
升级前存下来的凭据没有 issuer 字段。discardIfIssuerMismatch() 对这种值不做拒绝,源码分支是 typeof stored.issuer !== 'string',此时原样返回,旧数据仍然可用。同一个分支还处理了读回来是字符串以外类型的脏数据,把它归一化成 issuer 为 undefined。
auth() 为这种情况准备了 bindClientInformation():把没有戳的 client information 补上 issuer 再存一次。这次补存被 try/catch 包住,注释说明意图是一个只在注册之后才期望收到这次调用的 provider 会继续工作,只是保持未绑定状态。
tokens 一侧走的是另一条路。refresh_token 存在而 tokens.issuer 为空时,代码会打印第二条警告。原文是 [mcp-sdk] stored OAuth tokens have no issuer property (saved by an earlier version, or by a provider that does not keep it) and are used as-is; make sure your OAuthClientProvider stores what saveTokens() and saveClientInformation() receive unchanged.这条警告里的 used as-is 是关键:无戳的 refresh_token 会被照发,绑定只发生在它下一次被授权服务器接受之后。
读 client information 的动作被抽成 readClientInformation(),比对不通过就抛 boundElsewhereError。它的结果被缓存进外层变量,并用 ??= 合并,因为源码注释承认 provider 可能在被调用的过程中才补写 clientInformation,这属于「一个 provider 可以在请求准备期间填入自己的 client information」。
五、一个明确没被处理的方向:元数据文档里的 issuer 故意不用
绑定键的来源在源码注释里写得很直白:issuer 取的是 discovery 实际使用的授权服务器 URL,它可能来自资源服务器广播、可能来自 discoveryState()、也可能来自旧的以资源 origin 兜底的路径。
注释同时明确写了不用元数据文档里的 issuer,原文是 metadata.issuer is deliberately not used: this version does not check that the metadata document echoes the URL it was fetched from.,意思是这一版不校验授权服务器的元数据文档是否回显了它被获取的那个 URL。把这一条翻译成交付语言:如果客户环境里的授权服务器元数据把 issuer 写成了另一个域名,1.31.0 不会因此拦下来,它拦的是客户端这边换服务器后复用旧凭据。这是本轮的边界。
另一处边界在日志形态上。两条警告都只走 console.warn,没有回调、没有事件、也没有可配置的日志级别,所以它只能在进程标准错误里被检索到。前置部署 Forward Deployed Engineer(FDE)在容器化交付里要拿到这两条线索,得确认容器日志把 stderr 单独收了或者合并进了同一条流。
六、该谁动手、怎么做、怎么退
该动手的是这样三类人。前置部署 Forward Deployed Engineer(FDE)或平台团队在交付里用了 MCP 的官方 TypeScript SDK 并实现了 OAuthClientProvider;或者把凭据存在数据库、KV 或密钥管理服务里,读回时要过一次 schema 校验;或者一个客户端要同时连多个 MCP 服务器,而这些服务器广播的授权服务器地址并不相同。只用 SDK 的 stdio 传输、不带 OAuth 的集成不受影响。
具体动作按顺序是四步。第一步,升级并确认版本:npm i @modelcontextprotocol/sdk@1.31.0,然后跑 npm ls @modelcontextprotocol/sdk 核对解析到的版本是不是 1.31.0,因为这一版改的是运行期行为而不是类型定义,依赖里被间接锁到 1.30.1 不会报任何错。第二步,给存储层放行新字段:Postgres 表加一列,KV 不做字段白名单校验,用 zod 校验读回值的实现要把 strict() 换成 passthrough() 或者显式加上 issuer 字段。第三步,给三个 provider 的构造函数补上 expectedIssuer,值写客户授权服务器的 issuer URL。第四步,上线后在进程标准错误里检索两条字符串,分别是 mcp-sdk 与 stored OAuth tokens have no,命中就说明 provider 没有把 saveTokens() 收到的对象原样存回去。
适用条件要写清。绑定只在「同一个 provider 实例面对不同授权服务器」时才会触发,固定连一个授权服务器的部署不会因此改变任何行为,唯一可见变化是构造 provider 时多一条弃用警告。issuer 比对是按 URL 归一化后比较的,所以把授权服务器从 http 换到 https、或者补上默认端口,都会被判成不同的服务器。
换句话说,收益与代价都落在多授权服务器的部署上。前置部署 Forward Deployed Engineer(FDE)只连一个客户内网授权服务器的那部分集成,升级的真实收益只是不再把 refresh_token 发给不该拿到的地址这一层的对称性,功能上没有可感知变化。
还有一条容易被漏掉的适用条件。绑定键取的是 discovery 实际使用的授权服务器 URL,所以走的是资源服务器广播、discoveryState() 还是旧的以资源 origin 兜底的路径,会直接决定戳上去的值是什么;同一套代码在三种路径下可能得到三个不同的绑定键。
回退分两层。只想消掉抛错而不回滚版本,就在 provider 上显式传 expectedIssuer,让存量凭据的绑定值与新值一致;如果存量凭据已经绑到了错误的值上,报错文本自己给出了动作,即清除已存的 client information,或者修正 expectedIssuer。整版回退就是把依赖锁回 npm i @modelcontextprotocol/sdk@1.30.1,1.30.1 发布于 2026 年 9 月 23 日。回退是安全的:1.31.0 写进存储的 issuer 字段在 1.30.1 里不会被读取,也不会导致 schema 校验失败,因为 OAuthTokensSchema 在 1.30.1 里本来就把未知字段放过去。
七、局限与未覆盖场景
第一条局限是绑定键本身不可信来源的校验缺失,这一点由上游注释自己写明:metadata.issuer 故意不用,本版不校验元数据文档是否回显它被获取的 URL。
第二条局限是旧数据只被放行一次而不是被纠正。没有 issuer 的存量凭据会被照用,并在它第一次被某个授权服务器接受之后绑定到那个服务器上;如果这次接受本身就是发生在一个不该用的服务器上,绑定就固化了一个错误结果。
第三条局限是补存动作的失败被静默吞掉。bindClientInformation() 里的 saveClientInformation 调用外面包了 try/catch 且没有日志,所以一个每次保存都抛错的 provider 会一直保持未绑定,而现场看不到任何提示。
第四条是这一页升级说明只覆盖了一条。官方给出的迁移动作只有「存储层要放行未知字段」一句,对「存量 refresh_token 会被照发」这一点没有给出清理动作,也没有给出如何枚举存量凭据的方法。
第五条是观测面只有标准错误。这一版没有新增指标,也没有把绑定结果暴露成返回值,所以无法从监控侧统计「有多少次因为归属不符而重新走了注册」,只能靠日志检索。前置部署 FDE 若要向客户证明这次升级生效,能拿出的证据只有标准错误里的警告条数,拿不到一个可以写进验收单的计数指标。
本文所有版本号、字段名、函数名、报错文本与源码路径均取自 modelcontextprotocol/typescript-sdk 官方仓库下 1.30.1 与 1.31.0 两个 tag 的源码文件、1.31.0 的 release 页与 changeset 文件,以及 npm registry 上 @modelcontextprotocol/sdk 的包元数据,未在本机运行复现,升级前请按自己环境的实际存储实现与进程日志再核对一遍。
原始来源:
- https://github.com/modelcontextprotocol/typescript-sdk/releases/tag/1.31.0
- https://github.com/modelcontextprotocol/typescript-sdk/pull/2888
- https://github.com/modelcontextprotocol/typescript-sdk/blob/1.31.0/src/client/auth.ts
- https://github.com/modelcontextprotocol/typescript-sdk/blob/1.31.0/src/client/auth-extensions.ts
- https://github.com/modelcontextprotocol/typescript-sdk/blob/1.31.0/src/shared/auth.ts
- https://github.com/modelcontextprotocol/typescript-sdk/blob/1.31.0/.changeset/bind-oauth-credentials-to-issuer.md
- https://github.com/modelcontextprotocol/typescript-sdk/blob/1.30.1/src/client/auth.ts
- https://www.npmjs.com/package/@modelcontextprotocol/sdk
评论(0)
- 暂无评论。