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,690 @@
1
+ import { createMCPServer } from '../src';
2
+ import { InMemoryStore } from '../src/stores';
3
+ import { Octokit } from '@octokit/rest';
4
+ import ngrok from '@ngrok/ngrok';
5
+ import crypto from 'crypto';
6
+
7
+ /**
8
+ * GitHub MCP Server - LIVE Multi-User Example
9
+ *
10
+ * This example supports multiple users with different GitHub 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
+ */
18
+
19
+ /**
20
+ * User credentials interface
21
+ */
22
+ interface UserCredentials {
23
+ githubToken: string;
24
+ githubOwner: string;
25
+ githubRepo: string;
26
+ githubLogin: string;
27
+ }
28
+
29
+ /**
30
+ * Simulated credentials store
31
+ * In production, this would be replaced by a service call
32
+ * that fetches credentials based on token and MCP server name
33
+ */
34
+ const CREDENTIALS_STORE = new Map<string, UserCredentials>([
35
+ // Example user 1
36
+ [
37
+ 'user-token-123',
38
+ {
39
+ githubToken: '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',
40
+ githubOwner: 'surajbhan',
41
+ githubRepo: 'test',
42
+ githubLogin: 'surajbhan',
43
+ },
44
+ ],
45
+ // Example user 2
46
+ [
47
+ 'user-token-456',
48
+ {
49
+ githubToken: 'ghp_example_token_2',
50
+ githubOwner: 'anotheruser',
51
+ githubRepo: 'demo',
52
+ githubLogin: 'anotheruser',
53
+ },
54
+ ],
55
+ // Add more users as needed
56
+ ]);
57
+
58
+ /**
59
+ * Fetch user credentials from the store (simulates a service call)
60
+ * In production, this would make an HTTP call to a credentials service
61
+ */
62
+ async function fetchCredentials(
63
+ token: string,
64
+ mcpServerName: string
65
+ ): Promise<UserCredentials | null> {
66
+ // Simulate async service call
67
+ await new Promise((resolve) => setTimeout(resolve, 10));
68
+
69
+ // In production, this would be:
70
+ // const response = await fetch(`https://credentials-service.com/api/credentials`, {
71
+ // method: 'POST',
72
+ // headers: { 'Authorization': `Bearer ${token}` },
73
+ // body: JSON.stringify({ mcpServerName })
74
+ // });
75
+ // return await response.json();
76
+
77
+ return CREDENTIALS_STORE.get(token) || null;
78
+ }
79
+
80
+ /**
81
+ * Create an Octokit instance for a specific user
82
+ */
83
+ function createOctokit(credentials: UserCredentials): Octokit {
84
+ return new Octokit({ auth: credentials.githubToken });
85
+ }
86
+
87
+ async function main() {
88
+ console.log('╔═══════════════════════════════════════════════════════════╗');
89
+ console.log('║ GitHub MCP Server - Multi-User Live Integration ║');
90
+ console.log('╚═══════════════════════════════════════════════════════════╝');
91
+ console.log('');
92
+
93
+ // Configuration
94
+ const ngrokAuthToken =
95
+ process.env.NGROK_AUTH_TOKEN || '343iE4JabU5sRNM235xJ0N1iFQK_4GHuxi3va6GFVDn5pW92W';
96
+ const webhookSecret = process.env.WEBHOOK_SECRET || crypto.randomBytes(32).toString('hex');
97
+ const mcpServerName = 'github-mcp-live';
98
+
99
+ console.log('🚀 Starting multi-user MCP server...');
100
+ console.log('');
101
+
102
+ // Start ngrok tunnel
103
+ console.log('🌐 Starting ngrok tunnel...');
104
+ let ngrokUrl: string;
105
+
106
+ try {
107
+ ngrokUrl = await ngrok
108
+ .connect({
109
+ addr: 3000,
110
+ authtoken: ngrokAuthToken || undefined,
111
+ })
112
+ .then((x) => {
113
+ const url = x.url();
114
+ console.log(`✅ ngrok tunnel established: ${url}`);
115
+ if (!url) {
116
+ console.log('❌ ngrok did not return a URL!');
117
+ process.exit(1);
118
+ }
119
+ return url;
120
+ })
121
+ .catch((err) => {
122
+ console.log('❌ Failed to start ngrok tunnel:', err.message);
123
+ console.log(' Set NGROK_AUTH_TOKEN environment variable');
124
+ process.exit(1);
125
+ });
126
+ console.log(`✅ Public URL: ${ngrokUrl}`);
127
+ console.log('');
128
+ } catch (error: any) {
129
+ console.log('❌ Failed to start ngrok tunnel:', error.message);
130
+ console.log('');
131
+ console.log(' Get your ngrok auth token:');
132
+ console.log(' https://dashboard.ngrok.com/get-started/your-authtoken');
133
+ console.log(' Then set: export NGROK_AUTH_TOKEN="your_token"');
134
+ process.exit(1);
135
+ }
136
+
137
+ // Store for webhook IDs (keyed by subscriptionId)
138
+ const webhookStore = new Map<
139
+ string,
140
+ { hookId: number; userId: string; owner: string; repo: string }
141
+ >();
142
+ const store = new InMemoryStore();
143
+
144
+ const server = createMCPServer({
145
+ name: 'github-mcp-live',
146
+ version: '1.0.0',
147
+ publicUrl: ngrokUrl,
148
+ port: 3000,
149
+ store,
150
+
151
+ tools: [
152
+ {
153
+ name: 'create_issue',
154
+ description: 'Create a GitHub issue',
155
+ inputSchema: {
156
+ type: 'object',
157
+ properties: {
158
+ title: { type: 'string', description: 'Issue title' },
159
+ body: { type: 'string', description: 'Issue body' },
160
+ labels: {
161
+ type: 'array',
162
+ items: { type: 'string' },
163
+ description: 'Issue labels',
164
+ },
165
+ },
166
+ required: ['title'],
167
+ },
168
+ handler: async (input, context) => {
169
+ console.log('📝 Creating GitHub issue:', input.title);
170
+
171
+ // Get user credentials from context
172
+ const credentials = context.credentials as UserCredentials;
173
+ if (!credentials) {
174
+ return {
175
+ success: false,
176
+ error: 'User not authenticated or credentials not found',
177
+ };
178
+ }
179
+
180
+ const octokit = createOctokit(credentials);
181
+
182
+ try {
183
+ const { data: issue } = await octokit.issues.create({
184
+ owner: credentials.githubOwner,
185
+ repo: credentials.githubRepo,
186
+ title: input.title,
187
+ body: input.body,
188
+ labels: input.labels,
189
+ });
190
+
191
+ console.log(`✅ Issue created: #${issue.number} (user: ${credentials.githubLogin})`);
192
+
193
+ return {
194
+ success: true,
195
+ issue: {
196
+ number: issue.number,
197
+ title: issue.title,
198
+ state: issue.state,
199
+ html_url: issue.html_url,
200
+ created_at: issue.created_at,
201
+ },
202
+ };
203
+ } catch (error: any) {
204
+ console.log('❌ Failed to create issue:', error.message);
205
+ return {
206
+ success: false,
207
+ error: error.message,
208
+ };
209
+ }
210
+ },
211
+ },
212
+
213
+ {
214
+ name: 'list_issues',
215
+ description: 'List issues from the repository',
216
+ inputSchema: {
217
+ type: 'object',
218
+ properties: {
219
+ state: {
220
+ type: 'string',
221
+ description: 'Filter by state: open, closed, all',
222
+ enum: ['open', 'closed', 'all'],
223
+ },
224
+ limit: {
225
+ type: 'number',
226
+ description: 'Number of issues to return',
227
+ },
228
+ },
229
+ },
230
+ handler: async (input, context) => {
231
+ console.log('📋 Listing GitHub issues...');
232
+
233
+ // Get user credentials from context
234
+ const credentials = context.credentials as UserCredentials;
235
+ if (!credentials) {
236
+ return {
237
+ success: false,
238
+ error: 'User not authenticated or credentials not found',
239
+ };
240
+ }
241
+
242
+ const octokit = createOctokit(credentials);
243
+
244
+ try {
245
+ const { data: issues } = await octokit.issues.listForRepo({
246
+ owner: credentials.githubOwner,
247
+ repo: credentials.githubRepo,
248
+ state: (input.state as any) || 'open',
249
+ per_page: input.limit || 10,
250
+ });
251
+
252
+ console.log(`✅ Listed ${issues.length} issues (user: ${credentials.githubLogin})`);
253
+
254
+ return {
255
+ success: true,
256
+ count: issues.length,
257
+ issues: issues.map((issue: any) => ({
258
+ number: issue.number,
259
+ title: issue.title,
260
+ state: issue.state,
261
+ html_url: issue.html_url,
262
+ created_at: issue.created_at,
263
+ labels: issue.labels.map((l: any) => l.name),
264
+ })),
265
+ };
266
+ } catch (error: any) {
267
+ console.log('❌ Failed to list issues:', error.message);
268
+ return {
269
+ success: false,
270
+ error: error.message,
271
+ };
272
+ }
273
+ },
274
+ },
275
+ ],
276
+
277
+ resources: [
278
+ {
279
+ uri: 'github://repo/{owner}/{repo}/issues',
280
+ name: 'GitHub Repository Issues',
281
+ description: 'Live issues from GitHub repository',
282
+ mimeType: 'application/json',
283
+
284
+ read: async (uri, context) => {
285
+ console.log('📖 Reading repository issues...');
286
+
287
+ // Get user credentials from context
288
+ const credentials = context.credentials as UserCredentials;
289
+ if (!credentials) {
290
+ throw new Error('User not authenticated or credentials not found');
291
+ }
292
+
293
+ const octokit = createOctokit(credentials);
294
+
295
+ try {
296
+ const { data: issues } = await octokit.issues.listForRepo({
297
+ owner: credentials.githubOwner,
298
+ repo: credentials.githubRepo,
299
+ state: 'open',
300
+ per_page: 100,
301
+ });
302
+
303
+ console.log(`✅ Read ${issues.length} issues (user: ${credentials.githubLogin})`);
304
+
305
+ return {
306
+ contents: issues.map((issue: any) => ({
307
+ number: issue.number,
308
+ title: issue.title,
309
+ state: issue.state,
310
+ html_url: issue.html_url,
311
+ created_at: issue.created_at,
312
+ updated_at: issue.updated_at,
313
+ labels: issue.labels.map((l: any) => l.name),
314
+ })),
315
+ };
316
+ } catch (error: any) {
317
+ console.log('❌ Failed to read issues:', error.message);
318
+ throw error;
319
+ }
320
+ },
321
+
322
+ list: async (context) => {
323
+ // Get user credentials from context
324
+ const credentials = context.credentials as UserCredentials;
325
+ if (!credentials) {
326
+ return [];
327
+ }
328
+
329
+ return [
330
+ {
331
+ uri: `github://repo/${credentials.githubOwner}/${credentials.githubRepo}/issues`,
332
+ name: `${credentials.githubOwner}/${credentials.githubRepo} Issues`,
333
+ description: `Live issues from ${credentials.githubOwner}/${credentials.githubRepo} repository`,
334
+ },
335
+ ];
336
+ },
337
+
338
+ subscription: {
339
+ onSubscribe: async (uri, subscriptionId, thirdPartyWebhookUrl, context) => {
340
+ console.log('');
341
+ console.log('🔔 Setting up GitHub webhook...');
342
+ console.log(` Resource: ${uri}`);
343
+ console.log(` Subscription ID: ${subscriptionId}`);
344
+ console.log(` Webhook URL: ${thirdPartyWebhookUrl}`);
345
+
346
+ // Get user credentials from context
347
+ const credentials = context.credentials as UserCredentials;
348
+ if (!credentials) {
349
+ throw new Error('User not authenticated or credentials not found');
350
+ }
351
+
352
+ const octokit = createOctokit(credentials);
353
+
354
+ try {
355
+ // Create webhook on GitHub
356
+ const { data: hook } = await octokit.repos.createWebhook({
357
+ owner: credentials.githubOwner,
358
+ repo: credentials.githubRepo,
359
+ config: {
360
+ url: thirdPartyWebhookUrl,
361
+ content_type: 'json',
362
+ secret: webhookSecret,
363
+ insecure_ssl: '0',
364
+ },
365
+ events: ['issues', 'issue_comment'],
366
+ active: true,
367
+ });
368
+
369
+ webhookStore.set(subscriptionId, {
370
+ hookId: hook.id,
371
+ userId: context.userId,
372
+ owner: credentials.githubOwner,
373
+ repo: credentials.githubRepo,
374
+ });
375
+
376
+ console.log(
377
+ `✅ GitHub webhook created: ID ${hook.id} (user: ${credentials.githubLogin})`
378
+ );
379
+ console.log('');
380
+
381
+ return {
382
+ thirdPartyWebhookId: hook.id.toString(),
383
+ metadata: {
384
+ owner: credentials.githubOwner,
385
+ repo: credentials.githubRepo,
386
+ events: ['issues', 'issue_comment'],
387
+ webhook_url: hook.config.url,
388
+ user: credentials.githubLogin,
389
+ },
390
+ };
391
+ } catch (error: any) {
392
+ console.log('❌ Failed to create webhook:', error.message);
393
+ throw error;
394
+ }
395
+ },
396
+
397
+ onUnsubscribe: async (uri, subscriptionId, storedData, context) => {
398
+ console.log('');
399
+ console.log('🗑️ Removing GitHub webhook...');
400
+ console.log(` Subscription ID: ${subscriptionId}`);
401
+
402
+ const webhookInfo = webhookStore.get(subscriptionId);
403
+ if (!webhookInfo) {
404
+ console.log('⚠️ Webhook ID not found in store');
405
+ return;
406
+ }
407
+
408
+ // Get user credentials from context (or use stored info)
409
+ const credentials = context.credentials as UserCredentials;
410
+ if (!credentials) {
411
+ console.log('⚠️ User credentials not available for cleanup');
412
+ return;
413
+ }
414
+
415
+ const octokit = createOctokit(credentials);
416
+
417
+ try {
418
+ await octokit.repos.deleteWebhook({
419
+ owner: webhookInfo.owner,
420
+ repo: webhookInfo.repo,
421
+ hook_id: webhookInfo.hookId,
422
+ });
423
+
424
+ webhookStore.delete(subscriptionId);
425
+ console.log(`✅ GitHub webhook deleted: ID ${webhookInfo.hookId}`);
426
+ console.log('');
427
+ } catch (error: any) {
428
+ console.log('❌ Failed to delete webhook:', error.message);
429
+ }
430
+ },
431
+
432
+ onWebhook: async (subscriptionId, payload, headers) => {
433
+ const event = headers['x-github-event'];
434
+
435
+ console.log('');
436
+ console.log('📬 Received GitHub webhook');
437
+ console.log(` Event: ${event}`);
438
+ console.log(` Subscription: ${subscriptionId}`);
439
+
440
+ if (event === 'issues') {
441
+ const { action, issue, repository } = payload;
442
+ console.log(` Action: ${action}`);
443
+ console.log(` Issue: #${issue.number} - ${issue.title}`);
444
+
445
+ if (['opened', 'edited', 'closed', 'reopened'].includes(action)) {
446
+ return {
447
+ resourceUri: `github://repo/${repository.owner.login}/${repository.name}/issues/${issue.number}`,
448
+ changeType:
449
+ action === 'opened' ? 'created' : action === 'closed' ? 'deleted' : 'updated',
450
+ data: {
451
+ issueNumber: issue.number,
452
+ title: issue.title,
453
+ state: issue.state,
454
+ action,
455
+ html_url: issue.html_url,
456
+ },
457
+ };
458
+ }
459
+ }
460
+
461
+ if (event === 'issue_comment') {
462
+ const { action, issue, comment, repository } = payload;
463
+ console.log(` Comment ${action} on issue #${issue.number}`);
464
+
465
+ return {
466
+ resourceUri: `github://repo/${repository.owner.login}/${repository.name}/issues`,
467
+ changeType: 'updated',
468
+ data: {
469
+ issueNumber: issue.number,
470
+ commentAction: action,
471
+ comment: {
472
+ id: comment.id,
473
+ body: comment.body,
474
+ user: comment.user.login,
475
+ },
476
+ },
477
+ };
478
+ }
479
+
480
+ console.log(' ℹ️ Event not mapped to resource change');
481
+ return null;
482
+ },
483
+ },
484
+ },
485
+ {
486
+ uri: 'github://repo/{owner}/{repo}/issues/{issue_number}',
487
+ name: 'GitHub Repository Issue Detail',
488
+ description: 'Live issue details from GitHub repository',
489
+ mimeType: 'application/json',
490
+ read: async (uri, context) => {
491
+ console.log('📖 Reading repository issue detail...');
492
+ // Get user credentials from context
493
+ const credentials = context.credentials as UserCredentials;
494
+ if (!credentials) {
495
+ throw new Error('User not authenticated or credentials not found');
496
+ }
497
+
498
+ const parts = uri.split('/');
499
+ const owner = parts[3];
500
+ const repo = parts[4];
501
+ const issue_number = parseInt(parts[6], 10);
502
+
503
+ const octokit = createOctokit(credentials);
504
+
505
+ try {
506
+ const { data: issue } = await octokit.issues.get({
507
+ owner,
508
+ repo,
509
+ issue_number,
510
+ });
511
+ return {
512
+ contents: [
513
+ {
514
+ number: issue.number,
515
+ title: issue.title,
516
+ state: issue.state,
517
+ html_url: issue.html_url,
518
+ created_at: issue.created_at,
519
+ updated_at: issue.updated_at,
520
+ labels: issue.labels.map((l: any) => l.name),
521
+ },
522
+ ],
523
+ };
524
+ } catch (error: any) {
525
+ console.log('❌ Failed to read issue detail:', error.message);
526
+ throw error;
527
+ }
528
+ },
529
+ list: async (context) => {
530
+ // Get user credentials from context
531
+ const credentials = context.credentials as UserCredentials;
532
+ if (!credentials) {
533
+ return [];
534
+ }
535
+
536
+ return [
537
+ {
538
+ uri: `github://repo/${credentials.githubOwner}/${credentials.githubRepo}/issues/{issue_number}`,
539
+ name: `${credentials.githubOwner}/${credentials.githubRepo} Issues`,
540
+ description: `Live issues from ${credentials.githubOwner}/${credentials.githubRepo} repository`,
541
+ },
542
+ ];
543
+ }
544
+ },
545
+ ],
546
+
547
+ webhooks: {
548
+ incomingPath: '/webhooks/incoming',
549
+ incomingSecret: webhookSecret,
550
+
551
+ verifyIncomingSignature: (payload, signature, secret) => {
552
+ if (!signature) return false;
553
+
554
+ const hmac = crypto.createHmac('sha256', secret);
555
+ hmac.update(JSON.stringify(payload));
556
+ const expected = `sha256=${hmac.digest('hex')}`;
557
+
558
+ try {
559
+ return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
560
+ } catch {
561
+ return false;
562
+ }
563
+ },
564
+
565
+ outgoing: {
566
+ timeout: 5000,
567
+ retries: 3,
568
+ retryDelay: 1000,
569
+
570
+ signPayload: (payload, secret) => {
571
+ const hmac = crypto.createHmac('sha256', secret);
572
+ hmac.update(JSON.stringify(payload));
573
+ return `sha256=${hmac.digest('hex')}`;
574
+ },
575
+ },
576
+ },
577
+
578
+ // Authentication: Extract token and fetch credentials
579
+ authenticate: async (req) => {
580
+ // Extract token from Authorization header
581
+ const authHeader = req.headers.authorization;
582
+ console.log('Authorization Header:', authHeader);
583
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
584
+ throw new Error('Missing or invalid Authorization header');
585
+ }
586
+
587
+ const token = authHeader.substring(7); // Remove 'Bearer ' prefix
588
+
589
+ // Fetch credentials from store (or service in production)
590
+ const credentials = await fetchCredentials(token, mcpServerName);
591
+ if (!credentials) {
592
+ throw new Error('Invalid token or credentials not found');
593
+ }
594
+
595
+ console.log(`✅ User authenticated: ${credentials.githubLogin}`);
596
+
597
+ return {
598
+ userId: credentials.githubLogin,
599
+ credentials, // Store credentials in context for later use
600
+ };
601
+ },
602
+
603
+ logLevel: 'info',
604
+ });
605
+
606
+ await server.start();
607
+
608
+ console.log('');
609
+ console.log('╔═══════════════════════════════════════════════════════════╗');
610
+ console.log('║ 🎉 GitHub MCP Server is LIVE! (Multi-User) ║');
611
+ console.log('╚═══════════════════════════════════════════════════════════╝');
612
+ console.log('');
613
+ console.log('📍 MCP Endpoint:');
614
+ console.log(` ${ngrokUrl}/mcp`);
615
+ console.log('');
616
+ console.log('� Authentication:');
617
+ console.log(' Add header: Authorization: Bearer <user-token>');
618
+ console.log(' Example tokens: user-token-123, user-token-456');
619
+ console.log('');
620
+ console.log('�🔍 Test with MCP Inspector:');
621
+ console.log(` npx @modelcontextprotocol/inspector ${ngrokUrl}/mcp`);
622
+ console.log('');
623
+ console.log('📋 Available Tools:');
624
+ console.log(' - create_issue: Create a new GitHub issue');
625
+ console.log(' - list_issues: List repository issues');
626
+ console.log('');
627
+ console.log('📚 Available Resources (dynamic per user):');
628
+ console.log(' - github://repo/{owner}/{repo}/issues');
629
+ console.log('');
630
+ console.log('🔔 Webhook Subscription:');
631
+ console.log(' 1. Authenticate with user token');
632
+ console.log(' 2. Use MCP Inspector or POST to /mcp with resources/subscribe');
633
+ console.log(' 3. Provide your callback URL in _meta.webhookUrl');
634
+ console.log(" 4. GitHub webhook will be created for user's repo");
635
+ console.log(' 5. Create/edit/close issues to see live updates!');
636
+ console.log('');
637
+ console.log('📝 Example: Create a test issue');
638
+ console.log(' curl -X POST http://localhost:3000/mcp \\');
639
+ console.log(' -H "Content-Type: application/json" \\');
640
+ console.log(' -H "Authorization: Bearer user-token-123" \\');
641
+ console.log(' -d \'{"jsonrpc":"2.0","id":1,"method":"tools/call",');
642
+ console.log(' "params":{"name":"create_issue",');
643
+ console.log(' "arguments":{"title":"Test Issue"}}}\'');
644
+ console.log('');
645
+ console.log('⚠️ Press Ctrl+C to stop and cleanup webhooks');
646
+ console.log('');
647
+
648
+ // Graceful shutdown
649
+ const cleanup = async () => {
650
+ console.log('');
651
+ console.log('🧹 Shutting down...');
652
+
653
+ // Delete all webhooks
654
+ for (const [subscriptionId, webhookInfo] of webhookStore.entries()) {
655
+ try {
656
+ // Try to get credentials for cleanup
657
+ const credentials = CREDENTIALS_STORE.get(`user-token-${webhookInfo.userId}`);
658
+ if (credentials) {
659
+ const octokit = createOctokit(credentials);
660
+ await octokit.repos.deleteWebhook({
661
+ owner: webhookInfo.owner,
662
+ repo: webhookInfo.repo,
663
+ hook_id: webhookInfo.hookId,
664
+ });
665
+ console.log(`✅ Deleted webhook: ${webhookInfo.hookId} (user: ${webhookInfo.userId})`);
666
+ } else {
667
+ console.log(`⚠️ Cannot delete webhook ${webhookInfo.hookId}: credentials not found`);
668
+ }
669
+ } catch (error: any) {
670
+ console.log(`⚠️ Failed to delete webhook ${webhookInfo.hookId}: ${error.message}`);
671
+ }
672
+ }
673
+
674
+ await server.stop();
675
+ await ngrok.disconnect();
676
+ await ngrok.kill();
677
+ store.destroy();
678
+
679
+ console.log('👋 Goodbye!');
680
+ process.exit(0);
681
+ };
682
+
683
+ process.on('SIGTERM', cleanup);
684
+ process.on('SIGINT', cleanup);
685
+ }
686
+
687
+ main().catch((error) => {
688
+ console.error('💥 Fatal error:', error);
689
+ process.exit(1);
690
+ });