UNPKG

facebook-ads-mcp-server

Version:

Universal Facebook Ads Server - Supports MCP, OpenAI Function Calling, and Gemini Function Calling

203 lines (174 loc) 6.48 kB
import express from 'express'; import { v4 as uuidv4 } from 'uuid'; import open from 'open'; import { TokenStorage } from './token-storage.js'; export class OAuthServer { constructor() { this.app = express(); this.server = null; this.pendingAuth = new Map(); // state -> resolve function this.setupRoutes(); } setupRoutes() { // OAuth callback endpoint this.app.get('/auth/callback', async (req, res) => { const { code, state, error } = req.query; if (error) { console.error('❌ Facebook OAuth error:', error); res.send(` <html> <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;"> <h2 style="color: #e74c3c;">❌ Authentication Failed</h2> <p>Error: ${error}</p> <p>You can close this window and try again.</p> </body> </html> `); const resolver = this.pendingAuth.get(state); if (resolver) { resolver.reject(new Error(`OAuth error: ${error}`)); this.pendingAuth.delete(state); } return; } if (!code || !state) { console.error('❌ Missing code or state in OAuth callback'); res.send(` <html> <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;"> <h2 style="color: #e74c3c;">❌ Authentication Failed</h2> <p>Missing authorization code or state parameter.</p> <p>You can close this window and try again.</p> </body> </html> `); return; } try { // Exchange code for access token const tokenResponse = await this.exchangeCodeForToken(code); // Store token securely await TokenStorage.storeToken(tokenResponse.access_token, tokenResponse.expires_in); res.send(` <html> <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;"> <h2 style="color: #27ae60;">✅ Successfully Connected to Facebook!</h2> <p>Your Facebook account has been linked successfully.</p> <p>You can now close this window and return to Claude.</p> </body> </html> `); // Resolve the pending promise const resolver = this.pendingAuth.get(state); if (resolver) { resolver.resolve(tokenResponse); this.pendingAuth.delete(state); } } catch (error) { console.error('❌ Error exchanging code for token:', error.message); res.send(` <html> <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;"> <h2 style="color: #e74c3c;">❌ Authentication Failed</h2> <p>Failed to complete authentication: ${error.message}</p> <p>You can close this window and try again.</p> </body> </html> `); const resolver = this.pendingAuth.get(state); if (resolver) { resolver.reject(error); this.pendingAuth.delete(state); } } }); // Health check endpoint this.app.get('/health', (req, res) => { res.json({ status: 'ok', service: 'facebook-ads-mcp-oauth' }); }); } async exchangeCodeForToken(code) { const tokenUrl = 'https://graph.facebook.com/v23.0/oauth/access_token'; const params = new URLSearchParams({ client_id: process.env.FACEBOOK_APP_ID, client_secret: process.env.FACEBOOK_APP_SECRET, redirect_uri: process.env.FACEBOOK_REDIRECT_URI || 'http://localhost:3002/auth/callback', code: code }); const response = await fetch(`${tokenUrl}?${params}`, { method: 'GET', headers: { 'Accept': 'application/json' } }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Token exchange failed: ${errorText}`); } return await response.json(); } async startOAuthFlow() { return new Promise((resolve, reject) => { // Generate state parameter for security const state = uuidv4(); // Store the resolver for this auth attempt this.pendingAuth.set(state, { resolve, reject }); // Build Facebook OAuth URL const authUrl = this.buildAuthUrl(state); console.error('🌐 Opening Facebook login in browser...'); console.error(`📱 If browser doesn't open, visit: ${authUrl}`); // Open browser open(authUrl).catch(error => { console.error('❌ Failed to open browser:', error.message); console.error(`🔗 Please manually visit: ${authUrl}`); }); // Set timeout for auth attempt (5 minutes) setTimeout(() => { if (this.pendingAuth.has(state)) { this.pendingAuth.delete(state); reject(new Error('OAuth timeout: User did not complete authentication within 5 minutes')); } }, 5 * 60 * 1000); }); } buildAuthUrl(state) { const baseUrl = 'https://www.facebook.com/v23.0/dialog/oauth'; const redirectUri = process.env.FACEBOOK_REDIRECT_URI || 'http://localhost:3002/auth/callback'; const params = new URLSearchParams({ client_id: process.env.FACEBOOK_APP_ID, redirect_uri: redirectUri, scope: 'ads_read,ads_management,business_management', response_type: 'code', state: state }); const fullUrl = `${baseUrl}?${params}`; console.error('🔍 OAuth URL Configuration:'); console.error(' App ID:', process.env.FACEBOOK_APP_ID); console.error(' Redirect URI:', redirectUri); console.error(' Full OAuth URL:', fullUrl); return fullUrl; } async start(port = 3002) { return new Promise((resolve, reject) => { this.server = this.app.listen(port, 'localhost', (error) => { if (error) { console.error(`❌ Failed to start OAuth server on port ${port}:`, error.message); reject(error); } else { console.error(`🚀 OAuth server running on http://localhost:${port}`); resolve(port); } }); }); } async stop() { if (this.server) { return new Promise((resolve) => { this.server.close(() => { console.error('🛑 OAuth server stopped'); resolve(); }); }); } } }