Skip to main content
Authenticating users wires sign-in through a hosted identity provider in one constructor option, so your tools receive a signed-in user. ChatGPT and Claude drive the flow the same way; what varies is the provider. The sections below cover one each: Auth0, Clerk, Descope, Stytch, and WorkOS, plus a custom provider for any other.
These providers enforce sign-in on every request. To mix public and authenticated tools in one server, wire the middleware by hand instead.

Auth0

Auth0 issues opaque tokens by default, so you register an API to get verifiable JWTs.
  1. In the Auth0 dashboard, create a Regular Web Application and note its Domain.
  2. Create an API (Applications → APIs) with an Identifier (e.g. https://your-mcp-server.com); this identifier is your audience and need not be a real URL.
  3. Under Settings → Tenant Settings → API Authorization Settings, set Default Audience to that identifier and enable Dynamic Client Registration.
  4. Enable Application Connections, promote your login connection to domain level, and set the API’s user access policy to Allow, so DCR-created clients can sign users in.
Pass the tenant domain, the API Identifier as audience, and this server’s public URL as serverUrl to auth0Provider:
server.ts
import { McpServer, auth0Provider } from "skybridge/server";

const server = new McpServer(
  { name: "personal-shopper", version: "0.0.1" },
  { capabilities: {} },
  {
    oauth: await auth0Provider({
      domain: process.env.AUTH0_DOMAIN,
      audience: process.env.AUTH0_API_IDENTIFIER,
      serverUrl: process.env.SERVER_URL,
    }),
  },
).registerTool(/* search-products, requires oauth2 */);
See the runnable auth-auth0 example.

Clerk

Clerk access tokens carry no aud claim, so there is no audience to configure.
  1. Sign up at clerk.com and create an application.
  2. Create an OAuth application with Dynamic client registration enabled and Generate access tokens as JWTs turned on.
  3. Copy your Frontend API URL (API keys), e.g. acme.clerk.accounts.dev.
Pass the domain alone to clerkProvider, no audience:
server.ts
const server = new McpServer(serverInfo, capabilities, {
  oauth: await clerkProvider({
    domain: process.env.CLERK_FRONTEND_API,
  }),
}).registerTool(/* search-products, requires oauth2 */);
See the auth-clerk example.

Descope

A Descope MCP Server binds the token’s aud to the project, so the provider derives the audience from the Discovery URL you pass.
  1. Sign up at descope.com and create a project.
  2. In the console’s MCP Servers section, create an MCP Server and enable Dynamic Client Registration on it.
  3. Copy the MCP Server’s Discovery URL from its Connection Information.
Pass that URL to descopeProvider; the audience defaults to the Project ID derived from it:
server.ts
const server = new McpServer(serverInfo, capabilities, {
  oauth: await descopeProvider({
    url: process.env.DESCOPE_MCP_SERVER_URL,
  }),
}).registerTool(/* search-products, requires oauth2 */);
See the auth-descope example.

Stytch

Stytch Connected Apps ships its consent screen only as a React component, so the login, consent, and callback pages are served as static HTML from your server, and the Connected App points at them.
  1. Sign up at stytch.com and create a Consumer project.
  2. Under Connected Apps, create a Connected App with Dynamic Client Registration enabled.
  3. Note your Project ID, project domain (e.g. acme.customers.stytch.dev), and Public Token.
  4. Set the Connected App’s Authorization URL to your server’s consent page and add its callback to the Redirect URLs allowlist. The auth-stytch example ships those HTML pages.
Pass the project domain and the Project ID as the audience to stytchProvider:
server.ts
const server = new McpServer(serverInfo, capabilities, {
  oauth: await stytchProvider({
    domain: process.env.STYTCH_DOMAIN,
    audience: process.env.STYTCH_PROJECT_ID,
  }),
}).registerTool(/* search-products, requires oauth2 */);

WorkOS

WorkOS AuthKit needs DCR enabled and your server registered as a Resource Indicator so issued tokens carry the right aud.
  1. Sign up at workos.com and enable AuthKit.
  2. Enable Dynamic Client Registration under Connect → Configuration.
  3. Register your server URL as a Resource Indicator, so AuthKit sets each token’s aud to it.
  4. Copy your AuthKit domain (e.g. acme.authkit.app).
Pass the domain and your server URL as the audience to workosProvider:
server.ts
const server = new McpServer(serverInfo, capabilities, {
  oauth: await workosProvider({
    domain: process.env.AUTHKIT_DOMAIN,
    audience: process.env.SERVER_URL,
  }),
}).registerTool(/* search-products, requires oauth2 */);
See the runnable auth-workos example.

Any Other Provider

For an identity provider without a branded wrapper, customProvider wires any IdP that meets three requirements:
  • Dynamic Client Registration (DCR) enabled, so hosts can register themselves without a hand-issued client ID.
  • JWT access tokens (not opaque ones), so the server can verify a token by its signature.
  • An audience the provider binds into the token’s aud claim.
Pass the issuer and the audience it binds:
server.ts
const server = new McpServer(serverInfo, capabilities, {
  oauth: await customProvider({
    issuer: "https://auth.example.com",
    audience: process.env.SERVER_URL,
  }),
}).registerTool(/* search-products, requires oauth2 */);
Omit audience for a provider that binds none, and set serverUrl when Skybridge must advertise itself as the authorization server, as Auth0 and the Alpic DCR proxy require. Google and GitHub don’t support DCR, so customProvider can’t wire them directly. To offer Google or GitHub sign-in, configure a branded provider (Auth0, Clerk, Stytch, or WorkOS) with them as an upstream social connection. After sign-in, reading the user and scoping data to them is the same across every provider: handlers read extra.authInfo, and a tool declaring securitySchemes with scopes gates access to them.

Authenticate Users

Publish metadata, verify tokens, read the user

Register Tools

Declare which tools require sign-in

Tunnel

Test the OAuth flow in a real host