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,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
|
+
});
|