npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

mcp-oauth-provider

v1.0.0

Published

MCP OAuth client provider for streamable HTTP clients

Readme

MCP OAuth Provider

OAuth client provider implementation for the Model Context Protocol (MCP) HTTP stream transport.

Features

Full MCP SDK Integration - Implements OAuthClientProvider from @modelcontextprotocol/sdk
Automatic Token Refresh - Tokens refresh automatically when expired or about to expire
Smart Token Storage - Stores expires_at (absolute time) for accurate expiry tracking
PKCE Support - Automatic PKCE (Proof Key for Code Exchange) via MCP SDK
Retry Logic - Configurable retry attempts with exponential backoff for token refresh
Multiple Storage Backends - Memory and file-based storage with simple interface
Session Management - Support for multiple concurrent OAuth sessions
Callback Server - Bun-native HTTP server for handling OAuth callbacks
Error Handling - Automatic credential invalidation on auth failures
TypeScript - Full type safety with types from MCP SDK
Bun-Only - Optimized for Bun runtime

Installation

bun add mcp-oauth-provider @modelcontextprotocol/sdk

Examples

Check out the examples directory for complete working examples:

Notion MCP - Connect to Notion's MCP server

Two complete examples demonstrating OAuth integration with Notion's MCP server at https://mcp.notion.com/mcp:

1. Basic OAuth Flow (index.ts)

cd examples/notion
NOTION_CLIENT_ID=your_id NOTION_CLIENT_SECRET=your_secret bun index.ts

Demonstrates:

  • OAuth 2.0 authentication flow
  • Token retrieval and display
  • Authorization server metadata
  • Error handling

2. Advanced MCP Integration (advanced.ts)

cd examples/notion
NOTION_CLIENT_ID=your_id NOTION_CLIENT_SECRET=your_secret bun advanced.ts

Demonstrates:

  • Full MCP client integration with StreamableHTTPClientTransport
  • Automatic token refresh during MCP operations
  • Listing and using MCP tools, resources, and prompts
  • OAuth provider integration with MCP SDK

Each example includes detailed setup instructions and demonstrates different features of the library.

Quick Start

Basic OAuth Flow

import { createOAuthProvider } from 'mcp-oauth-provider';
import { auth } from '@modelcontextprotocol/sdk/client/auth.js';
import { createCallbackServer } from 'mcp-oauth-provider/server';

// Create OAuth provider
const provider = createOAuthProvider({
  clientId: 'your-client-id',
  clientSecret: 'your-client-secret',
  redirectUri: 'http://localhost:8080/callback',
  scope: 'openid profile email',
});

// Start callback server
const server = await createCallbackServer({
  port: 8080,
  hostname: 'localhost',
});

const serverUrl = 'https://mcp.notion.com/mcp'; // or your MCP server URL

try {
  // Execute OAuth flow (PKCE handled automatically by SDK)
  const result = await auth(provider, {
    serverUrl,
  });

  if (result === 'REDIRECT') {
    console.log('Browser opened for authorization...');

    // Wait for callback
    const callbackResult = await server.waitForCallback('/callback', 120000);

    // Exchange code for tokens
    await auth(provider, {
      serverUrl,
      authorizationCode: callbackResult.code,
    });
  }

  console.log('Authorization successful!');

  // Get tokens (automatically refreshed if expired)
  const tokens = await provider.tokens();
  console.log('Access token:', tokens?.access_token);
} finally {
  await server.stop();
}

Using with MCP Client

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { createOAuthProvider } from 'mcp-oauth-provider';

// Create provider and authenticate (see above)
const provider = createOAuthProvider({
  clientId: 'your-client-id',
  clientSecret: 'your-client-secret',
  redirectUri: 'http://localhost:8080/callback',
});

// Perform OAuth flow...
await auth(provider, { serverUrl: 'https://mcp.notion.com/mcp' });

// Create MCP client with OAuth provider
const transport = new StreamableHTTPClientTransport(
  new URL('https://mcp.notion.com/mcp'),
  {
    authProvider: provider, // Provider handles automatic token refresh!
  }
);

