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.
- package/.eslintrc.json +16 -0
- package/.prettierrc.json +8 -0
- package/ARCHITECTURE.md +269 -0
- package/CONTRIBUTING.md +136 -0
- package/GETTING_STARTED.md +310 -0
- package/IMPLEMENTATION.md +294 -0
- package/LICENSE +21 -0
- package/MIGRATION_TO_SDK.md +263 -0
- package/README.md +496 -0
- package/SDK_INTEGRATION_COMPLETE.md +300 -0
- package/STANDARD_SUBSCRIPTIONS.md +268 -0
- package/STANDARD_SUBSCRIPTIONS_COMPLETE.md +309 -0
- package/SUMMARY.md +272 -0
- package/Spec.md +2778 -0
- package/dist/errors/index.d.ts +52 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +81 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol/ProtocolHandler.d.ts +37 -0
- package/dist/protocol/ProtocolHandler.d.ts.map +1 -0
- package/dist/protocol/ProtocolHandler.js +172 -0
- package/dist/protocol/ProtocolHandler.js.map +1 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +502 -0
- package/dist/server.js.map +1 -0
- package/dist/stores/InMemoryStore.d.ts +27 -0
- package/dist/stores/InMemoryStore.d.ts.map +1 -0
- package/dist/stores/InMemoryStore.js +73 -0
- package/dist/stores/InMemoryStore.js.map +1 -0
- package/dist/stores/RedisStore.d.ts +18 -0
- package/dist/stores/RedisStore.d.ts.map +1 -0
- package/dist/stores/RedisStore.js +45 -0
- package/dist/stores/RedisStore.js.map +1 -0
- package/dist/stores/index.d.ts +3 -0
- package/dist/stores/index.d.ts.map +1 -0
- package/dist/stores/index.js +9 -0
- package/dist/stores/index.js.map +1 -0
- package/dist/subscriptions/SubscriptionManager.d.ts +49 -0
- package/dist/subscriptions/SubscriptionManager.d.ts.map +1 -0
- package/dist/subscriptions/SubscriptionManager.js +181 -0
- package/dist/subscriptions/SubscriptionManager.js.map +1 -0
- package/dist/types/index.d.ts +271 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +16 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/index.d.ts +51 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +154 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/webhooks/WebhookManager.d.ts +27 -0
- package/dist/webhooks/WebhookManager.d.ts.map +1 -0
- package/dist/webhooks/WebhookManager.js +174 -0
- package/dist/webhooks/WebhookManager.js.map +1 -0
- package/examples/GITHUB_LIVE_EXAMPLE.md +308 -0
- package/examples/GITHUB_LIVE_SETUP.md +253 -0
- package/examples/QUICKSTART.md +130 -0
- package/examples/basic-setup.ts +142 -0
- package/examples/github-server-live.ts +690 -0
- package/examples/github-server.ts +223 -0
- package/examples/google-drive-server-live.ts +773 -0
- package/examples/start-github-live.sh +53 -0
- package/jest.config.js +20 -0
- package/package.json +58 -0
- package/src/errors/index.ts +81 -0
- package/src/index.ts +19 -0
- package/src/server.ts +595 -0
- package/src/stores/InMemoryStore.ts +87 -0
- package/src/stores/RedisStore.ts +51 -0
- package/src/stores/index.ts +2 -0
- package/src/subscriptions/SubscriptionManager.ts +240 -0
- package/src/types/index.ts +341 -0
- package/src/utils/index.ts +156 -0
- package/src/webhooks/WebhookManager.ts +230 -0
- package/test-sdk-integration.sh +157 -0
- 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
|
+
});
|