Skip to main content
Glama
auth.ts•35.6 kB
import { OAuth2Client } from 'google-auth-library'; import fs from 'fs'; import path from 'path'; import os from 'os'; import readline from 'readline'; import http from 'http'; import { URL } from 'url'; import { exec } from 'child_process'; import { fileURLToPath } from 'url'; // Get directory path for ES modules - robust cross-platform resolution const currentDir = (() => { try { // Use fileURLToPath for proper cross-platform path resolution return path.dirname(fileURLToPath(import.meta.url)); } catch { // Fallback to process.cwd() if import.meta.url is not available return process.cwd(); } })(); // Get project root directory (one level up from src/) const projectRoot = path.dirname(currentDir); function getAuthSuccessHTML(): string { const htmlPath = path.join(projectRoot, 'public', 'auth-pages', 'auth-success.html'); const cssPath = path.join(projectRoot, 'public', 'css', 'auth-success.css'); const jsPath = path.join(projectRoot, 'public', 'js', 'auth-success.js'); const commandsPath = path.join(projectRoot, 'public', 'data', 'commands.json'); let html = fs.readFileSync(htmlPath, 'utf8'); // If CSS file exists, inject it inline if (fs.existsSync(cssPath)) { const css = fs.readFileSync(cssPath, 'utf8'); html = html.replace( '<link rel="stylesheet" href="/css/auth-success.css">', `<style>\n${css}\n </style>` ); } // If JavaScript file exists, inject it inline if (fs.existsSync(jsPath)) { let js = fs.readFileSync(jsPath, 'utf8'); // If commands.json exists, inject the data inline to avoid fetch issues if (fs.existsSync(commandsPath)) { const commandsData = fs.readFileSync(commandsPath, 'utf8'); // Replace the fetch call with inline data js = js.replace( /const response = await fetch\('\/data\/commands\.json'\);[\s\S]*?const data = await response\.json\(\);/, `const data = ${commandsData};` ); // Remove the console logs related to fetch since we're using inline data js = js.replace(/console\.log\('🔄 Loading commands from \/data\/commands\.json\.\.\.'\);/, ''); js = js.replace(/console\.log\('Current URL:', window\.location\.href\);/, ''); js = js.replace(/console\.log\('Fetch URL will be:', new URL\('\/data\/commands\.json', window\.location\.origin\)\.href\);/, ''); js = js.replace(/console\.log\('📡 Response received:', \{[\s\S]*?\}\);/, ''); js = js.replace(/if \(!response\.ok\) \{[\s\S]*?\}/, ''); } html = html.replace( '<script src="/js/auth-success.js"></script>', `<script>\n${js}\n </script>` ); } return html; } function getAuthFailedHTML(): string { const htmlPath = path.join(projectRoot, 'public', 'auth-pages', 'auth-failed.html'); const cssPath = path.join(projectRoot, 'public', 'css', 'auth-failed.css'); const jsPath = path.join(projectRoot, 'public', 'js', 'auth-failed.js'); let html = fs.readFileSync(htmlPath, 'utf8'); // If CSS file exists, inject it inline if (fs.existsSync(cssPath)) { const css = fs.readFileSync(cssPath, 'utf8'); html = html.replace( '<link rel="stylesheet" href="/css/auth-failed.css">', `<style>\n${css}\n </style>` ); } // If JavaScript file exists, inject it inline if (fs.existsSync(jsPath)) { const js = fs.readFileSync(jsPath, 'utf8'); html = html.replace( '<script src="/js/auth-failed.js"></script>', `<script>\n${js}\n </script>` ); } return html; } function getAuthErrorHTML(): string { const htmlPath = path.join(projectRoot, 'public', 'auth-pages', 'auth-failed.html'); return fs.readFileSync(htmlPath, 'utf8'); } const CONFIG_DIR = path.join(os.homedir(), '.gmail-mcp'); function findFileInPaths(filename: string, possiblePaths: string[]): string | null { for (const basePath of possiblePaths) { if (!basePath) continue; const filePath = path.join(basePath, filename); if (fs.existsSync(filePath)) { return filePath; } } return null; } // Get all possible base paths for file resolution function getPossibleBasePaths(): string[] { const paths = [ process.cwd(), currentDir, // src/ projectRoot, // project root path.dirname(projectRoot), // parent of project root CONFIG_DIR ]; // Add environment variable paths if (process.env.GMAIL_OAUTH_PATH) { paths.unshift(path.dirname(process.env.GMAIL_OAUTH_PATH)); } if (process.env.GMAIL_CREDENTIALS_PATH) { paths.unshift(path.dirname(process.env.GMAIL_CREDENTIALS_PATH)); } // Remove duplicates and non-existent paths return [...new Set(paths)].filter(p => p && fs.existsSync(p)); } // Find OAuth keys file function findOAuthKeys(): string | null { const possiblePaths = getPossibleBasePaths(); return findFileInPaths('gcp-oauth.keys.json', possiblePaths); } // Find credentials file function findCredentials(): string | null { if (process.env.GMAIL_CREDENTIALS_PATH) { return fs.existsSync(process.env.GMAIL_CREDENTIALS_PATH) ? process.env.GMAIL_CREDENTIALS_PATH : null; } const defaultPath = path.join(CONFIG_DIR, 'credentials.json'); return fs.existsSync(defaultPath) ? defaultPath : null; } // Setup authentication export async function setupAuth(): Promise<OAuth2Client> { console.error('Setting up Gmail authentication...'); const oauthKeysPath = findOAuthKeys(); if (!oauthKeysPath) { console.error('\nError: gcp-oauth.keys.json not found!'); console.error('Please ensure the OAuth keys file is in one of these locations:'); getPossibleBasePaths().forEach(p => console.error(` - ${p}/gcp-oauth.keys.json`)); console.error('\nOr set the GMAIL_OAUTH_PATH environment variable to point to the file.'); throw new Error('OAuth keys file not found'); } try { const credentials = JSON.parse(fs.readFileSync(oauthKeysPath, 'utf8')); if (!credentials.web && !credentials.installed) { throw new Error('Invalid OAuth credentials file format'); } const clientConfig = credentials.web || credentials.installed; const oauth2Client = new OAuth2Client( clientConfig.client_id, clientConfig.client_secret, clientConfig.redirect_uris[0] ); return authenticateUser(oauth2Client); } catch (error) { console.error('Error reading OAuth keys:', error); throw error; } } // Authenticate user with browser flow async function authenticateUser(oauth2Client: OAuth2Client): Promise<OAuth2Client> { const scopes = ['https://mail.google.com/']; return new Promise((resolve, reject) => { const server = http.createServer(async (req, res) => { try { const url = new URL(req.url!, `http://${req.headers.host}`); if (url.pathname === '/') { if (url.searchParams.has('code')) { const code = url.searchParams.get('code')!; const { tokens } = await oauth2Client.getToken(code); oauth2Client.setCredentials(tokens); // Save credentials if (!fs.existsSync(CONFIG_DIR)) { fs.mkdirSync(CONFIG_DIR, { recursive: true }); } const credentialsPath = process.env.GMAIL_CREDENTIALS_PATH || path.join(CONFIG_DIR, 'credentials.json'); fs.writeFileSync(credentialsPath, JSON.stringify(tokens, null, 2)); console.error(`Credentials saved to: ${credentialsPath}`); res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(getAuthSuccessHTML()); server.close(() => { resolve(oauth2Client); }); } else if (url.searchParams.has('error')) { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(getAuthFailedHTML()); server.close(() => { reject(new Error('Authentication failed: ' + url.searchParams.get('error'))); }); } else { res.writeHead(400, { 'Content-Type': 'text/html' }); res.end(getAuthErrorHTML()); } } else if (url.pathname.startsWith('/images/')) { // Serve static images const imagePath = path.join(projectRoot, 'public', url.pathname); try { if (fs.existsSync(imagePath)) { const imageData = fs.readFileSync(imagePath); const ext = path.extname(imagePath).toLowerCase(); let contentType = 'application/octet-stream'; if (ext === '.gif') contentType = 'image/gif'; else if (ext === '.png') contentType = 'image/png'; else if (ext === '.jpg' || ext === '.jpeg') contentType = 'image/jpeg'; res.writeHead(200, { 'Content-Type': contentType }); res.end(imageData); } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Image not found'); } } catch (error) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error serving image'); } } else if (url.pathname.startsWith('/css/')) { // Serve CSS files const cssPath = path.join(projectRoot, 'public', url.pathname); try { if (fs.existsSync(cssPath) && path.extname(cssPath).toLowerCase() === '.css') { const cssData = fs.readFileSync(cssPath, 'utf8'); res.writeHead(200, { 'Content-Type': 'text/css' }); res.end(cssData); } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('CSS file not found'); } } catch (error) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error serving CSS file'); } } else if (url.pathname.startsWith('/data/')) { // Serve JSON data files const dataPath = path.join(projectRoot, 'public', url.pathname); try { if (fs.existsSync(dataPath) && path.extname(dataPath).toLowerCase() === '.json') { const jsonData = fs.readFileSync(dataPath, 'utf8'); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(jsonData); } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Data file not found'); } } catch (error) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error serving data file'); } } else if (url.pathname.startsWith('/js/')) { // Serve JavaScript files const jsPath = path.join(projectRoot, 'public', url.pathname); try { if (fs.existsSync(jsPath) && path.extname(jsPath).toLowerCase() === '.js') { const jsData = fs.readFileSync(jsPath, 'utf8'); res.writeHead(200, { 'Content-Type': 'application/javascript' }); res.end(jsData); } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('JavaScript file not found'); } } catch (error) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error serving JavaScript file'); } } else { res.writeHead(404); res.end('Not found'); } } catch (error) { console.error('Error in OAuth callback:', error); // Only send error response if headers haven't been sent yet if (!res.headersSent) { res.writeHead(500, { 'Content-Type': 'text/html' }); res.end(getAuthErrorHTML()); } server.close(() => { reject(error); }); } }); server.listen(0, () => { const address = server.address(); if (!address || typeof address === 'string') { reject(new Error('Failed to start OAuth server')); return; } const port = address.port; const redirectUri = `http://localhost:${port}`; // Update the OAuth client with the dynamic redirect URI const clientConfig = oauth2Client._clientId ? { client_id: oauth2Client._clientId, client_secret: oauth2Client._clientSecret, redirect_uris: [redirectUri] } : null; if (clientConfig) { oauth2Client = new OAuth2Client( clientConfig.client_id, clientConfig.client_secret, redirectUri ); } const authUrl = oauth2Client.generateAuthUrl({ access_type: 'offline', scope: scopes, prompt: 'consent' }); console.error(`\nPlease visit this URL to authorize the application:`); console.error(`${authUrl}\n`); console.error(`Waiting for authorization...`); // Try to open the URL automatically const platform = process.platform; let command = ''; if (platform === 'darwin') { command = `open "${authUrl}"`; } else if (platform === 'win32') { command = `start "" "${authUrl}"`; } else { command = `xdg-open "${authUrl}"`; } exec(command, (error: any) => { if (error) { console.error('Could not automatically open browser. Please manually visit the URL above.'); } }); }); // Add timeout const timeout = setTimeout(() => { server.close(); reject(new Error('Authentication timeout (5 minutes)')); }, 5 * 60 * 1000); // 5 minutes server.on('close', () => { clearTimeout(timeout); }); }); } // Load existing credentials export function loadCredentials(): OAuth2Client | null { try { const credentialsPath = findCredentials(); if (!credentialsPath) { return null; } const oauthKeysPath = findOAuthKeys(); if (!oauthKeysPath) { console.error('OAuth keys file not found, cannot load credentials'); return null; } const credentials = JSON.parse(fs.readFileSync(oauthKeysPath, 'utf8')); const clientConfig = credentials.web || credentials.installed; const oauth2Client = new OAuth2Client( clientConfig.client_id, clientConfig.client_secret, clientConfig.redirect_uris[0] ); const tokens = JSON.parse(fs.readFileSync(credentialsPath, 'utf8')); oauth2Client.setCredentials(tokens); return oauth2Client; } catch (error) { console.error('Error loading credentials:', error); return null; } } // Get authenticated client (load existing or setup new) export async function getAuthenticatedClient(): Promise<OAuth2Client> { const existingClient = loadCredentials(); if (existingClient) { return existingClient; } return setupAuth(); } // Debug authentication export async function debugAuth(): Promise<void> { console.error('=== Gmail MCP Authentication Debug ===\n'); // Check OAuth keys const oauthKeysPath = findOAuthKeys(); console.error('OAuth Keys File:'); if (oauthKeysPath) { console.error(` Found: ${oauthKeysPath}`); try { const credentials = JSON.parse(fs.readFileSync(oauthKeysPath, 'utf8')); const clientConfig = credentials.web || credentials.installed; console.error(` Client ID: ${clientConfig.client_id.substring(0, 10)}...`); console.error(` Redirect URIs: ${clientConfig.redirect_uris.length} configured`); } catch (error) { console.error(` Error reading file: ${error}`); } } else { console.error(' Not found in any of these locations:'); getPossibleBasePaths().forEach(p => console.error(` - ${p}/gcp-oauth.keys.json`)); } console.error('\nSaved Credentials:'); const credentialsPath = findCredentials(); if (credentialsPath) { console.error(` Found: ${credentialsPath}`); try { const tokens = JSON.parse(fs.readFileSync(credentialsPath, 'utf8')); console.error(` Access token: ${tokens.access_token ? 'Present' : 'Missing'}`); console.error(` Refresh token: ${tokens.refresh_token ? 'Present' : 'Missing'}`); console.error(` Expiry: ${tokens.expiry_date ? new Date(tokens.expiry_date).toISOString() : 'Not set'}`); } catch (error) { console.error(` Error reading file: ${error}`); } } else { console.error(' Not found'); } console.error('\nEnvironment Variables:'); console.error(` GMAIL_OAUTH_PATH: ${process.env.GMAIL_OAUTH_PATH || 'Not set'}`); console.error(` GMAIL_CREDENTIALS_PATH: ${process.env.GMAIL_CREDENTIALS_PATH || 'Not set'}`); console.error('\nTesting Authentication:'); try { const client = await getAuthenticatedClient(); console.error(' Authentication successful!'); // Test API access const { google } = await import('googleapis'); const gmail = google.gmail({ version: 'v1', auth: client }); const profile = await gmail.users.getProfile({ userId: 'me' }); console.error(` Gmail API access successful for: ${profile.data.emailAddress}`); } catch (error) { console.error(` Authentication failed: ${error}`); } console.error('\n=== Debug Complete ==='); } export async function getCredentials(): Promise<OAuth2Client | null> { // Use OAuth keys from environment variable or project root const oauthPath = process.env.GMAIL_OAUTH_PATH || path.join(projectRoot, 'gcp-oauth.keys.json'); if (!fs.existsSync(oauthPath)) { return null; // Return null instead of throwing } // Find credentials file - only use the configured location const credentialsPath = process.env.GMAIL_CREDENTIALS_PATH || path.join(CONFIG_DIR, 'credentials.json'); // Create config directory if needed if (!process.env.GMAIL_OAUTH_PATH && !fs.existsSync(path.join(process.cwd(), 'gcp-oauth.keys.json')) && !fs.existsSync(CONFIG_DIR)) { try { fs.mkdirSync(CONFIG_DIR, { recursive: true }); } catch (error) { // Ignore mkdir errors in read-only environments } } const keysContent = JSON.parse(fs.readFileSync(oauthPath, 'utf8')); const keys = keysContent.installed || keysContent.web; if (!keys) { throw new Error('Invalid OAuth keys file format. Expected "installed" or "web" key in OAuth file.'); } // For desktop apps, use the redirect URI from the keys file or OOB flow const redirectUri = keys.redirect_uris?.[0] || "urn:ietf:wg:oauth:2.0:oob"; const oauth2Client = new OAuth2Client(keys.client_id, keys.client_secret, redirectUri); if (fs.existsSync(credentialsPath)) { const credentials = JSON.parse(fs.readFileSync(credentialsPath, 'utf8')); oauth2Client.setCredentials(credentials); // Try to refresh the token if it's expired try { await oauth2Client.getAccessToken(); } catch (error) { // If token refresh fails, return null to force re-authentication return null; } } else { // No credentials file exists, return null to indicate authentication needed return null; } return oauth2Client; } export async function checkAuthStatus(): Promise<{hasOAuthKeys: boolean, hasCredentials: boolean, credentialsValid: boolean}> { // Use OAuth keys from environment variable or project root const oauthPath = process.env.GMAIL_OAUTH_PATH || path.join(projectRoot, 'gcp-oauth.keys.json'); // Find credentials file - only use the configured location const credentialsPath = process.env.GMAIL_CREDENTIALS_PATH || path.join(CONFIG_DIR, 'credentials.json'); const hasOAuthKeys = oauthPath ? fs.existsSync(oauthPath) : false; const hasCredentials = credentialsPath ? fs.existsSync(credentialsPath) : false; let credentialsValid = false; if (hasOAuthKeys && hasCredentials) { try { const client = await getCredentials(); credentialsValid = client !== null; } catch { credentialsValid = false; } } return { hasOAuthKeys, hasCredentials, credentialsValid }; } export async function getOAuthClient(): Promise<OAuth2Client | null> { // Use OAuth keys from environment variable or project root const oauthPath = process.env.GMAIL_OAUTH_PATH || path.join(projectRoot, 'gcp-oauth.keys.json'); if (!fs.existsSync(oauthPath)) { return null; } const keysContent = JSON.parse(fs.readFileSync(oauthPath, 'utf8')); const keys = keysContent.installed || keysContent.web; if (!keys) { throw new Error('Invalid OAuth keys file format. Expected "installed" or "web" key in OAuth file.'); } const redirectUri = keys.redirect_uris?.[0] || "urn:ietf:wg:oauth:2.0:oob"; return new OAuth2Client(keys.client_id, keys.client_secret, redirectUri); } export async function hasValidCredentials(oauth2Client: OAuth2Client): Promise<boolean> { try { const credentials = oauth2Client.credentials; if (!credentials || !credentials.refresh_token) { return false; } await oauth2Client.getAccessToken(); return true; } catch { return false; } } export async function authenticateWeb(oauth2Client: OAuth2Client, credentialsPath?: string): Promise<void> { const creds = credentialsPath || path.join(CONFIG_DIR, 'credentials.json'); return new Promise((resolve, reject) => { let server: http.Server; let port: number; let redirectUri: string; server = http.createServer(async (req, res) => { try { // Get the actual server address for URL parsing const serverAddress = server.address(); const currentPort = serverAddress && typeof serverAddress === 'object' ? serverAddress.port : port; const url = new URL(req.url!, `http://localhost:${currentPort}`); if (url.pathname === '/oauth/callback') { const code = url.searchParams.get('code'); if (code) { // Create OAuth client with current redirect URI const currentWebOAuth2Client = new OAuth2Client( (oauth2Client as any)._clientId, (oauth2Client as any)._clientSecret, redirectUri ); const { tokens } = await currentWebOAuth2Client.getToken(code); oauth2Client.setCredentials(tokens); // Ensure the directory exists const credsDir = path.dirname(creds); if (!fs.existsSync(credsDir)) { fs.mkdirSync(credsDir, { recursive: true }); } // Save credentials fs.writeFileSync(creds, JSON.stringify(tokens, null, 2)); res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(getAuthSuccessHTML()); server.close(); resolve(); } else { res.writeHead(400, { 'Content-Type': 'text/html' }); res.end(getAuthFailedHTML()); server.close(); reject(new Error('No authorization code received')); } } else if (url.pathname === '/') { // Landing page - redirect to Google OAuth // Create OAuth client with current redirect URI for auth URL generation const currentWebOAuth2Client = new OAuth2Client( (oauth2Client as any)._clientId, (oauth2Client as any)._clientSecret, redirectUri ); const currentAuthUrl = currentWebOAuth2Client.generateAuthUrl({ access_type: 'offline', scope: ['https://mail.google.com/'] }); res.writeHead(302, { 'Location': currentAuthUrl }); res.end(); } else if (url.pathname.startsWith('/images/')) { // Serve static images const imagePath = path.join(projectRoot, 'public', url.pathname); try { if (fs.existsSync(imagePath)) { const imageData = fs.readFileSync(imagePath); const ext = path.extname(imagePath).toLowerCase(); let contentType = 'application/octet-stream'; if (ext === '.gif') contentType = 'image/gif'; else if (ext === '.png') contentType = 'image/png'; else if (ext === '.jpg' || ext === '.jpeg') contentType = 'image/jpeg'; res.writeHead(200, { 'Content-Type': contentType }); res.end(imageData); } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Image not found'); } } catch (error) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error serving image'); } } else if (url.pathname.startsWith('/css/')) { // Serve CSS files const cssPath = path.join(projectRoot, 'public', url.pathname); try { if (fs.existsSync(cssPath) && path.extname(cssPath).toLowerCase() === '.css') { const cssData = fs.readFileSync(cssPath, 'utf8'); res.writeHead(200, { 'Content-Type': 'text/css' }); res.end(cssData); } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('CSS file not found'); } } catch (error) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error serving CSS file'); } } else if (url.pathname.startsWith('/data/')) { // Serve JSON data files const dataPath = path.join(projectRoot, 'public', url.pathname); try { if (fs.existsSync(dataPath) && path.extname(dataPath).toLowerCase() === '.json') { const jsonData = fs.readFileSync(dataPath, 'utf8'); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(jsonData); } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Data file not found'); } } catch (error) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error serving data file'); } } else if (url.pathname.startsWith('/js/')) { // Serve JavaScript files const jsPath = path.join(projectRoot, 'public', url.pathname); try { if (fs.existsSync(jsPath) && path.extname(jsPath).toLowerCase() === '.js') { const jsData = fs.readFileSync(jsPath, 'utf8'); res.writeHead(200, { 'Content-Type': 'application/javascript' }); res.end(jsData); } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('JavaScript file not found'); } } catch (error) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error serving JavaScript file'); } } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); } } catch (error: any) { // Only send error response if headers haven't been sent yet if (!res.headersSent) { res.writeHead(500, { 'Content-Type': 'text/html' }); res.end(`<h1>Authentication Error</h1><p>${error instanceof Error ? error.message : 'Unknown error'}</p>`); } server.close(); reject(error); } }); server.listen(0, async () => { // Get the actual port that was assigned const address = server.address(); if (!address || typeof address === 'string') { reject(new Error('Failed to start OAuth server')); return; } port = address.port; redirectUri = `http://localhost:${port}/oauth/callback`; // Update the OAuth client with the dynamic redirect URI const webOAuth2Client = new OAuth2Client( (oauth2Client as any)._clientId, (oauth2Client as any)._clientSecret, redirectUri ); const authUrl = webOAuth2Client.generateAuthUrl({ access_type: 'offline', scope: ['https://mail.google.com/'] }); console.error(`\nOpening authentication in your browser...`); console.error(`\nIf the browser doesn't open automatically, please visit:`); console.error(`\n${authUrl}\n`); // Open browser (platform-agnostic) const platform = os.platform(); // Check if we're in WSL const isWSL = fs.existsSync('/proc/version') && fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft'); if (isWSL) { // In WSL, use Windows' cmd.exe to open the browser exec(`cmd.exe /c start "${authUrl}"`, (error: any) => { if (error) { // Try PowerShell as fallback exec(`powershell.exe -Command "Start-Process '${authUrl}'"`, (error2: any) => { if (error2) { console.error('Could not open browser automatically. Please open the URL manually.'); } }); } }); } else if (platform === 'darwin') { exec(`open "${authUrl}"`, (error: any) => { if (error) { console.error('Could not open browser automatically. Please open the URL manually.'); } }); } else if (platform === 'win32') { exec(`cmd.exe /c start "" "${authUrl}"`, (error: any) => { if (error) { console.error('Could not open browser automatically. Please open the URL manually.'); } }); } else { // Linux exec(`xdg-open "${authUrl}"`, (error: any) => { if (error) { // Try alternative methods exec(`sensible-browser "${authUrl}"`, (error2: any) => { if (error2) { console.error('Could not open browser automatically. Please open the URL manually.'); } }); } }); } }); server.on('error', (error) => { if ((error as any).code === 'EADDRINUSE') { // Port 3000 is in use } reject(error); }); }); }

Latest Blog Posts

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/muammar-yacoob/GMail-Manager-MCP'

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