const client = new Client(
  { name: 'my-app', version: '1.0.0' },
  { capabilities: {} }
);

await client.connect(transport);

// Use MCP features
const tools = await client.listTools();
const resources = await client.listResources();

Initial Tokens Configuration

You can provide initial tokens in the config. These will be stored on first use and can be updated:

import { createOAuthProvider } from 'mcp-oauth-provider';

// Provider with initial tokens
const provider = createOAuthProvider({
  clientId: 'your-client-id',
  clientSecret: 'your-client-secret',
  redirectUri: 'http://localhost:8080/callback',
  tokens: {
    access_token: 'existing-access-token',
    refresh_token: 'existing-refresh-token',
    token_type: 'Bearer',
    expires_in: 3600,
  },
});

// First call returns initial tokens (and stores them)
const tokens1 = await provider.tokens();
console.log(tokens1.access_token); // 'existing-access-token'

// Tokens can be updated
await provider.saveTokens({
  access_token: 'new-access-token',
  refresh_token: 'new-refresh-token',
  token_type: 'Bearer',
  expires_in: 3600,
});

// Subsequent calls return updated tokens from storage
const tokens2 = await provider.tokens();
console.log(tokens2.access_token); // 'new-access-token'

Note: Unlike clientId/clientSecret (which always take precedence from config), tokens from config are initial values only. Once stored, the storage takes over. This is because tokens change over time (via refresh), while client credentials are permanent.

Automatic Token Refresh

The provider automatically refreshes expired tokens when you call tokens():

// Tokens are automatically refreshed if expired or expiring soon (< 5 minutes)
const tokens = await provider.tokens(); // May refresh behind the scenes!

// Requirements for automatic refresh:
// 1. authorizationServerMetadata must be set (done by auth() function)
// 2. A refresh_token must be available
// 3. Token must be expired or expiring within 5 minutes

// For manual refresh (after auth() sets metadata):
const newTokens = await provider.refreshTokens();

How it works:

  • Tokens are stored with expires_at (absolute timestamp) instead of expires_in
  • When retrieved, expires_in is calculated from expires_at and current time
  • This ensures expires_in is always accurate, even hours after tokens were saved
  • Tokens auto-refresh when expires_in < 300 seconds (5-minute buffer)

Storage Options

Memory Storage (Default)

import { createOAuthProvider, MemoryStorage } from 'mcp-oauth-provider';

const provider = createOAuthProvider({
  redirectUri: 'http://localhost:8080/callback',
  storage: new MemoryStorage(), // Data lost when process exits
});

File Storage

import { createOAuthProvider, FileStorage } from 'mcp-oauth-provider';

const provider = createOAuthProvider({
  redirectUri: 'http://localhost:8080/callback',
  storage: new FileStorage('./oauth-data'), // Persists to filesystem
});

Custom Storage

Implement the simple StorageAdapter interface:

import type { StorageAdapter } from 'mcp-oauth-provider';

class RedisStorage implements StorageAdapter {
  constructor(private redis: RedisClient) {}

  async get(key: string): Promise<string | undefined> {
    return await this.redis.get(key);
  }

  async set(key: string, value: string): Promise<void> {
    await this.redis.set(key, value);
  }

  async delete(key: string): Promise<void> {
    await this.redis.del(key);
  }
}

const provider = createOAuthProvider({
  redirectUri: 'http://localhost:8080/callback',
  storage: new RedisStorage(redisClient),
});

Callback Server

Basic Usage

import { createCallbackServer } from 'mcp-oauth-provider/server';

const server = await createCallbackServer({
  port: 8080,
  hostname: 'localhost',
});

// Wait for OAuth callback
const result = await server.waitForCallback('/callback', 30000);

console.log('Authorization code:', result.code);
console.log('State:', result.state);

await server.stop();

Custom Templates

const server = await createCallbackServer({
  port: 8080,
  successHtml: '<html><body><h1>Success!</h1></body></html>',
  errorHtml: '<html><body><h1>Error: {{error}}</h1></body></html>',
});

