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.
- package/README.md +668 -0
- package/dist/__tests__/config.test.js +56 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/integration.test.js +341 -0
- package/dist/__tests__/integration.test.js.map +1 -0
- package/dist/__tests__/oauth-flow.test.js +201 -0
- package/dist/__tests__/oauth-flow.test.js.map +1 -0
- package/dist/__tests__/server.test.js +271 -0
- package/dist/__tests__/server.test.js.map +1 -0
- package/dist/__tests__/storage.test.js +256 -0
- package/dist/__tests__/storage.test.js.map +1 -0
- package/dist/client/config.js +30 -0
- package/dist/client/config.js.map +1 -0
- package/dist/client/factory.js +16 -0
- package/dist/client/factory.js.map +1 -0
- package/dist/client/index.js +237 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/oauth-flow.js +73 -0
- package/dist/client/oauth-flow.js.map +1 -0
- package/dist/client/storage.js +237 -0
- package/dist/client/storage.js.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/server/callback.js +164 -0
- package/dist/server/callback.js.map +1 -0
- package/dist/server/index.js +8 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/templates.js +245 -0
- package/dist/server/templates.js.map +1 -0
- package/package.json +66 -0
- package/src/__tests__/config.test.ts +78 -0
- package/src/__tests__/integration.test.ts +398 -0
- package/src/__tests__/oauth-flow.test.ts +276 -0
- package/src/__tests__/server.test.ts +391 -0
- package/src/__tests__/storage.test.ts +329 -0
- package/src/client/config.ts +134 -0
- package/src/client/factory.ts +19 -0
- package/src/client/index.ts +361 -0
- package/src/client/oauth-flow.ts +115 -0
- package/src/client/storage.ts +335 -0
- package/src/index.ts +31 -0
- package/src/server/callback.ts +257 -0
- package/src/server/index.ts +21 -0
- 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)
|