import { createMCPServer } from '../src'; import { InMemoryStore } from '../src/stores'; import { google } from 'googleapis'; import ngrok from '@ngrok/ngrok'; import crypto from 'crypto'; /** * Google Drive MCP Server - LIVE Multi-User Example * * This example supports multiple users with different Google Drive credentials. * Each user is authenticated via a token, and their credentials are fetched * from a credentials store (simulated here, but would be a service in production). * * Prerequisites: * 1. ngrok auth token for public URL tunneling * 2. User tokens configured in CREDENTIALS_STORE below * 3. Google Cloud project with Drive API enabled * 4. OAuth2 credentials with push notification permissions */ /** * User credentials interface */ interface UserCredentials { accessToken: string; refreshToken: string; scope: string; tokenType: string; expiryDate: number; clientId: string; clientSecret: string; userEmail: string; } /** * Simulated credentials store * In production, this would be replaced by a service call * that fetches credentials based on token and MCP server name */ const CREDENTIALS_STORE = new Map([ // Example user 1 ['user-token-123', { accessToken: "ya29.a0AQQ_BDTPIJQbkfJtJ3X2qlAvaUbn07qubQ8CzgrtLxwCcvKzHWIEp5L79509AFgxyB3zshGA88K55KExi0SN-JkgglQtakNSEPfptTqSPlNxZSB5O4xfrPK1FdA70bhuSZtkkw_j69eP-ktNFed0P3hyfUs_iu4lNYafgVuXxFNVs_IU8P3yjLxOXgFlnC0Ue1jQlC9-aCgYKAewSARUSFQHGX2MiNkiaO2r8IOBIA5ZGNzoUBg0207", scope: "https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/spreadsheets", tokenType: "Bearer", expiryDate: 1760011303441, refreshToken: "1//0gx51TpUb2HxJCgYIARAAGBASNwF-L9IrUQq0by-ReRNCwftjoVhI-MTLK5tJSNKdp_w_yLiRLPJKns2vo0fKL6LibqbyMgCyG0c", clientId: "747030937811-4ucgh4jebiju1niume6hu1pd2ti4kknk.apps.googleusercontent.com", clientSecret: "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", userEmail: "satyajeet.acharya@yoctotta.com", }], // Example user 2 ['user-token-456', { accessToken: 'ya29.example_access_token_2', scope: "https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/spreadsheets", tokenType: "Bearer", expiryDate: 1760011303441, refreshToken: '1//example_refresh_token_2', clientId: 'your-client-id.apps.googleusercontent.com', clientSecret: 'your-client-secret', userEmail: 'user2@example.com', }], // Add more users as needed ]); /** * Fetch user credentials from the store (simulates a service call) * In production, this would make an HTTP call to a credentials service */ async function fetchCredentials(token: string, _mcpServerName: string): Promise { // Simulate async service call await new Promise(resolve => setTimeout(resolve, 10)); // In production, this would be: // const response = await fetch(`https://credentials-service.com/api/credentials`, { // method: 'POST', // headers: { 'Authorization': `Bearer ${token}` }, // body: JSON.stringify({ mcpServerName: _mcpServerName }) // }); // return await response.json(); return CREDENTIALS_STORE.get(token) || null; } /** * Create a Google Drive client for a specific user */ function createDriveClient(credentials: UserCredentials) { const oauth2Client = new google.auth.OAuth2( credentials.clientId, credentials.clientSecret, 'http://localhost' ); oauth2Client.setCredentials({ access_token: credentials.accessToken, refresh_token: credentials.refreshToken, }); return google.drive({ version: 'v3', auth: oauth2Client }); } async function main() { console.log('╔═══════════════════════════════════════════════════════════╗'); console.log('║ Google Drive MCP Server - Multi-User Live Integration ║'); console.log('╚═══════════════════════════════════════════════════════════╝'); console.log(''); // Configuration const ngrokAuthToken = process.env.NGROK_AUTH_TOKEN || '343iE4JabU5sRNM235xJ0N1iFQK_4GHuxi3va6GFVDn5pW92W'; const webhookSecret = process.env.WEBHOOK_SECRET || crypto.randomBytes(32).toString('hex'); const mcpServerName = 'google-drive-mcp-live'; console.log('🚀 Starting multi-user MCP server...'); console.log(''); // Start ngrok tunnel console.log('🌐 Starting ngrok tunnel...'); let ngrokUrl: string; try { ngrokUrl = await ngrok.connect({ addr: 3001, authtoken: ngrokAuthToken || undefined }) .then(x => { const url = x.url(); console.log(`✅ ngrok tunnel established: ${url}`); if (!url) { console.log('❌ ngrok did not return a URL!'); process.exit(1); } return url; }) .catch((err) => { console.log('❌ Failed to start ngrok tunnel:', err.message); console.log(' Set NGROK_AUTH_TOKEN environment variable'); process.exit(1); }); console.log(`✅ Public URL: ${ngrokUrl}`); console.log(''); } catch (error: any) { console.log('❌ Failed to start ngrok tunnel:', error.message); console.log(''); console.log(' Get your ngrok auth token:'); console.log(' https://dashboard.ngrok.com/get-started/your-authtoken'); console.log(' Then set: export NGROK_AUTH_TOKEN="your_token"'); process.exit(1); } // Store for push notification channels (keyed by subscriptionId) const channelStore = new Map(); const store = new InMemoryStore(); const server = createMCPServer({ name: 'google-drive-mcp-live', version: '1.0.0', publicUrl: ngrokUrl, port: 3001, store, tools: [ { name: 'list_files', description: 'List files and folders in Google Drive', inputSchema: { type: 'object', properties: { folderId: { type: 'string', description: 'Folder ID to list files from (omit for root)', }, query: { type: 'string', description: 'Search query (e.g., "name contains \'document\'")', }, pageSize: { type: 'number', description: 'Number of files to return', }, }, }, handler: async (input, context) => { console.log('📂 Listing Google Drive files...'); // Get user credentials from context const credentials = context.credentials as UserCredentials; if (!credentials) { return { success: false, error: 'User not authenticated or credentials not found', }; } const drive = createDriveClient(credentials); try { let q = "trashed=false"; if (input.folderId) { q += ` and '${input.folderId}' in parents`; } else { q += " and 'root' in parents"; } if (input.query) { q += ` and ${input.query}`; } const response = await drive.files.list({ q, pageSize: input.pageSize || 100, fields: 'files(id, name, mimeType, modifiedTime, size, webViewLink, parents)', }); const files = response.data.files || []; console.log(`✅ Listed ${files.length} files (user: ${credentials.userEmail})`); return { success: true, count: files.length, files: files.map((file: any) => ({ id: file.id, name: file.name, mimeType: file.mimeType, modifiedTime: file.modifiedTime, size: file.size, webViewLink: file.webViewLink, parents: file.parents, })), }; } catch (error: any) { console.log('❌ Failed to list files:', error.message); return { success: false, error: error.message, }; } }, }, { name: 'create_file', description: 'Create a new file in Google Drive', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'File name', }, content: { type: 'string', description: 'File content', }, mimeType: { type: 'string', description: 'MIME type (default: text/plain)', }, folderId: { type: 'string', description: 'Parent folder ID (omit for root)', }, }, required: ['name', 'content'], }, handler: async (input, context) => { console.log('📄 Creating Google Drive file:', input.name); // Get user credentials from context const credentials = context.credentials as UserCredentials; if (!credentials) { return { success: false, error: 'User not authenticated or credentials not found', }; } const drive = createDriveClient(credentials); try { const fileMetadata: any = { name: input.name, mimeType: input.mimeType || 'text/plain', }; if (input.folderId) { fileMetadata.parents = [input.folderId]; } const media = { mimeType: input.mimeType || 'text/plain', body: input.content, }; const response = await drive.files.create({ requestBody: fileMetadata, media, fields: 'id, name, mimeType, webViewLink, createdTime', }); const file = response.data; console.log(`✅ File created: ${file.name} (user: ${credentials.userEmail})`); return { success: true, file: { id: file.id, name: file.name, mimeType: file.mimeType, webViewLink: file.webViewLink, createdTime: file.createdTime, }, }; } catch (error: any) { console.log('❌ Failed to create file:', error.message); return { success: false, error: error.message, }; } }, }, { name: 'read_file', description: 'Read content of a file from Google Drive', inputSchema: { type: 'object', properties: { fileId: { type: 'string', description: 'File ID to read', }, }, required: ['fileId'], }, handler: async (input, context) => { console.log('📖 Reading Google Drive file:', input.fileId); // Get user credentials from context const credentials = context.credentials as UserCredentials; if (!credentials) { return { success: false, error: 'User not authenticated or credentials not found', }; } const drive = createDriveClient(credentials); try { // Get file metadata const metaResponse = await drive.files.get({ fileId: input.fileId, fields: 'id, name, mimeType, size, modifiedTime', }); // Get file content const contentResponse = await drive.files.get( { fileId: input.fileId, alt: 'media' }, { responseType: 'text' } ); console.log(`✅ File read: ${metaResponse.data.name} (user: ${credentials.userEmail})`); return { success: true, file: { id: metaResponse.data.id, name: metaResponse.data.name, mimeType: metaResponse.data.mimeType, size: metaResponse.data.size, modifiedTime: metaResponse.data.modifiedTime, content: contentResponse.data, }, }; } catch (error: any) { console.log('❌ Failed to read file:', error.message); return { success: false, error: error.message, }; } }, }, { name: 'delete_file', description: 'Delete a file from Google Drive', inputSchema: { type: 'object', properties: { fileId: { type: 'string', description: 'File ID to delete', }, }, required: ['fileId'], }, handler: async (input, context) => { console.log('🗑️ Deleting Google Drive file:', input.fileId); // Get user credentials from context const credentials = context.credentials as UserCredentials; if (!credentials) { return { success: false, error: 'User not authenticated or credentials not found', }; } const drive = createDriveClient(credentials); try { await drive.files.delete({ fileId: input.fileId, }); console.log(`✅ File deleted: ${input.fileId} (user: ${credentials.userEmail})`); return { success: true, fileId: input.fileId, }; } catch (error: any) { console.log('❌ Failed to delete file:', error.message); return { success: false, error: error.message, }; } }, }, ], resources: [ { uri: 'gdrive://files', name: 'Google Drive Files', description: 'Live files from Google Drive', mimeType: 'application/json', read: async (uri, context) => { console.log('📖 Reading Google Drive files...'); // Get user credentials from context const credentials = context.credentials as UserCredentials; if (!credentials) { throw new Error('User not authenticated or credentials not found'); } const drive = createDriveClient(credentials); try { const response = await drive.files.list({ q: "trashed=false", pageSize: 100, fields: 'files(id, name, mimeType, modifiedTime, size, webViewLink, parents)', }); const files = response.data.files || []; console.log(`✅ Read ${files.length} files (user: ${credentials.userEmail})`); return { contents: files.map(file => ({ id: file.id, name: file.name, mimeType: file.mimeType, modifiedTime: file.modifiedTime, size: file.size, webViewLink: file.webViewLink, })), }; } catch (error: any) { console.log('❌ Failed to read files:', error.message); throw error; } }, list: async (context) => { // Get user credentials from context const credentials = context.credentials as UserCredentials; if (!credentials) { return []; } return [ { uri: 'gdrive://files', name: `${credentials.userEmail} Drive Files`, description: `Live files from ${credentials.userEmail}'s Google Drive`, }, ]; }, subscription: { onSubscribe: async (uri, subscriptionId, thirdPartyWebhookUrl, context) => { console.log(''); console.log('🔔 Setting up Google Drive push notification...'); console.log(` Resource: ${uri}`); console.log(` Subscription ID: ${subscriptionId}`); console.log(` Webhook URL: ${thirdPartyWebhookUrl}`); // Get user credentials from context const credentials = context.credentials as UserCredentials; if (!credentials) { throw new Error('User not authenticated or credentials not found'); } const drive = createDriveClient(credentials); try { // Generate unique channel ID const channelId = `gdrive-channel-${subscriptionId}`; const expiration = Date.now() + (7 * 24 * 60 * 60 * 1000); // 7 days // Watch for changes const response = await drive.files.watch({ fileId: 'root', // Watch root folder (or specific folder) requestBody: { id: channelId, type: 'web_hook', address: thirdPartyWebhookUrl, expiration: expiration.toString(), }, }); channelStore.set(subscriptionId, { channelId, resourceId: response.data.resourceId!, userId: context.userId, }); console.log(`✅ Google Drive push notification created: Channel ${channelId} (user: ${credentials.userEmail})`); console.log(''); return { thirdPartyWebhookId: channelId, metadata: { channelId, resourceId: response.data.resourceId, expiration: new Date(expiration).toISOString(), user: credentials.userEmail, }, }; } catch (error: any) { console.log('❌ Failed to create push notification:', error.message); throw error; } }, onUnsubscribe: async (uri, subscriptionId, storedData, context) => { console.log(''); console.log('🗑️ Removing Google Drive push notification...'); console.log(` Subscription ID: ${subscriptionId}`); const channelInfo = channelStore.get(subscriptionId); if (!channelInfo) { console.log('⚠️ Channel ID not found in store'); return; } // Get user credentials from context const credentials = context.credentials as UserCredentials; if (!credentials) { console.log('⚠️ User credentials not available for cleanup'); return; } const drive = createDriveClient(credentials); try { await drive.channels.stop({ requestBody: { id: channelInfo.channelId, resourceId: channelInfo.resourceId, }, }); channelStore.delete(subscriptionId); console.log(`✅ Google Drive push notification stopped: Channel ${channelInfo.channelId}`); console.log(''); } catch (error: any) { console.log('❌ Failed to stop push notification:', error.message); } }, onWebhook: async (subscriptionId, payload, headers) => { const resourceState = headers['x-goog-resource-state']; const resourceId = headers['x-goog-resource-id']; const channelId = headers['x-goog-channel-id']; console.log(''); console.log('📬 Received Google Drive webhook'); console.log(` State: ${resourceState}`); console.log(` Resource ID: ${resourceId}`); console.log(` Channel ID: ${channelId}`); console.log(` Subscription: ${subscriptionId}`); // Google Drive sends various states: sync, update, remove, trash, untrash, change if (['update', 'remove', 'trash', 'change'].includes(resourceState)) { const changeType = resourceState === 'remove' || resourceState === 'trash' ? 'deleted' : resourceState === 'change' ? 'created' : 'updated'; return { resourceUri: 'gdrive://files', changeType, data: { resourceState, resourceId, channelId, changed: headers['x-goog-changed'], }, }; } if (resourceState === 'sync') { console.log(' ℹ️ Initial sync message (no action needed)'); } else { console.log(' ℹ️ State not mapped to resource change'); } return null; }, }, }, ], webhooks: { incomingPath: '/webhooks/incoming', incomingSecret: webhookSecret, verifyIncomingSignature: (_payload, _signature, _secret) => { // Google Drive doesn't use HMAC signatures for push notifications // Instead, it uses X-Goog-Channel-Token header for verification // For this example, we'll accept all webhooks with valid headers return true; }, outgoing: { timeout: 5000, retries: 3, retryDelay: 1000, signPayload: (payload, secret) => { const hmac = crypto.createHmac('sha256', secret); hmac.update(JSON.stringify(payload)); return `sha256=${hmac.digest('hex')}`; }, }, }, // Authentication: Extract token and fetch credentials authenticate: async (req) => { // Extract token from Authorization header const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { throw new Error('Missing or invalid Authorization header'); } const token = authHeader.substring(7); // Remove 'Bearer ' prefix // Fetch credentials from store (or service in production) const credentials = await fetchCredentials(token, mcpServerName); if (!credentials) { throw new Error('Invalid token or credentials not found'); } console.log(`✅ User authenticated: ${credentials.userEmail}`); return { userId: credentials.userEmail, credentials, // Store credentials in context for later use }; }, logLevel: 'info', }); await server.start(); console.log(''); console.log('╔═══════════════════════════════════════════════════════════╗'); console.log('║ 🎉 Google Drive MCP Server is LIVE! (Multi-User) ║'); console.log('╚═══════════════════════════════════════════════════════════╝'); console.log(''); console.log('📍 MCP Endpoint:'); console.log(` ${ngrokUrl}/mcp`); console.log(''); console.log('🔐 Authentication:'); console.log(' Add header: Authorization: Bearer '); console.log(' Example tokens: user-token-123, user-token-456'); console.log(''); console.log('🔍 Test with MCP Inspector:'); console.log(` npx @modelcontextprotocol/inspector ${ngrokUrl}/mcp`); console.log(''); console.log('📋 Available Tools:'); console.log(' - list_files: List files and folders'); console.log(' - create_file: Create a new file'); console.log(' - read_file: Read file content'); console.log(' - delete_file: Delete a file'); console.log(''); console.log('📚 Available Resources (dynamic per user):'); console.log(' - gdrive://files'); console.log(''); console.log('🔔 Webhook Subscription:'); console.log(' 1. Authenticate with user token'); console.log(' 2. Use MCP Inspector or POST to /mcp with resources/subscribe'); console.log(' 3. Provide your callback URL in _meta.webhookUrl'); console.log(' 4. Google Drive push notification will be created'); console.log(' 5. Create/edit/delete files to see live updates!'); console.log(''); console.log('📝 Example: List files'); console.log(' curl -X POST http://localhost:3001/mcp \\'); console.log(' -H "Content-Type: application/json" \\'); console.log(' -H "Authorization: Bearer user-token-123" \\'); console.log(' -d \'{"jsonrpc":"2.0","id":1,"method":"tools/call",'); console.log(' "params":{"name":"list_files",'); console.log(' "arguments":{}}}\''); console.log(''); console.log('⚠️ Note: Update CREDENTIALS_STORE with real Google OAuth tokens'); console.log('⚠️ Press Ctrl+C to stop and cleanup push notifications'); console.log(''); // Graceful shutdown const cleanup = async () => { console.log(''); console.log('🧹 Shutting down...'); // Stop all push notifications for (const channelInfo of channelStore.values()) { try { // Try to get credentials for cleanup const userTokenEntry = Array.from(CREDENTIALS_STORE.entries()) .find(([, creds]) => creds.userEmail === channelInfo.userId); if (userTokenEntry) { const credentials = userTokenEntry[1]; const drive = createDriveClient(credentials); await drive.channels.stop({ requestBody: { id: channelInfo.channelId, resourceId: channelInfo.resourceId, }, }); console.log(`✅ Stopped push notification: ${channelInfo.channelId} (user: ${channelInfo.userId})`); } else { console.log(`⚠️ Cannot stop push notification ${channelInfo.channelId}: credentials not found`); } } catch (error: any) { console.log(`⚠️ Failed to stop push notification ${channelInfo.channelId}: ${error.message}`); } } await server.stop(); await ngrok.disconnect(); await ngrok.kill(); store.destroy(); console.log('👋 Goodbye!'); process.exit(0); }; process.on('SIGTERM', cleanup); process.on('SIGINT', cleanup); } main().catch((error) => { console.error('💥 Fatal error:', error); process.exit(1); });