#
tokens: 14656/50000 21/21 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | };
```