mcp-oauth-provider 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +668 -0
  2. package/dist/__tests__/config.test.js +56 -0
  3. package/dist/__tests__/config.test.js.map +1 -0
  4. package/dist/__tests__/integration.test.js +341 -0
  5. package/dist/__tests__/integration.test.js.map +1 -0
  6. package/dist/__tests__/oauth-flow.test.js +201 -0
  7. package/dist/__tests__/oauth-flow.test.js.map +1 -0
  8. package/dist/__tests__/server.test.js +271 -0
  9. package/dist/__tests__/server.test.js.map +1 -0
  10. package/dist/__tests__/storage.test.js +256 -0
  11. package/dist/__tests__/storage.test.js.map +1 -0
  12. package/dist/client/config.js +30 -0
  13. package/dist/client/config.js.map +1 -0
  14. package/dist/client/factory.js +16 -0
  15. package/dist/client/factory.js.map +1 -0
  16. package/dist/client/index.js +237 -0
  17. package/dist/client/index.js.map +1 -0
  18. package/dist/client/oauth-flow.js +73 -0
  19. package/dist/client/oauth-flow.js.map +1 -0
  20. package/dist/client/storage.js +237 -0
  21. package/dist/client/storage.js.map +1 -0
  22. package/dist/index.js +12 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/server/callback.js +164 -0
  25. package/dist/server/callback.js.map +1 -0
  26. package/dist/server/index.js +8 -0
  27. package/dist/server/index.js.map +1 -0
  28. package/dist/server/templates.js +245 -0
  29. package/dist/server/templates.js.map +1 -0
  30. package/package.json +66 -0
  31. package/src/__tests__/config.test.ts +78 -0
  32. package/src/__tests__/integration.test.ts +398 -0
  33. package/src/__tests__/oauth-flow.test.ts +276 -0
  34. package/src/__tests__/server.test.ts +391 -0
  35. package/src/__tests__/storage.test.ts +329 -0
  36. package/src/client/config.ts +134 -0
  37. package/src/client/factory.ts +19 -0
  38. package/src/client/index.ts +361 -0
  39. package/src/client/oauth-flow.ts +115 -0
  40. package/src/client/storage.ts +335 -0
  41. package/src/index.ts +31 -0
  42. package/src/server/callback.ts +257 -0
  43. package/src/server/index.ts +21 -0
  44. package/src/server/templates.ts +271 -0
