Agent skill
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.
Install this agent skill to your Project
npx add-skill https://github.com/cyanheads/mcp-ts-core/tree/main/skills/api-auth
Metadata
Additional technical details for this skill
- type
- reference
- author
- cyanheads
- version
- 1.0
- audience
- external
SKILL.md
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.
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:
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 missingMcpError(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 |
scp / scope |
Space-separated list of granted scopes |
sub |
sub |
Subject (user or service identity) |
tenantId |
tid |
Tenant identifier — drives ctx.state scoping |
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
| Transport | Source | Value |
|---|---|---|
| HTTP with auth | JWT tid claim |
Auto-propagated from token |
| Stdio | Hardcoded default | 'default' |
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
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. In stdio mode, tenantId defaults to 'default' so ctx.state works without auth.
Auth context shape
Available on ctx.auth inside handlers (when auth is enabled):
interface AuthContext {
clientId: string; // Required — 'cid' or 'client_id' JWT claim
scopes: string[]; // Required — derived from 'scp' or 'scope' claim
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:
handler: async (input, ctx) => {
const isAdmin = ctx.auth?.scopes.includes('admin:write') ?? false;
// ...
},
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
add-resource
Scaffold a new MCP resource definition. Use when the user asks to add a resource, expose data via URI, or create a readable endpoint.
field-test
Exercise tools, resources, and prompts with real-world inputs to verify behavior end-to-end. Use after adding or modifying definitions, or when the user asks to test, try out, or verify their MCP surface. Calls each definition with realistic and adversarial inputs and produces a report of issues, pain points, and recommendations.
release
Verify release readiness and publish. The git wrapup protocol handles version bumps, changelog, README, commits, and tagging during the coding session. This skill verifies nothing was missed, runs final checks, and presents the irreversible publish commands.
add-export
Add a new subpath export to the @cyanheads/mcp-ts-core package. Use when creating a new public API surface that consumers import from a dedicated subpath (e.g., @cyanheads/mcp-ts-core/newutil).
api-errors
McpError constructor, JsonRpcErrorCode reference, and error handling patterns for `@cyanheads/mcp-ts-core`. Use when looking up error codes, understanding where errors should be thrown vs. caught, or using ErrorHandler.tryCatch in services.
api-utils
API reference for all utilities exported from `@cyanheads/mcp-ts-core/utils`. Use when looking up utility method signatures, options, peer dependencies, or usage patterns.
Didn't find tool you were looking for?