OAuth Provider Adapter Library for MCP Servers
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.
Benefits
- 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
Code Quality and Testing
This project uses pnpm for package management and includes comprehensive code
quality tools to maintain high standards.
Available Scripts
A list of useful scripts when developing against the codebase:
pnpm test
pnpm lint
pnpm format
pnpm type-check
pnpm check
pnpm ci
Implementing OIDC in a Remote MCP Server (with Auth0 Example)
This 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.
Prerequisites
- 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
Install
-
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
Choose a Configuration Mode
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.
Quickstart with Auth0 (Discovery)
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'],
redirectUri: process.env.IDENTITY_REDIRECT_URI,
customParameters: process.env.AUTH0_AUDIENCE
? { audience: process.env.AUTH0_AUDIENCE }
: undefined,
});
await adapter.initialize();
const state = crypto.randomUUID();
const authUrl = await adapter.generateAuthUrl(
state,
process.env.IDENTITY_REDIRECT_URI!
);
const tokens = await adapter.exchangeCode(
code,
codeVerifier,
process.env.IDENTITY_REDIRECT_URI!
);
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/callback
For 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).
Using Static Metadata Example (No Discovery)
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();
PKCE State Storage
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.
Connecting to a Remote MCP Server
At minimum, your server needs routes that:
- Start the auth flow and redirect to
await adapter.generateAuthUrl(...)
- Handle the callback, verify
state, load code_verifier, and call
adapter.exchangeCode(...)
- Optionally expose a refresh path that calls
adapter.refreshToken(...)
Error Handling and Logging
Errors are normalized to:
type OAuthError = {
statusCode: number;
error: string;
error_description?: string;
endpoint?: string;
issuer?: string;
};
Logger Injection
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 {
fromEnvironmentAsync,
DefaultLogger,
LogLevel,
} from '@heroku/oauth-provider-adapters-for-mcp';
import winstonLogger from './winstonLogger.js';
- Create a LogTransport wrapper:
const winstonTransport = {
log: (message) => {
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:
const adapterLogger = new DefaultLogger(
{ component: 'oidc-adapter' },
{
level: LogLevel.Info,
redactPaths: [],
},
winstonTransport
);
- Pass the logger to the adapter:
const oidcAdapter = await fromEnvironmentAsync({
env: adapterEnv,
storageHook,
defaultScopes: IDENTITY_SCOPE_parsed,
logger: adapterLogger,
});
Development Workflow
For the best development experience:
- Before starting work: Ensure dependencies are installed with
pnpm install.
- During development: Run
pnpm type-check periodically to catch type
errors early.
- Before committing: Run
pnpm check to ensure all quality standards are
met.
- Fix issues quickly: Use
pnpm format to auto-fix formatting and linting
issues.
Testing Requirements
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.
Build Outputs
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.
License
Apache-2.0. See LICENSE for details.
Contributing
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.