this.authProviders
A tool that calls a CRM, a billing API or GitHub for the caller needs that service's credential, not the token the caller signed in to your server with. this.authProviders gets it. The server registers a credential provider for each service in @FrontMcp({ authProvidersVault }): a factory that acquires the credential, told who the caller is. A tool reads the credential with get() or headers(), or lets this.fetch() send it with credentials: { provider }, and a tool that names the provider in its authProviders option doesn't run without it. New in 1.11.1: before, nothing registered the providers, and this.authProviders was undefined.
@FrontMcp({ authProvidersVault: { providers: [{ name, scope, loading, factory }] } })
const { credential } = await this.authProviders.get(name)
const headers = await this.authProviders.headers(name)
await this.fetch(url, { credentials: { provider: name } })
Reference
authProvidersVault
Register the providers on @FrontMcp:
import { FrontMcp } from "@frontmcp/sdk";
import { HelpDesk } from "./help-desk.app";
import { crmTokenFor } from "./crm-tokens";
@FrontMcp({
info: { name: "help-desk", version: "1.0.0" },
apps: [HelpDesk],
authProvidersVault: {
providers: [
{
name: "crm",
scope: "user",
loading: "lazy",
factory: async ({ userSub }) => {
const token = userSub ? await crmTokenFor(userSub) : undefined;
return token ? { type: "bearer", token } : null;
},
},
],
},
})
export default class Server {}| Option | Type | Default | Description |
|---|---|---|---|
providers | CredentialProviderConfig[] | none | One provider per service. A name listed twice stops the server from starting with Credential provider "…" is already registered. |
enabled | boolean | on when providers has an entry | false leaves this.authProviders unconfigured even with providers listed. |
defaultCacheTtl | number | 3600000 (an hour) | The cacheTtl of a provider that doesn't set one, in milliseconds. 0 means an hour too. |
maxCredentialsPerSession | number | 100 | The most credentials one request's cache holds; the oldest goes first. |
namespace | string | "authproviders:" | The prefix of every key the credentials are kept under. Servers that share a store but not their credentials each need their own. |
useSharedStorage | boolean | true | Keep the credentials in the server's Redis or SQLite auth.tokenStorage, when it has one. false keeps them in memory: see Where credentials are kept. |
Every entry point reads it: @FrontMcp, createFetchHandler(), create() and createDirect(), and the Playground in your browser.
A provider
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | What tools ask for: get("crm"), authProviders: ["crm"], credentials: { provider: "crm" }. |
scope | "global" | "user" | "session" | required | Whose credential it is: one for the whole server, one per signed-in user, or one per session. |
loading | "lazy" | "eager" | required | Both load the credential the first time a request reads it. "eager" loads nothing ahead. |
factory | (context) => Promise<Credential | null> | required | Acquires the credential for the caller in context. Return null when this caller has none; a factory that throws fails the read. |
refresh | (context) => Promise<Credential | null> | factory | What refresh(name) runs. context.existingCredential is the credential this request has read, and undefined if it hasn't read one yet. |
toHeaders | (credential) => Record<string, string> | by the credential's type | The headers headers() and this.fetch() send. |
cacheTtl | number | defaultCacheTtl | How long a credential without an expiresAt is kept, in milliseconds; one with an expiresAt is kept until then. 0 means defaultCacheTtl. |
metadata | Record<string, unknown> | none | Handed to the factory as context.metadata. |
description, required | Accepted and not read. |
A field the schema doesn't know stops the server from starting: Unrecognized key.
The credential is one of @frontmcp/auth's credential types. Without toHeaders, these headers are sent:
type | Fields | Headers |
|---|---|---|
"bearer" | token, expiresAt? | Authorization: Bearer <token> |
"api_key" | key, headerName, headerPrefix? | <headerName>: <headerPrefix><key> |
"oauth", "oauth_pkce" | accessToken, tokenType, refreshToken?, expiresAt? | Authorization: <tokenType> <accessToken> |
"basic" | username, password | Authorization: Basic <base64 of username:password> |
"custom" | customType, data, headers? | headers |
"ssh_key", "mtls", "private_key", "service_account" | none |
expiresAt is in epoch milliseconds. Past it, the credential is expired, and the next read runs the factory again.
Where credentials are kept
A credential the factory returns is kept between requests, under its scope: one for the server, one per signed-in user, or one per session. The next read, in this request or a later one, gets it without running the factory, until it expires: at its expiresAt, or after the provider's cacheTtl. How long a credential is kept shows it.
Two callers have nothing kept, so the factory runs on each of their requests. A caller who isn't signed in has no user credential kept. An anonymous client on MCP 2026-07-28 has no session the server verified, so it has no session credential kept either. A client on an earlier revision has a verified session, and so does a caller of create() or createDirect().
They're kept in the server's memory, one store per server, so a restart, or another instance, acquires them again. A server with auth.tokenStorage set to Redis or SQLite keeps them there instead, unless useSharedStorage is false, so every instance on that store, and a restart, finds them. Each is encrypted with a key derived from VAULT_SECRET, else JWT_SECRET. With neither set, the key is random and only that process has it: see the warning it logs. A server in public or transparent mode has no tokenStorage, so it keeps them in memory.
What the factory is told
| Field | What it is |
|---|---|
userSub, userEmail, userName | The signed-in caller's sub, email and name. undefined for an anonymous caller. |
sessionId | The session the server verified for the request; else principal: and the caller's sub; else, for an anonymous caller without a session, unidentified: and an id that's new on every request, so a session credential isn't kept. |
metadata | The provider's metadata. |
vault | An in-memory AuthorizationVault the server makes for its factories when it starts. It isn't the caller's this.credentials. |
this.authProviders
ExecutionContextBase defines it, so every context class has it.
| Method | Returns | What it does |
|---|---|---|
get(name, { forceRefresh?, scopes?, timeout? }) | Promise<ResolvedCredential | null> | The credential, as { credential, providerId, scope, acquiredAt, expiresAt?, isValid }. Runs the factory unless a valid one is kept; forceRefresh: true runs it anyway and keeps what it returns. null when the factory returned null, or no provider has that name. scopes and timeout are accepted and not read. |
headers(name) | Promise<Record<string, string>> | The credential's headers, from toHeaders or its type. {} without a credential. |
getMany(names, options?), headersMany(names) | Promise<Map<string, ResolvedCredential | null>>, Promise<Record<string, string>> | The same for several providers. getMany() takes get()'s options, and leaves a name whose factory throws out of the map. headersMany() merges the headers, later names winning, and fails as headers() does. |
refresh(name) | Promise<ResolvedCredential | null> | Runs the provider's refresh, or its factory, and keeps the result. null when that returns null or throws, or no provider has that name. |
has(name) | Promise<boolean> | Whether a valid credential is kept for the caller. It never runs the factory. |
listAvailable() | Promise<string[]> | The names has() is true for. |
invalidate(name), invalidateAll() | void | Drop this request's copy. The kept credential stays, so the next get() returns it: use refresh(name) or forceRefresh: true for a new one. |
isRegistered(name), listProviders() | boolean, string[] | The provider names the server registers. |
On a server without authProvidersVault, get(), getMany(), headers(), headersMany() and refresh() throw AuthProvidersNotConfiguredError, naming the provider. has() is false, and the lists are empty.
A tool's authProviders
With authProvidersVault set, each required entry of a tool's authProviders is checked before execute() runs: FrontMCP calls get(name), so the factory runs, and the tool runs only when it returns a credential. Otherwise the caller's credential vault, this.credentials, is checked for the same key, where the server's auth gives one. Without either, the call is refused with JSON-RPC error -32001: requires credentials for crm which are not connected for this session. { name, required: false } lets the tool run without it.
A server without authProvidersVault, whose auth gives no this.credentials, doesn't check authProviders at all.
this.fetch() with credentials: { provider }
this.fetch(url, { credentials: { provider: "billing" } }) adds the headers headers("billing") gives, never with the caller's token, and doesn't follow redirects, so a redirect can't take the credential to another origin. On a server without authProvidersVault, it sends nothing and logs a warning: see this.fetch().
The AuthProvidersVault class
Code that uses @frontmcp/auth directly can keep credentials the way FrontMCP does, with new AuthProvidersVault(storage, { namespace?, defaultCacheTtl?, pepper?, logger? }) (AuthProvidersVaultStorageOptions). storage is a StorageAdapter or NamespacedStorage from @frontmcp/utils. Each credential is encrypted with a key derived from pepper; without one, the key is random, and only that instance can read what it stored. storeCredential(sessionId, providerId, credential, scope, userId?, cacheTtl?) keeps a credential until its expiresAt, else for cacheTtl, else defaultCacheTtl. It doesn't keep a session credential for a session id that starts with UNIDENTIFIED_SESSION_PREFIX ("unidentified:"). Changed in 1.11.1: before, the constructor took an AuthorizationVault, a namespace and a logger, and kept the credentials in that vault; the class doesn't read credentials kept that way.
Caveats
- A revoked credential is served until it expires. Nothing tells the server when a service revokes a token. When the service turns it down, call
refresh(name)and retry;invalidate(name)doesn't drop the kept one. this.authProvidersisn't@App({ authProviders }). An app'sauthProviderslists@AuthProviderclasses, whichthis.authProvidersdoesn't read, and neither is itthis.scope.authProviders, how the server authenticates callers.- The factory is your code. It decides which credential a caller gets: read the caller from
userSub, never from the tool's arguments, which the model writes.
Usage
Sending a service's credential for the caller
The billing service takes one API key for the whole server, so its provider is global, and this.fetch() sends it. The CRM has a token for each user, so its provider is user, and my_crm_contact lists it in authProviders: a caller who has no CRM token doesn't get to run it.
import { App, FrontMcp, Tool, ToolContext, z, type FrontMcpMetadata } from "@frontmcp/sdk";
import { BILLING_API_KEY, crmTokenFor } from "./secrets";
@Tool({ name: "get_invoice", description: "Get an invoice from the billing service", inputSchema: { id: z.string() } })
export class GetInvoice extends ToolContext {
async execute({ id }: { id: string }) {
const response = await this.fetch(`https://billing.example/invoices/${id}`, { credentials: { provider: "billing" } });
return await response.json();
}
}
@Tool({ name: "my_crm_contact", description: "Get the caller's own contact record from the CRM", inputSchema: {}, authProviders: ["crm"] })
export class MyCrmContact extends ToolContext {
async execute() {
const response = await this.fetch("https://crm.example/me", { headers: await this.authProviders.headers("crm") });
return await response.json();
}
}
@App({ id: "help-desk", name: "Help Desk", tools: [GetInvoice, MyCrmContact] })
class HelpDesk {}
export const config = {
info: { name: "help-desk", version: "1.0.0" },
apps: [HelpDesk],
authProvidersVault: {
providers: [
{
name: "billing",
scope: "global",
loading: "lazy",
factory: async () => ({ type: "api_key", key: BILLING_API_KEY, headerName: "X-Api-Key" }),
},
{
name: "crm",
scope: "user",
loading: "lazy",
factory: async ({ userSub }) => {
const token = userSub ? crmTokenFor(userSub) : undefined;
return token ? { type: "bearer", token } : null;
},
},
],
},
} satisfies FrontMcpMetadata;
@FrontMcp(config)
export default class Server {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
The Playground's own calls are anonymous, so get_invoice works from the Call tab and my_crm_contact is refused there; the second test calls it as the signed-in user nour. The headers come from the credential's type: X-Api-Key from headerName, and Authorization: Bearer for a bearer token.
Reading the credential yourself
get() gives the credential itself, for a client library that takes a token rather than headers, and null when the caller has none. A tool that can work without it reads it and decides, instead of listing it in authProviders:
import { Tool, ToolContext } from "@frontmcp/sdk";
@Tool({ name: "crm_status", description: "Say whether the caller's CRM account is connected", inputSchema: {} })
export class CrmStatus extends ToolContext {
async execute() {
const resolved = await this.authProviders.get("crm");
if (resolved?.credential.type !== "bearer") return { connected: false };
return { connected: true, scope: resolved.scope, tokenEndsWith: resolved.credential.token.slice(-4) };
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
How long a credential is kept
The billing key is read from a secret manager once, then kept for an hour, the default cacheTtl. Its provider is global, so every caller shares it. The CRM issues each user a token that expires, so its provider is user, and its factory returns the token's expiresAt: each user's token is kept until then, and the read after it gets them a new one.
import { App, FrontMcp, Tool, ToolContext, z, type FrontMcpMetadata } from "@frontmcp/sdk";
import { issueCrmToken } from "./crm";
import { readSecret } from "./secrets";
@Tool({ name: "service_headers", description: "Show the headers a service gets for the caller", inputSchema: { provider: z.enum(["billing", "crm"]) } })
export class ServiceHeaders extends ToolContext {
async execute({ provider }: { provider: "billing" | "crm" }) {
return await this.authProviders.headers(provider);
}
}
@App({ id: "help-desk", name: "Help Desk", tools: [ServiceHeaders] })
class HelpDesk {}
export const config = {
info: { name: "help-desk", version: "1.0.0" },
apps: [HelpDesk],
authProvidersVault: {
providers: [
{
name: "billing",
scope: "global",
loading: "lazy",
factory: async () => ({ type: "api_key", key: await readSecret("billing-api-key"), headerName: "X-Api-Key" }),
},
{
name: "crm",
scope: "user",
loading: "lazy",
factory: async ({ userSub }) => {
if (!userSub) return null;
const { token, expiresAt } = await issueCrmToken(userSub);
return { type: "bearer", token, expiresAt };
},
},
],
},
} satisfies FrontMcpMetadata;
@FrontMcp(config)
export default class Server {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
The Playground's own calls are anonymous, so the Call tab gets the billing key and no CRM token. The createDirect() server in the second test has a store of its own.
Troubleshooting
Auth provider "…" is not configured: this server registers no auth providers.
The full message is Auth provider "crm" is not configured: this server registers no auth providers. Register it in @FrontMcp({ authProvidersVault: { providers: [...] } }). The tool read a credential on a server without authProvidersVault. The class is AuthProvidersNotConfiguredError, code AUTH_PROVIDERS_NOT_CONFIGURED; thrown from execute(), it keeps that code and its public message, in production too. Changed in 1.11.3: before, it became TOOL_EXECUTION_ERROR, with "Internal FrontMCP error" in production:
import { Tool, ToolContext } from "@frontmcp/sdk";
// The server this tool runs on has no authProvidersVault.
@Tool({ name: "crm_status", description: "Say whether the caller's CRM account is connected", inputSchema: {} })
export class CrmStatus extends ToolContext {
async execute() {
return { connected: (await this.authProviders.get("crm")) !== null };
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
Register the provider in authProvidersVault. If the credential is one the user connects through a link, it's in this.credentials instead. Changed in 1.11.1: before, this.authProviders was undefined on every server, and the read failed with a TypeError.
Tool "…" requires credentials for … which are not connected for this session. Please authorize the required provider(s) and retry.
The tool lists the provider in authProviders, and no credential was found: its factory returned null for this caller (a user provider for an anonymous caller, say), no provider has that name, and the caller's this.credentials doesn't have it either. It's JSON-RPC error -32001, with data.providers, and execute() didn't run. With no link to connect one, there's no authUrl. Sign the caller in, make the factory find a credential, or mark the entry required: false and handle null in the tool, as in Reading the credential yourself. When the user can connect it through a link, the message says so instead: see Progressive auth.
Credential provider "…" is already registered
Two entries of authProvidersVault.providers have the same name, and the server doesn't start (CredentialProviderAlreadyRegisteredError). Give each provider its own name. The first example's last test shows it.
Unrecognized key with path: ["authProvidersVault", "providers", 0]
A provider entry has a field the schema doesn't take, like a misspelled cacheTTL, and the server doesn't start. The keys are listed in A provider.
The factory runs again on every request
Nothing is kept for this caller. A session provider keeps nothing for a caller without a session the server verified, like an MCP client on 2026-07-28 that doesn't sign in, or the Playground's own calls. A user provider keeps nothing for a caller who isn't signed in. And a credential whose expiresAt has passed by the time it's kept isn't kept either.
import { App, FrontMcp, Tool, ToolContext, type FrontMcpMetadata } from "@frontmcp/sdk";
export const factoryRuns = { count: 0 };
@Tool({ name: "desk_ready", description: "Check that the help desk's session token is available", inputSchema: {} })
class DeskReady extends ToolContext {
async execute() {
const first = await this.authProviders.headers("desk");
const second = await this.authProviders.headers("desk");
return { ready: "Authorization" in first && "Authorization" in second };
}
}
@App({ id: "help-desk", name: "Help Desk", tools: [DeskReady] })
class HelpDesk {}
export const config = {
info: { name: "help-desk", version: "1.0.0" },
apps: [HelpDesk],
authProvidersVault: {
providers: [
{
name: "desk",
scope: "session",
loading: "lazy",
factory: async () => ({ type: "bearer", token: `desk_${++factoryRuns.count}` }),
},
],
},
} satisfies FrontMcpMetadata;
@FrontMcp(config)
export default class Server {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
Use a user provider when the credential belongs to the user rather than to one session, and sign the caller in. If the factory runs again after a restart, or on another instance, the server keeps credentials in memory: see Where credentials are kept.
authProvidersVault keeps credentials in auth.tokenStorage, but neither VAULT_SECRET nor JWT_SECRET is set
The full warning goes on: they are encrypted with a random key of this process, so other instances and a restart do not find them. The server keeps credentials in its Redis or SQLite auth.tokenStorage, and has no secret to derive their encryption key from, so it picked a random one at startup. This process still reads them. Another instance, or this one after a restart, can't, and runs the factory again. Set VAULT_SECRET, or JWT_SECRET, to the same value on every instance.
fetch(): credentials.provider was given, but no auth providers are configured; sent without them
The server has no authProvidersVault, so this.fetch() has no provider to ask: see this.fetch().