mcp-http-webhook 1.0.0

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 (80) hide show
  1. package/.eslintrc.json +16 -0
  2. package/.prettierrc.json +8 -0
  3. package/ARCHITECTURE.md +269 -0
  4. package/CONTRIBUTING.md +136 -0
  5. package/GETTING_STARTED.md +310 -0
  6. package/IMPLEMENTATION.md +294 -0
  7. package/LICENSE +21 -0
  8. package/MIGRATION_TO_SDK.md +263 -0
  9. package/README.md +496 -0
  10. package/SDK_INTEGRATION_COMPLETE.md +300 -0
  11. package/STANDARD_SUBSCRIPTIONS.md +268 -0
  12. package/STANDARD_SUBSCRIPTIONS_COMPLETE.md +309 -0
  13. package/SUMMARY.md +272 -0
  14. package/Spec.md +2778 -0
  15. package/dist/errors/index.d.ts +52 -0
  16. package/dist/errors/index.d.ts.map +1 -0
  17. package/dist/errors/index.js +81 -0
  18. package/dist/errors/index.js.map +1 -0
  19. package/dist/index.d.ts +9 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +37 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/protocol/ProtocolHandler.d.ts +37 -0
  24. package/dist/protocol/ProtocolHandler.d.ts.map +1 -0
  25. package/dist/protocol/ProtocolHandler.js +172 -0
  26. package/dist/protocol/ProtocolHandler.js.map +1 -0
  27. package/dist/server.d.ts +6 -0
  28. package/dist/server.d.ts.map +1 -0
  29. package/dist/server.js +502 -0
  30. package/dist/server.js.map +1 -0
  31. package/dist/stores/InMemoryStore.d.ts +27 -0
  32. package/dist/stores/InMemoryStore.d.ts.map +1 -0
  33. package/dist/stores/InMemoryStore.js +73 -0
  34. package/dist/stores/InMemoryStore.js.map +1 -0
  35. package/dist/stores/RedisStore.d.ts +18 -0
  36. package/dist/stores/RedisStore.d.ts.map +1 -0
  37. package/dist/stores/RedisStore.js +45 -0
  38. package/dist/stores/RedisStore.js.map +1 -0
  39. package/dist/stores/index.d.ts +3 -0
  40. package/dist/stores/index.d.ts.map +1 -0
  41. package/dist/stores/index.js +9 -0
  42. package/dist/stores/index.js.map +1 -0
  43. package/dist/subscriptions/SubscriptionManager.d.ts +49 -0
  44. package/dist/subscriptions/SubscriptionManager.d.ts.map +1 -0
  45. package/dist/subscriptions/SubscriptionManager.js +181 -0
  46. package/dist/subscriptions/SubscriptionManager.js.map +1 -0
  47. package/dist/types/index.d.ts +271 -0
  48. package/dist/types/index.d.ts.map +1 -0
  49. package/dist/types/index.js +16 -0
  50. package/dist/types/index.js.map +1 -0
  51. package/dist/utils/index.d.ts +51 -0
  52. package/dist/utils/index.d.ts.map +1 -0
  53. package/dist/utils/index.js +154 -0
  54. package/dist/utils/index.js.map +1 -0
  55. package/dist/webhooks/WebhookManager.d.ts +27 -0
  56. package/dist/webhooks/WebhookManager.d.ts.map +1 -0
  57. package/dist/webhooks/WebhookManager.js +174 -0
  58. package/dist/webhooks/WebhookManager.js.map +1 -0
  59. package/examples/GITHUB_LIVE_EXAMPLE.md +308 -0
  60. package/examples/GITHUB_LIVE_SETUP.md +253 -0
  61. package/examples/QUICKSTART.md +130 -0
  62. package/examples/basic-setup.ts +142 -0
  63. package/examples/github-server-live.ts +690 -0
  64. package/examples/github-server.ts +223 -0
  65. package/examples/google-drive-server-live.ts +773 -0
  66. package/examples/start-github-live.sh +53 -0
  67. package/jest.config.js +20 -0
  68. package/package.json +58 -0
  69. package/src/errors/index.ts +81 -0
  70. package/src/index.ts +19 -0
  71. package/src/server.ts +595 -0
  72. package/src/stores/InMemoryStore.ts +87 -0
  73. package/src/stores/RedisStore.ts +51 -0
  74. package/src/stores/index.ts +2 -0
  75. package/src/subscriptions/SubscriptionManager.ts +240 -0
  76. package/src/types/index.ts +341 -0
  77. package/src/utils/index.ts +156 -0
  78. package/src/webhooks/WebhookManager.ts +230 -0
  79. package/test-sdk-integration.sh +157 -0
  80. package/tsconfig.json +21 -0
