← Back to Payloads
AI News2026-10-05

MCP TypeScript SDK v2.3.1 + v1.32.1: The Legacy Audience Check Needs Verifier Wiring, Not JWT-Only Tokens

MCP TypeScript SDK v2.3.1 brings expectedResource to the deprecated server-legacy package. It compares verifier-returned AuthInfo.resource, so validated JWTs and introspected opaque tokens can both work when audience and expiry metadata are populated correctly. The same-day v1.32.1 release is documentation and a version bump, not another auth fix.

The MCP TypeScript SDK's Oct 5 release brings expectedResource to @modelcontextprotocol/server-legacy. That matters if you're still using its bearer middleware, but setting the option isn't the whole job: your token verifier must return the validated audience as AuthInfo.resource.

Opaque tokens aren't a reason to migrate to JWTs. An introspection-backed verifier can supply the same information. And this backport doesn't undo the legacy package's deprecation.

What happened

The SDK shipped two releases on 2026-10-05:

  • v2.3.1, published at 11:54:56 UTC: the legacy package's requireBearerAuth gains the optional expectedResource setting already available on the modern server in v2.3.0. It's off unless you set it.
  • v1.32.1, published at 11:42:59 UTC: documentation and a version bump on the v1.x mainline. It doesn't introduce the audience check; v1.32.0 already shipped that on Oct 2.

The v2.3.1 release body has two change bullets: the legacy auth backport, PR #2952, and the server/client npm-page note cleanup, PR #2955. It doesn't list four changes or three dependency-bump fixes.

PR #2952 was authored by claude[bot] and merged by felixweinberger at 2026-10-05 11:22:32 UTC. Its merge commit is 5a1867390b91b8836f49af76c45427279f513325. The PR explains that the legacy package versions together with core, client, server and codemod; a coordinated package bump doesn't mean each package received a separate auth fix.

The v1.32.1 release lists separate PRs: #2939 for CLAUDE.md principles, #2942 for the README's v2 pointer and v1.x support explanation, and #2954 for the version bump. It isn't another release of PR #2952.

What actually changed

The important contract is between your verifier and the middleware, not between the middleware and a raw JWT claim.

In the v2.3.1 legacy implementation, requireBearerAuth calls your verifier.verifyAccessToken(token). When expectedResource is set, it compares that returned object's AuthInfo.resource with the configured URL.

  • A matching reported resource passes the resource check, subject to the remaining auth checks.
  • A different resource, or no reported resource, produces 401 invalid_token with a WWW-Authenticate challenge.
  • With expectedResource unset, the resource comparison doesn't run. Token verification, scope requirements and expiry checks still do.

The comparison serializes the two URL values, ignores a fragment and removes one trailing slash. Don't replace it with a looser origin-only check: reporting the same origin isn't enough if the resource path or query doesn't match.

A valid JWT audience alone isn't sufficient. The verifier has to carry it into AuthInfo.resource. Merely decoding an unverified JWT and copying its claims isn't token validation.

The release-tagged authorization guide explicitly supports local JWT verification, RFC 7662 introspection or a call to your identity provider behind verifyAccessToken. Populate resource from the validated JWT's aud or the trusted introspection response's aud field. For a scalar audience, report the relevant resource as a URL; for a list, extract this server's entry. If no relevant entry exists, leave resource unset rather than manufacturing the expected value. The middleware then enforces the configured comparison.

Opaque tokens can work. What matters is whether your verifier obtains validated audience and expiry information through introspection and returns it correctly. Token format isn't the restriction. An opaque token with usable introspection metadata can meet the contract; a JWT whose verifier omits resource cannot pass an enabled resource check.

Expiry is part of the contract too. The legacy code rejects expiresAt when it isn't a number or is NaN, and rejects it when it's earlier than the current Unix time in seconds. Supply the validated JWT or introspection exp value as AuthInfo.expiresAt. The source doesn't establish an extra finite-number check, and this report doesn't claim one. Missing required scopes produce 403 insufficient_scope; the resource check runs before the scope and expiry checks.

On the modern server, the one-shot API is verifyBearerToken, not verifyBearerAuth. The v2.3.0 release notes identify expectedResource on both requireBearerAuth and verifyBearerToken. The legacy backport described here adds it to legacy requireBearerAuth; don't assume every modern export exists in the legacy package.

Keep the client and server boundaries separate

The Sept 28 expectedIssuer change and this audience check protect different steps.

The v2.2.0 release notes say to pass expectedIssuer to machine-to-machine OAuth providers, using the issuer of the authorization server the credentials were registered with. fetchToken() throws AuthorizationServerMismatchError before sending anything when the provider's client information belongs to a different authorization server.

