TL;DR — The MCP 2026-07-28 release candidate closes three separate authorization gaps: SEP-2468 forces MCP clients to validate the OAuth iss claim on every authorization response, SEP-837 makes MCP clients declare an OpenID Connect application_type during Dynamic Client Registration, and the base spec now requires every MCP server to implement OAuth 2.0 Protected Resource Metadata (RFC 9728) for authorization-server discovery. None of that reads like production advice until you run a multi-server gateway — something like the Hermes Tool Gateway we operate daily — where issuer validation collides with your circuit breaker, the new DCR field changes how outbound stdio clients register, and PRM turns your inbound MCP server into an OAuth resource server whether you planned for it or not.
Every migration guide published this month walks the same list: stateless sessions, new error codes, deprecated transports. Ours is not that guide — there is already a solid breaking-changes rundown at MCP Spec Ships July 28 if that is what you need. What none of those checklists cover is what happens when the three auth SEPs hit a gateway that already has a circuit breaker, a tool allowlist, and pinned server versions in production. That is the part we actually had to change.
What "Release Candidate" Means for 2026-07-28
MCP protocol versions are named for the date they lock, not the date they ship. 2026-07-28 is the target version string, and as of this writing it is still in its release-candidate window — not the final spec.[1] Beta SDKs for Python, TypeScript, Go, and C# shipped with the RC, and the maintainers were explicit about the risk window: "public APIs may still change between the betas and the stable releases, so pin exact versions."[2]
That single line of guidance is the same discipline the Hermes Tool Gateway reference already drills into every mcpServers entry — pin @modelcontextprotocol/server-filesystem@1.9.2, not @latest, because a patch version silently changed list_directory's output format and broke a downstream parser. The RC SDKs deserve the identical treatment. Anything you wire up against the 2026-07-28 betas this week goes in with a version pin and a note to re-test at GA, not as a permanent production dependency.
Change 1: Issuer Validation Meets Multi-Server Composition (SEP-2468)
SEP-2468 requires MCP clients to validate the OAuth iss parameter on every authorization response, closing what the spec calls a mix-up attack: a flow where a client associates an authorization response with the wrong authorization server and leaks a token to, or accepts a token from, the wrong identity provider.[3] The draft authorization spec spells out the exact client behavior as a decision table keyed on whether the authorization server advertises authorization_response_iss_parameter_supported and whether iss is present in the response — and in three of the four combinations, the client validates or rejects. Only the combination of no advertised support and no iss present lets the flow proceed unchanged.[6]
This is where a single-client tutorial and a gateway diverge. A single MCP client talks to one authorization server per session. A gateway running the Hermes Tool Gateway's multi-server composition pattern — filesystem, GitHub, Postgres, and an OAuth-protected remote server like Stripe, all wired into one mcpServers block — is exactly the "single client, many authorization servers" shape SEP-2468 was written for. Before this RC, nothing stopped a bug in your callback router from associating the GitHub app's authorization code with the Stripe client's expected issuer. After it, that mismatch is a hard rejection instead of a silent cross-wire.
The behavior client-side looks like this, and both the Go and TypeScript SDK reference implementations follow it exactly — record the expected issuer before redirecting, then compare on the way back:[3]
// Before SEP-2468: authorization code is trusted on arrival
function handleCallback(code, state) {
return exchangeCodeForToken(code, state)
}
// After SEP-2468: issuer is recorded at redirect time and
// checked before the code is ever sent to a token endpoint
function startAuthorization(server) {
pendingFlows.set(state, {
expectedIssuer: server.metadata.issuer, // from validated RFC 8414 metadata
codeVerifier,
})
}
function handleCallback(code, state, iss) {
const flow = pendingFlows.get(state)
if (iss !== undefined && iss !== flow.expectedIssuer) {
throw new Error('issuer mismatch — aborting authorization flow')
}
return exchangeCodeForToken(code, flow.codeVerifier)
}
Here's the trap: if your gateway's circuit breaker — the use_gateway block from Pattern 8 of the Hermes reference — counts every failed tool-server interaction toward its failure_threshold without distinguishing an issuer mismatch from a timeout, a host that has not yet updated its callback handler to pass iss through will look identical to a dead upstream. Five rejected callbacks and the circuit opens, the server gets marked unavailable for the full recovery_timeout_s, and the actual problem — a callback signature that needs a new optional argument — never surfaces in the alert. We cover the fix for that specifically in the circuit-breaker section below.
Change 2: Dynamic Client Registration Stops Guessing What Your Gateway Is (SEP-837)
SEP-837 requires MCP clients to declare an appropriate OpenID Connect application_type during Dynamic Client Registration.[4] Before this change, the field was left empty, and most OIDC providers default an unspecified client to "web" — which then rejects the localhost redirect URI that every stdio-based client, CLI tool, or desktop agent needs. A gateway process spawning MCP servers over stdio, exactly as Pattern 1 of the Hermes Tool Gateway guide does with command: npx, is a native/CLI client by definition. It was getting classified as a browser-based web app and failing registration against any OIDC-strict authorization server.
The before/after at the registration request is small but decisive:
// Before SEP-837 — application_type omitted, AS defaults to "web"
POST /register HTTP/1.1
Content-Type: application/json
{
"client_name": "hermes-orchestrator",
"redirect_uris": ["http://localhost:33421/callback"],
"grant_types": ["authorization_code", "refresh_token"]
}
// → many OIDC providers reject: redirect_uri not permitted for "web" clients
// After SEP-837 — application_type declared explicitly
POST /register HTTP/1.1
Content-Type: application/json
{
"client_name": "hermes-orchestrator",
"application_type": "native",
"redirect_uris": ["http://localhost:33421/callback"],
"grant_types": ["authorization_code", "refresh_token"]
}
// → localhost redirect accepted under native-client rules
Fold that into the config from Pattern 3 of the Hermes reference — the OAuth-protected remote server pattern used for Stripe — and the client registration step that Hermes performs against mcp.stripe.com/oauth/token now needs application_type: "native" set explicitly if you are on Dynamic Client Registration rather than a pre-registered client ID. Get this wrong under the current spec and the failure is silent right up until token exchange; get it wrong under the RC and beta SDKs and it fails at registration, which is a faster and more honest failure mode.
There is a second thread worth pulling here. The base spec now recommends OAuth Client ID Metadata Documents (CIMD) over Dynamic Client Registration, and formally deprecates DCR — retained only for backward compatibility with authorization servers that have not adopted CIMD, under a twelve-month deprecation window.[6] SEP-837 fixes DCR's biggest practical bug at the exact moment DCR itself stops being the recommended path. Fix the application_type gap this week if you are still on DCR — every server you talk to still needs it — but the actual migration target for new outbound connections is CIMD, where your client ID is a hosted HTTPS metadata document instead of a registration round trip.
Change 3: Protected Resource Metadata Makes Your Server Side an OAuth Citizen (RFC 9728)
The base authorization spec is unambiguous here: "MCP servers MUST implement OAuth 2.0 Protected Resource Metadata (RFC9728). MCP clients MUST use OAuth 2.0 Protected Resource Metadata for authorization server discovery."[6] RFC 9728 itself defines the mechanism: a protected resource publishes a JSON document at /.well-known/oauth-protected-resource that names the authorization servers a client should use, and the resource server signals its location in a WWW-Authenticate challenge on a 401.[8]
Everything up to this point in the RC is about your gateway as an OAuth client — the outbound side, Patterns 1 through 4 and 8 of the Hermes reference. RFC 9728 flips it. The moment your gateway runs Pattern 5, Hermes serving itself as an MCP server to Claude Code or any other external agent, it is now the OAuth resource server, and this MUST clause applies to it directly. That means your own hermes mcp serve process needs to answer a discovery request, not just make one:
GET /.well-known/oauth-protected-resource HTTP/1.1
Host: hermes-orchestrator.internal
HTTP/1.1 200 OK
Content-Type: application/json
{
"resource": "https://hermes-orchestrator.internal/mcp",
"authorization_servers": ["https://auth.yourcompany.com"],
"scopes_supported": ["run_task:write", "run_task:read"]
}
And on an unauthenticated call, the 401 needs to point there:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://hermes-orchestrator.internal/.well-known/oauth-protected-resource",
scope="run_task:write"
The scope parameter in that header is doing work you already understand from the client side of your own gateway. The spec's scope-selection strategy — use the challenged scope if present, fall back to scopes_supported otherwise — is the OAuth-layer counterpart to the include/exclude tool whitelist from Patterns 2 and 11 of the Hermes reference. Where include: [list_issues, get_issue, create_issue] hides tools your gateway should never call, a well-scoped run_task:write now hides operations an external caller's token should never be able to invoke, enforced by the authorization server instead of by your tool registry alone. Run both layers together and a compromised token buys an attacker nothing your tool allowlist did not already refuse to expose.
What the Stateless Redesign Does to Your Circuit Breaker
The RC's largest change is not an auth SEP at all — it removes the initialize/initialized handshake and the Mcp-Session-Id header entirely, making every request self-describing instead of tied to a stateful connection.[5] That is good news for infrastructure: a gateway can now sit behind a plain round-robin load balancer instead of needing sticky sessions, because there is no server-side session to route back to.
It is also the reason the issuer-mismatch trap from Change 1 matters more than it looks. Without sessions, every request re-authenticates independently, so an integration gap in your callback handling does not fail once at connection time — it fails on every single request through every replica behind the load balancer. That is a fast way to trip a naive failure_threshold. The fix is to make your circuit breaker status-code-aware instead of failure-count-blind:
# ~/.hermes/config.yaml — circuit breaker that separates auth
# rejections from actual upstream failures (extends Pattern 8)
mcpServers:
stripe:
transport: http
url: "https://mcp.stripe.com/v1"
use_gateway:
timeout_seconds: 30
circuit_breaker:
failure_threshold: 5
recovery_timeout_s: 60
# New in the auth-hardening pass: don't let issuer
# mismatches or 401s from a stale callback signature
# count toward the same threshold as timeouts/5xx.
count_toward_threshold:
- timeout
- "5xx"
alert_separately:
- "401_iss_mismatch"
- "403_insufficient_scope"
That split is not cosmetic. A circuit that opens because Stripe's MCP endpoint is genuinely down should recover on its own in 60 seconds. A circuit that opens because your host forgot to extract iss from the redirect URI will still be open in 60 seconds, and in 60 minutes, because nothing about waiting fixes a code path that was never updated. Routing those two failure shapes to different alerts is the difference between a self-healing gateway and a page at 2 a.m. for a bug that has been sitting in production for a week.
This Week vs. When the Spec Locks
| Change | Do this week (RC + beta SDKs) | Do when 2026-07-28 goes final |
|---|
| SEP-2468 (issuer validation) | Pin the beta SDK; audit every OAuth callback handler in your gateway for whether it extracts and forwards iss | Treat client-side validation as MUST, not best-effort — the spec expects the current SHOULD on servers to become MUST in a future revision |
| SEP-837 (DCR application_type) | Set application_type: "native" on every stdio/CLI client still using Dynamic Client Registration | Start migrating new outbound OAuth connections to Client ID Metadata Documents; DCR is deprecated but usable for 12 months |
| RFC 9728 (Protected Resource Metadata) | Stand up /.well-known/oauth-protected-resource on any gateway you expose as an inbound MCP server | Wire real per-scope enforcement server-side so the WWW-Authenticate scope claim matches what your tool allowlist already restricts |
| Stateless redesign | Split circuit-breaker alerting by status-code shape, not just failure count | Remove any lingering session-affinity assumptions from load balancer config once GA SDKs confirm the handshake is gone for good |
None of this is deploy-today territory. WorkOS's read on the RC is the right framing for an operator: "the path from 'unauthenticated MCP server' to 'properly secured MCP server' is now defined at the spec level rather than left as an exercise."[7] That is true, and it is also why a gateway with real production traffic cannot just flip the switch on RC behavior — you test the issuer-validation path against your own callback handlers with a scratch OIDC tenant, you set application_type on new registrations without ripping out working DCR flows, and you publish Protected Resource Metadata on a staging gateway before your production one. The spec locking on 2026-07-28 is the deadline for having done that testing, not the moment you start it.
A JWT decoder earns its keep here — run any access token your gateway issues or consumes through our free JWT Decoder to eyeball the iss and aud claims before you wire up the validation logic above, and our JSON Formatter is the fastest way to sanity-check a hand-written Protected Resource Metadata document before you serve it. If the config surface here looks familiar, it should — the MCP Server Pack ships the pinned, circuit-breaker-hardened server configs this RC now needs to account for, and the AI Agent Ops Bundle covers the observability and cost-control side of running exactly this kind of multi-server gateway in production.
For the governance layer that sits on top of all three changes — least-privilege scopes, audit trails, tool-level allowlists — see the WOWHOW Least-Privilege MCP Governance Layer. And if you are building or buying tools for a gateway like this, browse the full WOWHOW catalog for the rest of the stack.
Sources
The 2026-07-28 MCP Specification Release Candidate — Model Context Protocol Blog
Beta SDKs for the 2026-07-28 MCP Spec Release Candidate — Model Context Protocol Blog
SEP-2468: Recommend Issuer (iss) Parameter in MCP Auth Responses — Model Context Protocol
SEP-837: Update authorization spec to clarify client type requirements — GitHub PR
Key Changes — Model Context Protocol Specification Changelog
Authorization — Model Context Protocol Specification (draft)
The biggest MCP spec update ships July 28: What changes for AI agent authentication — WorkOS
8. RFC 9728 — OAuth 2.0 Protected Resource Metadata — IETF Datatracker
Originally published at wowhow.cloud
Top comments (0)