package/README.md ADDED
@@ -0,0 +1,668 @@
1
+ # MCP OAuth Provider
2
+
3
+ OAuth client provider implementation for the Model Context Protocol (MCP) HTTP stream transport.
4
+
5
+ ## Features
6
+
7
+ ✅ **Full MCP SDK Integration** - Implements `OAuthClientProvider` from `@modelcontextprotocol/sdk`
8
+ ✅ **Automatic Token Refresh** - Tokens refresh automatically when expired or about to expire
9
+ ✅ **Smart Token Storage** - Stores `expires_at` (absolute time) for accurate expiry tracking
10
+ ✅ **PKCE Support** - Automatic PKCE (Proof Key for Code Exchange) via MCP SDK
11
+ ✅ **Retry Logic** - Configurable retry attempts with exponential backoff for token refresh
12
+ ✅ **Multiple Storage Backends** - Memory and file-based storage with simple interface
13
+ ✅ **Session Management** - Support for multiple concurrent OAuth sessions
14
+ ✅ **Callback Server** - Bun-native HTTP server for handling OAuth callbacks
15
+ ✅ **Error Handling** - Automatic credential invalidation on auth failures
16
+ ✅ **TypeScript** - Full type safety with types from MCP SDK
17
+ ✅ **Bun-Only** - Optimized for Bun runtime
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ bun add mcp-oauth-provider @modelcontextprotocol/sdk
23
+ ```
24
+
25
+ ## Examples
26
+
27
+ Check out the [examples](./examples) directory for complete working examples:
28
+
29
+ ### **[Notion MCP](./examples/notion)** - Connect to Notion's MCP server
30
+
31
+ Two complete examples demonstrating OAuth integration with Notion's MCP server at `https://mcp.notion.com/mcp`:
32
+
33
+ #### 1. **Basic OAuth Flow** ([index.ts](./examples/notion/index.ts))
34
+
35
+ ```bash
36
+ cd examples/notion
37
+ NOTION_CLIENT_ID=your_id NOTION_CLIENT_SECRET=your_secret bun index.ts
38
+ ```
39
+
40
+ Demonstrates:
41
+
42
+ - OAuth 2.0 authentication flow
43
+ - Token retrieval and display
44
+ - Authorization server metadata
45
+ - Error handling
46
+
47
+ #### 2. **Advanced MCP Integration** ([advanced.ts](./examples/notion/advanced.ts))
48
+
49
+ ```bash
50
+ cd examples/notion
51
+ NOTION_CLIENT_ID=your_id NOTION_CLIENT_SECRET=your_secret bun advanced.ts
52
+ ```
53
+
54
+ Demonstrates:
55
+
56
+ - Full MCP client integration with `StreamableHTTPClientTransport`
57
+ - Automatic token refresh during MCP operations
58
+ - Listing and using MCP tools, resources, and prompts
59
+ - OAuth provider integration with MCP SDK
60
+
61
+ Each example includes detailed setup instructions and demonstrates different features of the library.
62
+
63
+ ## Quick Start
64
+
65
+ ### Basic OAuth Flow
66
+
67
+ ```typescript
68
+ import { createOAuthProvider } from 'mcp-oauth-provider';
69
+ import { auth } from '@modelcontextprotocol/sdk/client/auth.js';
70
+ import { createCallbackServer } from 'mcp-oauth-provider/server';
71
+
72
+ // Create OAuth provider
73
+ const provider = createOAuthProvider({
74
+ clientId: 'your-client-id',
75
+ clientSecret: 'your-client-secret',
76
+ redirectUri: 'http://localhost:8080/callback',
77
+ scope: 'openid profile email',
78
+ });
79
+
80
+ // Start callback server
81
+ const server = await createCallbackServer({
82
+ port: 8080,
83
+ hostname: 'localhost',
84
+ });
85
+
86
+ const serverUrl = 'https://mcp.notion.com/mcp'; // or your MCP server URL
87
+
88
+ try {
89
+ // Execute OAuth flow (PKCE handled automatically by SDK)
90
+ const result = await auth(provider, {
91
+ serverUrl,
92
+ });
93
+
94
+ if (result === 'REDIRECT') {
95
+ console.log('Browser opened for authorization...');
96
+
97
+ // Wait for callback
98
+ const callbackResult = await server.waitForCallback('/callback', 120000);
99
+
100
+ // Exchange code for tokens
101
+ await auth(provider, {
102
+ serverUrl,
103
+ authorizationCode: callbackResult.code,
104
+ });
105
+ }
106
+
107
+ console.log('Authorization successful!');
108
+
109
+ // Get tokens (automatically refreshed if expired)
110
+ const tokens = await provider.tokens();
111
+ console.log('Access token:', tokens?.access_token);
112
+ } finally {
113
+ await server.stop();
114
+ }
115
+ ```
116
+
117
+ ### Using with MCP Client
118
+
119
+ ```typescript
120
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
121
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
122
+ import { createOAuthProvider } from 'mcp-oauth-provider';
123
+
124
+ // Create provider and authenticate (see above)
125
+ const provider = createOAuthProvider({
126
+ clientId: 'your-client-id',
127
+ clientSecret: 'your-client-secret',
128
+ redirectUri: 'http://localhost:8080/callback',
129
+ });
130
+
131
+ // Perform OAuth flow...
132
+ await auth(provider, { serverUrl: 'https://mcp.notion.com/mcp' });
133
+
134
+ // Create MCP client with OAuth provider
135
+ const transport = new StreamableHTTPClientTransport(
136
+ new URL('https://mcp.notion.com/mcp'),
137
+ {
138
+ authProvider: provider, // Provider handles automatic token refresh!
139
+ }
140
+ );
141
+
142
+ const client = new Client(
143
+ { name: 'my-app', version: '1.0.0' },
144
+ { capabilities: {} }
145
+ );
146
+
147
+ await client.connect(transport);
148
+
149
+ // Use MCP features
150
+ const tools = await client.listTools();
151
+ const resources = await client.listResources();
152
+ ```
153
+
154
+ ### Initial Tokens Configuration
155
+
156
+ You can provide initial tokens in the config. These will be stored on first use and can be updated:
157
+
158
+ ```typescript
159
+ import { createOAuthProvider } from 'mcp-oauth-provider';
160
+
161
+ // Provider with initial tokens
162
+ const provider = createOAuthProvider({
163
+ clientId: 'your-client-id',
164
+ clientSecret: 'your-client-secret',
165
+ redirectUri: 'http://localhost:8080/callback',
166
+ tokens: {
167
+ access_token: 'existing-access-token',
168
+ refresh_token: 'existing-refresh-token',
169
+ token_type: 'Bearer',
170
+ expires_in: 3600,
171
+ },
172
+ });
173
+
174
+ // First call returns initial tokens (and stores them)
175
+ const tokens1 = await provider.tokens();
176
+ console.log(tokens1.access_token); // 'existing-access-token'
177
+
178
+ // Tokens can be updated
179
+ await provider.saveTokens({
180
+ access_token: 'new-access-token',
181
+ refresh_token: 'new-refresh-token',
182
+ token_type: 'Bearer',
183
+ expires_in: 3600,
184
+ });
185
+
186
+ // Subsequent calls return updated tokens from storage
187
+ const tokens2 = await provider.tokens();
188
+ console.log(tokens2.access_token); // 'new-access-token'
189
+ ```
190
+
191
+ **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.
192
+
193
+ ### Automatic Token Refresh
194
+
195
+ The provider automatically refreshes expired tokens when you call `tokens()`:
196
+
197
+ ```typescript
198
+ // Tokens are automatically refreshed if expired or expiring soon (< 5 minutes)
199
+ const tokens = await provider.tokens(); // May refresh behind the scenes!
200
+
201
+ // Requirements for automatic refresh:
202
+ // 1. authorizationServerMetadata must be set (done by auth() function)
203
+ // 2. A refresh_token must be available
204
+ // 3. Token must be expired or expiring within 5 minutes
205
+
206
+ // For manual refresh (after auth() sets metadata):
207
+ const newTokens = await provider.refreshTokens();
208
+ ```
209
+
210
+ **How it works:**
211
+
212
+ - Tokens are stored with `expires_at` (absolute timestamp) instead of `expires_in`
213
+ - When retrieved, `expires_in` is calculated from `expires_at` and current time
214
+ - This ensures `expires_in` is always accurate, even hours after tokens were saved
215
+ - Tokens auto-refresh when `expires_in < 300 seconds` (5-minute buffer)
216
+
217
+ ## Storage Options
218
+
219
+ ### Memory Storage (Default)
220
+
221
+ ```typescript
222
+ import { createOAuthProvider, MemoryStorage } from 'mcp-oauth-provider';
223
+
224
+ const provider = createOAuthProvider({
225
+ redirectUri: 'http://localhost:8080/callback',
226
+ storage: new MemoryStorage(), // Data lost when process exits
227
+ });
228
+ ```
229
+
230
+ ### File Storage
231
+
232
+ ```typescript
233
+ import { createOAuthProvider, FileStorage } from 'mcp-oauth-provider';
234
+
235
+ const provider = createOAuthProvider({
236
+ redirectUri: 'http://localhost:8080/callback',
237
+ storage: new FileStorage('./oauth-data'), // Persists to filesystem
238
+ });
239
+ ```
240
+
241
+ ### Custom Storage
242
+
243
+ Implement the simple `StorageAdapter` interface:
244
+
245
+ ```typescript
246
+ import type { StorageAdapter } from 'mcp-oauth-provider';
247
+
248
+ class RedisStorage implements StorageAdapter {
249
+ constructor(private redis: RedisClient) {}
250
+
251
+ async get(key: string): Promise<string | undefined> {
252
+ return await this.redis.get(key);
253
+ }
254
+
255
+ async set(key: string, value: string): Promise<void> {
256
+ await this.redis.set(key, value);
257
+ }
258
+
259
+ async delete(key: string): Promise<void> {
260
+ await this.redis.del(key);
261
+ }
262
+ }
263
+
264
+ const provider = createOAuthProvider({
265
+ redirectUri: 'http://localhost:8080/callback',
266
+ storage: new RedisStorage(redisClient),
267
+ });
268
+ ```
269
+
270
+ ## Callback Server
271
+
272
+ ### Basic Usage
273
+
274
+ ```typescript
275
+ import { createCallbackServer } from 'mcp-oauth-provider/server';
276
+
277
+ const server = await createCallbackServer({
278
+ port: 8080,
279
+ hostname: 'localhost',
280
+ });
281
+
282
+ // Wait for OAuth callback
283
+ const result = await server.waitForCallback('/callback', 30000);
284
+
285
+ console.log('Authorization code:', result.code);
286
+ console.log('State:', result.state);
287
+
288
+ await server.stop();
289
+ ```
290
+
291
+ ### Custom Templates
292
+
293
+ ```typescript
294
+ const server = await createCallbackServer({
295
+ port: 8080,
296
+ successHtml: '<html><body><h1>Success!</h1></body></html>',
297
+ errorHtml: '<html><body><h1>Error: {{error}}</h1></body></html>',
298
+ });
299
+ ```
300
+
301
+ ### One-Shot Callback
302
+
303
+ ```typescript
304
+ import { waitForOAuthCallback } from 'mcp-oauth-provider/server';
305
+
306
+ // Server automatically starts and stops
307
+ const result = await waitForOAuthCallback('/callback', {
308
+ port: 8080,
309
+ timeout: 30000,
310
+ });
311
+ ```
312
+
313
+ ## API Reference
314
+
315
+ ### Client Provider
316
+
317
+ #### `createOAuthProvider(config: OAuthConfig)`
318
+
319
+ Creates an OAuth client provider instance.
320
+
321
+ **Config Options:**
322
+
323
+ - `redirectUri` (required) - OAuth callback URL
324
+ - `clientId` - OAuth client ID (optional for dynamic registration)
325
+ - `clientSecret` - OAuth client secret (optional for public clients)
326
+ - `scope` - OAuth scope to request
327
+ - `sessionId` - Session identifier (generated automatically if not provided)
328
+ - `storage` - Storage adapter (defaults to MemoryStorage)
329
+ - `tokens` - Static OAuth tokens (takes precedence over storage)
330
+ - `clientMetadata` - OAuth client metadata for registration
331
+ - `tokenRefresh` - Token refresh configuration
332
+ - `maxRetries` - Maximum retry attempts (default: 3)
333
+ - `retryDelay` - Delay between retries in ms (default: 1000)
334
+ - `server` - Callback server configuration
335
+
336
+ #### `provider.tokens()`
337
+
338
+ Get OAuth tokens with automatic refresh.
339
+
340
+ **Behavior:**
341
+
342
+ - Returns stored tokens if valid (> 5 minutes until expiry)
343
+ - Automatically refreshes tokens if expired or expiring soon (< 5 minutes)
344
+ - Requires `authorizationServerMetadata` to be set for automatic refresh
345
+ - Falls back to current tokens if refresh fails
346
+
347
+ **Returns:** `Promise<OAuthTokens | undefined>`
348
+
349
+ **Example:**
350
+
351
+ ```typescript
352
+ // Tokens are automatically refreshed if needed
353
+ const tokens = await provider.tokens();
354
+ ```
355
+
356
+ #### `provider.refreshTokens()`
357
+
358
+ Manually refresh OAuth tokens using the refresh token.
359
+
360
+ **Requirements:**
361
+
362
+ - `authorizationServerMetadata.token_endpoint` must be set (done by `auth()`)
363
+ - Stored tokens must include a `refresh_token`
364
+
365
+ **Returns:** `Promise<OAuthTokens>`
366
+
367
+ **Throws:** Error if no authorization server metadata or refresh token available
368
+
369
+ **Example:**
370
+
371
+ ```typescript
372
+ // Manual refresh (after calling auth())
373
+ const newTokens = await provider.refreshTokens();
374
+ ```
375
+
376
+ #### `provider.getStoredTokens()`
377
+
378
+ Get stored tokens without triggering automatic refresh.
379
+
380
+ **Use Case:** Check token state without side effects
381
+
382
+ **Returns:** `Promise<OAuthTokens | undefined>`
383
+
384
+ **Example:**
385
+
386
+ ```typescript
387
+ // Get tokens without auto-refresh
388
+ const tokens = await provider.getStoredTokens();
389
+ if (tokens && tokens.expires_in < 300) {
390
+ console.log('Tokens expiring soon!');
391
+ }
392
+ ```
393
+
394
+ #### `provider.authorizationServerMetadata`
395
+
396
+ OAuth server metadata set during the auth flow. Contains endpoints for token operations.
397
+
398
+ **Type:** `AuthorizationServerMetadata | undefined`
399
+
400
+ **Properties:**
401
+
402
+ - `token_endpoint` - URL for token refresh
403
+ - `authorization_endpoint` - URL for authorization
404
+ - `issuer` - OAuth server identifier
405
+ - And more...
406
+
407
+ **Example:**
408
+
409
+ ```typescript
410
+ // Metadata is set automatically by auth()
411
+ await auth(provider, { serverUrl: 'https://mcp.notion.com/mcp' });
412
+
413
+ console.log(provider.authorizationServerMetadata?.token_endpoint);
414
+ // Output: "https://auth.notion.com/token"
415
+ ```
416
+
417
+ ### Storage
418
+
419
+ #### `MemoryStorage`
420
+
421
+ In-memory storage (data lost on process exit).
422
+
423
+ #### `FileStorage`
424
+
425
+ File-based storage using Bun's file API.
426
+
427
+ #### `OAuthStorage`
428
+
429
+ Helper class that wraps a storage adapter with OAuth-specific methods.
430
+
431
+ ### Server
432
+
433
+ #### `createCallbackServer(options)`
434
+
435
+ Create and start an OAuth callback server.
436
+
437
+ **Options:**
438
+
439
+ - `port` (required) - Server port
440
+ - `hostname` - Server hostname (default: 'localhost')
441
+ - `successHtml` - Custom success page HTML
442
+ - `errorHtml` - Custom error page HTML
443
+ - `signal` - AbortSignal for cancellation
444
+ - `onRequest` - Request handler callback
445
+
446
+ #### `waitForOAuthCallback(path, options)`
447
+
448
+ Convenience function that starts server, waits for callback, and stops server automatically.
449
+
450
+ ## Session Management
451
+
452
+ The provider supports multiple concurrent OAuth sessions:
453
+
454
+ ```typescript
455
+ // Create session-specific providers
456
+ const session1 = createOAuthProvider({
457
+ redirectUri: 'http://localhost:8080/callback',
458
+ sessionId: 'user-123',
459
+ });
460
+
461
+ const session2 = createOAuthProvider({
462
+ redirectUri: 'http://localhost:8080/callback',
463
+ sessionId: 'user-456',
464
+ });
465
+
466
+ // Each session has isolated tokens and credentials
467
+ ```
468
+
469
+ ## Error Handling
470
+
471
+ The library handles several OAuth-specific errors:
472
+
473
+ - **Invalid state/verifier:** Throws `Error` with descriptive message
474
+ - **Missing authorization code:** Throws `Error`
475
+ - **Network errors:** Propagated from fetch calls
476
+ - **Token refresh failures:** Throws `Error` with details
477
+ - **Automatic refresh failures:** Logged as warning, returns existing tokens
478
+
479
+ ### Automatic Refresh Error Behavior
480
+
481
+ When `tokens()` attempts automatic refresh and fails:
482
+
483
+ - Logs a warning to console
484
+ - Returns existing (expired) tokens instead of throwing
485
+ - Allows application to continue and handle expiry
486
+
487
+ ```typescript
488
+ // Automatic refresh handles errors gracefully
489
+ const tokens = await provider.tokens();
490
+ // If refresh failed, you'll get expired tokens
491
+ // Check expires_in to detect this scenario
492
+ if (tokens && tokens.expires_in < 0) {
493
+ console.log('Tokens expired and refresh failed');
494
+ }
495
+ ```
496
+
497
+ ### Manual Operations
498
+
499
+ Always wrap manual OAuth operations in try-catch:
500
+
501
+ ```typescript
502
+ import { executeOAuthFlow } from 'mcp-oauth-provider';
503
+
504
+ try {
505
+ await executeOAuthFlow(provider, {
506
+ serverUrl: 'https://your-mcp-server.com',
507
+ });
508
+ } catch (error) {
509
+ if (error.message.includes('access_denied')) {
510
+ console.log('User denied authorization');
511
+ } else if (error.message.includes('invalid_client')) {
512
+ console.log('Invalid client credentials');
513
+ // Credentials automatically invalidated by library
514
+ }
515
+ }
516
+
517
+ try {
518
+ await provider.refreshTokens();
519
+ } catch (error) {
520
+ console.error('Manual refresh failed:', error.message);
521
+ }
522
+ ```
523
+
524
+ ## Security Considerations
525
+
526
+ - **Never log tokens or secrets** - The library avoids logging sensitive data
527
+ - **PKCE is automatic** - Code challenge/verifier handled by MCP SDK
528
+ - **State parameter** - CSRF protection with random state generation
529
+ - **Token expiry** - Automatic refresh before expiration
530
+ - **Credential invalidation** - Automatic cleanup on auth errors
531
+
532
+ ## Testing
533
+
534
+ This package includes comprehensive unit and integration tests using `bun:test`.
535
+
536
+ ### Running Tests
537
+
538
+ ```bash
539
+ # Run all tests
540
+ bun test
541
+
542
+ # Run tests in watch mode
543
+ bun test --watch
544
+
545
+ # Run tests with coverage
546
+ bun test --coverage
547
+ ```
548
+
549
+ ### Test Coverage
550
+
551
+ The test suite covers:
552
+
553
+ - ✅ **OAuth Flow Utilities** - Token expiry detection, token refresh with retry logic, exponential backoff
554
+ - ✅ **Storage Adapters** - MemoryStorage, FileStorage, OAuthStorage with session isolation, time-based expiry
555
+ - ✅ **Configuration** - Session ID generation, state generation, default metadata
556
+ - ✅ **OAuth Client Provider** - Token management, automatic refresh, client information handling, storage isolation
557
+ - ✅ **Callback Server** - Server lifecycle, callback handling, timeout management, custom templates
558
+
559
+ 71 tests pass with high coverage of critical OAuth functionality including automatic token refresh.
560
+
561
+ ### Test Structure
562
+
563
+ ```
564
+ src/__tests__/
565
+ ├── config.test.ts # Configuration utilities
566
+ ├── storage.test.ts # Storage adapters
567
+ ├── oauth-flow.test.ts # OAuth flow helpers
568
+ ├── integration.test.ts # MCPOAuthClientProvider integration
569
+ └── server.test.ts # Callback server functionality
570
+ ```
571
+
572
+ ### Writing Tests
573
+
574
+ When contributing, please:
575
+
576
+ 1. Add tests for new features
577
+ 2. Ensure existing tests pass
578
+ 3. Use descriptive test names
579
+ 4. Test error conditions and edge cases
580
+ 5. Mock external dependencies appropriately
581
+
582
+ Example test pattern:
583
+
584
+ ```typescript
585
+ import { describe, expect, test, beforeEach } from 'bun:test';
586
+
587
+ describe('MyFeature', () => {
588
+ beforeEach(() => {
589
+ // Setup
590
+ });
591
+
592
+ test('should do something', () => {
593
+ // Test implementation
594
+ expect(result).toBe(expected);
595
+ });
596
+ });
597
+ ```
598
+
599
+ ## Development
600
+
601
+ ```bash
602
+ # Install dependencies
603
+ bun install
604
+
605
+ # Build
606
+ bun run build
607
+
608
+ # Type check
609
+ bun run check-types
610
+
611
+ # Lint
612
+ bun run lint
613
+
614
+ # Format
615
+ bun run format
616
+
617
+ # Run tests
618
+ bun test
619
+
620
+ # Run tests in watch mode
621
+ bun test --watch
622
+ ```
623
+
624
+ ### Preview OAuth Templates
625
+
626
+ You can preview the OAuth success and error page templates locally:
627
+
628
+ ```bash
629
+ # Preview success page (runs on http://localhost:3000)
630
+ bun run server:success
631
+
632
+ # Preview error page (runs on http://localhost:3001)
633
+ bun run server:error
634
+
635
+ # Customize error page with environment variables
636
+ ERROR="invalid_client" ERROR_DESCRIPTION="Custom error message" bun run server:error
637
+ ```
638
+
639
+ Available environment variables for `server:error`:
640
+
641
+ - `ERROR` - The error code (default: `access_denied`)
642
+ - `ERROR_DESCRIPTION` - Detailed error message (default: "The user denied the authorization request.")
643
+ - `ERROR_URI` - Optional URL for more information
644
+ - `PORT` - Server port (default: 3001)
645
+
646
+ For `server:success`:
647
+
648
+ - `PORT` - Server port (default: 3000)
649
+
650
+ ## License
651
+
652
+ MIT
653
+
654
+ ## Contributing
655
+
656
+ Contributions welcome! Please open an issue or PR.
657
+
658
+ ## Documentation
659
+
660
+ - [Automatic Token Refresh Guide](./docs/AUTOMATIC_TOKEN_REFRESH.md) - Deep dive into automatic token refresh behavior
661
+ - [Token Storage Changelog](./CHANGELOG_TOKEN_STORAGE.md) - Details on expires_at storage implementation
662
+
663
+ ## Links
664
+
665
+ - [Model Context Protocol](https://modelcontextprotocol.io/)
666
+ - [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
667
+ - [OAuth 2.0 RFC](https://tools.ietf.org/html/rfc6749)
668
+ - [PKCE RFC](https://tools.ietf.org/html/rfc7636)