different-ai/openwork · error · EnterpriseMcpOAuthContractError
MCP_OAUTH_ISSUER_MISMATCH
MCP_OAUTH_ISSUER_MISMATCH
Error message
The stored OAuth discovery state no longer matches the issuer bound to this MCP connection.
What it means
The stored OAuth discovery state for an MCP connection must agree with the expected issuer bound to the connection: metadata issuer (when present) must equal the expected issuer, and any authorization_servers advertised in resource metadata must include it. When discoveryIsBound is false, MCP_OAUTH_ISSUER_MISMATCH is thrown because the cached/recorded discovery no longer matches the connection's issuer.
Source
Thrown at packages/enterprise-mcp-client/src/authorization-response.ts:75
const advertisedIssuers = Array.isArray(resourceMetadata?.authorization_servers)
? resourceMetadata.authorization_servers.filter((value): value is string => typeof value === "string")
: undefined
const discoveryIsBound = discoveryIssuer
? isAuthorizationServerDiscoveryBound({
authorizationServerUrl: discoveryIssuer,
authorizationServerMetadata: metadataIssuer ? { issuer: metadataIssuer } : undefined,
resourceMetadata: resourceMetadata
? {
resource: optionalString(resourceMetadata.resource),
authorization_servers: advertisedIssuers,
}
: undefined,
}, expectedIssuer)
: (metadataIssuer === undefined || metadataIssuer === expectedIssuer)
&& (advertisedIssuers === undefined || advertisedIssuers.includes(expectedIssuer))
if (!discoveryIsBound) {
throw new EnterpriseMcpOAuthContractError(
"MCP_OAUTH_ISSUER_MISMATCH",
"The stored OAuth discovery state no longer matches the issuer bound to this MCP connection.",
)
}
if (input.responseIssuer !== undefined) {
if (input.responseIssuer !== expectedIssuer) {
if (
input.mixUpDefense === "distinct-redirect-uri"
&& authorizationServerMetadata?.authorization_response_iss_parameter_supported !== true
) {
return {
defense: "distinct-redirect-uri",
ignoredResponseIssuer: input.responseIssuer,
}
}
throw new EnterpriseMcpOAuthContractError(
"MCP_OAUTH_ISSUER_MISMATCH",View on GitHub (pinned to 2b7df46e8a)
Solutions
- Re-run authorization-server discovery to refresh stored metadata so it matches the current issuer
- Verify expectedIssuer matches the AS's current metadata issuer (check for trailing-slash or URL differences)
- Clear stale discovery/cache records for the MCP connection and re-bind it
- Ensure resource metadata's authorization_servers includes the issuer used for this connection
Example fix
// before
const metadata = cachedMetadata // issuer: https://old-as.example.com
validateMcpAuthorizationResponseIssuer({ response, expectedIssuer: 'https://new-as.example.com', authorizationServerMetadata: metadata })
// after
const metadata = await discoverAuthorizationServerMetadata('https://new-as.example.com') // fresh, matching issuer
validateMcpAuthorizationResponseIssuer({ response, expectedIssuer: 'https://new-as.example.com', authorizationServerMetadata: metadata }) Defensive patterns
Strategy: validation
Validate before calling
function assertDiscoveryBound(metadataIssuer, discoveryUrl, expectedIssuer) {
const metadataOk = metadataIssuer === undefined || metadataIssuer === expectedIssuer;
const urlOk = discoveryUrl === undefined || normalizeIssuer(discoveryUrl) === normalizeIssuer(expectedIssuer);
if (!metadataOk || !urlOk) throw new Error(`Stored discovery (${discoveryUrl}) no longer matches expected issuer ${expectedIssuer} — re-run discovery`);
} Try / catch
try {
validateMcpAuthorizationResponseIssuer(input)
} catch (e) {
if (e instanceof EnterpriseMcpOAuthContractError && e.code === 'MCP_OAUTH_ISSUER_MISMATCH') {
// refresh discovery metadata and re-bind the connection
} else throw e
} Prevention
- Refresh discovery metadata on each OAuth flow instead of trusting long-lived caches
- Normalize issuer URLs (trailing slash, case) when comparing
- Re-bind connections after an AS issuer URL migration
- Ensure resource metadata's authorization_servers includes the bound issuer
When it happens
Trigger: Validating an authorization response where authorizationServerMetadata.issuer differs from expectedIssuer, or the resource metadata's authorization_servers array omits the expected issuer — typically after the AS rotated/renamed its issuer URL or stale discovery state is reused.
Common situations: Authorization server changed its issuer URL (rename, migration to new domain) while the client kept cached metadata; resource metadata lists a different set of authorization servers than the one used for this connection; mixing metadata from two ASes (e.g. staging vs production) in one connection; stale cache after an AS upgrade.
Related errors
- OpenWork-managed MCP OAuth is currently available for local
- OpenWork-managed OAuth requires a remote MCP URL.
- Authorization for ${input.connectionName} did not finish. Co
- MCP_OAUTH_CONFIGURATION_REQUIRED
- AUTH_RESOURCE_DISCOVERY
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/e70faecb75aa9ad6.
Report an issue: GitHub.