{"_id":"@clastines/klasto-mcp-oauth","name":"@clastines/klasto-mcp-oauth","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@clastines/klasto-mcp-oauth","version":"0.1.0","description":"OAuth library for connecting to OAuth-protected MCP servers in web/browser environments","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"build":"tsup","dev":"tsup --watch","typecheck":"tsc --noEmit","prepublishOnly":"npm run build"},"keywords":["oauth","mcp","pkce","authentication","browser","nextjs","react"],"author":"","license":"MIT","devDependencies":{"@types/node":"^20.10.0","tsup":"^8.0.1","typescript":"^5.3.3"},"engines":{"node":">=18"},"_id":"@clastines/klasto-mcp-oauth@0.1.0","_nodeVersion":"25.1.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-nz6Okbcvk1zBKUUGQCeoOTTM8C20PVglIa/D0v2mV7N18dJJGij+63ZeO5cEMoOhZr92wp9hsr8YH2fy9scVxw==","shasum":"422a9934f48a8f1236397187b14d1e6863a1f021","tarball":"https://registry.npmjs.org/@clastines/klasto-mcp-oauth/-/klasto-mcp-oauth-0.1.0.tgz","fileCount":9,"unpackedSize":255792,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIFcPfZQAcKR2xw3Danw0FmocsAIt3NhOJqbhIdT0GRzVAiBTwvHzGs3Fhh0H1eOlWMTRGlHuicPPlsu5dP7eaY0V0w=="}]},"_npmUser":{"name":"clastine","email":"sam.jesumuthu@clastines.com"},"directories":{},"maintainers":[{"name":"clastine","email":"sam.jesumuthu@clastines.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/klasto-mcp-oauth_0.1.0_1766085052711_0.993952379796522"},"_hasShrinkwrap":false}},"time":{"created":"2025-12-18T19:10:52.560Z","0.1.0":"2025-12-18T19:10:52.845Z","modified":"2025-12-18T19:10:53.299Z"},"maintainers":[{"name":"clastine","email":"sam.jesumuthu@clastines.com"}],"description":"OAuth library for connecting to OAuth-protected MCP servers in web/browser environments","keywords":["oauth","mcp","pkce","authentication","browser","nextjs","react"],"license":"MIT","readme":"# klasto-mcp-oauth\r\n\r\n> OAuth library for connecting to OAuth-protected MCP servers in web/browser environments\r\n\r\n`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.\r\n\r\n## Features\r\n\r\n- ✅ **OAuth 2.0 Authorization Code + PKCE (S256)** - Secure public client flow\r\n- ✅ **Protected Resource Metadata (PRM) Discovery** - Auto-discover auth requirements\r\n- ✅ **Authorization Server Metadata Discovery** - Support OAuth 2.0 and OIDC patterns\r\n- ✅ **Dynamic Client Registration** - Automatic public client registration\r\n- ✅ **Token Bucket Storage** - Manage tokens per MCP server + issuer\r\n- ✅ **Auto-Refresh with Singleflight** - Prevent token refresh storms\r\n- ✅ **401/403 Handling** - Auto-retry on 401, step-up auth on insufficient scope\r\n- ✅ **SSE Support** - Parse `text/event-stream` responses into JSON\r\n- ✅ **Web-Only** - Uses WebCrypto, fetch, IndexedDB/localStorage (no Node dependencies)\r\n\r\n## Installation\r\n\r\n```bash\r\nnpm install klasto-mcp-oauth\r\n```\r\n\r\n## Quick Start\r\n\r\n### 1. Initialize the OAuth Handler\r\n\r\n```typescript\r\nimport {\r\n  OAuthHandler,\r\n  IndexedDbBucketStore,\r\n  LocalStorageRegistryStore,\r\n} from \"klasto-mcp-oauth\";\r\n\r\nconst handler = new OAuthHandler({\r\n  bucketStore: new IndexedDbBucketStore(),\r\n  registry: new LocalStorageRegistryStore(),\r\n  redirectUri: \"http://localhost:3000/auth/callback\",\r\n  clientName: \"My MCP Client\",\r\n});\r\n```\r\n\r\n### 2. Start Authorization Flow\r\n\r\nWhen the user wants to connect to an MCP server:\r\n\r\n```typescript\r\n// In your connect handler\r\nasync function connectToMcpServer() {\r\n  const result = await handler.beginAuthorization({\r\n    serverName: \"My MCP Server\",\r\n    mcpUrl: \"https://mcp.example.com/api\",\r\n    scopes: [\"read\", \"write\"], // Optional\r\n  });\r\n\r\n  // Redirect the user to the authorization URL\r\n  window.location.href = result.authorizationUrl;\r\n}\r\n```\r\n\r\n### 3. Handle OAuth Callback\r\n\r\nCreate a callback route/page (e.g., `/auth/callback`):\r\n\r\n```typescript\r\n// In your callback page (Next.js example)\r\nimport { useEffect } from \"react\";\r\nimport { useRouter } from \"next/router\";\r\n\r\nexport default function AuthCallback() {\r\n  const router = useRouter();\r\n\r\n  useEffect(() => {\r\n    async function finishAuth() {\r\n      try {\r\n        const key = await handler.finishAuthorizationFromUrl({\r\n          serverName: \"My MCP Server\",\r\n          mcpUrl: \"https://mcp.example.com/api\",\r\n          callbackUrl: window.location.href,\r\n        });\r\n\r\n        console.log(\"Connected!\", key);\r\n        router.push(\"/dashboard\");\r\n      } catch (error) {\r\n        console.error(\"Auth failed:\", error);\r\n        router.push(\"/error\");\r\n      }\r\n    }\r\n\r\n    finishAuth();\r\n  }, []);\r\n\r\n  return <div>Completing authentication...</div>;\r\n}\r\n```\r\n\r\n### 4. Make Authenticated Requests\r\n\r\nOnce connected, use `authenticatedFetch` to make requests:\r\n\r\n```typescript\r\nimport { StepUpRequiredError } from \"klasto-mcp-oauth\";\r\n\r\nasync function callMcpApi(key) {\r\n  try {\r\n    const response = await handler.authenticatedFetch({\r\n      key,\r\n      url: \"https://mcp.example.com/api/resources\",\r\n      acceptSse: true, // For SSE responses\r\n    });\r\n\r\n    if (response.ok) {\r\n      const data = await response.json();\r\n      console.log(data);\r\n    }\r\n  } catch (error) {\r\n    if (error instanceof StepUpRequiredError) {\r\n      // User needs to grant additional scopes\r\n      console.log(\"Step-up required:\", error.requiredScopes);\r\n      window.location.href = error.authorizationUrl;\r\n    } else {\r\n      console.error(\"Request failed:\", error);\r\n    }\r\n  }\r\n}\r\n```\r\n\r\n## Multi-Server Support\r\n\r\nThe library manages separate token buckets for each combination of:\r\n- `serverName` - Human-readable label\r\n- `resource` - MCP resource URI (from PRM)\r\n- `issuer` - Authorization server issuer\r\n\r\nYou can connect to multiple MCP servers simultaneously:\r\n\r\n```typescript\r\n// Connect to Server A\r\nconst keyA = await handler.beginAuthorization({\r\n  serverName: \"Server A\",\r\n  mcpUrl: \"https://server-a.example.com/api\",\r\n});\r\n\r\n// Connect to Server B (different server, same or different issuer)\r\nconst keyB = await handler.beginAuthorization({\r\n  serverName: \"Server B\",\r\n  mcpUrl: \"https://server-b.example.com/api\",\r\n});\r\n\r\n// List all connected servers\r\nconst connections = await handler.listConnections();\r\nconsole.log(connections);\r\n```\r\n\r\n## API Reference\r\n\r\n### `OAuthHandler`\r\n\r\nMain class for managing OAuth flows.\r\n\r\n#### Constructor Options\r\n\r\n```typescript\r\nnew OAuthHandler({\r\n  bucketStore: TokenBucketStore,         // Required: Token storage\r\n  registry: RegistryStore,                // Required: Connection registry\r\n  redirectUri: string,                    // Required: OAuth redirect URI\r\n  clientName?: string,                    // Optional: Client name for registration\r\n  fetchFn?: typeof fetch,                 // Optional: Custom fetch\r\n  expiryLeewayMs?: number,                // Optional: Token expiry leeway (default: 5min)\r\n  refreshSingleflight?: boolean,          // Optional: Enable singleflight (default: true)\r\n  includeResourceInAuthorize?: boolean,   // Optional: Send resource in authz (default: true)\r\n  maxAuthRetries?: number,                // Optional: Max retries (default: 2)\r\n})\r\n```\r\n\r\n#### Methods\r\n\r\n**`beginAuthorization(args)`**\r\n\r\nStart the OAuth flow. Returns an authorization URL.\r\n\r\n```typescript\r\nconst { authorizationUrl, keyHint } = await handler.beginAuthorization({\r\n  serverName: \"My Server\",\r\n  mcpUrl: \"https://mcp.example.com/api\",\r\n  scopes?: [\"read\", \"write\"],\r\n});\r\n```\r\n\r\n**`finishAuthorizationFromUrl(args)`**\r\n\r\nComplete the OAuth flow from the callback URL.\r\n\r\n```typescript\r\nconst key = await handler.finishAuthorizationFromUrl({\r\n  serverName: \"My Server\",\r\n  mcpUrl: \"https://mcp.example.com/api\",\r\n  callbackUrl: window.location.href,\r\n});\r\n```\r\n\r\n**`authenticatedFetch(args)`**\r\n\r\nMake an authenticated request with auto-refresh and retry.\r\n\r\n```typescript\r\nconst response = await handler.authenticatedFetch({\r\n  key: TokenBucketKey,\r\n  url: string,\r\n  init?: RequestInit,\r\n  acceptSse?: boolean,                    // Add SSE Accept header\r\n  retryOn401?: boolean,                   // Auto-retry on 401 (default: true)\r\n  retryOnInsufficientScope?: boolean,     // Throw StepUpRequiredError (default: true)\r\n});\r\n```\r\n\r\n**`prepareHeaders(args)`**\r\n\r\nGet authorization headers for a request.\r\n\r\n```typescript\r\nconst headers = await handler.prepareHeaders({ key });\r\n// { Authorization: \"Bearer <token>\" }\r\n```\r\n\r\n**`listConnections()`**\r\n\r\nList all connected servers.\r\n\r\n```typescript\r\nconst connections = await handler.listConnections();\r\n// [{ key: TokenBucketKey, meta: {...} }, ...]\r\n```\r\n\r\n**`logout(args)`**\r\n\r\nDisconnect from a server and optionally revoke tokens.\r\n\r\n```typescript\r\nawait handler.logout({\r\n  key: TokenBucketKey,\r\n  revoke: true, // Revoke tokens at AS\r\n});\r\n```\r\n\r\n**`clear(args)`**\r\n\r\nRemove tokens and registration without revocation.\r\n\r\n```typescript\r\nawait handler.clear({ key: TokenBucketKey });\r\n```\r\n\r\n### Storage Implementations\r\n\r\n**`IndexedDbBucketStore`** (Recommended)\r\n\r\nUses IndexedDB for token storage.\r\n\r\n```typescript\r\nimport { IndexedDbBucketStore } from \"klasto-mcp-oauth\";\r\nconst store = new IndexedDbBucketStore();\r\n```\r\n\r\n**`LocalStorageBucketStore`** (Fallback)\r\n\r\nUses localStorage for token storage.\r\n\r\n```typescript\r\nimport { LocalStorageBucketStore } from \"klasto-mcp-oauth\";\r\nconst store = new LocalStorageBucketStore();\r\n```\r\n\r\n**`LocalStorageRegistryStore`**\r\n\r\nUses localStorage for connection metadata.\r\n\r\n```typescript\r\nimport { LocalStorageRegistryStore } from \"klasto-mcp-oauth\";\r\nconst registry = new LocalStorageRegistryStore();\r\n```\r\n\r\n### Error Handling\r\n\r\n**`StepUpRequiredError`**\r\n\r\nThrown when a request fails with `403 insufficient_scope`. Contains the authorization URL for step-up.\r\n\r\n```typescript\r\nimport { StepUpRequiredError } from \"klasto-mcp-oauth\";\r\n\r\ntry {\r\n  await handler.authenticatedFetch({ ... });\r\n} catch (error) {\r\n  if (error instanceof StepUpRequiredError) {\r\n    console.log(\"Required scopes:\", error.requiredScopes);\r\n    window.location.href = error.authorizationUrl;\r\n  }\r\n}\r\n```\r\n\r\nOther errors: `OAuthFlowError`, `TokenError`, `DiscoveryError`\r\n\r\n### SSE Utilities\r\n\r\n**`parseSseJson(input)`**\r\n\r\nParse SSE text into JSON.\r\n\r\n```typescript\r\nimport { parseSseJson } from \"klasto-mcp-oauth\";\r\n\r\nconst data = parseSseJson(\"data: {\\\"message\\\":\\\"hello\\\"}\\n\\n\");\r\n```\r\n\r\n**`parseSseResponse(response)`**\r\n\r\nParse an SSE Response object.\r\n\r\n```typescript\r\nimport { parseSseResponse } from \"klasto-mcp-oauth\";\r\n\r\nconst response = await fetch(...);\r\nconst data = await parseSseResponse(response);\r\n```\r\n\r\n**`streamSseEvents(response)`**\r\n\r\nStream SSE events as async iterator.\r\n\r\n```typescript\r\nimport { streamSseEvents } from \"klasto-mcp-oauth\";\r\n\r\nconst response = await fetch(...);\r\nfor await (const event of streamSseEvents(response)) {\r\n  console.log(event);\r\n}\r\n```\r\n\r\n## Security Considerations\r\n\r\n⚠️ **Browser Token Storage is Risky**\r\n\r\n- Tokens stored in IndexedDB/localStorage are accessible to JavaScript running on the same origin\r\n- XSS attacks can steal tokens\r\n- Use Content Security Policy (CSP) to mitigate XSS\r\n- Request minimal scopes (least privilege principle)\r\n- Consider token rotation and short expiry times\r\n- For highly sensitive applications, consider backend-for-frontend (BFF) pattern\r\n\r\n**Best Practices:**\r\n\r\n1. **Use HTTPS** - Always use HTTPS in production\r\n2. **Validate Redirect URIs** - Ensure redirect URIs are registered and validated\r\n3. **Short-Lived Tokens** - Prefer short-lived access tokens with refresh tokens\r\n4. **Minimal Scopes** - Only request scopes you need\r\n5. **Content Security Policy** - Use strict CSP headers\r\n6. **Regular Audits** - Review connected servers and revoke unused tokens\r\n\r\n## Next.js Example\r\n\r\n```typescript\r\n// lib/oauth.ts\r\nimport {\r\n  OAuthHandler,\r\n  IndexedDbBucketStore,\r\n  LocalStorageRegistryStore,\r\n} from \"klasto-mcp-oauth\";\r\n\r\nexport const oauthHandler = new OAuthHandler({\r\n  bucketStore: new IndexedDbBucketStore(),\r\n  registry: new LocalStorageRegistryStore(),\r\n  redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/auth/callback`,\r\n  clientName: \"My Next.js App\",\r\n});\r\n\r\n// pages/connect.tsx\r\nexport default function ConnectPage() {\r\n  async function handleConnect() {\r\n    const result = await oauthHandler.beginAuthorization({\r\n      serverName: \"MCP Server\",\r\n      mcpUrl: \"https://mcp.example.com/api\",\r\n    });\r\n    window.location.href = result.authorizationUrl;\r\n  }\r\n\r\n  return <button onClick={handleConnect}>Connect to MCP Server</button>;\r\n}\r\n\r\n// pages/auth/callback.tsx\r\nimport { useEffect } from \"react\";\r\nimport { useRouter } from \"next/router\";\r\nimport { oauthHandler } from \"../../lib/oauth\";\r\n\r\nexport default function AuthCallback() {\r\n  const router = useRouter();\r\n\r\n  useEffect(() => {\r\n    async function finish() {\r\n      try {\r\n        await oauthHandler.finishAuthorizationFromUrl({\r\n          serverName: \"MCP Server\",\r\n          mcpUrl: \"https://mcp.example.com/api\",\r\n          callbackUrl: window.location.href,\r\n        });\r\n        router.push(\"/dashboard\");\r\n      } catch (error) {\r\n        console.error(error);\r\n        router.push(\"/error\");\r\n      }\r\n    }\r\n    finish();\r\n  }, [router]);\r\n\r\n  return <div>Completing authentication...</div>;\r\n}\r\n```\r\n\r\n## How It Works\r\n\r\n1. **Discovery Phase**\r\n   - Fetch Protected Resource Metadata from MCP server\r\n   - Discover Authorization Server metadata\r\n   - Validate PKCE S256 support\r\n\r\n2. **Registration Phase** (if needed)\r\n   - Register as public client via Dynamic Client Registration\r\n   - Store client_id for reuse\r\n\r\n3. **Authorization Phase**\r\n   - Generate PKCE challenge\r\n   - Redirect to authorization endpoint\r\n   - User authorizes the app\r\n\r\n4. **Token Exchange**\r\n   - Exchange authorization code for tokens\r\n   - Store tokens in bucket (keyed by server + issuer)\r\n\r\n5. **Authenticated Requests**\r\n   - Auto-refresh expired tokens (with singleflight)\r\n   - Retry on 401\r\n   - Step-up on 403 insufficient_scope\r\n\r\n## Requirements\r\n\r\n- Modern browser with:\r\n  - WebCrypto API\r\n  - fetch API\r\n  - IndexedDB or localStorage\r\n- TypeScript 5.0+ (for type definitions)\r\n- Bundler: Webpack, Vite, Next.js, etc.\r\n\r\n## License\r\n\r\nMIT\r\n\r\n## Contributing\r\n\r\nContributions welcome! Please open an issue or PR.\r\n\r\n## Support\r\n\r\nFor issues or questions, please open a GitHub issue.\r\n","readmeFilename":"README.md","_rev":"1-1f18faf6aff38ce299a1e4ff755b0716"}