UNPKG

uhbarp-gmail-mcp-server

Version:

Gmail MCP Server for managing Gmail through natural language interactions with full OAuth2 authentication support

329 lines (328 loc) 12.1 kB
import { google } from 'googleapis'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import open from 'open'; import { createServer } from 'http'; import { URL } from 'url'; import { logger } from './api.js'; // Scopes required for Gmail operations const SCOPES = [ 'https://www.googleapis.com/auth/gmail.readonly', 'https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/gmail.modify', 'https://www.googleapis.com/auth/gmail.labels' ]; // Configuration directory and file paths const CONFIG_DIR = path.join(os.homedir(), '.gmail-mcp'); const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials.json'); const TOKEN_FILE = path.join(CONFIG_DIR, 'token.json'); /** * Gmail authentication manager class */ export class GmailAuth { constructor() { this.oAuth2Client = null; this.credentials = null; this.ensureConfigDir(); } /** * Ensure configuration directory exists */ ensureConfigDir() { if (!fs.existsSync(CONFIG_DIR)) { fs.mkdirSync(CONFIG_DIR, { recursive: true }); logger.log('Created Gmail MCP configuration directory'); } } /** * Load credentials from file or environment */ async loadCredentials() { try { // Try environment variables first if (process.env.GMAIL_CLIENT_ID && process.env.GMAIL_CLIENT_SECRET) { this.credentials = { client_id: process.env.GMAIL_CLIENT_ID, client_secret: process.env.GMAIL_CLIENT_SECRET, redirect_uris: ['http://localhost:3000/oauth2callback'] }; logger.log('Loaded Gmail credentials from environment variables'); return true; } // Try credentials file if (fs.existsSync(CREDENTIALS_FILE)) { const credentialsData = fs.readFileSync(CREDENTIALS_FILE, 'utf8'); const parsedCredentials = JSON.parse(credentialsData); // Handle both desktop and web app credential formats if (parsedCredentials.installed) { this.credentials = parsedCredentials.installed; } else if (parsedCredentials.web) { this.credentials = parsedCredentials.web; } else { this.credentials = parsedCredentials; } logger.log('Loaded Gmail credentials from file'); return true; } return false; } catch (error) { logger.error('Error loading Gmail credentials:', error); return false; } } /** * Save credentials to file */ async saveCredentials(credentials) { try { fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2)); logger.log('Saved Gmail credentials to file'); } catch (error) { logger.error('Error saving Gmail credentials:', error); throw new Error('Failed to save credentials'); } } /** * Initialize OAuth2 client */ initializeOAuth2Client() { if (!this.credentials) { throw new Error('Credentials not loaded'); } this.oAuth2Client = new google.auth.OAuth2(this.credentials.client_id, this.credentials.client_secret, this.credentials.redirect_uris?.[0] || 'http://localhost:3000/oauth2callback'); return this.oAuth2Client; } /** * Load stored access token */ loadStoredToken() { try { if (fs.existsSync(TOKEN_FILE)) { const tokenData = fs.readFileSync(TOKEN_FILE, 'utf8'); return JSON.parse(tokenData); } } catch (error) { logger.error('Error loading stored token:', error); } return null; } /** * Save access token to file */ saveToken(token) { try { fs.writeFileSync(TOKEN_FILE, JSON.stringify(token, null, 2)); logger.log('Saved Gmail access token'); } catch (error) { logger.error('Error saving token:', error); } } /** * Start local server for OAuth2 callback */ startCallbackServer() { return new Promise((resolve, reject) => { const server = createServer((req, res) => { if (req.url?.startsWith('/oauth2callback')) { const url = new URL(req.url, 'http://localhost:3000'); const code = url.searchParams.get('code'); const error = url.searchParams.get('error'); if (error) { res.end(`<html><body><h1>Authentication Error</h1><p>${error}</p></body></html>`); server.close(); reject(new Error(`OAuth2 error: ${error}`)); return; } if (code) { res.end(`<html><body><h1>Authentication Successful!</h1><p>You can close this window and return to the terminal.</p></body></html>`); server.close(); resolve(code); return; } res.end('<html><body><h1>Invalid Request</h1></body></html>'); } else { res.end('<html><body><h1>Gmail MCP Server OAuth2</h1><p>Waiting for authentication...</p></body></html>'); } }); server.listen(3000, () => { logger.log('OAuth2 callback server started on port 3000'); }); server.on('error', (err) => { reject(err); }); }); } /** * Perform OAuth2 authentication flow */ async authenticate() { if (!await this.loadCredentials()) { throw new Error('Gmail credentials not found. Please run with --setup-auth flag first.'); } const auth = this.initializeOAuth2Client(); // Try to load existing token const storedToken = this.loadStoredToken(); if (storedToken) { auth.setCredentials(storedToken); // Check if token is still valid try { await auth.getAccessToken(); logger.log('Using existing valid token'); return auth; } catch (error) { logger.log('Stored token invalid, requesting new token'); } } // Generate authentication URL const authUrl = auth.generateAuthUrl({ access_type: 'offline', scope: SCOPES, prompt: 'consent' }); logger.log('Starting OAuth2 authentication flow'); console.error('\nStarting Gmail authentication...'); console.error('If browser doesn\'t open automatically, visit this URL:'); console.error(authUrl); try { // Start callback server and open browser const codePromise = this.startCallbackServer(); await open(authUrl); // Wait for authorization code const code = await codePromise; // Exchange code for tokens const { tokens } = await auth.getToken(code); auth.setCredentials(tokens); // Save tokens for future use this.saveToken(tokens); logger.log('Gmail authentication completed successfully'); console.error('Authentication successful!\n'); return auth; } catch (error) { logger.error('Authentication failed:', error); throw new Error(`Authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Get authenticated Gmail API client */ async getGmailClient() { const auth = await this.authenticate(); return google.gmail({ version: 'v1', auth }); } /** * Prompt user for credentials setup */ async setupCredentials() { console.log('\n=== Gmail MCP Server Setup ==='); console.log('To use this server, you need to set up Google API credentials.'); console.log('\nPlease follow these steps:'); console.log('1. Go to https://console.developers.google.com/'); console.log('2. Create a new project or select existing project'); console.log('3. Enable the Gmail API'); console.log('4. Create credentials (OAuth 2.0 Client ID)'); console.log('5. Set redirect URI to: http://localhost:3000/oauth2callback'); console.log('6. Download the credentials JSON file'); console.log(`7. Save it as: ${CREDENTIALS_FILE}`); console.log('\nAlternatively, set environment variables:'); console.log('- GMAIL_CLIENT_ID'); console.log('- GMAIL_CLIENT_SECRET'); console.log('\nFor detailed instructions, visit: https://developers.google.com/gmail/api/quickstart/nodejs'); } /** * Check if user credentials are configured */ async isConfigured() { try { fs.accessSync(CREDENTIALS_FILE); return true; } catch { return false; } } /** * Check if user is currently authenticated (has valid tokens) */ async isAuthenticated() { try { fs.accessSync(TOKEN_FILE); const tokenData = fs.readFileSync(TOKEN_FILE, 'utf8'); const tokens = JSON.parse(tokenData); // Check if we have both access and refresh tokens if (!tokens.access_token) { return false; } // If we have a refresh token, we can always get a new access token if (tokens.refresh_token) { return true; } // Check if access token is still valid (not expired) if (tokens.expiry_date && tokens.expiry_date > Date.now()) { return true; } return false; } catch { return false; } } /** * Reset authentication (clear stored tokens) */ resetAuth() { try { if (fs.existsSync(TOKEN_FILE)) { fs.unlinkSync(TOKEN_FILE); logger.log('Cleared stored authentication tokens'); } } catch (error) { logger.error('Error clearing tokens:', error); } } /** * Get authentication URL without starting the full authentication flow * This allows for manual authentication control */ async getAuthUrl() { if (!await this.loadCredentials()) { throw new Error('Gmail credentials not found. Please configure OAuth2 credentials first.'); } const auth = this.initializeOAuth2Client(); // Generate authentication URL const authUrl = auth.generateAuthUrl({ access_type: 'offline', scope: SCOPES, prompt: 'consent' }); // Start callback server to handle the response this.startCallbackServer().then(async (code) => { try { // Exchange code for tokens const { tokens } = await auth.getToken(code); auth.setCredentials(tokens); // Save tokens for future use this.saveToken(tokens); logger.log('Gmail authentication completed successfully via manual link'); } catch (error) { logger.error('Authentication failed via manual link:', error); } }).catch((error) => { logger.error('Callback server error:', error); }); return authUrl; } } // Export singleton instance export const gmailAuth = new GmailAuth();