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