Auth
Auth in mcify is between the agent and your MCP server. Don’t confuse it with the auth between your server and the upstream API it wraps — those are two different layers.
[ Agent ] ← bearer token → [ mcify server ] ← upstream API key → [ Khipu / Bsale / ... ]Your auth config governs the left arrow only.
Bearer (recommended default)
import { bearer, defineConfig } from '@mcify/core';
defineConfig({ auth: bearer({ env: 'MCIFY_AUTH_TOKEN' }), ...});The agent sends Authorization: Bearer <token>. The runtime compares with process.env.MCIFY_AUTH_TOKEN using a constant-time check.
Generate the token: openssl rand -hex 32. Store it where you store other secrets (Workers wrangler secret, Fly flyctl secrets, Railway env vars, Kubernetes Secret).
API key
import { apiKey } from '@mcify/core';
auth: apiKey({ headerName: 'x-api-key', env: 'MCIFY_API_KEY' }),Same shape as bearer, different header. Useful when the consuming agent already speaks x-api-key to its other backends.
OAuth
For multi-tenant setups where each user has their own credentials:
import { oauth } from '@mcify/core';
auth: oauth({ provider: 'workos', audience: 'mcify-server', // Provider-specific options.}),The runtime validates JWTs against the provider’s JWKS. The decoded claims land in ctx.auth.claims so your handlers can do per-user authorization.
Building a multi-user server? Read the Multi-user / multi-tenant guide — it walks the full pattern (OAuth, custom verify, scoping queries by
userId, the antipattern that breaks isolation).
OAuth provider (mcify as the authorization server)
The oauth() above delegates to an external IdP. To instead let your server issue its own
tokens — the “Connect Claude” flow, where a user pastes your URL, approves in the browser, and no
API key is copied — use oauthProvider(). mcify becomes a full OAuth 2.1 authorization server: it
mounts the discovery metadata, Dynamic Client Registration, /authorize, and /token, enforces
PKCE S256 + single-use codes + refresh rotation, and answers 401 + WWW-Authenticate so the agent
bootstraps itself.
import { defineConfig, oauthProvider, MemoryOAuthStore } from '@mcify/core';
defineConfig({ auth: oauthProvider({ issuer: process.env.MCIFY_ISSUER, // or derived from the request origin store: new MemoryOAuthStore(), // bring a Postgres/KV adapter for production authorize: async (request) => { const session = await readSession(request); // your cookie/session if (!session) return { status: 'redirect', url: `${DASHBOARD}/consent?...` }; return { status: 'authenticated', subject: { userId: session.userId } }; }, }), tools: [ /* ... */ ],});You provide two pluggable pieces: an OAuthStore (persistence) and an authorize(request) hook
(your session + consent UI). The authenticated subject is opaque to mcify and surfaces on
ctx.auth.subject in your handlers. Everything runs on Web Crypto, so it works on Node, Bun, and
Cloudflare Workers unchanged.
Read Securing an OAuth 2.1 authorization server for MCP for the discovery chain, the non-negotiables, and the production gotchas.
Custom verify
If your token shape doesn’t fit bearer / apiKey, pass a verify function:
auth: bearer({ verify: async (token, ctx) => { const session = await mySessionStore.lookup(token); if (!session) return null; // → 401 return { token, claims: { userId: session.userId, scopes: session.scopes } }; },}),The runtime calls verify once per request, caches the result for the duration of that request, and exposes the return value as ctx.auth.
None
For local dev or fully-public servers:
import { auth } from '@mcify/core';
auth: auth.none(),Don’t ship this to production unless your server only exposes idempotent reads of public data. Even then, rateLimit middleware on every tool is mandatory.
Per-tool auth
The server-level auth is the gate. To require an additional check per tool, use requireAuth middleware with a predicate:
defineTool({ middlewares: [ requireAuth({ check: (auth) => auth.claims.scopes?.includes('payments:write'), message: 'requires the payments:write scope', }), ], ...});requireAuth returns 403 (not 401) when the request authenticated but lacks the right scope. The agent gets a useful error.