One-Shot Callback

import { waitForOAuthCallback } from 'mcp-oauth-provider/server';

// Server automatically starts and stops
const result = await waitForOAuthCallback('/callback', {
  port: 8080,
  timeout: 30000,
});

API Reference

Client Provider

createOAuthProvider(config: OAuthConfig)

Creates an OAuth client provider instance.

Config Options:

  • redirectUri (required) - OAuth callback URL
  • clientId - OAuth client ID (optional for dynamic registration)
  • clientSecret - OAuth client secret (optional for public clients)
  • scope - OAuth scope to request
  • sessionId - Session identifier (generated automatically if not provided)
  • storage - Storage adapter (defaults to MemoryStorage)
  • tokens - Static OAuth tokens (takes precedence over storage)
  • clientMetadata - OAuth client metadata for registration
  • tokenRefresh - Token refresh configuration
    • maxRetries - Maximum retry attempts (default: 3)
    • retryDelay - Delay between retries in ms (default: 1000)
  • server - Callback server configuration

provider.tokens()

Get OAuth tokens with automatic refresh.

Behavior:

  • Returns stored tokens if valid (> 5 minutes until expiry)
  • Automatically refreshes tokens if expired or expiring soon (< 5 minutes)
  • Requires authorizationServerMetadata to be set for automatic refresh
  • Falls back to current tokens if refresh fails

Returns: Promise<OAuthTokens | undefined>

Example:

// Tokens are automatically refreshed if needed
const tokens = await provider.tokens();

provider.refreshTokens()

Manually refresh OAuth tokens using the refresh token.

Requirements:

  • authorizationServerMetadata.token_endpoint must be set (done by auth())
  • Stored tokens must include a refresh_token

Returns: Promise<OAuthTokens>

Throws: Error if no authorization server metadata or refresh token available

Example:

// Manual refresh (after calling auth())
const newTokens = await provider.refreshTokens();

provider.getStoredTokens()

Get stored tokens without triggering automatic refresh.

Use Case: Check token state without side effects

Returns: Promise<OAuthTokens | undefined>

Example:

// Get tokens without auto-refresh
const tokens = await provider.getStoredTokens();
if (tokens && tokens.expires_in < 300) {
  console.log('Tokens expiring soon!');
}

provider.authorizationServerMetadata

OAuth server metadata set during the auth flow. Contains endpoints for token operations.

Type: AuthorizationServerMetadata | undefined

Properties:

  • token_endpoint - URL for token refresh
  • authorization_endpoint - URL for authorization
  • issuer - OAuth server identifier
  • And more...

Example:

// Metadata is set automatically by auth()
await auth(provider, { serverUrl: 'https://mcp.notion.com/mcp' });

console.log(provider.authorizationServerMetadata?.token_endpoint);
// Output: "https://auth.notion.com/token"

Storage

MemoryStorage

In-memory storage (data lost on process exit).

FileStorage

File-based storage using Bun's file API.

OAuthStorage

Helper class that wraps a storage adapter with OAuth-specific methods.

Server

createCallbackServer(options)

Create and start an OAuth callback server.

Options:

  • port (required) - Server port
  • hostname - Server hostname (default: 'localhost')
  • successHtml - Custom success page HTML
  • errorHtml - Custom error page HTML
  • signal - AbortSignal for cancellation
  • onRequest - Request handler callback

waitForOAuthCallback(path, options)

Convenience function that starts server, waits for callback, and stops server automatically.

Session Management

The provider supports multiple concurrent OAuth sessions:

// Create session-specific providers
const session1 = createOAuthProvider({
  redirectUri: 'http://localhost:8080/callback',
  sessionId: 'user-123',
});

const session2 = createOAuthProvider({
  redirectUri: 'http://localhost:8080/callback',
  sessionId: 'user-456',
});

// Each session has isolated tokens and credentials

Error Handling

