An OAuth/OIDC adapter framework designed to make adding identity providers to Remote MCP servers simple, consistent, and testable.
This project provides a base adapter contract, structured logging, and a standards-compliant OIDC provider implementation (discovery, PKCE S256, code exchange, refresh) with normalized errors and configuration validation.
- Normalize provider integrations behind a single contract
- Enforce consistent error handling and structured, PII-safe logging
- Support OIDC discovery + PKCE, authorization code exchange, and token refresh
- Make adapters independently testable with clear types and acceptance criteria
This project uses pnpm for package management and includes comprehensive code quality tools to maintain high standards.
A list of useful scripts when developing against the codebase:
# Run the full Mocha test suite with c8 coverage reporting
pnpm test
# Check code quality with ESLint
pnpm lint
# Automatically fix linting issues and format code with Prettier
pnpm format
# Run TypeScript type checking on *.ts files
pnpm type-check
# All code quality checks and tests
pnpm check
# Run the continuous integration checks (linting, type checking, and tests):
pnpm ciThis guide shows how to integrate oauth-provider-adapters-for-mcp into a
remote MCP server and implement an OIDC provider using both discovery and static
metadata. It uses
remote MCP with Auth0 from Cloudflare
as an example.
- Node.js ≥ 20
- An OIDC provider (for example, Auth0 Tenant)
- A remote MCP server (for example, Cloudflare Workers or Node server)
- MCP Remote Auth Proxy - An MCP auth proxy within your Heroku app that enables you to use a remote MCP server
- 
npm: npm install @heroku/oauth-provider-adapters-for-mcp 
- 
pnpm: pnpm add @heroku/oauth-provider-adapters-for-mcp 
- 
yarn: yarn add @heroku/oauth-provider-adapters-for-mcp 
You can configure the OIDCProviderAdapter with:
- Discovery: Provide an issuer(recommended)
- Static metadata: Provide metadata(useful in restricted environments)
You must provide exactly one of issuer or metadata.
Auth0 issuer pattern: https://<your-tenant>.auth0.com
import { OIDCProviderAdapter } from '@heroku/oauth-provider-adapters-for-mcp';
const adapter = new OIDCProviderAdapter({
  clientId: process.env.IDENTITY_CLIENT_ID!,
  clientSecret: process.env.IDENTITY_CLIENT_SECRET,
  issuer: `https://${process.env.AUTH0_TENANT}.auth0.com`,
  scopes: ['openid', 'profile', 'email', 'offline_access'],
  // Redirect URI must match your app registration
  redirectUri: process.env.IDENTITY_REDIRECT_URI,
  // Auth0 often requires audience for API access (optional)
  customParameters: process.env.AUTH0_AUDIENCE
    ? { audience: process.env.AUTH0_AUDIENCE }
    : undefined,
});
await adapter.initialize();
// Begin login flow (inside your authorize endpoint)
const state = crypto.randomUUID();
const authUrl = await adapter.generateAuthUrl(
  state,
  process.env.IDENTITY_REDIRECT_URI!
);
// Redirect user to authUrl
// Handle OAuth callback
// Retrieve the `code` from the OAuth callback query parameters (for example, `req.query.code` or `event.queryStringParameters.code`)
// Retrieve the PKCE `code_verifier` you previously stored for this interaction (for example, from a secure session, database, or in-memory store keyed by `state`)
const tokens = await adapter.exchangeCode(
  code,
  codeVerifier,
  process.env.IDENTITY_REDIRECT_URI!
);
// tokens: { accessToken, refreshToken?, idToken?, expiresIn?, scope? }
// Later: refresh token
const refreshed = await adapter.refreshToken(tokens.refreshToken!);Environment variables commonly used with Auth0:
IDENTITY_CLIENT_ID=<auth0 client id>
IDENTITY_CLIENT_SECRET=<auth0 client secret>
AUTH0_TENANT=<tenant subdomain>
AUTH0_AUDIENCE=<optional resource API identifier>
IDENTITY_REDIRECT_URI=https://<your-remote-mcp-host>/oauth/callbackFor most use cases, you can simplify configuration by using the
fromEnvironmentAsync convenience helper. It reduces boilerplate and helps
ensure your adapter is configured consistently across environments. It's
especially useful in production or CI/CD setups, where secrets and configuration
are injected via environment variables, and helps prevent accidental
misconfiguration. This helper automatically reads all required OIDC
configurations from the supported environment variables.
Supported environment variables:
- IDENTITY_CLIENT_ID-> clientId
- IDENTITY_CLIENT_SECRET-> clientSecret
- IDENTITY_SERVER_URL-> issuer (for OIDC discovery)
- IDENTITY_SERVER_METADATA_FILE-> metadata (static metadata file, skips discovery)
- IDENTITY_REDIRECT_URI-> redirectUri
- IDENTITY_SCOPE-> scopes (split by spaces and commas)
You can still override or extend the configuration by passing additional
options, such as customParameters for provider-specific needs (for example,
Auth0's audience).
Fetch your provider’s metadata from /.well-known/openid-configuration, then
embed a subset:
import { OIDCProviderAdapter } from '@heroku/oauth-provider-adapters-for-mcp';
const adapter = new OIDCProviderAdapter({
  clientId: process.env.IDENTITY_CLIENT_ID!,
  clientSecret: process.env.IDENTITY_CLIENT_SECRET,
  scopes: ['openid', 'profile', 'email', 'offline_access'],
  redirectUri: process.env.IDENTITY_REDIRECT_URI,
  metadata: {
    issuer: `https://${process.env.AUTH0_TENANT}.auth0.com`,
    authorization_endpoint: `https://${process.env.AUTH0_TENANT}.auth0.com/authorize`,
    token_endpoint: `https://${process.env.AUTH0_TENANT}.auth0.com/oauth/token`,
    jwks_uri: `https://${process.env.AUTH0_TENANT}.auth0.com/.well-known/jwks.json`,
    response_types_supported: ['code'],
    grant_types_supported: ['authorization_code', 'refresh_token'],
    code_challenge_methods_supported: ['S256'],
    subject_types_supported: ['public'],
    id_token_signing_alg_values_supported: ['RS256'],
  },
  customParameters: process.env.AUTH0_AUDIENCE
    ? { audience: process.env.AUTH0_AUDIENCE }
    : undefined,
});
await adapter.initialize();OIDCProviderAdapter requires storing the PKCE verifier securely between the
authorize and callback steps. In development, an in-memory mock is used. In
production, you must provide a durable storageHook:
interface PKCEStorageHook {
  storePKCEState(
    interactionId: string,
    state: string,
    codeVerifier: string,
    expiresAt: number
  ): Promise<void>;
  retrievePKCEState(
    interactionId: string,
    state: string
  ): Promise<string | null>;
  cleanupExpiredState(beforeTimestamp: number): Promise<void>;
}Examples: Heroku Key-Value Store, Redis, or your database.
At minimum, your server needs routes that:
- Start the auth flow and redirect to await adapter.generateAuthUrl(...)
- Handle the callback, verify state, loadcode_verifier, and calladapter.exchangeCode(...)
- Optionally expose a refresh path that calls adapter.refreshToken(...)
Errors are normalized to:
type OAuthError = {
  statusCode: number;
  error: string;
  error_description?: string;
  endpoint?: string;
  issuer?: string;
};The adapter performs PII-safe, structured logging and retries with backoff for
discovery. In addition, we support logger injection with LogTransport so logs
integrate with your observability stack. Here's an example of how to demo
logging capabilities using the winston logger used in mcp-remote-auth-proxy.
- Import required types:
import {
  fromEnvironmentAsync,
  DefaultLogger,
  LogLevel,
} from '@heroku/oauth-provider-adapters-for-mcp';
import winstonLogger from './winstonLogger.js';- Create a LogTransport wrapper:
// Create a LogTransport that wraps Winston
const winstonTransport = {
  log: (message) => {
    // Winston child logger preserves request context and Splunk formatting
    const contextLogger = winstonLogger.child({ component: 'oidc-adapter' });
    contextLogger.info(message);
  },
  error: (message) => {
    const contextLogger = winstonLogger.child({ component: 'oidc-adapter' });
    contextLogger.error(message);
  },
};- Create DefaultLogger with the Winston transport:
// Create DefaultLogger that uses Winston as transport
const adapterLogger = new DefaultLogger(
  { component: 'oidc-adapter' }, // Base context
  {
    level: LogLevel.Info,
    redactPaths: [], // DefaultLogger already has OAuth redaction built-in
  },
  winstonTransport // Use Winston as the transport
);- Pass the logger to the adapter:
const oidcAdapter = await fromEnvironmentAsync({
  env: adapterEnv,
  storageHook,
  defaultScopes: IDENTITY_SCOPE_parsed,
  logger: adapterLogger,
});For the best development experience:
- Before starting work: Ensure dependencies are installed with
pnpm install.
- During development: Run pnpm type-checkperiodically to catch type errors early.
- Before committing: Run pnpm checkto ensure all quality standards are met.
- Fix issues quickly: Use pnpm formatto auto-fix formatting and linting issues.
Tests are located in src/**/*.test.ts and run against the compiled JavaScript
in dist/cjs/. The test suite includes:
- Unit tests for all public APIs
- Mock-based testing for external dependencies
- Coverage reporting with c8 (HTML and text-summary)
Tests automatically run in silent mode (LOG_LEVEL=silent) to keep output
clean.
The library produces dual builds for maximum compatibility:
- CommonJS (dist/cjs/): Use for Node.js and older bundlers
- ES Modules (dist/esm/): Use for modern bundlers and tree-shaking support
Both outputs include TypeScript declaration files (.d.ts) for type
information.
Apache-2.0. See LICENSE for details.
We welcome issues and PRs. Please follow conventional commits, keep changes
under 200 lines per commit, and ensure tests and type checks pass. See
CONTRIBUTING.md for details.