
The MCP TypeScript SDK 2.3.0, released October 2, 2026, adds an optional expectedResource parameter to requireBearerAuth and verifyBearerToken so a Model Context Protocol server can reject a bearer token minted for a different server, and it changes Server.connect() to serve one connection at a time, which breaks any deployment that reused a single server instance across concurrent HTTP requests. If you run an MCP server in front of Docusign, NetSuite, or any other platform API, both changes need action before your next deploy.
The release bumped the core packages together: @modelcontextprotocol/client, @modelcontextprotocol/server, @modelcontextprotocol/core, @modelcontextprotocol/server-legacy, and @modelcontextprotocol/codemod all moved to 2.3.0. The framework adapters trailed slightly behind on their own version lines: @modelcontextprotocol/express and @modelcontextprotocol/hono shipped 2.0.2, @modelcontextprotocol/fastify shipped 2.0.1, and @modelcontextprotocol/node shipped 2.1.1, according to the 2.3.0 release notes. If you pin exact versions in package.json (and you should for a server that handles auth), upgrade the adapter package together with @modelcontextprotocol/server, not independently, since the audience-binding feature depends on both halves agreeing on the expectedResource option.
Two changes matter for anyone running an MCP server against a real backend: audience-bound tokens, and the one-connection, one-request transport model. A smaller but still relevant change: validateOriginHeader and the allowedOrigins option on the Express, Fastify, and Hono helpers now accept ://* wildcard entries, letting a server admit MCP clients that run as browser extensions.
An audience-bound token is a bearer token that an OAuth authorization server issued for one specific resource server (its "audience"), and that the receiving MCP server can now verify was actually meant for it. Before 2.3.0, requireBearerAuth and verifyBearerToken checked that a token was valid and unexpired, but not that it was issued for this server. In a deployment with multiple MCP servers sharing one authorization server, a token minted for Server A would still pass verification on Server B. That is a real cross-server token confusion risk in any multi-tenant agent deployment where several MCP servers trust the same identity provider.
The fix is the new expectedResource parameter, which the docs/serving/authorization.md guide in the SDK repo documents directly: "requireBearerAuth in @modelcontextprotocol/server-legacy takes the optional expectedResource that @modelcontextprotocol/server 2.3.0... added: it accepts only tokens issued for this server (the token's audience)." The option is off by default, so existing deployments do not break on upgrade, but they also do not get the protection until you turn it on.
import { requireBearerAuth } from "@modelcontextprotocol/express";
app.use(
"/mcp",
requireBearerAuth({
verifier: tokenVerifier,
expectedResource: "https://mcp.yourcompany.com/mcp",
})
);Set expectedResource to the canonical resource URL your MCP server registers in its protected resource metadata. Any token whose audience claim does not match gets rejected with a 401, even if it is otherwise valid and unexpired. If you run more than one MCP server behind the same OAuth provider, for example one server scoped to a CRM integration and another scoped to a finance system, turning this on is the single highest-leverage change in the release. Do it on upgrade, not later.
This is the same family of issue the SDK's 2.1.0 release started addressing with DPoP-bound tokens and scope checks: 2.1.0 made sure a stolen token could not be replayed from a different client; 2.3.0 makes sure a valid token cannot be replayed against a different server.
Because MCP SDK 2.3.0 changed Server.connect() to reject a second connection attempt while the instance is already connected, and a stateless Streamable HTTP transport (one built with sessionIdGenerator: undefined) now serves exactly one request before it needs to be replaced. The upgrade notes in the Fastify 2.0.1 release describe it plainly: "One stateless transport for every request... the second HTTP request fails."
Before 2.3.0, a lot of production MCP servers built one McpServer instance and one transport at startup, then reused both across every incoming HTTP request. That pattern worked by accident. In 2.3.0 it fails outright: the second concurrent request either gets a 500 or an ALREADY_CONNECTED error, because the server object can only be attached to one transport connection at a time.
The fix, per the release's upgrade notes, is to create the McpServer and the transport inside the request handler itself, not once at module load:
// Before 2.3.0 (now breaks under concurrent requests)
const server = new McpServer({ name: "docusign-mcp", version: "1.0.0" });
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
await server.connect(transport);
app.post("/mcp", (req, res) => transport.handleRequest(req, res, req.body));// After 2.3.0
app.post("/mcp", async (req, res) => {
const server = new McpServer({ name: "docusign-mcp", version: "1.0.0" });
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});This costs less than it sounds like. The SDK's own changelog notes that creating a server is cheap as of an earlier fix (#2889), so per-request instantiation is the intended pattern going forward, not a workaround. If your server keeps session state across requests (a stateful Streamable HTTP transport with a real sessionIdGenerator), the one-connection-per-transport rule still applies, but a session-bound transport legitimately lives for the life of that session, not one request. Stateless tool servers, the kind you'd put in front of a read-mostly API, are the ones that need the per-request rebuild.
Treat it as a required re-architecture for stateless servers, not an optional cleanup. If you built an MCP server that exposes, say, Docusign Workflow Builder operations or Docusign Navigator agreement data as MCP tools, and you deployed it as a single long-lived McpServer instance behind a load balancer, that server will start throwing connection errors under any real concurrent load the moment you upgrade. The fix is mechanical (per-request instantiation) but it touches every route that calls server.connect(), and it is the kind of regression that only shows up under load, after the upgrade already shipped.
The audience-binding fix matters just as much for this pattern. An MCP server fronting a platform API is usually one of several MCP servers an enterprise runs behind the same identity provider, one for CRM tools, one for the document platform, one for the finance system. Without expectedResource set, a token meant for the CRM server would also authenticate against the document server, which is exactly the kind of lateral-movement risk that Docusign Connect's HMAC verification exists to prevent on the webhook side. If you're already building webhook-driven automation into Docusign Workflow Builder, the same discipline (verify the source, not just the signature) belongs in every MCP server you put in front of it. Teams running inbound webhooks at scale often hand that reliability layer to Baton, which handles HMAC verification and retries so the MCP and workflow layers above it can stay focused on business logic.
If you're earlier in this build, our Claude + Docusign MCP integration guide and the writeup on production Claude agents using the Docusign MCP walk through the auth and tool-design choices this release now forces you to get right from day one.
Partially, and it is worth a five-minute check if you are an ISV doing license review before shipping an MCP server. A September 2026 pull request set the license field in the v2 package manifests to Apache-2.0, after an earlier GitHub issue flagged that the v2.0.0 @modelcontextprotocol/server and @modelcontextprotocol/core packages declared MIT in package.json while the repository's actual LICENSE file described mixed MIT/Apache-2.0 coverage depending on which code a given file descended from. The v1.x line (@modelcontextprotocol/sdk) still ships as MIT-licensed, per its npm page. If your legal or procurement process tracks SDK licenses by package manifest, re-run that check against the v2 packages specifically, not against what v1.x said.
Does expectedResource break existing MCP servers on upgrade?
No. expectedResource is an opt-in parameter on requireBearerAuth and verifyBearerToken; it is off unless you set it, so upgrading to 2.3.0 alone does not reject any tokens that passed before, per the release notes.
Do I need to rebuild my MCP server on every request after upgrading to 2.3.0?
Only if it uses a stateless Streamable HTTP transport (sessionIdGenerator: undefined) and currently shares one McpServer instance across requests. Stateful, session-bound transports keep living for the life of the session; they just can't be reused across unrelated connections.
What error will I see if I don't fix the shared-instance pattern?
A second concurrent request against a reused server and stateless transport fails, either with an HTTP 500 or an ALREADY_CONNECTED error, because Server.connect() now rejects a connection while the instance is already attached to one.
Which packages do I need to upgrade together?
@modelcontextprotocol/server and whichever framework adapter you use (express, fastify, or hono) need to move together, since expectedResource support depends on both sides implementing the option. Mixing an old adapter with a new server package will not error, it will just silently skip the audience check.
If you're running (or planning) an MCP server in front of Docusign, NetSuite, or another platform your team integrates against, the transport change alone is reason enough to audit your deployment before the next release train picks it up automatically. Talk to the fluidlabs team about a working session on your MCP server's auth and transport architecture, or browse the Docusign IAM resources library for more on building production-grade integrations on top of it.
Thirty minutes with an engineer. Within 48 hours you get a scoped plan and a fixed price in writing, or a straight “you don't need us.” No charge, nothing to sign.
Talk to an engineer →or email us at hello@fluidlabs.com