# this.authProviders

> Get another service's credential for the current caller, from the credential providers a server registers in authProvidersVault, and send it with this.fetch().

Source: https://frontmcp.dev/reference/sdk/auth-providers

[Checked against FrontMCP 1.11.3](https://frontmcp.dev/versions#how-pages-are-checked) on November 10, 2026

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()`](https://frontmcp.dev/reference/sdk/fetch) send it with `credentials: { provider }`, and a tool that names the provider in its [`authProviders`](https://frontmcp.dev/reference/sdk/tool) option doesn't run without it. New in 1.11.1: before, nothing registered the providers, and `this.authProviders` was `undefined`.

```ts
@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 } })
```



---

<a id="reference"></a>

## Reference

<a id="authprovidersvault"></a>

### `authProvidersVault`

Register the providers on `@FrontMcp`:

```ts main.ts
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 {}
```

[See more examples below.](https://frontmcp.dev/reference/sdk/auth-providers#usage)

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `providers` | `CredentialProviderConfig[]` | none | One [provider](https://frontmcp.dev/reference/sdk/auth-providers#a-provider) per service. A name listed twice stops the server from starting with [`Credential provider "…" is already registered`](https://frontmcp.dev/reference/sdk/auth-providers#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](https://frontmcp.dev/reference/sdk/auth-providers#where-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](https://frontmcp.dev/reference/sdk/auth-providers#where-credentials-are-kept). |

Every entry point reads it: `@FrontMcp`, [`createFetchHandler()`](https://frontmcp.dev/reference/sdk/create-fetch-handler), [`create()` and `createDirect()`](https://frontmcp.dev/reference/sdk/create), and the Playground in your browser.

<a id="a-provider"></a>

#### 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`](https://frontmcp.dev/reference/sdk/auth-providers#what-the-factory-is-told). 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`](https://frontmcp.dev/reference/sdk/auth-providers#unrecognized-key-with-path-authprovidersvault-providers-0).

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.

<a id="where-credentials-are-kept"></a>

#### 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](https://frontmcp.dev/reference/sdk/auth-providers#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`](https://frontmcp.dev/reference/auth/local#storage) 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](https://frontmcp.dev/reference/sdk/auth-providers#authprovidersvault-keeps-credentials-in-authtokenstorage-but-neither-vault_secret-nor-jwt_secret-is-set). A server in `public` or `transparent` mode has no `tokenStorage`, so it keeps them in memory.

<a id="what-the-factory-is-told"></a>

#### 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`](https://frontmcp.dev/reference/auth/progressive#credentials-authproviders-and-thiscredentials). |

<a id="thisauthproviders"></a>

### `this.authProviders`

`ExecutionContextBase` defines it, so every [context class](https://frontmcp.dev/reference/sdk/contexts) 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](https://frontmcp.dev/reference/sdk/auth-providers#where-credentials-are-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`](https://frontmcp.dev/reference/sdk/auth-providers#auth-provider--is-not-configured-this-server-registers-no-auth-providers), naming the provider. `has()` is `false`, and the lists are empty.

<a id="a-tools-authproviders"></a>

#### A tool's `authProviders`

With `authProvidersVault` set, each required entry of a tool's [`authProviders`](https://frontmcp.dev/reference/sdk/tool) 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`](https://frontmcp.dev/reference/auth/progressive#credentials-authproviders-and-thiscredentials), 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`](https://frontmcp.dev/reference/sdk/auth-providers#tool--requires-credentials-for--which-are-not-connected-for-this-session-please-authorize-the-required-providers-and-retry). `{ 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.

<a id="thisfetch-with-credentials--provider-"></a>

#### `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()`](https://frontmcp.dev/reference/sdk/fetch#the-service-gets-no-credentials-from-credentialsprovider).

<a id="the-authprovidersvault-class"></a>

#### 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.

<a id="caveats"></a>

#### 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.authProviders` isn't `@App({ authProviders })`.** An app's `authProviders` lists `@AuthProvider` classes, which `this.authProviders` doesn't read, and neither is it [`this.scope.authProviders`](https://frontmcp.dev/reference/sdk/scope#registries), 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.

---

<a id="usage"></a>

## Usage

<a id="sending-a-services-credential-for-the-caller"></a>

### 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.

Initial call configuration:

```js
{ tool: "get_invoice", arguments: { id: "INV-7" } }
```

```ts main.ts active
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 {}
```

```ts secrets.ts
// Stands in for where your server keeps its secrets, like its environment or a secret manager.
export const BILLING_API_KEY = "bk_test_7Hq2";

const crmTokens: Record<string, string> = { nour: "crm_nour_51x" };
export const crmTokenFor = (userSub: string): string | undefined => crmTokens[userSub];
```

```ts services.example.ts
// Stands in for the billing service and the CRM, which the Playground can't reach.
// Each answers with the credentials it received. A real server doesn't need this file.
const passThrough = globalThis.fetch;

globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
  const request = new Request(input, init);
  const { hostname } = new URL(request.url);
  if (hostname !== "billing.example" && hostname !== "crm.example") return passThrough(input, init);
  return Response.json({ service: hostname, apiKey: request.headers.get("x-api-key"), authorization: request.headers.get("authorization") });
};
```

```ts credentials.test.ts
import { test, expect } from "@frontmcp/testing";
import { FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./main";

test("this.fetch() sends the billing key from its provider", async ({ mcp }) => {
  const result = await mcp.tools.call("get_invoice", { id: "INV-7" });
  expect(result.json()).toEqual({ service: "billing.example", apiKey: "bk_test_7Hq2", authorization: null });
});

test("a signed-in caller gets their own CRM token", async () => {
  const server = await FrontMcpInstance.createDirect(config);
  try {
    const result = await server.callTool("my_crm_contact", {}, { authContext: { user: { sub: "nour" } } });
    expect(result.structuredContent).toEqual({ service: "crm.example", apiKey: null, authorization: "Bearer crm_nour_51x" });
  } finally {
    await server.dispose();
  }
});

test("a caller without a CRM token is refused before the tool runs", async ({ mcp }) => {
  const { error } = await mcp.raw.request({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "my_crm_contact", arguments: {} } });
  expect(error).toMatchObject({
    code: -32001,
    message: 'Tool "help-desk:my_crm_contact" requires credentials for crm which are not connected for this session. Please authorize the required provider(s) and retry.',
    data: { tool: "help-desk:my_crm_contact", providers: ["crm"] },
  });
});

test("a provider name listed twice stops the server from starting", async () => {
  const billing = { name: "billing", scope: "global" as const, loading: "lazy" as const, factory: async () => null };
  const starting = FrontMcpInstance.createDirect({ info: { name: "help-desk", version: "1.0.0" }, apps: [], authProvidersVault: { providers: [billing, billing] } });
  await expect(starting).rejects.toThrow('Credential provider "billing" is already registered');
});
```

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.

<a id="reading-the-credential-yourself"></a>

### 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`:

Initial call configuration:

```js
{ tool: "crm_status", arguments: {} }
```

```ts crm-status.tool.ts active
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) };
  }
}
```

```ts main.ts
import { App, FrontMcp, type FrontMcpMetadata } from "@frontmcp/sdk";
import { CrmStatus } from "./crm-status.tool";

export const seen: { userSub?: string; userEmail?: string }[] = [];

@App({ id: "help-desk", name: "Help Desk", tools: [CrmStatus] })
class HelpDesk {}

export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDesk],
  authProvidersVault: {
    providers: [
      {
        name: "crm",
        scope: "user",
        loading: "lazy",
        factory: async ({ userSub, userEmail }) => {
          seen.push({ userSub, userEmail });
          return userSub === "nour" ? { type: "bearer", token: "crm_nour_51x" } : null;
        },
      },
    ],
  },
} satisfies FrontMcpMetadata;

@FrontMcp(config)
export default class Server {}
```

```ts crm-status.test.ts
import { test, expect } from "@frontmcp/testing";
import { FrontMcpInstance, Tool, ToolContext } from "@frontmcp/sdk";
import { config, seen } from "./main";

test("an anonymous caller has no credential, and the tool still runs", async ({ mcp }) => {
  expect((await mcp.tools.call("crm_status", {})).json()).toEqual({ connected: false });
});

test("the factory is told who the caller is", async () => {
  const server = await FrontMcpInstance.createDirect(config);
  try {
    const result = await server.callTool("crm_status", {}, { authContext: { user: { sub: "nour", email: "nour@example.com" } } });
    expect(result.structuredContent).toEqual({ connected: true, scope: "user", tokenEndsWith: "_51x" });
    expect(seen.at(-1)).toEqual({ userSub: "nour", userEmail: "nour@example.com" });
  } finally {
    await server.dispose();
  }
});

test("getMany() leaves out a provider whose factory throws, and headersMany() fails", async () => {
  @Tool({ name: "read_both", description: "Read the CRM and billing credentials", inputSchema: {} })
  class ReadBoth extends ToolContext {
    async execute() {
      const resolved = await this.authProviders.getMany(["crm", "billing"]);
      const headers = await this.authProviders.headersMany(["crm", "billing"]).then(
        () => "sent",
        (error: Error) => error.message,
      );
      return { names: [...resolved.keys()], headers };
    }
  }
  const billing = { name: "billing", scope: "global" as const, loading: "lazy" as const, factory: async () => { throw new Error("billing is down"); } };
  const server = await FrontMcpInstance.createDirect({ ...config, tools: [ReadBoth], authProvidersVault: { providers: [...config.authProvidersVault.providers, billing] } });
  try {
    const result = await server.callTool("read_both", {}, { authContext: { user: { sub: "nour" } } });
    expect(result.structuredContent).toEqual({ names: ["crm"], headers: "billing is down" });
  } finally {
    await server.dispose();
  }
});
```

<a id="how-long-a-credential-is-kept"></a>

### 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.

Initial call configuration:

```js
{ tool: "service_headers", arguments: { provider: "billing" } }
```

```ts main.ts active
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 {}
```

```ts secrets.ts
// Stands in for a secret manager, counting how often the server asks it.
export const secretReads = { count: 0 };

export const readSecret = async (name: string): Promise<string> => {
  secretReads.count++;
  return name === "billing-api-key" ? "bk_test_7Hq2" : "";
};
```

```ts crm.ts
// Stands in for the CRM's token endpoint. Its tokens last a second here, so the test doesn't wait long.
let issued = 0;

export const issueCrmToken = async (userSub: string) => ({ token: `crm_${userSub}_${++issued}`, expiresAt: Date.now() + 1_000 });
```

```ts kept.test.ts
import { test, expect } from "@frontmcp/testing";
import { FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./main";
import { secretReads } from "./secrets";

test("the billing key is read once, then kept between requests", async ({ mcp }) => {
  await mcp.tools.call("service_headers", { provider: "billing" });
  const readsBefore = secretReads.count;
  for (const _ of [1, 2, 3]) {
    expect((await mcp.tools.call("service_headers", { provider: "billing" })).json()).toEqual({ "X-Api-Key": "bk_test_7Hq2" });
  }
  expect(secretReads.count - readsBefore).toBe(0);
});

test("each user keeps their own CRM token until it expires", async () => {
  const server = await FrontMcpInstance.createDirect(config);
  const crmHeadersFor = async (sub: string) =>
    (await server.callTool("service_headers", { provider: "crm" }, { authContext: { user: { sub } } })).structuredContent;
  try {
    expect(await crmHeadersFor("nour")).toEqual({ Authorization: "Bearer crm_nour_1" });
    expect(await crmHeadersFor("nour")).toEqual({ Authorization: "Bearer crm_nour_1" });
    expect(await crmHeadersFor("sam")).toEqual({ Authorization: "Bearer crm_sam_2" });
    await new Promise((resolve) => setTimeout(resolve, 1_100));
    expect(await crmHeadersFor("nour")).toEqual({ Authorization: "Bearer crm_nour_3" });
  } finally {
    await server.dispose();
  }
});

test("`defaultCacheTtl: 0` still keeps the billing key between requests", async () => {
  const server = await FrontMcpInstance.createDirect({ ...config, authProvidersVault: { ...config.authProvidersVault, defaultCacheTtl: 0 } });
  try {
    const readsBefore = secretReads.count;
    await server.callTool("service_headers", { provider: "billing" });
    await server.callTool("service_headers", { provider: "billing" });
    expect(secretReads.count - readsBefore).toBe(1);
  } finally {
    await server.dispose();
  }
});
```

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.

---

<a id="troubleshooting"></a>

## Troubleshooting

<a id="auth-provider--is-not-configured-this-server-registers-no-auth-providers"></a>

### `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:

Initial call configuration:

```js
{ tool: "crm_status", arguments: {} }
```

```ts crm-status.tool.ts active
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 };
  }
}
```

```ts crm-status.test.ts
import { test, expect } from "@frontmcp/testing";

test("the read fails, naming the provider", async ({ mcp }) => {
  const result = await mcp.tools.call("crm_status", {});
  expect(result).toBeError("AUTH_PROVIDERS_NOT_CONFIGURED");
  expect(result.text()).toContain('Auth provider "crm" is not configured: this server registers no auth providers.');
});
```

Register the provider in `authProvidersVault`. If the credential is one the user connects through a link, it's in [`this.credentials`](https://frontmcp.dev/reference/auth/progressive#credentials-authproviders-and-thiscredentials) instead. Changed in 1.11.1: before, `this.authProviders` was `undefined` on every server, and the read failed with a `TypeError`.

<a id="tool--requires-credentials-for--which-are-not-connected-for-this-session-please-authorize-the-required-providers-and-retry"></a>

### `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](https://frontmcp.dev/reference/sdk/auth-providers#reading-the-credential-yourself). When the user can connect it through a link, the message says so instead: see [Progressive auth](https://frontmcp.dev/reference/auth/progressive#credentials-authproviders-and-thiscredentials).

<a id="credential-provider--is-already-registered"></a>

### `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](https://frontmcp.dev/reference/sdk/auth-providers#sending-a-services-credential-for-the-caller) last test shows it.

<a id="unrecognized-key-with-path-authprovidersvault-providers-0"></a>

### `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](https://frontmcp.dev/reference/sdk/auth-providers#a-provider).

<a id="the-factory-runs-again-on-every-request"></a>

### 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.

```ts main.ts active
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 {}
```

```ts factory-runs.test.ts
import { test, expect } from "@frontmcp/testing";
import { FrontMcpInstance } from "@frontmcp/sdk";
import { config, factoryRuns } from "./main";

test("reads in one request share one run of the factory", async ({ mcp }) => {
  const before = factoryRuns.count;
  expect((await mcp.tools.call("desk_ready", {})).json()).toEqual({ ready: true });
  expect(factoryRuns.count - before).toBe(1);
});

test("without a verified session, each request runs it again", async ({ mcp }) => {
  const before = factoryRuns.count;
  await mcp.tools.call("desk_ready", {});
  await mcp.tools.call("desk_ready", {});
  expect(factoryRuns.count - before).toBe(2);
});

test("a createDirect() caller keeps it for its session", async () => {
  const server = await FrontMcpInstance.createDirect(config);
  const nour = { authContext: { user: { sub: "nour" } } };
  try {
    const before = factoryRuns.count;
    await server.callTool("desk_ready", {}, nour);
    await server.callTool("desk_ready", {}, nour);
    expect(factoryRuns.count - before).toBe(1);
  } finally {
    await server.dispose();
  }
});
```

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](https://frontmcp.dev/reference/sdk/auth-providers#where-credentials-are-kept).

<a id="authprovidersvault-keeps-credentials-in-authtokenstorage-but-neither-vault_secret-nor-jwt_secret-is-set"></a>

### `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`](https://frontmcp.dev/reference/auth/local#storage), 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.

<a id="fetch-credentialsprovider-was-given-but-no-auth-providers-are-configured-sent-without-them"></a>

### `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()`](https://frontmcp.dev/reference/sdk/fetch#the-service-gets-no-credentials-from-credentialsprovider).
