api-auth
Authentication, authorization, and multi-tenancy patterns for `@cyanheads/mcp-ts-core`. Use when implementing auth scopes on tools/resources, configuring auth modes (none/jwt/oauth), working with JWT/OAuth env vars, or understanding how tenantId flows through ctx.state.
Best use case
api-auth is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Authentication, authorization, and multi-tenancy patterns for `@cyanheads/mcp-ts-core`. Use when implementing auth scopes on tools/resources, configuring auth modes (none/jwt/oauth), working with JWT/OAuth env vars, or understanding how tenantId flows through ctx.state.
Teams using api-auth should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/api-auth/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How api-auth Compares
| Feature / Agent | api-auth | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Authentication, authorization, and multi-tenancy patterns for `@cyanheads/mcp-ts-core`. Use when implementing auth scopes on tools/resources, configuring auth modes (none/jwt/oauth), working with JWT/OAuth env vars, or understanding how tenantId flows through ctx.state.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
## Overview
The framework handles auth at the handler factory level — tools and resources declare required scopes declaratively, and the framework enforces them before calling the handler. No try/catch or manual scope checking required for the common case.
---
## Inline auth (primary pattern)
Declare required scopes directly on the tool or resource definition via the `auth` property. The handler factory checks `ctx.auth.scopes` against these before calling `handler`.
```ts
import { tool } from '@cyanheads/mcp-ts-core';
const myTool = tool('my_tool', {
input: z.object({ query: z.string().describe('Search query') }),
output: z.object({ result: z.string().describe('Search result') }),
auth: ['tool:my_tool:read'],
async handler(input, ctx) {
// Only reached if caller has 'tool:my_tool:read' scope
},
});
```
When `MCP_AUTH_MODE=none`, auth checks are skipped and defaults are allowed.
---
## Dynamic auth
For runtime-computed scopes (e.g., scopes that depend on input values like a team or resource ID), use `checkScopes` from `@cyanheads/mcp-ts-core/auth` inside the handler:
```ts
import { checkScopes } from '@cyanheads/mcp-ts-core/auth';
handler: async (input, ctx) => {
checkScopes(ctx, [`team:${input.teamId}:write`]);
// Continues only if scope is satisfied
},
```
**Signature:** `checkScopes(ctx: Context, requiredScopes: string[]): void`
**Throws:**
- `McpError(Forbidden)` — auth is active and one or more required scopes are missing
- `McpError(Unauthorized)` — auth is enabled but no auth context exists on the request
- No-ops when `MCP_AUTH_MODE=none`
---
## Auth modes
Set via `MCP_AUTH_MODE` environment variable.
| Mode | Value | Behavior |
|:-----|:------|:---------|
| Disabled | `none` | No auth enforcement. All requests allowed. |
| JWT | `jwt` | Local secret verification via `MCP_AUTH_SECRET_KEY`. Requires explicit `DEV_MCP_AUTH_BYPASS=true` to bypass in development. |
| OAuth | `oauth` | JWKS verification against an external issuer. |
### JWT config
| Variable | Required | Purpose |
|:---------|:---------|:--------|
| `MCP_AUTH_SECRET_KEY` | Yes (unless bypass) | Signing secret for HS256 JWT verification. Must be ≥ 32 characters. |
| `DEV_MCP_AUTH_BYPASS` | No | Set to `true` to skip JWT verification in development. Blocked in `NODE_ENV=production`. |
| `DEV_MCP_CLIENT_ID` | No | Client ID injected when bypass is active (default: `'dev-client-id'`). |
| `DEV_MCP_SCOPES` | No | Comma-separated scopes injected when bypass is active (default: `['dev-scope']`). |
**Important:** With `MCP_AUTH_MODE=jwt`, a missing `MCP_AUTH_SECRET_KEY` is a **fatal startup error** unless `DEV_MCP_AUTH_BYPASS=true` is explicitly set. Setting `DEV_MCP_AUTH_BYPASS` in production (`NODE_ENV=production`) is rejected at config parse time.
### OAuth config
| Variable | Required | Purpose |
|:---------|:---------|:--------|
| `OAUTH_ISSUER_URL` | Yes | Token issuer URL (used for JWKS discovery) |
| `OAUTH_AUDIENCE` | Yes | Expected `aud` claim value |
| `OAUTH_JWKS_URI` | No | Override JWKS endpoint (defaults to `{issuer}/.well-known/jwks.json`) |
| `MCP_SERVER_RESOURCE_IDENTIFIER` | No | RFC 8707 resource indicator URI. When set, the OAuth strategy validates that the token's `resource` or `aud` claim matches this value — throws `Forbidden` on mismatch. |
### JWT claims mapping
| Claim | JWT Field | Purpose |
|:------|:----------|:--------|
| `clientId` | `cid` / `client_id` | Identifies the calling client |
| `scopes` | union of `scp`, `scope`, `mcp_tool_scopes` | Granted scope list (see below) |
| `sub` | `sub` | Subject (user or service identity) |
| `tenantId` | `tid` | Tenant identifier — drives `ctx.state` scoping |
`scopes` is the **union** of three claims, in this order:
| Claim | Form | Source |
|:------|:-----|:-------|
| `scp` | array of strings | Okta-style |
| `scope` | space-delimited string | OAuth 2.1 / OIDC standard |
| `mcp_tool_scopes` | array of strings **or** space-delimited string | Custom claim for OIDC providers that cannot inject scopes into `scope` during the `authorization_code` flow (Authentik, Keycloak < 26.5, Zitadel) |
Auth0/Okta-style providers that already populate `scp` or `scope` need no migration. Other deployments add a property mapping returning `{"mcp_tool_scopes": "tool:foo:read tool:bar:write"}` — the framework unions it into `ctx.auth.scopes` alongside the standard claims. Hardcoded claim name; deployments whose IdP cannot emit `mcp_tool_scopes` use the bypass flag below.
### OIDC operator setup (Authentik / Keycloak / Zitadel)
Standard OIDC providers compute the JWT `scope` claim from what the OAuth client requested at the authorization endpoint and ignore property mappings that try to override `scope` in the `authorization_code` flow. Property mappings that inject **other** claim names work fine. To grant per-tool scopes to a Claude.ai or ChatGPT custom connector that doesn't expose scope customization, configure your IdP to return the per-tool scopes under `mcp_tool_scopes` instead of overriding `scope`.
| Provider | Where to configure |
|:---------|:--------------------|
| Authentik | Customization → Property Mappings → new "Scope Mapping" returning `{"mcp_tool_scopes": "tool:foo:read tool:bar:write"}`; bind to the OAuth2/OpenID provider |
| Keycloak (< 26.5) | Client → Client Scopes → Mappers → new "Hardcoded claim" or "Script Mapper" emitting `mcp_tool_scopes` |
| Zitadel | Project → Roles + Action returning `{"mcp_tool_scopes": "..."}` from a pre-token script |
Keycloak ≥ 26.5 ships native MCP integration support; check its release notes before falling back to a custom claim.
### Bypass flag
For environments where no custom claim can be injected (managed services, restricted IdPs), set `MCP_AUTH_DISABLE_SCOPE_CHECKS=true` to bypass scope enforcement entirely.
| Variable | Default | Effect |
|:---------|:--------|:-------|
| `MCP_AUTH_DISABLE_SCOPE_CHECKS` | `false` | When `true`, both `withRequiredScopes` (declared `auth: [...]`) and `checkScopes` (runtime-computed scopes inside handlers) early-return after the auth-context presence check. Token signature, audience, issuer, and expiry validation remain intact. |
The flag bypasses **both** declared `auth: [...]` enforcement and runtime `checkScopes` calls — including tenant isolation patterns like `team:${input.teamId}:write`. Naming is deliberate: this disables all scope checks, not just per-tool ones. Applies to `MCP_AUTH_MODE=jwt` and `MCP_AUTH_MODE=oauth` (no effect under `none`).
A `WARNING`-level log is emitted at startup whenever the flag is active so operators don't lose track of it. Combine with server-side ACLs (path filters, allowlists, tenant rules) — without an in-handler ACL, every authenticated user effectively has every scope.
---
## Endpoints
| Endpoint | Protected |
|:---------|:----------|
| `GET /healthz` | No |
| `GET /mcp` | No |
| `POST /mcp` | Yes (when auth enabled) |
| `OPTIONS /mcp` | Yes (when auth enabled) |
**CORS:** Set `MCP_ALLOWED_ORIGINS` to a comma-separated list of allowed origins, or `*` for open access.
**Stdio mode:** No HTTP auth layer. Authorization is handled entirely by the host process.
---
## Multi-tenancy
`ctx.state` is automatically scoped to the current tenant — no manual key prefixing needed.
### tenantId sources
| Mode | Source | Value |
|:-----|:-------|:------|
| Stdio (any auth mode) | Hardcoded default | `'default'` |
| HTTP + `MCP_AUTH_MODE=none` | Hardcoded default | `'default'` (single-tenant by design) |
| HTTP + `MCP_AUTH_MODE=jwt`/`oauth` | JWT `tid` claim | Auto-propagated from token; `undefined` if absent (fail-closed) |
### Tenant ID validation rules
- Max 128 characters
- Characters: alphanumeric, hyphens, underscores, dots
- Must start and end with an alphanumeric character
- No path traversal sequences (`../`)
- No consecutive dots (`..`)
### Using `ctx.state`
```ts
handler: async (input, ctx) => {
// Automatically scoped to ctx.tenantId — no manual prefixing
await ctx.state.set('item:123', { name: 'Widget', count: 42 });
const item = await ctx.state.get<Item>('item:123');
await ctx.state.delete('item:123');
const page = await ctx.state.list('item:', { cursor, limit: 20 });
// page: { items: Array<{ key, value }>, cursor?: string }
},
```
`ctx.state` throws `McpError(InvalidRequest)` if `tenantId` is missing. Stdio (any auth mode) and HTTP+`MCP_AUTH_MODE=none` default `tenantId` to `'default'` so `ctx.state` works without forcing operators to mint tokens. HTTP+`jwt`/`oauth` deliberately fails closed when the token lacks a `tid` claim — distinct authenticated callers must not silently share state.
---
## Auth context shape
Available on `ctx.auth` inside handlers (when auth is enabled):
```ts
interface AuthContext {
clientId: string; // Required — 'cid' or 'client_id' JWT claim
scopes: string[]; // Required — union of 'scp', 'scope', and 'mcp_tool_scopes' claims
sub: string; // Required — 'sub' claim; falls back to clientId when absent
token: string; // Required — raw JWT or OAuth bearer token string
tenantId?: string; // Optional — 'tid' claim; present only for multi-tenant tokens
}
```
Access directly for conditional logic:
```ts
handler: async (input, ctx) => {
const isAdmin = ctx.auth?.scopes.includes('admin:write') ?? false;
// ...
},
```Related Skills
tool-defs-analysis
Read-only audit of MCP definition language across an existing surface — tools, resources, prompts. Walks every definition file and checks 12 categories the LLM reads to decide whether and how to call: voice & tense, internal leaks, audience leaks, defaults, recovery hints, output descriptions, cross-references, sparsity, examples, structure, mutator observability, unit-bearing numeric names. Produces grouped findings with file:line citations and a numbered options list. Use during polish, after a refactor, or before a release. Complements `field-test` (behavior testing) and `security-pass` (security audit).
setup
Post-init orientation for an MCP server built on @cyanheads/mcp-ts-core. Use after running `@cyanheads/mcp-ts-core init` to understand the project structure, conventions, and skill sync model. Also use when onboarding to an existing project for the first time.
security-pass
Review an MCP server for common security gaps: LLM-facing surfaces as injection vector (tools, resources, prompts, descriptions), scope blast radius, destructive ops without consent, upstream auth shape, input sinks (URL / path / roots / shell / sampling / schema strictness / ReDoS), tenant isolation, leakage through errors and telemetry, unbounded resources, and HTTP-mode deployment surface. Use before a release, after a batch of handler changes, or when the user asks for a security review, audit, or hardening pass. Produces grouped findings and a numbered options list.
report-issue-local
File a bug or feature request against this MCP server's own repo. Use for server-specific issues — tool logic, service integrations, config problems, or domain bugs that aren't caused by the framework.
report-issue-framework
File a bug or feature request against @cyanheads/mcp-ts-core when you hit a framework issue. Use when a builder, utility, context method, or config behaves contrary to the documented API — not for server-specific application bugs.
release-and-publish
Ship a release end-to-end across every registry the project targets (npm, MCP Registry, GitHub Releases for `.mcpb` bundles, GHCR). Runs the final verification gate, pushes commits and tags, then publishes to each applicable destination. Assumes git wrapup (version bumps, changelog, commit, annotated tag) is already complete — this skill is the post-wrapup publish workflow. Retries transient network failures on publish steps; halts with a partial-state report when retries are exhausted or the failure is terminal.
polish-docs-meta
Finalize documentation and project metadata for a ship-ready MCP server. Use after implementation is complete, tests pass, and devcheck is clean. Safe to run at any stage — each step checks current state and only acts on what still needs work.
orchestrations
Pick and run a multi-phase workflow that chains foundational task skills (`git-wrapup`, `release-and-publish`, `maintenance`, `field-test`, `setup`, etc.) end-to-end. Routes user intent to a workflow file under `workflows/` — greenfield builds, maintenance + release, field-test + fix, or known-work + release. Single source for the universal rules (no commits without authorization, no destructive git, no marketing language), the orchestrator posture (own the goal, ground sub-agents in primary sources, verify against the goal), and the sub-agent strategy (orient block, parallel fanout, isolation, normalization) that apply across every workflow. Sub-agents are an optional capability — workflows run linearly when fanout isn't available.
maintenance
Investigate, adopt, and verify dependency updates — with special handling for `@cyanheads/mcp-ts-core`. Captures what changed, understands why, cross-references against the codebase, adopts framework improvements, syncs project skills, and runs final checks. Supports two entry modes: run the full flow end-to-end, or review updates you already applied.
git-wrapup
Land working-tree changes as logical commits — the work grouped by concern, topped by a release commit (version bump, changelog, regenerated artifacts) and an annotated tag. Verify, commit, tag. Stops at "committed and tagged locally" — no push, no publish. The release-and-publish skill picks up from here. Distilled from the git_wrapup_instructions protocol.
field-test
Exercise tools, resources, and prompts against a live HTTP server via MCP JSON-RPC over curl. Starts the server, surfaces the catalog, runs real and adversarial inputs, and produces a tight report with concrete findings and numbered follow-up options. Use after adding or modifying definitions, or when the user asks to test, try out, or verify their MCP surface.
devcheck
Lint, format, typecheck, and verify the project is clean. Use after making changes, before committing, or when the user asks to verify quality.