The library handles several OAuth-specific errors:

  • Invalid state/verifier: Throws Error with descriptive message
  • Missing authorization code: Throws Error
  • Network errors: Propagated from fetch calls
  • Token refresh failures: Throws Error with details
  • Automatic refresh failures: Logged as warning, returns existing tokens

Automatic Refresh Error Behavior

When tokens() attempts automatic refresh and fails:

  • Logs a warning to console
  • Returns existing (expired) tokens instead of throwing
  • Allows application to continue and handle expiry
// Automatic refresh handles errors gracefully
const tokens = await provider.tokens();
// If refresh failed, you'll get expired tokens
// Check expires_in to detect this scenario
if (tokens && tokens.expires_in < 0) {
  console.log('Tokens expired and refresh failed');
}

Manual Operations

Always wrap manual OAuth operations in try-catch:

import { executeOAuthFlow } from 'mcp-oauth-provider';

try {
  await executeOAuthFlow(provider, {
    serverUrl: 'https://your-mcp-server.com',
  });
} catch (error) {
  if (error.message.includes('access_denied')) {
    console.log('User denied authorization');
  } else if (error.message.includes('invalid_client')) {
    console.log('Invalid client credentials');
    // Credentials automatically invalidated by library
  }
}

try {
  await provider.refreshTokens();
} catch (error) {
  console.error('Manual refresh failed:', error.message);
}

Security Considerations

  • Never log tokens or secrets - The library avoids logging sensitive data
  • PKCE is automatic - Code challenge/verifier handled by MCP SDK
  • State parameter - CSRF protection with random state generation
  • Token expiry - Automatic refresh before expiration
  • Credential invalidation - Automatic cleanup on auth errors

Testing

This package includes comprehensive unit and integration tests using bun:test.

Running Tests

# Run all tests
bun test

# Run tests in watch mode
bun test --watch

# Run tests with coverage
bun test --coverage

Test Coverage

The test suite covers:

  • OAuth Flow Utilities - Token expiry detection, token refresh with retry logic, exponential backoff
  • Storage Adapters - MemoryStorage, FileStorage, OAuthStorage with session isolation, time-based expiry
  • Configuration - Session ID generation, state generation, default metadata
  • OAuth Client Provider - Token management, automatic refresh, client information handling, storage isolation
  • Callback Server - Server lifecycle, callback handling, timeout management, custom templates

71 tests pass with high coverage of critical OAuth functionality including automatic token refresh.

Test Structure

src/__tests__/
├── config.test.ts          # Configuration utilities
├── storage.test.ts         # Storage adapters
├── oauth-flow.test.ts      # OAuth flow helpers
├── integration.test.ts     # MCPOAuthClientProvider integration
└── server.test.ts          # Callback server functionality

Writing Tests

When contributing, please:

  1. Add tests for new features
  2. Ensure existing tests pass
  3. Use descriptive test names
  4. Test error conditions and edge cases
  5. Mock external dependencies appropriately

Example test pattern:

import { describe, expect, test, beforeEach } from 'bun:test';

describe('MyFeature', () => {
  beforeEach(() => {
    // Setup
  });

  test('should do something', () => {
    // Test implementation
    expect(result).toBe(expected);
  });
});

Development

# Install dependencies
bun install

# Build
bun run build

# Type check
bun run check-types

# Lint
bun run lint

# Format
bun run format

# Run tests
bun test

# Run tests in watch mode
bun test --watch

Preview OAuth Templates

You can preview the OAuth success and error page templates locally:

# Preview success page (runs on http://localhost:3000)
bun run server:success

# Preview error page (runs on http://localhost:3001)
bun run server:error

# Customize error page with environment variables
ERROR="invalid_client" ERROR_DESCRIPTION="Custom error message" bun run server:error

Available environment variables for server:error:

  • ERROR - The error code (default: access_denied)
  • ERROR_DESCRIPTION - Detailed error message (default: "The user denied the authorization request.")
  • ERROR_URI - Optional URL for more information
  • PORT - Server port (default: 3001)

For server:success:

  • PORT - Server port (default: 3000)

License

MIT

Contributing

Contributions welcome! Please open an issue or PR.

Documentation

Links