# Directory Structure ``` ├── .dockerignore ├── .env ├── .env.example ├── .gitignore ├── docker-compose.yml ├── Dockerfile ├── mcp.json ├── package-lock.json ├── package.json ├── README.md ├── scripts │ └── get_refresh_token.js ├── smithery.json ├── smithery.yaml └── src ├── config │ └── googleAdsConfig.js ├── controllers │ └── keywordController.js ├── index.js ├── mcp_fixed.js ├── mcp.js ├── routes │ ├── competitorRoutes.js │ ├── keywordRoutes.js │ └── serpRoutes.js └── services └── keywordPlannerService.js ``` # Files -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- ``` node_modules npm-debug.log .git .gitignore .env .env.* !.env.example .DS_Store README.md .vscode .idea *.log logs ``` -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- ``` # Server configuration PORT=3000 HOST=0.0.0.0 NODE_ENV=production # Google Ads API configuration GOOGLE_ADS_DEVELOPER_TOKEN=6mEoXl5C5vR0oxlHDhUFQQ GOOGLE_ADS_CLIENT_ID=your-client-id GOOGLE_ADS_CLIENT_SECRET=your-client-secret GOOGLE_ADS_REFRESH_TOKEN=your-refresh-token GOOGLE_ADS_LOGIN_CUSTOMER_ID=your-customer-id ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` # Server Configuration PORT=3000 NODE_ENV=development # Google Ads API Configuration GOOGLE_ADS_DEVELOPER_TOKEN=6mEoXl5C5vR0oxlHDhUFQQ GOOGLE_ADS_CLIENT_ID=your_client_id_here GOOGLE_ADS_CLIENT_SECRET=your_client_secret_here GOOGLE_ADS_REFRESH_TOKEN=your_refresh_token_here GOOGLE_ADS_LOGIN_CUSTOMER_ID=your_customer_id_without_dashes # SERP API Configuration SERP_API_KEY=your_serp_api_key_here ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Dependency directories node_modules/ # Environment variables .env .env.local .env.development.local .env.test.local .env.production.local # Debug logs npm-debug.log* yarn-debug.log* yarn-error.log* # Build directories dist/ build/ # OS specific files .DS_Store Thumbs.db # Editor directories and files .idea/ .vscode/ *.suo *.ntvs* *.njsproj *.sln *.sw? # Google Ads API credentials google-ads.yaml client_secret.json ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # App SEO AI Application for SEO automation and AI-powered optimization with Google Ads Keyword Planner integration. ## Features - Keyword research using Google Ads API - SERP analysis - Competitor analysis - SEO recommendations - MCP (Model Context Protocol) integration for AI assistants ## Prerequisites - Node.js (v14 or higher) - npm or yarn - Google Ads account with API access - Google Cloud Platform project with Google Ads API enabled ## Setup ### 1. Clone the repository ```bash git clone https://github.com/ccnn2509/app-seo-ai.git cd app-seo-ai ``` ### 2. Install dependencies ```bash npm install ``` ### 3. Configure environment variables Copy the example environment file: ```bash cp .env.example .env ``` Edit the `.env` file and fill in your Google Ads API credentials: ``` # Server Configuration PORT=3000 NODE_ENV=development # Google Ads API Configuration GOOGLE_ADS_DEVELOPER_TOKEN=your_developer_token GOOGLE_ADS_CLIENT_ID=your_client_id GOOGLE_ADS_CLIENT_SECRET=your_client_secret GOOGLE_ADS_REFRESH_TOKEN=your_refresh_token GOOGLE_ADS_LOGIN_CUSTOMER_ID=your_customer_id_without_dashes # SERP API Configuration (optional) SERP_API_KEY=your_serp_api_key ``` ### 4. Get Google Ads API refresh token Run the following command to get a refresh token: ```bash npm run get-token ``` This will open your browser and guide you through the OAuth2 authentication process. The refresh token will be automatically saved to your `.env` file. ### 5. Start the server For development: ```bash npm run dev ``` For production: ```bash npm start ``` The server will start on the port specified in your `.env` file (default: 3000). ## API Documentation API documentation is available at `/api-docs` when the server is running: ``` http://localhost:3000/api-docs ``` ## MCP Integration This project includes MCP (Model Context Protocol) integration, allowing AI assistants to use the API. The MCP configuration is in the `mcp.json` file. To use this with Smithery: 1. Go to [Smithery](https://smithery.ai/) 2. Create a new MCP server 3. Select the `app-seo-ai` repository 4. Configure the server settings 5. Deploy the server ## Available MCP Tools - `research_keywords` - Research keywords related to a given topic or seed keyword - `analyze_serp` - Analyze a SERP (Search Engine Results Page) for a given query - `analyze_competitors` - Analyze competitors for a given keyword or domain - `_health` - Health check endpoint ## Example Usage ### Research Keywords ```javascript // Example request to research keywords fetch('http://localhost:3000/api/keywords/ideas?keyword=seo%20tools&language=en') .then(response => response.json()) .then(data => console.log(data)); ``` ### Analyze SERP ```javascript // Example request to analyze SERP fetch('http://localhost:3000/api/serp/analyze?query=best%20seo%20tools&location=United%20States') .then(response => response.json()) .then(data => console.log(data)); ``` ### Analyze Competitors ```javascript // Example request to analyze competitors fetch('http://localhost:3000/api/competitors/analyze?domain=example.com') .then(response => response.json()) .then(data => console.log(data)); ``` ## License MIT ``` -------------------------------------------------------------------------------- /src/mcp_fixed.js: -------------------------------------------------------------------------------- ```javascript // This file is a redirect to mcp.js // It exists because Smithery is looking for it according to the error message import app from './mcp.js'; export default app; ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile FROM node:18-alpine WORKDIR /app # Copy package.json and package-lock.json COPY package*.json ./ # Install dependencies RUN npm install --only=production # Copy application code COPY . . # Expose the port the app runs on EXPOSE 3000 # The command will be provided by smithery.yaml # This is just a default command for local testing CMD ["node", "src/mcp.js", "--mcp"] ``` -------------------------------------------------------------------------------- /src/config/googleAdsConfig.js: -------------------------------------------------------------------------------- ```javascript import dotenv from 'dotenv'; dotenv.config(); // Google Ads API configuration const googleAdsConfig = { developer_token: process.env.GOOGLE_ADS_DEVELOPER_TOKEN, client_id: process.env.GOOGLE_ADS_CLIENT_ID, client_secret: process.env.GOOGLE_ADS_CLIENT_SECRET, refresh_token: process.env.GOOGLE_ADS_REFRESH_TOKEN, login_customer_id: process.env.GOOGLE_ADS_LOGIN_CUSTOMER_ID, use_proto_plus: true, }; export default googleAdsConfig; ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml # Smithery.ai configuration startCommand: type: stdio configSchema: # JSON Schema defining the configuration options for the MCP. {} commandFunction: |- (config) => ({ "command": "node", "args": [ "src/mcp.js", "--mcp" ], "env": { "PORT": "3000", "HOST": "0.0.0.0", "NODE_ENV": "production", "GOOGLE_ADS_DEVELOPER_TOKEN": "6mEoXl5C5vR0oxlHDhUFQQ" } }) # Build configuration build: dockerfile: Dockerfile dockerBuildPath: . ``` -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- ```yaml version: '3' services: app: build: . ports: - "3000:3000" environment: - PORT=3000 - NODE_ENV=production - GOOGLE_ADS_DEVELOPER_TOKEN=${GOOGLE_ADS_DEVELOPER_TOKEN} - GOOGLE_ADS_CLIENT_ID=${GOOGLE_ADS_CLIENT_ID} - GOOGLE_ADS_CLIENT_SECRET=${GOOGLE_ADS_CLIENT_SECRET} - GOOGLE_ADS_REFRESH_TOKEN=${GOOGLE_ADS_REFRESH_TOKEN} - GOOGLE_ADS_LOGIN_CUSTOMER_ID=${GOOGLE_ADS_LOGIN_CUSTOMER_ID} - SERP_API_KEY=${SERP_API_KEY} volumes: - ./logs:/app/logs restart: unless-stopped ``` -------------------------------------------------------------------------------- /smithery.json: -------------------------------------------------------------------------------- ```json { "name": "SEO AI Tools", "description": "SEO automation and AI-powered optimization with Google Ads Keyword Planner integration", "version": "1.0.0", "repository": "https://github.com/ccnn2509/app-seo-ai", "main": "mcp.json", "scripts": { "start": "node src/index.js", "dev": "nodemon src/index.js" }, "env": { "PORT": "3000", "NODE_ENV": "production", "GOOGLE_ADS_DEVELOPER_TOKEN": "6mEoXl5C5vR0oxlHDhUFQQ" }, "buildCommand": "npm install", "startCommand": "npm start", "healthCheckPath": "/health", "resources": { "cpu": 1, "memory": "512Mi" } } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "app-seo-ai", "version": "1.0.0", "main": "src/index.js", "type": "module", "scripts": { "start": "node src/index.js", "dev": "nodemon src/index.js", "mcp": "node src/mcp.js --mcp", "get-token": "node scripts/get_refresh_token.js" }, "keywords": [ "seo", "ai", "google-ads", "keyword-planner", "serp" ], "author": "", "license": "ISC", "description": "SEO automation and AI-powered optimization with Google Ads Keyword Planner integration", "dependencies": { "cors": "^2.8.5", "dotenv": "^16.4.7", "express": "^4.21.2", "google-ads-api": "^17.1.0-rest-beta", "google-auth-library": "^9.15.1", "open": "^10.1.0", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1" }, "devDependencies": { "nodemon": "^3.1.9" } } ``` -------------------------------------------------------------------------------- /src/mcp.js: -------------------------------------------------------------------------------- ```javascript import express from 'express'; import cors from 'cors'; import dotenv from 'dotenv'; import keywordRoutes from './routes/keywordRoutes.js'; import serpRoutes from './routes/serpRoutes.js'; import competitorRoutes from './routes/competitorRoutes.js'; // Load environment variables dotenv.config(); // Initialize express app const app = express(); const PORT = process.env.PORT || 3000; const HOST = process.env.HOST || '0.0.0.0'; // Listen on all network interfaces // Middleware app.use(cors()); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Routes app.use('/api/keywords', keywordRoutes); app.use('/api/serp', serpRoutes); app.use('/api/competitors', competitorRoutes); // Health check endpoint app.get('/health', (req, res) => { res.status(200).json({ status: 'ok', message: 'MCP Server is running' }); }); // Start server app.listen(PORT, HOST, () => { console.log(`MCP Server running on ${HOST}:${PORT}`); console.log(`Health check available at http://${HOST}:${PORT}/health`); }); export default app; ``` -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- ```javascript import express from 'express'; import cors from 'cors'; import dotenv from 'dotenv'; import swaggerJsDoc from 'swagger-jsdoc'; import swaggerUi from 'swagger-ui-express'; // Import routes import keywordRoutes from './routes/keywordRoutes.js'; import serpRoutes from './routes/serpRoutes.js'; import competitorRoutes from './routes/competitorRoutes.js'; // Load environment variables dotenv.config(); // Initialize express app const app = express(); const PORT = process.env.PORT || 3000; // Middleware app.use(cors()); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Swagger configuration const swaggerOptions = { definition: { openapi: '3.0.0', info: { title: 'App SEO AI API', version: '1.0.0', description: 'API for SEO automation and AI-powered optimization', }, servers: [ { url: `http://localhost:${PORT}`, description: 'Development server', }, ], }, apis: ['./src/routes/*.js'], }; const swaggerDocs = swaggerJsDoc(swaggerOptions); app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocs)); // Routes app.use('/api/keywords', keywordRoutes); app.use('/api/serp', serpRoutes); app.use('/api/competitors', competitorRoutes); // Health check endpoint app.get('/health', (req, res) => { res.status(200).json({ status: 'ok', message: 'Server is running' }); }); // Start server app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); console.log(`API Documentation available at http://localhost:${PORT}/api-docs`); }); export default app; ``` -------------------------------------------------------------------------------- /src/controllers/keywordController.js: -------------------------------------------------------------------------------- ```javascript import keywordPlannerService from '../services/keywordPlannerService.js'; /** * Generate keyword ideas based on a seed keyword * @param {Object} req - Express request object * @param {Object} res - Express response object */ export const generateKeywordIdeas = async (req, res) => { try { const { keyword, language, locations, limit } = req.query; if (!keyword) { return res.status(400).json({ error: 'Keyword is required' }); } // Parse locations if provided const parsedLocations = locations ? JSON.parse(locations) : [2250]; // Default to US const parsedLimit = limit ? parseInt(limit) : 50; const keywordIdeas = await keywordPlannerService.generateKeywordIdeas( keyword, language || 'en', parsedLocations, parsedLimit ); res.status(200).json({ success: true, data: { seedKeyword: keyword, keywordIdeas, count: keywordIdeas.length, }, }); } catch (error) { console.error('Error in generateKeywordIdeas controller:', error); res.status(500).json({ success: false, error: error.message || 'Failed to generate keyword ideas', }); } }; /** * Get keyword metrics for a list of keywords * @param {Object} req - Express request object * @param {Object} res - Express response object */ export const getKeywordMetrics = async (req, res) => { try { const { keywords, language, locations } = req.body; if (!keywords || !Array.isArray(keywords) || keywords.length === 0) { return res.status(400).json({ error: 'Valid keywords array is required' }); } // Parse locations if provided const parsedLocations = locations || [2250]; // Default to US const keywordMetrics = await keywordPlannerService.getKeywordMetrics( keywords, language || 'en', parsedLocations ); res.status(200).json({ success: true, data: { keywords, keywordMetrics, count: keywordMetrics.length, }, }); } catch (error) { console.error('Error in getKeywordMetrics controller:', error); res.status(500).json({ success: false, error: error.message || 'Failed to get keyword metrics', }); } }; /** * Get historical metrics for keywords * @param {Object} req - Express request object * @param {Object} res - Express response object */ export const getHistoricalMetrics = async (req, res) => { try { const { keywords, language, locations } = req.body; if (!keywords || !Array.isArray(keywords) || keywords.length === 0) { return res.status(400).json({ error: 'Valid keywords array is required' }); } // Parse locations if provided const parsedLocations = locations || [2250]; // Default to US const historicalMetrics = await keywordPlannerService.getHistoricalMetrics( keywords, language || 'en', parsedLocations ); res.status(200).json({ success: true, data: { keywords, historicalMetrics, }, }); } catch (error) { console.error('Error in getHistoricalMetrics controller:', error); res.status(500).json({ success: false, error: error.message || 'Failed to get historical metrics', }); } }; export default { generateKeywordIdeas, getKeywordMetrics, getHistoricalMetrics, }; ``` -------------------------------------------------------------------------------- /src/routes/serpRoutes.js: -------------------------------------------------------------------------------- ```javascript import express from 'express'; const router = express.Router(); /** * @swagger * /api/serp/analyze: * get: * summary: Analyze a SERP (Search Engine Results Page) for a given query * tags: [SERP] * parameters: * - in: query * name: query * schema: * type: string * required: true * description: Search query to analyze * - in: query * name: location * schema: * type: string * description: Location for the search (e.g., 'United States') * - in: query * name: language * schema: * type: string * description: Language code (e.g., 'en') * responses: * 200: * description: Successful operation * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * data: * type: object * 400: * description: Bad request * 500: * description: Server error */ router.get('/analyze', (req, res) => { // This is a placeholder for the SERP analysis functionality // In a real implementation, this would call a service to analyze SERP data const { query, location, language } = req.query; if (!query) { return res.status(400).json({ error: 'Search query is required' }); } // Mock response for now res.status(200).json({ success: true, data: { query, location: location || 'United States', language: language || 'en', results: [ { position: 1, title: 'Example Result 1', url: 'https://example.com/1', description: 'This is an example search result description.', features: ['Featured Snippet', 'Site Links'] }, { position: 2, title: 'Example Result 2', url: 'https://example.com/2', description: 'Another example search result description.', features: [] } ], features: { featuredSnippet: true, peopleAlsoAsk: true, localPack: false, imageCarousel: false }, stats: { totalResults: 2, paidResults: 0, organicResults: 2 } } }); }); /** * @swagger * /api/serp/features: * get: * summary: Get SERP features for a given query * tags: [SERP] * parameters: * - in: query * name: query * schema: * type: string * required: true * description: Search query to analyze * - in: query * name: location * schema: * type: string * description: Location for the search * responses: * 200: * description: Successful operation * 400: * description: Bad request * 500: * description: Server error */ router.get('/features', (req, res) => { // This is a placeholder for the SERP features functionality const { query, location } = req.query; if (!query) { return res.status(400).json({ error: 'Search query is required' }); } // Mock response for now res.status(200).json({ success: true, data: { query, location: location || 'United States', features: { featuredSnippet: true, peopleAlsoAsk: true, knowledgePanel: false, localPack: false, imageCarousel: true, videoCarousel: false, topStories: false, relatedSearches: true } } }); }); export default router; ``` -------------------------------------------------------------------------------- /src/routes/keywordRoutes.js: -------------------------------------------------------------------------------- ```javascript import express from 'express'; import keywordController from '../controllers/keywordController.js'; const router = express.Router(); /** * @swagger * /api/keywords/ideas: * get: * summary: Generate keyword ideas based on a seed keyword * tags: [Keywords] * parameters: * - in: query * name: keyword * schema: * type: string * required: true * description: Seed keyword to generate ideas from * - in: query * name: language * schema: * type: string * description: Language code (e.g., 'en', 'fr', 'es') * - in: query * name: locations * schema: * type: string * description: JSON array of location IDs (e.g., '[2250]' for US) * - in: query * name: limit * schema: * type: integer * description: Maximum number of keyword ideas to return * responses: * 200: * description: Successful operation * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * data: * type: object * 400: * description: Bad request * 500: * description: Server error */ router.get('/ideas', keywordController.generateKeywordIdeas); /** * @swagger * /api/keywords/metrics: * post: * summary: Get keyword metrics for a list of keywords * tags: [Keywords] * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - keywords * properties: * keywords: * type: array * items: * type: string * language: * type: string * locations: * type: array * items: * type: integer * responses: * 200: * description: Successful operation * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * data: * type: object * 400: * description: Bad request * 500: * description: Server error */ router.post('/metrics', keywordController.getKeywordMetrics); /** * @swagger * /api/keywords/historical: * post: * summary: Get historical metrics for keywords * tags: [Keywords] * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - keywords * properties: * keywords: * type: array * items: * type: string * language: * type: string * locations: * type: array * items: * type: integer * responses: * 200: * description: Successful operation * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * data: * type: object * 400: * description: Bad request * 500: * description: Server error */ router.post('/historical', keywordController.getHistoricalMetrics); export default router; ``` -------------------------------------------------------------------------------- /src/routes/competitorRoutes.js: -------------------------------------------------------------------------------- ```javascript import express from 'express'; const router = express.Router(); /** * @swagger * /api/competitors/analyze: * get: * summary: Analyze competitors for a given keyword or domain * tags: [Competitors] * parameters: * - in: query * name: keyword * schema: * type: string * description: Keyword to analyze competitors for * - in: query * name: domain * schema: * type: string * description: Domain to analyze competitors for * - in: query * name: limit * schema: * type: integer * description: Maximum number of competitors to return * responses: * 200: * description: Successful operation * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * data: * type: object * 400: * description: Bad request * 500: * description: Server error */ router.get('/analyze', (req, res) => { // This is a placeholder for the competitor analysis functionality const { keyword, domain, limit } = req.query; if (!keyword && !domain) { return res.status(400).json({ error: 'Either keyword or domain is required' }); } const parsedLimit = limit ? parseInt(limit) : 10; // Mock response for now res.status(200).json({ success: true, data: { keyword: keyword || null, domain: domain || null, competitors: [ { domain: 'competitor1.com', title: 'Competitor 1 Website', position: 1, metrics: { domainAuthority: 85, pageAuthority: 76, backlinks: 15000, organicTraffic: 250000 }, content: { wordCount: 2500, headings: 12, images: 8, videos: 1 } }, { domain: 'competitor2.com', title: 'Competitor 2 Website', position: 2, metrics: { domainAuthority: 78, pageAuthority: 72, backlinks: 12000, organicTraffic: 180000 }, content: { wordCount: 1800, headings: 9, images: 6, videos: 0 } } ], count: 2, limit: parsedLimit } }); }); /** * @swagger * /api/competitors/backlinks: * get: * summary: Get backlink data for a competitor domain * tags: [Competitors] * parameters: * - in: query * name: domain * schema: * type: string * required: true * description: Domain to get backlink data for * - in: query * name: limit * schema: * type: integer * description: Maximum number of backlinks to return * responses: * 200: * description: Successful operation * 400: * description: Bad request * 500: * description: Server error */ router.get('/backlinks', (req, res) => { // This is a placeholder for the backlink analysis functionality const { domain, limit } = req.query; if (!domain) { return res.status(400).json({ error: 'Domain is required' }); } const parsedLimit = limit ? parseInt(limit) : 10; // Mock response for now res.status(200).json({ success: true, data: { domain, backlinks: [ { sourceDomain: 'example.com', sourceUrl: 'https://example.com/page1', targetUrl: `https://${domain}/target-page`, anchorText: 'Example anchor text', domainAuthority: 75, pageAuthority: 65, isDoFollow: true }, { sourceDomain: 'another-example.com', sourceUrl: 'https://another-example.com/page2', targetUrl: `https://${domain}/another-target`, anchorText: 'Another anchor text', domainAuthority: 68, pageAuthority: 60, isDoFollow: false } ], count: 2, limit: parsedLimit, totalBacklinks: 15000 } }); }); export default router; ``` -------------------------------------------------------------------------------- /scripts/get_refresh_token.js: -------------------------------------------------------------------------------- ```javascript import fs from 'fs'; import { OAuth2Client } from 'google-auth-library'; import http from 'http'; import url from 'url'; import open from 'open'; import dotenv from 'dotenv'; dotenv.config(); // Configuration const PORT = 8080; const SCOPES = ['https://www.googleapis.com/auth/adwords']; // Get client ID and client secret from environment variables const CLIENT_ID = process.env.GOOGLE_ADS_CLIENT_ID; const CLIENT_SECRET = process.env.GOOGLE_ADS_CLIENT_SECRET; if (!CLIENT_ID || !CLIENT_SECRET) { console.error('Error: GOOGLE_ADS_CLIENT_ID and GOOGLE_ADS_CLIENT_SECRET must be set in .env file'); process.exit(1); } // Create OAuth2 client const oauth2Client = new OAuth2Client( CLIENT_ID, CLIENT_SECRET, `http://localhost:${PORT}` ); // Generate authorization URL const authorizeUrl = oauth2Client.generateAuthUrl({ access_type: 'offline', scope: SCOPES, prompt: 'consent', // Force to get refresh token }); // Create HTTP server to handle the OAuth2 callback const server = http.createServer(async (req, res) => { try { // Parse the URL and get the code parameter const queryParams = url.parse(req.url, true).query; const code = queryParams.code; if (code) { // Exchange the authorization code for tokens const { tokens } = await oauth2Client.getToken(code); oauth2Client.setCredentials(tokens); // Display the refresh token console.log('\nRefresh Token:', tokens.refresh_token); console.log('\nAccess Token:', tokens.access_token); console.log('\nExpiry Date:', tokens.expiry_date); // Save the refresh token to .env file if (tokens.refresh_token) { try { let envContent = fs.readFileSync('.env', 'utf8'); // Check if GOOGLE_ADS_REFRESH_TOKEN already exists in .env if (envContent.includes('GOOGLE_ADS_REFRESH_TOKEN=')) { // Replace existing refresh token envContent = envContent.replace( /GOOGLE_ADS_REFRESH_TOKEN=.*/, `GOOGLE_ADS_REFRESH_TOKEN=${tokens.refresh_token}` ); } else { // Add new refresh token envContent += `\nGOOGLE_ADS_REFRESH_TOKEN=${tokens.refresh_token}`; } fs.writeFileSync('.env', envContent); console.log('\nRefresh token saved to .env file'); } catch (error) { console.error('Error saving refresh token to .env file:', error); } } else { console.warn('\nWarning: No refresh token received. Try again with prompt=consent parameter.'); } // Send success response to browser res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(` <html> <body> <h1>Authentication Successful</h1> <p>You have successfully authenticated with Google Ads API.</p> <p>The refresh token has been saved to your .env file.</p> <p>You can close this window now.</p> </body> </html> `); // Close the server after a short delay setTimeout(() => { server.close(); console.log('\nServer closed. You can close this terminal window.'); }, 2000); } else { // Handle error case res.writeHead(400, { 'Content-Type': 'text/html' }); res.end(` <html> <body> <h1>Authentication Failed</h1> <p>No authorization code received from Google.</p> <p>Please try again.</p> </body> </html> `); } } catch (error) { console.error('Error handling OAuth callback:', error); res.writeHead(500, { 'Content-Type': 'text/html' }); res.end(` <html> <body> <h1>Authentication Error</h1> <p>An error occurred during authentication: ${error.message}</p> </body> </html> `); } }); // Start the server server.listen(PORT, () => { console.log(`\nOAuth2 callback server listening on http://localhost:${PORT}`); console.log('\nOpening browser to authorize application...'); // Open the authorization URL in the default browser open(authorizeUrl); console.log('\nIf the browser does not open automatically, please visit:'); console.log(authorizeUrl); }); // Handle server errors server.on('error', (error) => { console.error('Server error:', error); process.exit(1); }); ``` -------------------------------------------------------------------------------- /mcp.json: -------------------------------------------------------------------------------- ```json { "name": "SEO AI Tools", "description": "SEO automation and AI-powered optimization with Google Ads Keyword Planner integration", "version": "1.0.0", "tools": [ { "name": "research_keywords", "description": "Research keywords related to a given topic or seed keyword", "parameters": { "type": "object", "required": ["keyword"], "properties": { "keyword": { "type": "string", "description": "Seed keyword to generate ideas from" }, "language": { "type": "string", "description": "Language code (e.g., 'en', 'fr', 'es')" }, "locations": { "type": "array", "items": { "type": "integer" }, "description": "Array of location IDs (e.g., [2250] for US)" }, "include_questions": { "type": "boolean", "default": false, "description": "Whether to include question-based keywords" }, "include_related": { "type": "boolean", "default": false, "description": "Whether to include related keywords" }, "include_suggestions": { "type": "boolean", "default": false, "description": "Whether to include Google's keyword suggestions" } } }, "handler": { "type": "http", "method": "GET", "url": "/api/keywords/ideas", "query": { "keyword": "{keyword}", "language": "{language}", "locations": "{locations}", "include_questions": "{include_questions}", "include_related": "{include_related}", "include_suggestions": "{include_suggestions}" } } }, { "name": "analyze_serp", "description": "Analyze a SERP (Search Engine Results Page) for a given query", "parameters": { "type": "object", "required": ["query"], "properties": { "query": { "type": "string", "description": "Search query to analyze" }, "location": { "type": "string", "description": "Location for the search (e.g., 'United States')" }, "language": { "type": "string", "description": "Language code (e.g., 'en')" }, "device": { "type": "string", "enum": ["desktop", "mobile"], "default": "desktop", "description": "Device type for the search" }, "num": { "type": "number", "default": 10, "minimum": 1, "maximum": 100, "description": "Number of results to return" } } }, "handler": { "type": "http", "method": "GET", "url": "/api/serp/analyze", "query": { "query": "{query}", "location": "{location}", "language": "{language}", "device": "{device}", "num": "{num}" } } }, { "name": "analyze_competitors", "description": "Analyze competitors for a given keyword or domain", "parameters": { "type": "object", "properties": { "keyword": { "type": "string", "description": "Keyword to analyze competitors for" }, "domain": { "type": "string", "description": "Domain to analyze competitors for" }, "include_features": { "type": "boolean", "default": false, "description": "Whether to include detailed features in the analysis" }, "num_results": { "type": "number", "minimum": 1, "maximum": 100, "default": 10, "description": "Number of competitor results to return" } } }, "handler": { "type": "http", "method": "GET", "url": "/api/competitors/analyze", "query": { "keyword": "{keyword}", "domain": "{domain}", "include_features": "{include_features}", "limit": "{num_results}" } } }, { "name": "_health", "description": "Health check endpoint", "parameters": { "type": "object", "properties": { "random_string": { "type": "string", "description": "Dummy parameter for no-parameter tools" } }, "required": ["random_string"] }, "handler": { "type": "http", "method": "GET", "url": "/health" } } ] } ``` -------------------------------------------------------------------------------- /src/services/keywordPlannerService.js: -------------------------------------------------------------------------------- ```javascript import { GoogleAdsApi } from 'google-ads-api'; import googleAdsConfig from '../config/googleAdsConfig.js'; // Initialize Google Ads API client const client = new GoogleAdsApi({ client_id: googleAdsConfig.client_id, client_secret: googleAdsConfig.client_secret, developer_token: googleAdsConfig.developer_token, refresh_token: googleAdsConfig.refresh_token, }); // Get customer instance const customer = client.Customer({ customer_id: googleAdsConfig.login_customer_id, }); /** * Generate keyword ideas based on a seed keyword * @param {string} keyword - Seed keyword * @param {string} language - Language code (e.g., 'en') * @param {Array} locations - Array of location IDs * @param {number} limit - Maximum number of keyword ideas to return * @returns {Promise<Array>} - Array of keyword ideas with metrics */ export const generateKeywordIdeas = async (keyword, language = 'en', locations = [2250], limit = 50) => { try { // Create keyword text list const keywordTexts = [keyword]; // Generate keyword ideas const keywordIdeas = await customer.keywordPlanner.generateKeywordIdeas({ keywordTexts, language, geoTargetConstants: locations, keywordPlanNetwork: 'GOOGLE_SEARCH_AND_PARTNERS', }); // Process and format the results const formattedResults = keywordIdeas.slice(0, limit).map((idea) => { return { keyword: idea.text, avgMonthlySearches: idea.keywordIdeaMetrics?.avgMonthlySearches || 0, competition: idea.keywordIdeaMetrics?.competition || 'UNKNOWN', competitionIndex: idea.keywordIdeaMetrics?.competitionIndex || 0, lowTopOfPageBidMicros: idea.keywordIdeaMetrics?.lowTopOfPageBidMicros ? parseFloat(idea.keywordIdeaMetrics.lowTopOfPageBidMicros) / 1000000 : 0, highTopOfPageBidMicros: idea.keywordIdeaMetrics?.highTopOfPageBidMicros ? parseFloat(idea.keywordIdeaMetrics.highTopOfPageBidMicros) / 1000000 : 0, }; }); return formattedResults; } catch (error) { console.error('Error generating keyword ideas:', error); throw new Error(`Failed to generate keyword ideas: ${error.message}`); } }; /** * Get keyword volume and metrics for a list of keywords * @param {Array} keywords - Array of keywords to get metrics for * @param {string} language - Language code (e.g., 'en') * @param {Array} locations - Array of location IDs * @returns {Promise<Array>} - Array of keywords with metrics */ export const getKeywordMetrics = async (keywords, language = 'en', locations = [2250]) => { try { // Generate keyword ideas for the provided keywords const keywordIdeas = await customer.keywordPlanner.generateKeywordIdeas({ keywordTexts: keywords, language, geoTargetConstants: locations, keywordPlanNetwork: 'GOOGLE_SEARCH_AND_PARTNERS', }); // Process and format the results const formattedResults = keywordIdeas.map((idea) => { return { keyword: idea.text, avgMonthlySearches: idea.keywordIdeaMetrics?.avgMonthlySearches || 0, competition: idea.keywordIdeaMetrics?.competition || 'UNKNOWN', competitionIndex: idea.keywordIdeaMetrics?.competitionIndex || 0, lowTopOfPageBidMicros: idea.keywordIdeaMetrics?.lowTopOfPageBidMicros ? parseFloat(idea.keywordIdeaMetrics.lowTopOfPageBidMicros) / 1000000 : 0, highTopOfPageBidMicros: idea.keywordIdeaMetrics?.highTopOfPageBidMicros ? parseFloat(idea.keywordIdeaMetrics.highTopOfPageBidMicros) / 1000000 : 0, }; }); return formattedResults; } catch (error) { console.error('Error getting keyword metrics:', error); throw new Error(`Failed to get keyword metrics: ${error.message}`); } }; /** * Get historical metrics for keywords * @param {Array} keywords - Array of keywords to get historical metrics for * @param {string} language - Language code (e.g., 'en') * @param {Array} locations - Array of location IDs * @returns {Promise<Array>} - Array of keywords with historical metrics */ export const getHistoricalMetrics = async (keywords, language = 'en', locations = [2250]) => { try { // Create a keyword plan const keywordPlan = await customer.keywordPlans.create({ name: `Keyword Plan ${Date.now()}`, }); // Create a keyword plan campaign const keywordPlanCampaign = await customer.keywordPlanCampaigns.create({ keywordPlan: keywordPlan.resource_name, name: 'Keyword Plan Campaign', cpcBidMicros: 1000000, // $1.00 geoTargets: locations.map(locationId => ({ geoTargetConstant: `geoTargetConstants/${locationId}`, })), languageConstant: `languageConstants/${language}`, }); // Create a keyword plan ad group const keywordPlanAdGroup = await customer.keywordPlanAdGroups.create({ keywordPlanCampaign: keywordPlanCampaign.resource_name, name: 'Keyword Plan Ad Group', cpcBidMicros: 1000000, // $1.00 }); // Create keyword plan keywords await Promise.all( keywords.map(keyword => customer.keywordPlanKeywords.create({ keywordPlanAdGroup: keywordPlanAdGroup.resource_name, text: keyword, cpcBidMicros: 1000000, // $1.00 }) ) ); // Generate forecast metrics const forecastMetrics = await customer.keywordPlans.generateForecastMetrics({ keywordPlan: keywordPlan.resource_name, }); // Clean up by removing the keyword plan await customer.keywordPlans.delete({ resource_name: keywordPlan.resource_name, }); return forecastMetrics; } catch (error) { console.error('Error getting historical metrics:', error); throw new Error(`Failed to get historical metrics: ${error.message}`); } }; export default { generateKeywordIdeas, getKeywordMetrics, getHistoricalMetrics, }; ```