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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@clastines/klasto-mcp-oauth

v0.1.0

Published

OAuth library for connecting to OAuth-protected MCP servers in web/browser environments

Readme

klasto-mcp-oauth

OAuth library for connecting to OAuth-protected MCP servers in web/browser environments

klasto-mcp-oauth is a TypeScript library that enables web applications (Next.js, React, vanilla browser apps) to securely connect to multiple OAuth-protected Model Context Protocol (MCP) servers. It implements the complete OAuth 2.0 Authorization Code flow with PKCE, automatic token refresh, and handles complex scenarios like step-up authorization.

Features

  • OAuth 2.0 Authorization Code + PKCE (S256) - Secure public client flow
  • Protected Resource Metadata (PRM) Discovery - Auto-discover auth requirements
  • Authorization Server Metadata Discovery - Support OAuth 2.0 and OIDC patterns
  • Dynamic Client Registration - Automatic public client registration
  • Token Bucket Storage - Manage tokens per MCP server + issuer
  • Auto-Refresh with Singleflight - Prevent token refresh storms
  • 401/403 Handling - Auto-retry on 401, step-up auth on insufficient scope
  • SSE Support - Parse text/event-stream responses into JSON
  • Web-Only - Uses WebCrypto, fetch, IndexedDB/localStorage (no Node dependencies)

Installation

npm install klasto-mcp-oauth

Quick Start

1. Initialize the OAuth Handler

import {
  OAuthHandler,
  IndexedDbBucketStore,
  LocalStorageRegistryStore,
} from "klasto-mcp-oauth";

const handler = new OAuthHandler({
  bucketStore: new IndexedDbBucketStore(),
  registry: new LocalStorageRegistryStore(),
  redirectUri: "http://localhost:3000/auth/callback",
  clientName: "My MCP Client",
});

2. Start Authorization Flow

When the user wants to connect to an MCP server:

// In your connect handler
async function connectToMcpServer() {
  const result = await handler.beginAuthorization({
    serverName: "My MCP Server",
    mcpUrl: "https://mcp.example.com/api",
    scopes: ["read", "write"], // Optional
  });

  // Redirect the user to the authorization URL
  window.location.href = result.authorizationUrl;
}

3. Handle OAuth Callback

Create a callback route/page (e.g., /auth/callback):

// In your callback page (Next.js example)
import { useEffect } from "react";
import { useRouter } from "next/router";

export default function AuthCallback() {
  const router = useRouter();

  useEffect(() => {
    async function finishAuth() {
      try {
        const key = await handler.finishAuthorizationFromUrl({
          serverName: "My MCP Server",
          mcpUrl: "https://mcp.example.com/api",
          callbackUrl: window.location.href,
        });

        console.log("Connected!", key);
        router.push("/dashboard");
      } catch (error) {
        console.error("Auth failed:", error);
        router.push("/error");
      }
    }

    finishAuth();
  }, []);

  return <div>Completing authentication...</div>;
}

4. Make Authenticated Requests

Once connected, use authenticatedFetch to make requests:

import { StepUpRequiredError } from "klasto-mcp-oauth";

async function callMcpApi(key) {
  try {
    const response = await handler.authenticatedFetch({
      key,
      url: "https://mcp.example.com/api/resources",
      acceptSse: true, // For SSE responses
    });

    if (response.ok) {
      const data = await response.json();
      console.log(data);
    }
  } catch (error) {
    if (error instanceof StepUpRequiredError) {
      // User needs to grant additional scopes
      console.log("Step-up required:", error.requiredScopes);
      window.location.href = error.authorizationUrl;
    } else {
      console.error("Request failed:", error);
    }
  }
}

Multi-Server Support

The library manages separate token buckets for each combination of:

  • serverName - Human-readable label
  • resource - MCP resource URI (from PRM)
  • issuer - Authorization server issuer

You can connect to multiple MCP servers simultaneously:

