Skip to main content
Glama

Google Apps Script MCP Server

by mohalmah
oauth-setup.js17.9 kB
#!/usr/bin/env node /** * OAuth Setup Script for Google Apps Script API * This script helps you obtain and securely store OAuth tokens * Enhanced with detailed logging for debugging and monitoring */ import 'dotenv/config'; import { manualOAuthFlow } from './lib/oauth-helper.js'; import { TokenManager } from './lib/tokenManager.js'; import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; import os from 'os'; // Enhanced logging utilities const log = { info: (msg, data = null) => { const timestamp = new Date().toISOString(); console.log(`[${timestamp}] ℹ️ ${msg}`); if (data) console.log(`[${timestamp}] 📊 Data:`, data); }, success: (msg, data = null) => { const timestamp = new Date().toISOString(); console.log(`[${timestamp}] ✅ ${msg}`); if (data) console.log(`[${timestamp}] 📊 Data:`, data); }, error: (msg, error = null) => { const timestamp = new Date().toISOString(); console.error(`[${timestamp}] ❌ ${msg}`); if (error) { console.error(`[${timestamp}] 🐛 Error details:`, error.message); if (error.stack) console.error(`[${timestamp}] 📚 Stack trace:`, error.stack); } }, warn: (msg, data = null) => { const timestamp = new Date().toISOString(); console.warn(`[${timestamp}] ⚠️ ${msg}`); if (data) console.warn(`[${timestamp}] 📊 Data:`, data); }, debug: (msg, data = null) => { if (process.env.DEBUG || process.argv.includes('--debug')) { const timestamp = new Date().toISOString(); console.log(`[${timestamp}] 🔍 DEBUG: ${msg}`); if (data) console.log(`[${timestamp}] 🔍 DEBUG Data:`, JSON.stringify(data, null, 2)); } }, step: (step, total, msg) => { const timestamp = new Date().toISOString(); console.log(`[${timestamp}] 🚀 Step ${step}/${total}: ${msg}`); }, separator: () => { console.log('═'.repeat(80)); }, subseparator: () => { console.log('─'.repeat(60)); } }; // Performance timing utility class Timer { constructor(name) { this.name = name; this.start = Date.now(); log.debug(`Timer started: ${name}`); } lap(description) { const elapsed = Date.now() - this.start; log.debug(`Timer ${this.name} - ${description}: ${elapsed}ms`); return elapsed; } end(description = 'completed') { const elapsed = Date.now() - this.start; log.info(`Timer ${this.name} ${description} in ${elapsed}ms`); return elapsed; } } // System information logging function logSystemInfo() { log.separator(); log.info('📋 System Information'); log.subseparator(); const systemInfo = { platform: os.platform(), arch: os.arch(), nodeVersion: process.version, workingDirectory: process.cwd(), scriptPath: fileURLToPath(import.meta.url), arguments: process.argv.slice(2), environment: process.env.NODE_ENV || 'development', timestamp: new Date().toISOString() }; Object.entries(systemInfo).forEach(([key, value]) => { log.info(` ${key}: ${value}`); }); log.subseparator(); } // Environment validation with detailed logging function validateEnvironment() { const timer = new Timer('Environment Validation'); log.step(1, 8, 'Validating environment configuration'); const envVars = { 'GOOGLE_APP_SCRIPT_API_CLIENT_ID': process.env.GOOGLE_APP_SCRIPT_API_CLIENT_ID, 'GOOGLE_APP_SCRIPT_API_CLIENT_SECRET': process.env.GOOGLE_APP_SCRIPT_API_CLIENT_SECRET, 'GOOGLE_APP_SCRIPT_API_REDIRECT_URI': process.env.GOOGLE_APP_SCRIPT_API_REDIRECT_URI }; log.debug('Environment variables loaded:', { hasClientId: !!envVars.GOOGLE_APP_SCRIPT_API_CLIENT_ID, hasClientSecret: !!envVars.GOOGLE_APP_SCRIPT_API_CLIENT_SECRET, hasRedirectUri: !!envVars.GOOGLE_APP_SCRIPT_API_REDIRECT_URI, clientIdLength: envVars.GOOGLE_APP_SCRIPT_API_CLIENT_ID?.length || 0, clientSecretLength: envVars.GOOGLE_APP_SCRIPT_API_CLIENT_SECRET?.length || 0 }); const validation = { hasClientId: !!envVars.GOOGLE_APP_SCRIPT_API_CLIENT_ID && envVars.GOOGLE_APP_SCRIPT_API_CLIENT_ID !== 'your_client_id_here', hasClientSecret: !!envVars.GOOGLE_APP_SCRIPT_API_CLIENT_SECRET && envVars.GOOGLE_APP_SCRIPT_API_CLIENT_SECRET !== 'your_client_secret_here', hasRedirectUri: !!envVars.GOOGLE_APP_SCRIPT_API_REDIRECT_URI }; log.info('Environment validation results:', validation); if (!validation.hasClientId) { log.error('Missing or invalid CLIENT_ID in environment'); throw new Error('GOOGLE_APP_SCRIPT_API_CLIENT_ID is required'); } if (!validation.hasClientSecret) { log.error('Missing or invalid CLIENT_SECRET in environment'); throw new Error('GOOGLE_APP_SCRIPT_API_CLIENT_SECRET is required'); } if (!validation.hasRedirectUri) { log.warn('No REDIRECT_URI specified, using default'); } timer.end(); log.success('Environment validation completed successfully'); return envVars; } // File system operations with detailed logging function validateEnvFile() { const timer = new Timer('Env File Validation'); log.step(2, 8, 'Validating .env file'); const envPath = '.env'; let envContent = ''; try { log.debug(`Reading .env file from: ${envPath}`); envContent = readFileSync(envPath, 'utf8'); const lines = envContent.split('\n'); const nonEmptyLines = lines.filter(line => line.trim() && !line.trim().startsWith('#')); log.success('Successfully read .env file', { totalLines: lines.length, nonEmptyLines: nonEmptyLines.length, fileSize: envContent.length }); // Analyze .env file content const envAnalysis = { hasClientId: envContent.includes('GOOGLE_APP_SCRIPT_API_CLIENT_ID='), hasClientSecret: envContent.includes('GOOGLE_APP_SCRIPT_API_CLIENT_SECRET='), hasRedirectUri: envContent.includes('GOOGLE_APP_SCRIPT_API_REDIRECT_URI='), hasComments: envContent.includes('#'), hasPlaceholders: envContent.includes('your_client_id_here') || envContent.includes('your_client_secret_here') }; log.debug('Env file analysis:', envAnalysis); if (envAnalysis.hasPlaceholders) { log.warn('Found placeholder values in .env file - these need to be replaced with actual credentials'); } } catch (error) { log.error('Failed to read .env file', error); log.info('Expected .env file location:', process.cwd() + '/.env'); log.info('Please create .env file with required credentials'); throw error; } timer.end(); return envContent; } // Token management with detailed logging function analyzeExistingTokens(tokenManager) { const timer = new Timer('Token Analysis'); log.step(3, 8, 'Analyzing existing tokens'); try { const tokenInfo = tokenManager.getTokenInfo(); log.debug('Token analysis started'); log.info('Token information retrieved:', { hasTokens: tokenInfo.hasTokens, location: tokenInfo.location, savedAt: tokenInfo.savedAt, expiresAt: tokenInfo.expiresAt, status: tokenInfo.status, isExpired: tokenInfo.isExpired }); if (tokenInfo.hasTokens) { log.success('Found existing tokens'); if (tokenInfo.isExpired) { log.warn('Existing tokens are expired'); } else { log.success('Existing tokens are still valid'); } // Additional token validation try { const tokenData = tokenManager.loadTokens(); if (tokenData) { log.debug('Token data structure validation:', { hasAccessToken: !!tokenData.access_token, hasRefreshToken: !!tokenData.refresh_token, hasTokenType: !!tokenData.token_type, hasScope: !!tokenData.scope, expiresIn: tokenData.expires_in }); } } catch (tokenError) { log.warn('Could not validate token data structure', tokenError); } } else { log.info('No existing tokens found'); } timer.end(); return tokenInfo; } catch (error) { log.error('Failed to analyze existing tokens', error); timer.end(); throw error; } } // OAuth flow with enhanced logging async function executeOAuthFlow() { const timer = new Timer('OAuth Flow'); log.step(4, 8, 'Starting OAuth authorization flow'); try { log.info('Initiating manual OAuth flow'); log.debug('OAuth flow configuration:', { clientId: process.env.GOOGLE_APP_SCRIPT_API_CLIENT_ID?.substring(0, 10) + '...', redirectUri: process.env.GOOGLE_APP_SCRIPT_API_REDIRECT_URI, scope: 'https://www.googleapis.com/auth/script.projects' }); log.info('🌐 Opening browser for OAuth authorization...'); log.info('📱 Please complete the authorization in your browser'); log.info('⏳ Waiting for OAuth callback...'); const tokens = await manualOAuthFlow(); log.debug('OAuth flow completed, analyzing response'); const tokenAnalysis = { hasAccessToken: !!tokens.access_token, hasRefreshToken: !!tokens.refresh_token, hasTokenType: !!tokens.token_type, hasScope: !!tokens.scope, hasExpiresIn: !!tokens.expires_in, accessTokenLength: tokens.access_token?.length || 0, refreshTokenLength: tokens.refresh_token?.length || 0, tokenType: tokens.token_type, scope: tokens.scope, expiresIn: tokens.expires_in }; log.success('OAuth tokens received', tokenAnalysis); if (!tokens.refresh_token) { log.error('No refresh token received - this is required for long-term access'); throw new Error('Refresh token missing from OAuth response'); } timer.end(); return tokens; } catch (error) { log.error('OAuth flow failed', error); timer.end(); throw error; } } // Token storage with detailed logging function saveTokensSecurely(tokenManager, tokens) { const timer = new Timer('Token Storage'); log.step(5, 8, 'Saving tokens securely'); try { log.debug('Preparing to save tokens'); log.info('Token storage location:', tokenManager.getTokenInfo().location); // Pre-save validation const preValidation = { hasAccessToken: !!tokens.access_token, hasRefreshToken: !!tokens.refresh_token, tokenManagerReady: !!tokenManager }; log.debug('Pre-save validation:', preValidation); if (!preValidation.hasAccessToken || !preValidation.hasRefreshToken) { throw new Error('Invalid token data - missing required tokens'); } log.info('💾 Writing tokens to secure storage...'); tokenManager.saveTokens(tokens); // Post-save verification const verification = tokenManager.getTokenInfo(); log.success('Tokens saved successfully', { location: verification.location, hasTokens: verification.hasTokens, savedAt: verification.savedAt, status: verification.status }); // Additional verification - try to read back the tokens try { const savedTokens = tokenManager.loadTokens(); const verificationCheck = { canReadBack: !!savedTokens, accessTokenMatches: savedTokens?.access_token === tokens.access_token, refreshTokenMatches: savedTokens?.refresh_token === tokens.refresh_token }; log.debug('Token verification check:', verificationCheck); if (!verificationCheck.canReadBack) { throw new Error('Cannot read back saved tokens'); } if (!verificationCheck.accessTokenMatches || !verificationCheck.refreshTokenMatches) { log.warn('Saved tokens do not match original tokens'); } else { log.success('Token integrity verified'); } } catch (verificationError) { log.warn('Could not verify saved tokens', verificationError); } timer.end(); return verification; } catch (error) { log.error('Failed to save tokens securely', error); timer.end(); throw error; } } // Main setup function with comprehensive logging async function setupOAuth() { const mainTimer = new Timer('Complete OAuth Setup'); log.separator(); log.info('🔐 Google Apps Script API OAuth Setup - Enhanced Logging Version'); log.separator(); // Log system information logSystemInfo(); try { log.info('📋 Starting OAuth setup process...'); log.info('📝 This script will guide you through OAuth authentication setup'); // Initialize token manager log.step(0, 8, 'Initializing token manager'); const tokenManager = new TokenManager(); log.success('Token manager initialized'); // Handle command line arguments const args = process.argv.slice(2); log.debug('Command line arguments:', args); // Handle info command if (args.includes('--info')) { log.info('📊 Information mode requested'); const tokenInfo = analyzeExistingTokens(tokenManager); console.log('\n🔍 Token Information Summary:'); console.log('═'.repeat(40)); if (tokenInfo.hasTokens) { console.log('✅ Status: Tokens found'); console.log(`📁 Location: ${tokenInfo.location}`); console.log(`💾 Saved: ${tokenInfo.savedAt}`); console.log(`⏰ Expires: ${tokenInfo.expiresAt}`); console.log(`📊 Status: ${tokenInfo.status}`); console.log(`🔐 Scope: ${tokenInfo.scope || 'Not specified'}`); } else { console.log('❌ Status: No tokens found'); console.log(`📁 Expected location: ${tokenInfo.location}`); console.log('\n💡 Run "node oauth-setup.js" to set up OAuth tokens'); } mainTimer.end('info command completed'); process.exit(0); } // Handle clear command if (args.includes('--clear')) { log.info('🗑️ Clear tokens mode requested'); const tokenInfo = analyzeExistingTokens(tokenManager); if (tokenInfo.hasTokens) { log.info('Clearing existing tokens...'); tokenManager.clearTokens(); log.success('Tokens cleared successfully'); } else { log.info('No tokens found to clear'); } mainTimer.end('clear command completed'); process.exit(0); } // Validate environment const envVars = validateEnvironment(); const envContent = validateEnvFile(); // Analyze existing tokens const tokenInfo = analyzeExistingTokens(tokenManager); // Check if we should proceed with OAuth flow if (tokenInfo.hasTokens && !tokenInfo.isExpired && !args.includes('--force')) { log.success('Valid tokens already exist'); console.log('\n✅ You already have valid OAuth tokens stored.'); console.log('💡 To force new token generation: node oauth-setup.js --force'); console.log('🗑️ To clear existing tokens: node oauth-setup.js --clear'); console.log('📊 To view token info: node oauth-setup.js --info'); mainTimer.end('setup skipped - valid tokens exist'); process.exit(0); } if (tokenInfo.hasTokens && args.includes('--force')) { log.warn('Force mode enabled - will replace existing tokens'); } // Execute OAuth flow const tokens = await executeOAuthFlow(); // Save tokens securely const saveResult = saveTokensSecurely(tokenManager, tokens); // Final verification and success message log.step(6, 8, 'Performing final verification'); const finalVerification = analyzeExistingTokens(tokenManager); if (finalVerification.hasTokens && !finalVerification.isExpired) { log.success('Final verification passed - OAuth setup completed successfully'); } else { log.error('Final verification failed'); throw new Error('Setup completed but final verification failed'); } // Success summary log.step(7, 8, 'Generating setup summary'); log.separator(); log.success('🎉 OAuth Setup Completed Successfully!'); log.subseparator(); const summary = { tokenLocation: saveResult.location, setupDuration: mainTimer.end('setup completed'), tokenStatus: finalVerification.status, nextSteps: [ 'Test your setup: node test-token-management.js', 'Run MCP server: node mcpServer.js', 'Check token info: node oauth-setup.js --info' ] }; log.info('Setup Summary:', summary); console.log('\n🎉 Setup Complete! Next Steps:'); summary.nextSteps.forEach((step, index) => { console.log(` ${index + 1}. ${step}`); }); log.step(8, 8, 'OAuth setup process completed'); } catch (error) { log.error('OAuth setup failed', error); console.log('\n🔧 Troubleshooting Guide:'); console.log('════════════════════════'); console.log('1. 🌐 Check your internet connection'); console.log('2. 🔑 Verify CLIENT_ID and CLIENT_SECRET are correct'); console.log('3. 🔗 Ensure redirect URI is registered in Google Cloud Console'); console.log('4. 🔌 Confirm Google Apps Script API is enabled'); console.log('5. 🔄 Try revoking and re-creating OAuth credentials'); console.log('6. 🐛 Enable debug mode: node oauth-setup.js --debug'); console.log('\n📖 For detailed instructions, see OAUTH_SETUP.md'); mainTimer.end('setup failed'); process.exit(1); } } // Error handling and execution setupOAuth().catch((error) => { log.error('💥 Unexpected error during setup', error); console.error('\n🚨 Critical Error Details:'); console.error('Message:', error.message); if (error.stack) { console.error('Stack:', error.stack); } process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mohalmah/google-appscript-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server