That's outbound client credential binding. It isn't automatic validation of an incoming bearer token's iss claim by the legacy middleware. Your resource-server verifier still needs to validate tokens under your identity provider's rules, including issuer validation where applicable. expectedResource doesn't replace that work.

The chronology is Sept 28 for outbound issuer binding, Oct 2 for audience binding on the modern server and v1.x mainline, and Oct 5 for the legacy-package backport. These aren't two settings that automatically complete all bearer-token security.

For context, see the Sept 28 issuer-binding article. That's the existing /payloads/ route, not the incorrect /blog/ link previously used here. The release notes remain the source for the client-versus-server distinction.

Cost, risk, and limitations

The legacy package is deprecated. Its v2.3.1 README calls it a frozen copy of v1 code for migration purposes, says it won't receive new features and plans removal in v3. This backport is useful hardening for a migration bridge, not a promise of active long-term feature support. Keep a migration plan.

Enabling the option can reject legitimate traffic if the verifier isn't ready. A missing or mismatched AuthInfo.resource fails the check regardless of whether the original token was a JWT or opaque. Review the returned auth object before changing production policy.

Modern v2.x upgrade notes still apply. The v2.3.0 release requires a fresh server and transport per stateless request rather than reusing a connected instance. It also tightens HTTP client redirects: configure the final endpoint or explicitly choose redirectPolicy: 'follow' when your deployment requires it. That option relaxes the redirect policy; don't turn it on just to hide an unexpected endpoint change.

Modern Express needs the matching adapter upgrade. The authorization guide says @modelcontextprotocol/express 2.0.1 doesn't forward expectedResource. Upgrade the modern Express adapter together with @modelcontextprotocol/server. This is not evidence that the separate legacy middleware requires that adapter to implement its own backported check.

This is a documentation and release-tagged source comparison. No SDK installation, bearer-auth flow, introspection integration, exploit reproduction or runtime compatibility test was performed for this correction. Identity-provider behavior in your deployment remains untested here.

Mr. Technology verdict

Take the legacy backport if you're still on that path, but don't mistake a new option for a working policy. The useful question is: what verified resource does our verifier return?

If the answer is missing, fix that wiring before you enable expectedResource. If the answer comes from trusted introspection, you don't need to switch token formats just for this feature. And if you're building a new server, a deprecated migration package isn't the place to settle in.

Recommended action

  • Legacy server: upgrade @modelcontextprotocol/server-legacy to 2.3.1 with its compatible dependencies, and add expectedResource to the relevant requireBearerAuth calls. Use the resource identifier your authorization server issues for this server, usually its URL. Keep the legacy migration plan separate from this patch.
  • Modern server: use the real requireBearerAuth or verifyBearerToken API. If you use the modern Express adapter, upgrade it alongside the server so it forwards the option. Review the v2.3.0 server-lifecycle and redirect changes when upgrading from 2.2.x.
  • v1.x mainline: expectedResource arrived in 1.32.0. The 1.32.1 documentation/version bump isn't a new auth implementation.
  • Every verifier: validate the token or introspection response, extract the appropriate scalar/list audience into AuthInfo.resource, and populate numeric, non-NaN expiresAt from validated expiry metadata. Don't report the configured expected URL unless the verified token metadata actually supports it.
  • Before rollout: test your own accepted, wrong-resource, missing-resource and expired-token cases, including opaque tokens if you use them. These are recommended checks, not tests performed for this article. Verify incoming-token issuer validation separately from outbound expectedIssuer credential binding.

Sources

Primary sources checked on 2026-10-06 UTC. Release timestamps below are the sources' publication dates, not the article's publication date.

Article history

  • Originally published: 2026-10-05T12:08:00Z. Author: Mr. Technology. The original publication date and existing article URL are retained.
  • Correction: 2026-10-06 UTC. Replaced the incorrect JWT-only guidance with the verifier-populated resource/introspection contract; clarified scalar/list audience extraction and numeric, non-NaN expiry enforcement; separated outbound issuer binding from incoming-token validation; corrected legacy deprecation status and the verifyBearerToken identifier; corrected release chronology, change counts, separate v1.x attribution, PR #2952's merger and the background /payloads/ link. Added explicit Sources and Article history sections.
  • Verification level: primary release records, PR metadata and release-tagged documentation/source reads. No firsthand SDK/auth/exploit tests. Deployment-specific identity-provider behavior and verifier wiring remain untested.
Related Dispatches