// Connect to Server A
const keyA = await handler.beginAuthorization({
  serverName: "Server A",
  mcpUrl: "https://server-a.example.com/api",
});

// Connect to Server B (different server, same or different issuer)
const keyB = await handler.beginAuthorization({
  serverName: "Server B",
  mcpUrl: "https://server-b.example.com/api",
});

// List all connected servers
const connections = await handler.listConnections();
console.log(connections);

API Reference

OAuthHandler

Main class for managing OAuth flows.

Constructor Options

new OAuthHandler({
  bucketStore: TokenBucketStore,         // Required: Token storage
  registry: RegistryStore,                // Required: Connection registry
  redirectUri: string,                    // Required: OAuth redirect URI
  clientName?: string,                    // Optional: Client name for registration
  fetchFn?: typeof fetch,                 // Optional: Custom fetch
  expiryLeewayMs?: number,                // Optional: Token expiry leeway (default: 5min)
  refreshSingleflight?: boolean,          // Optional: Enable singleflight (default: true)
  includeResourceInAuthorize?: boolean,   // Optional: Send resource in authz (default: true)
  maxAuthRetries?: number,                // Optional: Max retries (default: 2)
})

Methods

beginAuthorization(args)

Start the OAuth flow. Returns an authorization URL.

const { authorizationUrl, keyHint } = await handler.beginAuthorization({
  serverName: "My Server",
  mcpUrl: "https://mcp.example.com/api",
  scopes?: ["read", "write"],
});

finishAuthorizationFromUrl(args)

Complete the OAuth flow from the callback URL.

const key = await handler.finishAuthorizationFromUrl({
  serverName: "My Server",
  mcpUrl: "https://mcp.example.com/api",
  callbackUrl: window.location.href,
});

authenticatedFetch(args)

Make an authenticated request with auto-refresh and retry.

const response = await handler.authenticatedFetch({
  key: TokenBucketKey,
  url: string,
  init?: RequestInit,
  acceptSse?: boolean,                    // Add SSE Accept header
  retryOn401?: boolean,                   // Auto-retry on 401 (default: true)
  retryOnInsufficientScope?: boolean,     // Throw StepUpRequiredError (default: true)
});

prepareHeaders(args)

Get authorization headers for a request.

const headers = await handler.prepareHeaders({ key });
// { Authorization: "Bearer <token>" }

listConnections()

List all connected servers.

const connections = await handler.listConnections();
// [{ key: TokenBucketKey, meta: {...} }, ...]

logout(args)

Disconnect from a server and optionally revoke tokens.

await handler.logout({
  key: TokenBucketKey,
  revoke: true, // Revoke tokens at AS
});

clear(args)

Remove tokens and registration without revocation.

await handler.clear({ key: TokenBucketKey });

Storage Implementations

IndexedDbBucketStore (Recommended)

Uses IndexedDB for token storage.

import { IndexedDbBucketStore } from "klasto-mcp-oauth";
const store = new IndexedDbBucketStore();

LocalStorageBucketStore (Fallback)

Uses localStorage for token storage.

import { LocalStorageBucketStore } from "klasto-mcp-oauth";
const store = new LocalStorageBucketStore();

LocalStorageRegistryStore

Uses localStorage for connection metadata.

import { LocalStorageRegistryStore } from "klasto-mcp-oauth";
const registry = new LocalStorageRegistryStore();

Error Handling

StepUpRequiredError

Thrown when a request fails with 403 insufficient_scope. Contains the authorization URL for step-up.

import { StepUpRequiredError } from "klasto-mcp-oauth";

try {
  await handler.authenticatedFetch({ ... });
} catch (error) {
  if (error instanceof StepUpRequiredError) {
    console.log("Required scopes:", error.requiredScopes);
    window.location.href = error.authorizationUrl;
  }
}

Other errors: OAuthFlowError, TokenError, DiscoveryError

SSE Utilities

parseSseJson(input)