@@ -0,0 +1,773 @@
1
+ import { createMCPServer } from '../src';
2
+ import { InMemoryStore } from '../src/stores';
3
+ import { google } from 'googleapis';
4
+ import ngrok from '@ngrok/ngrok';
5
+ import crypto from 'crypto';
6
+
7
+ /**
8
+ * Google Drive MCP Server - LIVE Multi-User Example
9
+ *
10
+ * This example supports multiple users with different Google Drive credentials.
11
+ * Each user is authenticated via a token, and their credentials are fetched
12
+ * from a credentials store (simulated here, but would be a service in production).
13
+ *
14
+ * Prerequisites:
15
+ * 1. ngrok auth token for public URL tunneling
16
+ * 2. User tokens configured in CREDENTIALS_STORE below
17
+ * 3. Google Cloud project with Drive API enabled
18
+ * 4. OAuth2 credentials with push notification permissions
19
+ */
20
+
21
+ /**
22
+ * User credentials interface
23
+ */
24
+ interface UserCredentials {
25
+ accessToken: string;
26
+ refreshToken: string;
27
+ scope: string;
28
+ tokenType: string;
29
+ expiryDate: number;
30
+ clientId: string;
31
+ clientSecret: string;
32
+ userEmail: string;
33
+ }
34
+
35
+ /**
36
+ * Simulated credentials store
37
+ * In production, this would be replaced by a service call
38
+ * that fetches credentials based on token and MCP server name
39
+ */
40
+ const CREDENTIALS_STORE = new Map<string, UserCredentials>([
41
+ // Example user 1
42
+ ['user-token-123', {
43
+ accessToken: "ya29.a0AQQ_BDTPIJQbkfJtJ3X2qlAvaUbn07qubQ8CzgrtLxwCcvKzHWIEp5L79509AFgxyB3zshGA88K55KExi0SN-JkgglQtakNSEPfptTqSPlNxZSB5O4xfrPK1FdA70bhuSZtkkw_j69eP-ktNFed0P3hyfUs_iu4lNYafgVuXxFNVs_IU8P3yjLxOXgFlnC0Ue1jQlC9-aCgYKAewSARUSFQHGX2MiNkiaO2r8IOBIA5ZGNzoUBg0207",
44
+ scope: "https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/spreadsheets",
45
+ tokenType: "Bearer",
46
+ expiryDate: 1760011303441,
47
+ refreshToken: "1//0gx51TpUb2HxJCgYIARAAGBASNwF-L9IrUQq0by-ReRNCwftjoVhI-MTLK5tJSNKdp_w_yLiRLPJKns2vo0fKL6LibqbyMgCyG0c",
48
+ clientId: "747030937811-4ucgh4jebiju1niume6hu1pd2ti4kknk.apps.googleusercontent.com",
49
+ clientSecret: "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",
50
+ userEmail: "satyajeet.acharya@yoctotta.com",
51
+ }],
52
+ // Example user 2
53
+ ['user-token-456', {
54
+ accessToken: 'ya29.example_access_token_2',
55
+ scope: "https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/spreadsheets",
56
+ tokenType: "Bearer",
57
+ expiryDate: 1760011303441,
58
+ refreshToken: '1//example_refresh_token_2',
59
+ clientId: 'your-client-id.apps.googleusercontent.com',
60
+ clientSecret: 'your-client-secret',
61
+ userEmail: 'user2@example.com',
62
+ }],
63
+ // Add more users as needed
64
+ ]);
65
+
66
+ /**
67
+ * Fetch user credentials from the store (simulates a service call)
68
+ * In production, this would make an HTTP call to a credentials service
69
+ */
70
+ async function fetchCredentials(token: string, _mcpServerName: string): Promise<UserCredentials | null> {
71
+ // Simulate async service call
72
+ await new Promise(resolve => setTimeout(resolve, 10));
73
+
74
+ // In production, this would be:
75
+ // const response = await fetch(`https://credentials-service.com/api/credentials`, {
76
+ // method: 'POST',
77
+ // headers: { 'Authorization': `Bearer ${token}` },
78
+ // body: JSON.stringify({ mcpServerName: _mcpServerName })
79
+ // });
80
+ // return await response.json();
81
+
82
+ return CREDENTIALS_STORE.get(token) || null;
83
+ }
84
+
85
+ /**
86
+ * Create a Google Drive client for a specific user
87
+ */
88
+ function createDriveClient(credentials: UserCredentials) {
89
+ const oauth2Client = new google.auth.OAuth2(
90
+ credentials.clientId,
91
+ credentials.clientSecret,
92
+ 'http://localhost'
93
+ );
94
+
95
+ oauth2Client.setCredentials({
96
+ access_token: credentials.accessToken,
97
+ refresh_token: credentials.refreshToken,
98
+ });
99
+
100
+ return google.drive({ version: 'v3', auth: oauth2Client });
101
+ }
102
+
103
+ async function main() {
104
+ console.log('╔═══════════════════════════════════════════════════════════╗');
105
+ console.log('║ Google Drive MCP Server - Multi-User Live Integration ║');
106
+ console.log('╚═══════════════════════════════════════════════════════════╝');
107
+ console.log('');
108
+
109
+ // Configuration
110
+ const ngrokAuthToken = process.env.NGROK_AUTH_TOKEN || '343iE4JabU5sRNM235xJ0N1iFQK_4GHuxi3va6GFVDn5pW92W';
111
+ const webhookSecret = process.env.WEBHOOK_SECRET || crypto.randomBytes(32).toString('hex');
112
+ const mcpServerName = 'google-drive-mcp-live';
113
+
114
+ console.log('🚀 Starting multi-user MCP server...');
115
+ console.log('');
116
+
117
+ // Start ngrok tunnel
118
+ console.log('🌐 Starting ngrok tunnel...');
119
+ let ngrokUrl: string;
120
+
121
+ try {
122
+ ngrokUrl = await ngrok.connect({
123
+ addr: 3001,
124
+ authtoken: ngrokAuthToken || undefined
125
+ })
126
+ .then(x => {
127
+ const url = x.url();
128
+ console.log(`✅ ngrok tunnel established: ${url}`);
129
+ if (!url) {
130
+ console.log('❌ ngrok did not return a URL!');
131
+ process.exit(1);
132
+ }
133
+ return url;
134
+ })
135
+ .catch((err) => {
136
+ console.log('❌ Failed to start ngrok tunnel:', err.message);
137
+ console.log(' Set NGROK_AUTH_TOKEN environment variable');
138
+ process.exit(1);
139
+ });
140
+ console.log(`✅ Public URL: ${ngrokUrl}`);
141
+ console.log('');
142
+ } catch (error: any) {
143
+ console.log('❌ Failed to start ngrok tunnel:', error.message);
144
+ console.log('');
145
+ console.log(' Get your ngrok auth token:');
146
+ console.log(' https://dashboard.ngrok.com/get-started/your-authtoken');
147
+ console.log(' Then set: export NGROK_AUTH_TOKEN="your_token"');
148
+ process.exit(1);
149
+ }
150
+
151
+ // Store for push notification channels (keyed by subscriptionId)
152
+ const channelStore = new Map<string, { channelId: string; resourceId: string; userId: string; folderId?: string }>();
153
+ const store = new InMemoryStore();
154
+
155
+ const server = createMCPServer({
156
+ name: 'google-drive-mcp-live',
157
+ version: '1.0.0',
158
+ publicUrl: ngrokUrl,
159
+ port: 3001,
160
+ store,
161
+
162
+ tools: [
163
+ {
164
+ name: 'list_files',
165
+ description: 'List files and folders in Google Drive',
166
+ inputSchema: {
167
+ type: 'object',
168
+ properties: {
169
+ folderId: {
170
+ type: 'string',
171
+ description: 'Folder ID to list files from (omit for root)',
172
+ },
173
+ query: {
174
+ type: 'string',
175
+ description: 'Search query (e.g., "name contains \'document\'")',
176
+ },
177
+ pageSize: {
178
+ type: 'number',
179
+ description: 'Number of files to return',
180
+ },
181
+ },
182
+ },
183
+ handler: async (input, context) => {
184
+ console.log('📂 Listing Google Drive files...');
185
+
186
+ // Get user credentials from context
187
+ const credentials = context.credentials as UserCredentials;
188
+ if (!credentials) {
189
+ return {
190
+ success: false,
191
+ error: 'User not authenticated or credentials not found',
192
+ };
193
+ }
194
+
195
+ const drive = createDriveClient(credentials);
196
+
197
+ try {
198
+ let q = "trashed=false";
199
+
200
+ if (input.folderId) {
201
+ q += ` and '${input.folderId}' in parents`;
202
+ } else {
203
+ q += " and 'root' in parents";
204
+ }
205
+
206
+ if (input.query) {
207
+ q += ` and ${input.query}`;
208
+ }
209
+
210
+ const response = await drive.files.list({
211
+ q,
212
+ pageSize: input.pageSize || 100,
213
+ fields: 'files(id, name, mimeType, modifiedTime, size, webViewLink, parents)',
214
+ });
215
+
216
+ const files = response.data.files || [];
217
+ console.log(`✅ Listed ${files.length} files (user: ${credentials.userEmail})`);
218
+
219
+ return {
220
+ success: true,
221
+ count: files.length,
222
+ files: files.map((file: any) => ({
223
+ id: file.id,
224
+ name: file.name,
225
+ mimeType: file.mimeType,
226
+ modifiedTime: file.modifiedTime,
227
+ size: file.size,
228
+ webViewLink: file.webViewLink,
229
+ parents: file.parents,
230
+ })),
231
+ };
232
+ } catch (error: any) {
233
+ console.log('❌ Failed to list files:', error.message);
234
+ return {
235
+ success: false,
236
+ error: error.message,
237
+ };
238
+ }
239
+ },
240
+ },
241
+
242
+ {
243
+ name: 'create_file',
244
+ description: 'Create a new file in Google Drive',
245
+ inputSchema: {
246
+ type: 'object',
247
+ properties: {
248
+ name: {
249
+ type: 'string',
250
+ description: 'File name',
251
+ },
252
+ content: {
253
+ type: 'string',
254
+ description: 'File content',
255
+ },
256
+ mimeType: {
257
+ type: 'string',
258
+ description: 'MIME type (default: text/plain)',
259
+ },
260
+ folderId: {
261
+ type: 'string',
262
+ description: 'Parent folder ID (omit for root)',
263
+ },
264
+ },
265
+ required: ['name', 'content'],
266
+ },
267
+ handler: async (input, context) => {
268
+ console.log('📄 Creating Google Drive file:', input.name);
269
+
270
+ // Get user credentials from context
271
+ const credentials = context.credentials as UserCredentials;
272
+ if (!credentials) {
273
+ return {
274
+ success: false,
275
+ error: 'User not authenticated or credentials not found',
276
+ };
277
+ }
278
+
279
+ const drive = createDriveClient(credentials);
280
+
281
+ try {
282
+ const fileMetadata: any = {
283
+ name: input.name,
284
+ mimeType: input.mimeType || 'text/plain',
285
+ };
286
+
287
+ if (input.folderId) {
288
+ fileMetadata.parents = [input.folderId];
289
+ }
290
+
291
+ const media = {
292
+ mimeType: input.mimeType || 'text/plain',
293
+ body: input.content,
294
+ };
295
+
296
+ const response = await drive.files.create({
297
+ requestBody: fileMetadata,
298
+ media,
299
+ fields: 'id, name, mimeType, webViewLink, createdTime',
300
+ });
301
+
302
+ const file = response.data;
303
+ console.log(`✅ File created: ${file.name} (user: ${credentials.userEmail})`);
304
+
305
+ return {
306
+ success: true,
307
+ file: {
308
+ id: file.id,
309
+ name: file.name,
310
+ mimeType: file.mimeType,
311
+ webViewLink: file.webViewLink,
312
+ createdTime: file.createdTime,
313
+ },
314
+ };
315
+ } catch (error: any) {
316
+ console.log('❌ Failed to create file:', error.message);
317
+ return {
318
+ success: false,
319
+ error: error.message,
320
+ };
321
+ }
322
+ },
323
+ },
324
+
325
+ {
326
+ name: 'read_file',
327
+ description: 'Read content of a file from Google Drive',
328
+ inputSchema: {
329
+ type: 'object',
330
+ properties: {
331
+ fileId: {
332
+ type: 'string',
333
+ description: 'File ID to read',
334
+ },
335
+ },
336
+ required: ['fileId'],
337
+ },
338
+ handler: async (input, context) => {
339
+ console.log('📖 Reading Google Drive file:', input.fileId);
340
+
341
+ // Get user credentials from context
342
+ const credentials = context.credentials as UserCredentials;
343
+ if (!credentials) {
344
+ return {
345
+ success: false,
346
+ error: 'User not authenticated or credentials not found',
347
+ };
348
+ }
349
+
350
+ const drive = createDriveClient(credentials);
351
+
352
+ try {
353
+ // Get file metadata
354
+ const metaResponse = await drive.files.get({
355
+ fileId: input.fileId,
356
+ fields: 'id, name, mimeType, size, modifiedTime',
357
+ });
358
+
359
+ // Get file content
360
+ const contentResponse = await drive.files.get(
361
+ { fileId: input.fileId, alt: 'media' },
362
+ { responseType: 'text' }
363
+ );
364
+
365
+ console.log(`✅ File read: ${metaResponse.data.name} (user: ${credentials.userEmail})`);
366
+
367
+ return {
368
+ success: true,
369
+ file: {
370
+ id: metaResponse.data.id,
371
+ name: metaResponse.data.name,
372
+ mimeType: metaResponse.data.mimeType,
373
+ size: metaResponse.data.size,
374
+ modifiedTime: metaResponse.data.modifiedTime,
375
+ content: contentResponse.data,
376
+ },
377
+ };
378
+ } catch (error: any) {
379
+ console.log('❌ Failed to read file:', error.message);
380
+ return {
381
+ success: false,
382
+ error: error.message,
383
+ };
384
+ }
385
+ },
386
+ },
387
+
388
+ {
389
+ name: 'delete_file',
390
+ description: 'Delete a file from Google Drive',
391
+ inputSchema: {
392
+ type: 'object',
393
+ properties: {
394
+ fileId: {
395
+ type: 'string',
396
+ description: 'File ID to delete',
397
+ },
398
+ },
399
+ required: ['fileId'],
400
+ },
401
+ handler: async (input, context) => {
402
+ console.log('🗑️ Deleting Google Drive file:', input.fileId);
403
+
404
+ // Get user credentials from context
405
+ const credentials = context.credentials as UserCredentials;
406
+ if (!credentials) {
407
+ return {
408
+ success: false,
409
+ error: 'User not authenticated or credentials not found',
410
+ };
411
+ }
412
+
413
+ const drive = createDriveClient(credentials);
414
+
415
+ try {
416
+ await drive.files.delete({
417
+ fileId: input.fileId,
418
+ });
419
+
420
+ console.log(`✅ File deleted: ${input.fileId} (user: ${credentials.userEmail})`);
421
+
422
+ return {
423
+ success: true,
424
+ fileId: input.fileId,
425
+ };
426
+ } catch (error: any) {
427
+ console.log('❌ Failed to delete file:', error.message);
428
+ return {
429
+ success: false,
430
+ error: error.message,
431
+ };
432
+ }
433
+ },
434
+ },
435
+ ],
436
+
437
+ resources: [
438
+ {
439
+ uri: 'gdrive://files',
440
+ name: 'Google Drive Files',
441
+ description: 'Live files from Google Drive',
442
+ mimeType: 'application/json',
443
+
444
+ read: async (uri, context) => {
445
+ console.log('📖 Reading Google Drive files...');
446
+
447
+ // Get user credentials from context
448
+ const credentials = context.credentials as UserCredentials;
449
+ if (!credentials) {
450
+ throw new Error('User not authenticated or credentials not found');
451
+ }
452
+
453
+ const drive = createDriveClient(credentials);
454
+
455
+ try {
456
+ const response = await drive.files.list({
457
+ q: "trashed=false",
458
+ pageSize: 100,
459
+ fields: 'files(id, name, mimeType, modifiedTime, size, webViewLink, parents)',
460
+ });
461
+
462
+ const files = response.data.files || [];
463
+ console.log(`✅ Read ${files.length} files (user: ${credentials.userEmail})`);
464
+
465
+ return {
466
+ contents: files.map(file => ({
467
+ id: file.id,
468
+ name: file.name,
469
+ mimeType: file.mimeType,
470
+ modifiedTime: file.modifiedTime,
471
+ size: file.size,
472
+ webViewLink: file.webViewLink,
473
+ })),
474
+ };
475
+ } catch (error: any) {
476
+ console.log('❌ Failed to read files:', error.message);
477
+ throw error;
478
+ }
479
+ },
480
+
481
+ list: async (context) => {
482
+ // Get user credentials from context
483
+ const credentials = context.credentials as UserCredentials;
484
+ if (!credentials) {
485
+ return [];
486
+ }
487
+
488
+ return [
489
+ {
490
+ uri: 'gdrive://files',
491
+ name: `${credentials.userEmail} Drive Files`,
492
+ description: `Live files from ${credentials.userEmail}'s Google Drive`,
493
+ },
494
+ ];
495
+ },
496
+
497
+ subscription: {
498
+ onSubscribe: async (uri, subscriptionId, thirdPartyWebhookUrl, context) => {
499
+ console.log('');
500
+ console.log('🔔 Setting up Google Drive push notification...');
501
+ console.log(` Resource: ${uri}`);
502
+ console.log(` Subscription ID: ${subscriptionId}`);
503
+ console.log(` Webhook URL: ${thirdPartyWebhookUrl}`);
504
+
505
+ // Get user credentials from context
506
+ const credentials = context.credentials as UserCredentials;
507
+ if (!credentials) {
508
+ throw new Error('User not authenticated or credentials not found');
509
+ }
510
+
511
+ const drive = createDriveClient(credentials);
512
+
513
+ try {
514
+ // Generate unique channel ID
515
+ const channelId = `gdrive-channel-${subscriptionId}`;
516
+ const expiration = Date.now() + (7 * 24 * 60 * 60 * 1000); // 7 days
517
+
518
+ // Watch for changes
519
+ const response = await drive.files.watch({
520
+ fileId: 'root', // Watch root folder (or specific folder)
521
+ requestBody: {
522
+ id: channelId,
523
+ type: 'web_hook',
524
+ address: thirdPartyWebhookUrl,
525
+ expiration: expiration.toString(),
526
+ },
527
+ });
528
+
529
+ channelStore.set(subscriptionId, {
530
+ channelId,
531
+ resourceId: response.data.resourceId!,
532
+ userId: context.userId,
533
+ });
534
+
535
+ console.log(`✅ Google Drive push notification created: Channel ${channelId} (user: ${credentials.userEmail})`);
536
+ console.log('');
537
+
538
+ return {
539
+ thirdPartyWebhookId: channelId,
540
+ metadata: {
541
+ channelId,
542
+ resourceId: response.data.resourceId,
543
+ expiration: new Date(expiration).toISOString(),
544
+ user: credentials.userEmail,
545
+ },
546
+ };
547
+ } catch (error: any) {
548
+ console.log('❌ Failed to create push notification:', error.message);
549
+ throw error;
550
+ }
551
+ },
552
+
553
+ onUnsubscribe: async (uri, subscriptionId, storedData, context) => {
554
+ console.log('');
555
+ console.log('🗑️ Removing Google Drive push notification...');
556
+ console.log(` Subscription ID: ${subscriptionId}`);
557
+
558
+ const channelInfo = channelStore.get(subscriptionId);
559
+ if (!channelInfo) {
560
+ console.log('⚠️ Channel ID not found in store');
561
+ return;
562
+ }
563
+
564
+ // Get user credentials from context
565
+ const credentials = context.credentials as UserCredentials;
566
+ if (!credentials) {
567
+ console.log('⚠️ User credentials not available for cleanup');
568
+ return;
569
+ }
570
+
571
+ const drive = createDriveClient(credentials);
572
+
573
+ try {
574
+ await drive.channels.stop({
575
+ requestBody: {
576
+ id: channelInfo.channelId,
577
+ resourceId: channelInfo.resourceId,
578
+ },
579
+ });
580
+
581
+ channelStore.delete(subscriptionId);
582
+ console.log(`✅ Google Drive push notification stopped: Channel ${channelInfo.channelId}`);
583
+ console.log('');
584
+ } catch (error: any) {
585
+ console.log('❌ Failed to stop push notification:', error.message);
586
+ }
587
+ },
588
+
589
+ onWebhook: async (subscriptionId, payload, headers) => {
590
+ const resourceState = headers['x-goog-resource-state'];
591
+ const resourceId = headers['x-goog-resource-id'];
592
+ const channelId = headers['x-goog-channel-id'];
593
+
594
+ console.log('');
595
+ console.log('📬 Received Google Drive webhook');
596
+ console.log(` State: ${resourceState}`);
597
+ console.log(` Resource ID: ${resourceId}`);
598
+ console.log(` Channel ID: ${channelId}`);
599
+ console.log(` Subscription: ${subscriptionId}`);
600
+
601
+ // Google Drive sends various states: sync, update, remove, trash, untrash, change
602
+ if (['update', 'remove', 'trash', 'change'].includes(resourceState)) {
603
+ const changeType = resourceState === 'remove' || resourceState === 'trash' ? 'deleted' :
604
+ resourceState === 'change' ? 'created' : 'updated';
605
+
606
+ return {
607
+ resourceUri: 'gdrive://files',
608
+ changeType,
609
+ data: {
610
+ resourceState,
611
+ resourceId,
612
+ channelId,
613
+ changed: headers['x-goog-changed'],
614
+ },
615
+ };
616
+ }
617
+
618
+ if (resourceState === 'sync') {
619
+ console.log(' ℹ️ Initial sync message (no action needed)');
620
+ } else {
621
+ console.log(' ℹ️ State not mapped to resource change');
622
+ }
623
+
624
+ return null;
625
+ },
626
+ },
627
+ },
628
+ ],
629
+
630
+ webhooks: {
631
+ incomingPath: '/webhooks/incoming',
632
+ incomingSecret: webhookSecret,
633
+
634
+ verifyIncomingSignature: (_payload, _signature, _secret) => {
635
+ // Google Drive doesn't use HMAC signatures for push notifications
636
+ // Instead, it uses X-Goog-Channel-Token header for verification
637
+ // For this example, we'll accept all webhooks with valid headers
638
+ return true;
639
+ },
640
+
641
+ outgoing: {
642
+ timeout: 5000,
643
+ retries: 3,
644
+ retryDelay: 1000,
645
+
646
+ signPayload: (payload, secret) => {
647
+ const hmac = crypto.createHmac('sha256', secret);
648
+ hmac.update(JSON.stringify(payload));
649
+ return `sha256=${hmac.digest('hex')}`;
650
+ },
651
+ },
652
+ },
653
+
654
+ // Authentication: Extract token and fetch credentials
655
+ authenticate: async (req) => {
656
+ // Extract token from Authorization header
657
+ const authHeader = req.headers.authorization;
658
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
659
+ throw new Error('Missing or invalid Authorization header');
660
+ }
661
+
662
+ const token = authHeader.substring(7); // Remove 'Bearer ' prefix
663
+
664
+ // Fetch credentials from store (or service in production)
665
+ const credentials = await fetchCredentials(token, mcpServerName);
666
+ if (!credentials) {
667
+ throw new Error('Invalid token or credentials not found');
668
+ }
669
+
670
+ console.log(`✅ User authenticated: ${credentials.userEmail}`);
671
+
672
+ return {
673
+ userId: credentials.userEmail,
674
+ credentials, // Store credentials in context for later use
675
+ };
676
+ },
677
+
678
+ logLevel: 'info',
679
+ });
680
+
681
+ await server.start();
682
+
683
+ console.log('');
684
+ console.log('╔═══════════════════════════════════════════════════════════╗');
685
+ console.log('║ 🎉 Google Drive MCP Server is LIVE! (Multi-User) ║');
686
+ console.log('╚═══════════════════════════════════════════════════════════╝');
687
+ console.log('');
688
+ console.log('📍 MCP Endpoint:');
689
+ console.log(` ${ngrokUrl}/mcp`);
690
+ console.log('');
691
+ console.log('🔐 Authentication:');
692
+ console.log(' Add header: Authorization: Bearer <user-token>');
693
+ console.log(' Example tokens: user-token-123, user-token-456');
694
+ console.log('');
695
+ console.log('🔍 Test with MCP Inspector:');
696
+ console.log(` npx @modelcontextprotocol/inspector ${ngrokUrl}/mcp`);
697
+ console.log('');
698
+ console.log('📋 Available Tools:');
699
+ console.log(' - list_files: List files and folders');
700
+ console.log(' - create_file: Create a new file');
701
+ console.log(' - read_file: Read file content');
702
+ console.log(' - delete_file: Delete a file');
703
+ console.log('');
704
+ console.log('📚 Available Resources (dynamic per user):');
705
+ console.log(' - gdrive://files');
706
+ console.log('');
707
+ console.log('🔔 Webhook Subscription:');
708
+ console.log(' 1. Authenticate with user token');
709
+ console.log(' 2. Use MCP Inspector or POST to /mcp with resources/subscribe');
710
+ console.log(' 3. Provide your callback URL in _meta.webhookUrl');
711
+ console.log(' 4. Google Drive push notification will be created');
712
+ console.log(' 5. Create/edit/delete files to see live updates!');
713
+ console.log('');
714
+ console.log('📝 Example: List files');
715
+ console.log(' curl -X POST http://localhost:3001/mcp \\');
716
+ console.log(' -H "Content-Type: application/json" \\');
717
+ console.log(' -H "Authorization: Bearer user-token-123" \\');
718
+ console.log(' -d \'{"jsonrpc":"2.0","id":1,"method":"tools/call",');
719
+ console.log(' "params":{"name":"list_files",');
720
+ console.log(' "arguments":{}}}\'');
721
+ console.log('');
722
+ console.log('⚠️ Note: Update CREDENTIALS_STORE with real Google OAuth tokens');
723
+ console.log('⚠️ Press Ctrl+C to stop and cleanup push notifications');
724
+ console.log('');
725
+
726
+ // Graceful shutdown
727
+ const cleanup = async () => {
728
+ console.log('');
729
+ console.log('🧹 Shutting down...');
730
+
731
+ // Stop all push notifications
732
+ for (const channelInfo of channelStore.values()) {
733
+ try {
734
+ // Try to get credentials for cleanup
735
+ const userTokenEntry = Array.from(CREDENTIALS_STORE.entries())
736
+ .find(([, creds]) => creds.userEmail === channelInfo.userId);
737
+
738
+ if (userTokenEntry) {
739
+ const credentials = userTokenEntry[1];
740
+ const drive = createDriveClient(credentials);
741
+
742
+ await drive.channels.stop({
743
+ requestBody: {
744
+ id: channelInfo.channelId,
745
+ resourceId: channelInfo.resourceId,
746
+ },
747
+ });
748
+ console.log(`✅ Stopped push notification: ${channelInfo.channelId} (user: ${channelInfo.userId})`);
749
+ } else {
750
+ console.log(`⚠️ Cannot stop push notification ${channelInfo.channelId}: credentials not found`);
751
+ }
752
+ } catch (error: any) {
753
+ console.log(`⚠️ Failed to stop push notification ${channelInfo.channelId}: ${error.message}`);
754
+ }
755
+ }
756
+
757
+ await server.stop();
758
+ await ngrok.disconnect();
759
+ await ngrok.kill();
760
+ store.destroy();
761
+
762
+ console.log('👋 Goodbye!');
763
+ process.exit(0);
764
+ };
765
+
766
+ process.on('SIGTERM', cleanup);
767
+ process.on('SIGINT', cleanup);
768
+ }
769
+
770
+ main().catch((error) => {
771
+ console.error('💥 Fatal error:', error);
772
+ process.exit(1);
773
+ });