---
summary: "`mcp_tool_scopes` claim union + `MCP_AUTH_DISABLE_SCOPE_CHECKS` bypass ([#128](https://github.com/cyanheads/mcp-ts-core/issues/128)) — operator escape hatches for OIDC providers that can't inject scopes into `scope`."
breaking: false
security: false
---

# 0.8.20 — 2026-05-09

Two operator escape hatches for the standard OIDC reality. Authentik, Keycloak < 26.5, and Zitadel issue `authorization_code` tokens whose `scope` claim is fixed at `openid email profile offline_access` — property mappings can't override it, so per-tool scopes can't be injected the standard way. The framework now reads granted scopes from a 3-claim union (`scp` + `scope` + new `mcp_tool_scopes`), and ships an explicit bypass for deployments where no claim-injection path exists at all.

## Added

- **`mcp_tool_scopes` JWT claim** ([#128](https://github.com/cyanheads/mcp-ts-core/issues/128)) — operator-defined custom claim parsed alongside `scp` and `scope`. Accepts space-delimited string (`"tool:foo:read tool:bar:write"`) or array form. Empty-string entries are dropped; arrays containing any non-string entry cause the claim to be ignored entirely. Documented in the `api-auth` skill as the OIDC operator setup path.
- **`MCP_AUTH_DISABLE_SCOPE_CHECKS` env var** (`mcpAuthDisableScopeChecks`, default `false`) — when `true`, bypasses both `withRequiredScopes` (declared `auth: [...]`) and `checkScopes` (runtime-computed scopes inside handlers, including tenant patterns like `team:${input.teamId}:write`) after the auth-context presence check. Token signature, audience, issuer, and expiry validation remain intact. `authFactory` logs a `WARNING` whenever the bypass is active under a non-`none` auth mode. Combine with server-side ACLs — without an in-handler ACL, every authenticated user effectively has every scope.
- **`extractStringScopes` helper** in `claimParser.ts` — shared parser for the three claim sites; handles both array and space-delimited string forms, drops empty-string array entries, ignores arrays with non-string members.

## Changed

- **Granted scopes are now a union of `scp`, `scope`, and `mcp_tool_scopes`** — previously `scp` took precedence and `scope` was only read as a fallback. Operator-visible behavior change for tokens that populate both `scp` and `scope`: those tokens now receive the union of both claims. The Okta-only and OIDC-only cases (the common shapes) are unaffected.
- **`authFactory`** — OAuth-mode startup log upgraded from `debug` to `info` with claim-resolution guidance and a pointer to the bypass flag for ops without claim-injection control.
- **`tool-defs-analysis` skill** 1.0 → 1.1 ([#127](https://github.com/cyanheads/mcp-ts-core/issues/127)) — drops "or env vars" from the recovery-hints smell list. Env var names are actionable: the agent can name the var the user must set or rotate. Internal class names and file paths remain smells.
- **Skill bumps:** `api-auth` 1.0 → 1.1 (claims mapping table, OIDC operator setup section, bypass flag docs), `api-config` 1.3 → 1.4 (env-var table row), `security-pass` 1.3 → 1.4 (Axis 2 check item: bypass-in-production warning).

## Fixed

- **`extractStringScopes` no longer leaks empty-string entries** through array-form claims. Pre-change array handling preserved `['', 'real-scope']` as-is, leaving an empty entry in the granted set. Practical exploitability was bounded (required scopes are non-empty static strings, so `Set.has('')` returned `false`), but the inconsistency vs. the string-path's `.trim()` check is now closed.