Parse SSE text into JSON.

import { parseSseJson } from "klasto-mcp-oauth";

const data = parseSseJson("data: {\"message\":\"hello\"}\n\n");

parseSseResponse(response)

Parse an SSE Response object.

import { parseSseResponse } from "klasto-mcp-oauth";

const response = await fetch(...);
const data = await parseSseResponse(response);

streamSseEvents(response)

Stream SSE events as async iterator.

import { streamSseEvents } from "klasto-mcp-oauth";

const response = await fetch(...);
for await (const event of streamSseEvents(response)) {
  console.log(event);
}

Security Considerations

⚠️ Browser Token Storage is Risky

  • Tokens stored in IndexedDB/localStorage are accessible to JavaScript running on the same origin
  • XSS attacks can steal tokens
  • Use Content Security Policy (CSP) to mitigate XSS
  • Request minimal scopes (least privilege principle)
  • Consider token rotation and short expiry times
  • For highly sensitive applications, consider backend-for-frontend (BFF) pattern

Best Practices:

  1. Use HTTPS - Always use HTTPS in production
  2. Validate Redirect URIs - Ensure redirect URIs are registered and validated
  3. Short-Lived Tokens - Prefer short-lived access tokens with refresh tokens
  4. Minimal Scopes - Only request scopes you need
  5. Content Security Policy - Use strict CSP headers
  6. Regular Audits - Review connected servers and revoke unused tokens

Next.js Example

// lib/oauth.ts
import {
  OAuthHandler,
  IndexedDbBucketStore,
  LocalStorageRegistryStore,
} from "klasto-mcp-oauth";

export const oauthHandler = new OAuthHandler({
  bucketStore: new IndexedDbBucketStore(),
  registry: new LocalStorageRegistryStore(),
  redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/auth/callback`,
  clientName: "My Next.js App",
});

// pages/connect.tsx
export default function ConnectPage() {
  async function handleConnect() {
    const result = await oauthHandler.beginAuthorization({
      serverName: "MCP Server",
      mcpUrl: "https://mcp.example.com/api",
    });
    window.location.href = result.authorizationUrl;
  }

  return <button onClick={handleConnect}>Connect to MCP Server</button>;
}

// pages/auth/callback.tsx
import { useEffect } from "react";
import { useRouter } from "next/router";
import { oauthHandler } from "../../lib/oauth";

export default function AuthCallback() {
  const router = useRouter();

  useEffect(() => {
    async function finish() {
      try {
        await oauthHandler.finishAuthorizationFromUrl({
          serverName: "MCP Server",
          mcpUrl: "https://mcp.example.com/api",
          callbackUrl: window.location.href,
        });
        router.push("/dashboard");
      } catch (error) {
        console.error(error);
        router.push("/error");
      }
    }
    finish();
  }, [router]);

  return <div>Completing authentication...</div>;
}

How It Works

  1. Discovery Phase

    • Fetch Protected Resource Metadata from MCP server
    • Discover Authorization Server metadata
    • Validate PKCE S256 support
  2. Registration Phase (if needed)

    • Register as public client via Dynamic Client Registration
    • Store client_id for reuse
  3. Authorization Phase

    • Generate PKCE challenge
    • Redirect to authorization endpoint
    • User authorizes the app
  4. Token Exchange

    • Exchange authorization code for tokens
    • Store tokens in bucket (keyed by server + issuer)
  5. Authenticated Requests

    • Auto-refresh expired tokens (with singleflight)
    • Retry on 401
    • Step-up on 403 insufficient_scope

Requirements

  • Modern browser with:
    • WebCrypto API
    • fetch API
    • IndexedDB or localStorage
  • TypeScript 5.0+ (for type definitions)
  • Bundler: Webpack, Vite, Next.js, etc.

License

MIT

Contributing

Contributions welcome! Please open an issue or PR.

Support

For issues or questions, please open a GitHub issue.