#
tokens: 10238/50000 21/21 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .dockerignore
├── .env
├── .env.example
├── .gitignore
├── docker-compose.yml
├── Dockerfile
├── mcp.json
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   └── get_refresh_token.js
├── smithery.json
├── smithery.yaml
└── src
    ├── config
    │   └── googleAdsConfig.js
    ├── controllers
    │   └── keywordController.js
    ├── index.js
    ├── mcp_fixed.js
    ├── mcp.js
    ├── routes
    │   ├── competitorRoutes.js
    │   ├── keywordRoutes.js
    │   └── serpRoutes.js
    └── services
        └── keywordPlannerService.js
```

# Files

--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------

```
node_modules
npm-debug.log
.git
.gitignore
.env
.env.*
!.env.example
.DS_Store
README.md
.vscode
.idea
*.log
logs
```

--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------

```
# Server configuration
PORT=3000
HOST=0.0.0.0
NODE_ENV=production

# Google Ads API configuration
GOOGLE_ADS_DEVELOPER_TOKEN=6mEoXl5C5vR0oxlHDhUFQQ
GOOGLE_ADS_CLIENT_ID=your-client-id
GOOGLE_ADS_CLIENT_SECRET=your-client-secret
GOOGLE_ADS_REFRESH_TOKEN=your-refresh-token
GOOGLE_ADS_LOGIN_CUSTOMER_ID=your-customer-id
```

--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------

```
# Server Configuration
PORT=3000
NODE_ENV=development

# Google Ads API Configuration
GOOGLE_ADS_DEVELOPER_TOKEN=6mEoXl5C5vR0oxlHDhUFQQ
GOOGLE_ADS_CLIENT_ID=your_client_id_here
GOOGLE_ADS_CLIENT_SECRET=your_client_secret_here
GOOGLE_ADS_REFRESH_TOKEN=your_refresh_token_here
GOOGLE_ADS_LOGIN_CUSTOMER_ID=your_customer_id_without_dashes

# SERP API Configuration
SERP_API_KEY=your_serp_api_key_here
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Dependency directories
node_modules/

# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# Debug logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Build directories
dist/
build/

# OS specific files
.DS_Store
Thumbs.db

# Editor directories and files
.idea/
.vscode/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

# Google Ads API credentials
google-ads.yaml
client_secret.json
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# App SEO AI

Application for SEO automation and AI-powered optimization with Google Ads Keyword Planner integration.

## Features

- Keyword research using Google Ads API
- SERP analysis
- Competitor analysis
- SEO recommendations
- MCP (Model Context Protocol) integration for AI assistants

## Prerequisites

- Node.js (v14 or higher)
- npm or yarn
- Google Ads account with API access
- Google Cloud Platform project with Google Ads API enabled

## Setup

### 1. Clone the repository

```bash
git clone https://github.com/ccnn2509/app-seo-ai.git
cd app-seo-ai
```

### 2. Install dependencies

```bash
npm install
```

### 3. Configure environment variables

Copy the example environment file:

```bash
cp .env.example .env
```

Edit the `.env` file and fill in your Google Ads API credentials:

```
# Server Configuration
PORT=3000
NODE_ENV=development

# Google Ads API Configuration
GOOGLE_ADS_DEVELOPER_TOKEN=your_developer_token
GOOGLE_ADS_CLIENT_ID=your_client_id
GOOGLE_ADS_CLIENT_SECRET=your_client_secret
GOOGLE_ADS_REFRESH_TOKEN=your_refresh_token
GOOGLE_ADS_LOGIN_CUSTOMER_ID=your_customer_id_without_dashes

# SERP API Configuration (optional)
SERP_API_KEY=your_serp_api_key
```

### 4. Get Google Ads API refresh token

Run the following command to get a refresh token:

```bash
npm run get-token
```

This will open your browser and guide you through the OAuth2 authentication process. The refresh token will be automatically saved to your `.env` file.

### 5. Start the server

For development:

```bash
npm run dev
```

For production:

```bash
npm start
```

The server will start on the port specified in your `.env` file (default: 3000).

## API Documentation

API documentation is available at `/api-docs` when the server is running:

```
http://localhost:3000/api-docs
```

## MCP Integration

This project includes MCP (Model Context Protocol) integration, allowing AI assistants to use the API. The MCP configuration is in the `mcp.json` file.

To use this with Smithery:

1. Go to [Smithery](https://smithery.ai/)
2. Create a new MCP server
3. Select the `app-seo-ai` repository
4. Configure the server settings
5. Deploy the server

## Available MCP Tools

- `research_keywords` - Research keywords related to a given topic or seed keyword
- `analyze_serp` - Analyze a SERP (Search Engine Results Page) for a given query
- `analyze_competitors` - Analyze competitors for a given keyword or domain
- `_health` - Health check endpoint

## Example Usage

### Research Keywords

```javascript
// Example request to research keywords
fetch('http://localhost:3000/api/keywords/ideas?keyword=seo%20tools&language=en')
  .then(response => response.json())
  .then(data => console.log(data));
```

### Analyze SERP

```javascript
// Example request to analyze SERP
fetch('http://localhost:3000/api/serp/analyze?query=best%20seo%20tools&location=United%20States')
  .then(response => response.json())
  .then(data => console.log(data));
```

### Analyze Competitors

```javascript
// Example request to analyze competitors
fetch('http://localhost:3000/api/competitors/analyze?domain=example.com')
  .then(response => response.json())
  .then(data => console.log(data));
```

## License

MIT
```

--------------------------------------------------------------------------------
/src/mcp_fixed.js:
--------------------------------------------------------------------------------

```javascript
// This file is a redirect to mcp.js
// It exists because Smithery is looking for it according to the error message

import app from './mcp.js';

export default app;
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
FROM node:18-alpine

WORKDIR /app

# Copy package.json and package-lock.json
COPY package*.json ./

# Install dependencies
RUN npm install --only=production

# Copy application code
COPY . .

# Expose the port the app runs on
EXPOSE 3000

# The command will be provided by smithery.yaml
# This is just a default command for local testing
CMD ["node", "src/mcp.js", "--mcp"]
```

--------------------------------------------------------------------------------
/src/config/googleAdsConfig.js:
--------------------------------------------------------------------------------

```javascript
import dotenv from 'dotenv';

dotenv.config();

// Google Ads API configuration
const googleAdsConfig = {
  developer_token: process.env.GOOGLE_ADS_DEVELOPER_TOKEN,
  client_id: process.env.GOOGLE_ADS_CLIENT_ID,
  client_secret: process.env.GOOGLE_ADS_CLIENT_SECRET,
  refresh_token: process.env.GOOGLE_ADS_REFRESH_TOKEN,
  login_customer_id: process.env.GOOGLE_ADS_LOGIN_CUSTOMER_ID,
  use_proto_plus: true,
};

export default googleAdsConfig;
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
# Smithery.ai configuration
startCommand:
  type: stdio
  configSchema:
    # JSON Schema defining the configuration options for the MCP.
    {}
  commandFunction: |-
    (config) => ({
      "command": "node",
      "args": [
        "src/mcp.js",
        "--mcp"
      ],
      "env": {
        "PORT": "3000",
        "HOST": "0.0.0.0",
        "NODE_ENV": "production",
        "GOOGLE_ADS_DEVELOPER_TOKEN": "6mEoXl5C5vR0oxlHDhUFQQ"
      }
    })

# Build configuration
build:
  dockerfile: Dockerfile
  dockerBuildPath: .
```

--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------

```yaml
version: '3'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - PORT=3000
      - NODE_ENV=production
      - GOOGLE_ADS_DEVELOPER_TOKEN=${GOOGLE_ADS_DEVELOPER_TOKEN}
      - GOOGLE_ADS_CLIENT_ID=${GOOGLE_ADS_CLIENT_ID}
      - GOOGLE_ADS_CLIENT_SECRET=${GOOGLE_ADS_CLIENT_SECRET}
      - GOOGLE_ADS_REFRESH_TOKEN=${GOOGLE_ADS_REFRESH_TOKEN}
      - GOOGLE_ADS_LOGIN_CUSTOMER_ID=${GOOGLE_ADS_LOGIN_CUSTOMER_ID}
      - SERP_API_KEY=${SERP_API_KEY}
    volumes:
      - ./logs:/app/logs
    restart: unless-stopped
```

--------------------------------------------------------------------------------
/smithery.json:
--------------------------------------------------------------------------------

```json
{
  "name": "SEO AI Tools",
  "description": "SEO automation and AI-powered optimization with Google Ads Keyword Planner integration",
  "version": "1.0.0",
  "repository": "https://github.com/ccnn2509/app-seo-ai",
  "main": "mcp.json",
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js"
  },
  "env": {
    "PORT": "3000",
    "NODE_ENV": "production",
    "GOOGLE_ADS_DEVELOPER_TOKEN": "6mEoXl5C5vR0oxlHDhUFQQ"
  },
  "buildCommand": "npm install",
  "startCommand": "npm start",
  "healthCheckPath": "/health",
  "resources": {
    "cpu": 1,
    "memory": "512Mi"
  }
}
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "app-seo-ai",
  "version": "1.0.0",
  "main": "src/index.js",
  "type": "module",
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js",
    "mcp": "node src/mcp.js --mcp",
    "get-token": "node scripts/get_refresh_token.js"
  },
  "keywords": [
    "seo",
    "ai",
    "google-ads",
    "keyword-planner",
    "serp"
  ],
  "author": "",
  "license": "ISC",
  "description": "SEO automation and AI-powered optimization with Google Ads Keyword Planner integration",
  "dependencies": {
    "cors": "^2.8.5",
    "dotenv": "^16.4.7",
    "express": "^4.21.2",
    "google-ads-api": "^17.1.0-rest-beta",
    "google-auth-library": "^9.15.1",
    "open": "^10.1.0",
    "swagger-jsdoc": "^6.2.8",
    "swagger-ui-express": "^5.0.1"
  },
  "devDependencies": {
    "nodemon": "^3.1.9"
  }
}
```

--------------------------------------------------------------------------------
/src/mcp.js:
--------------------------------------------------------------------------------

```javascript
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import keywordRoutes from './routes/keywordRoutes.js';
import serpRoutes from './routes/serpRoutes.js';
import competitorRoutes from './routes/competitorRoutes.js';

// Load environment variables
dotenv.config();

// Initialize express app
const app = express();
const PORT = process.env.PORT || 3000;
const HOST = process.env.HOST || '0.0.0.0'; // Listen on all network interfaces

// Middleware
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Routes
app.use('/api/keywords', keywordRoutes);
app.use('/api/serp', serpRoutes);
app.use('/api/competitors', competitorRoutes);

// Health check endpoint
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'ok', message: 'MCP Server is running' });
});

// Start server
app.listen(PORT, HOST, () => {
  console.log(`MCP Server running on ${HOST}:${PORT}`);
  console.log(`Health check available at http://${HOST}:${PORT}/health`);
});

export default app;
```

--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------

```javascript
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import swaggerJsDoc from 'swagger-jsdoc';
import swaggerUi from 'swagger-ui-express';

// Import routes
import keywordRoutes from './routes/keywordRoutes.js';
import serpRoutes from './routes/serpRoutes.js';
import competitorRoutes from './routes/competitorRoutes.js';

// Load environment variables
dotenv.config();

// Initialize express app
const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Swagger configuration
const swaggerOptions = {
  definition: {
    openapi: '3.0.0',
    info: {
      title: 'App SEO AI API',
      version: '1.0.0',
      description: 'API for SEO automation and AI-powered optimization',
    },
    servers: [
      {
        url: `http://localhost:${PORT}`,
        description: 'Development server',
      },
    ],
  },
  apis: ['./src/routes/*.js'],
};

const swaggerDocs = swaggerJsDoc(swaggerOptions);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocs));

// Routes
app.use('/api/keywords', keywordRoutes);
app.use('/api/serp', serpRoutes);
app.use('/api/competitors', competitorRoutes);

// Health check endpoint
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'ok', message: 'Server is running' });
});

// Start server
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
  console.log(`API Documentation available at http://localhost:${PORT}/api-docs`);
});

export default app;
```

--------------------------------------------------------------------------------
/src/controllers/keywordController.js:
--------------------------------------------------------------------------------

```javascript
import keywordPlannerService from '../services/keywordPlannerService.js';

/**
 * Generate keyword ideas based on a seed keyword
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 */
export const generateKeywordIdeas = async (req, res) => {
  try {
    const { keyword, language, locations, limit } = req.query;
    
    if (!keyword) {
      return res.status(400).json({ error: 'Keyword is required' });
    }

    // Parse locations if provided
    const parsedLocations = locations ? JSON.parse(locations) : [2250]; // Default to US
    const parsedLimit = limit ? parseInt(limit) : 50;
    
    const keywordIdeas = await keywordPlannerService.generateKeywordIdeas(
      keyword,
      language || 'en',
      parsedLocations,
      parsedLimit
    );
    
    res.status(200).json({
      success: true,
      data: {
        seedKeyword: keyword,
        keywordIdeas,
        count: keywordIdeas.length,
      },
    });
  } catch (error) {
    console.error('Error in generateKeywordIdeas controller:', error);
    res.status(500).json({
      success: false,
      error: error.message || 'Failed to generate keyword ideas',
    });
  }
};

/**
 * Get keyword metrics for a list of keywords
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 */
export const getKeywordMetrics = async (req, res) => {
  try {
    const { keywords, language, locations } = req.body;
    
    if (!keywords || !Array.isArray(keywords) || keywords.length === 0) {
      return res.status(400).json({ error: 'Valid keywords array is required' });
    }

    // Parse locations if provided
    const parsedLocations = locations || [2250]; // Default to US
    
    const keywordMetrics = await keywordPlannerService.getKeywordMetrics(
      keywords,
      language || 'en',
      parsedLocations
    );
    
    res.status(200).json({
      success: true,
      data: {
        keywords,
        keywordMetrics,
        count: keywordMetrics.length,
      },
    });
  } catch (error) {
    console.error('Error in getKeywordMetrics controller:', error);
    res.status(500).json({
      success: false,
      error: error.message || 'Failed to get keyword metrics',
    });
  }
};

/**
 * Get historical metrics for keywords
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 */
export const getHistoricalMetrics = async (req, res) => {
  try {
    const { keywords, language, locations } = req.body;
    
    if (!keywords || !Array.isArray(keywords) || keywords.length === 0) {
      return res.status(400).json({ error: 'Valid keywords array is required' });
    }

    // Parse locations if provided
    const parsedLocations = locations || [2250]; // Default to US
    
    const historicalMetrics = await keywordPlannerService.getHistoricalMetrics(
      keywords,
      language || 'en',
      parsedLocations
    );
    
    res.status(200).json({
      success: true,
      data: {
        keywords,
        historicalMetrics,
      },
    });
  } catch (error) {
    console.error('Error in getHistoricalMetrics controller:', error);
    res.status(500).json({
      success: false,
      error: error.message || 'Failed to get historical metrics',
    });
  }
};

export default {
  generateKeywordIdeas,
  getKeywordMetrics,
  getHistoricalMetrics,
};
```

--------------------------------------------------------------------------------
/src/routes/serpRoutes.js:
--------------------------------------------------------------------------------

```javascript
import express from 'express';

const router = express.Router();

/**
 * @swagger
 * /api/serp/analyze:
 *   get:
 *     summary: Analyze a SERP (Search Engine Results Page) for a given query
 *     tags: [SERP]
 *     parameters:
 *       - in: query
 *         name: query
 *         schema:
 *           type: string
 *         required: true
 *         description: Search query to analyze
 *       - in: query
 *         name: location
 *         schema:
 *           type: string
 *         description: Location for the search (e.g., 'United States')
 *       - in: query
 *         name: language
 *         schema:
 *           type: string
 *         description: Language code (e.g., 'en')
 *     responses:
 *       200:
 *         description: Successful operation
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                 data:
 *                   type: object
 *       400:
 *         description: Bad request
 *       500:
 *         description: Server error
 */
router.get('/analyze', (req, res) => {
  // This is a placeholder for the SERP analysis functionality
  // In a real implementation, this would call a service to analyze SERP data
  
  const { query, location, language } = req.query;
  
  if (!query) {
    return res.status(400).json({ error: 'Search query is required' });
  }
  
  // Mock response for now
  res.status(200).json({
    success: true,
    data: {
      query,
      location: location || 'United States',
      language: language || 'en',
      results: [
        {
          position: 1,
          title: 'Example Result 1',
          url: 'https://example.com/1',
          description: 'This is an example search result description.',
          features: ['Featured Snippet', 'Site Links']
        },
        {
          position: 2,
          title: 'Example Result 2',
          url: 'https://example.com/2',
          description: 'Another example search result description.',
          features: []
        }
      ],
      features: {
        featuredSnippet: true,
        peopleAlsoAsk: true,
        localPack: false,
        imageCarousel: false
      },
      stats: {
        totalResults: 2,
        paidResults: 0,
        organicResults: 2
      }
    }
  });
});

/**
 * @swagger
 * /api/serp/features:
 *   get:
 *     summary: Get SERP features for a given query
 *     tags: [SERP]
 *     parameters:
 *       - in: query
 *         name: query
 *         schema:
 *           type: string
 *         required: true
 *         description: Search query to analyze
 *       - in: query
 *         name: location
 *         schema:
 *           type: string
 *         description: Location for the search
 *     responses:
 *       200:
 *         description: Successful operation
 *       400:
 *         description: Bad request
 *       500:
 *         description: Server error
 */
router.get('/features', (req, res) => {
  // This is a placeholder for the SERP features functionality
  
  const { query, location } = req.query;
  
  if (!query) {
    return res.status(400).json({ error: 'Search query is required' });
  }
  
  // Mock response for now
  res.status(200).json({
    success: true,
    data: {
      query,
      location: location || 'United States',
      features: {
        featuredSnippet: true,
        peopleAlsoAsk: true,
        knowledgePanel: false,
        localPack: false,
        imageCarousel: true,
        videoCarousel: false,
        topStories: false,
        relatedSearches: true
      }
    }
  });
});

export default router;
```

--------------------------------------------------------------------------------
/src/routes/keywordRoutes.js:
--------------------------------------------------------------------------------

```javascript
import express from 'express';
import keywordController from '../controllers/keywordController.js';

const router = express.Router();

/**
 * @swagger
 * /api/keywords/ideas:
 *   get:
 *     summary: Generate keyword ideas based on a seed keyword
 *     tags: [Keywords]
 *     parameters:
 *       - in: query
 *         name: keyword
 *         schema:
 *           type: string
 *         required: true
 *         description: Seed keyword to generate ideas from
 *       - in: query
 *         name: language
 *         schema:
 *           type: string
 *         description: Language code (e.g., 'en', 'fr', 'es')
 *       - in: query
 *         name: locations
 *         schema:
 *           type: string
 *         description: JSON array of location IDs (e.g., '[2250]' for US)
 *       - in: query
 *         name: limit
 *         schema:
 *           type: integer
 *         description: Maximum number of keyword ideas to return
 *     responses:
 *       200:
 *         description: Successful operation
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                 data:
 *                   type: object
 *       400:
 *         description: Bad request
 *       500:
 *         description: Server error
 */
router.get('/ideas', keywordController.generateKeywordIdeas);

/**
 * @swagger
 * /api/keywords/metrics:
 *   post:
 *     summary: Get keyword metrics for a list of keywords
 *     tags: [Keywords]
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             required:
 *               - keywords
 *             properties:
 *               keywords:
 *                 type: array
 *                 items:
 *                   type: string
 *               language:
 *                 type: string
 *               locations:
 *                 type: array
 *                 items:
 *                   type: integer
 *     responses:
 *       200:
 *         description: Successful operation
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                 data:
 *                   type: object
 *       400:
 *         description: Bad request
 *       500:
 *         description: Server error
 */
router.post('/metrics', keywordController.getKeywordMetrics);

/**
 * @swagger
 * /api/keywords/historical:
 *   post:
 *     summary: Get historical metrics for keywords
 *     tags: [Keywords]
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             required:
 *               - keywords
 *             properties:
 *               keywords:
 *                 type: array
 *                 items:
 *                   type: string
 *               language:
 *                 type: string
 *               locations:
 *                 type: array
 *                 items:
 *                   type: integer
 *     responses:
 *       200:
 *         description: Successful operation
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                 data:
 *                   type: object
 *       400:
 *         description: Bad request
 *       500:
 *         description: Server error
 */
router.post('/historical', keywordController.getHistoricalMetrics);

export default router;
```

--------------------------------------------------------------------------------
/src/routes/competitorRoutes.js:
--------------------------------------------------------------------------------

```javascript
import express from 'express';

const router = express.Router();

/**
 * @swagger
 * /api/competitors/analyze:
 *   get:
 *     summary: Analyze competitors for a given keyword or domain
 *     tags: [Competitors]
 *     parameters:
 *       - in: query
 *         name: keyword
 *         schema:
 *           type: string
 *         description: Keyword to analyze competitors for
 *       - in: query
 *         name: domain
 *         schema:
 *           type: string
 *         description: Domain to analyze competitors for
 *       - in: query
 *         name: limit
 *         schema:
 *           type: integer
 *         description: Maximum number of competitors to return
 *     responses:
 *       200:
 *         description: Successful operation
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                 data:
 *                   type: object
 *       400:
 *         description: Bad request
 *       500:
 *         description: Server error
 */
router.get('/analyze', (req, res) => {
  // This is a placeholder for the competitor analysis functionality
  
  const { keyword, domain, limit } = req.query;
  
  if (!keyword && !domain) {
    return res.status(400).json({ error: 'Either keyword or domain is required' });
  }
  
  const parsedLimit = limit ? parseInt(limit) : 10;
  
  // Mock response for now
  res.status(200).json({
    success: true,
    data: {
      keyword: keyword || null,
      domain: domain || null,
      competitors: [
        {
          domain: 'competitor1.com',
          title: 'Competitor 1 Website',
          position: 1,
          metrics: {
            domainAuthority: 85,
            pageAuthority: 76,
            backlinks: 15000,
            organicTraffic: 250000
          },
          content: {
            wordCount: 2500,
            headings: 12,
            images: 8,
            videos: 1
          }
        },
        {
          domain: 'competitor2.com',
          title: 'Competitor 2 Website',
          position: 2,
          metrics: {
            domainAuthority: 78,
            pageAuthority: 72,
            backlinks: 12000,
            organicTraffic: 180000
          },
          content: {
            wordCount: 1800,
            headings: 9,
            images: 6,
            videos: 0
          }
        }
      ],
      count: 2,
      limit: parsedLimit
    }
  });
});

/**
 * @swagger
 * /api/competitors/backlinks:
 *   get:
 *     summary: Get backlink data for a competitor domain
 *     tags: [Competitors]
 *     parameters:
 *       - in: query
 *         name: domain
 *         schema:
 *           type: string
 *         required: true
 *         description: Domain to get backlink data for
 *       - in: query
 *         name: limit
 *         schema:
 *           type: integer
 *         description: Maximum number of backlinks to return
 *     responses:
 *       200:
 *         description: Successful operation
 *       400:
 *         description: Bad request
 *       500:
 *         description: Server error
 */
router.get('/backlinks', (req, res) => {
  // This is a placeholder for the backlink analysis functionality
  
  const { domain, limit } = req.query;
  
  if (!domain) {
    return res.status(400).json({ error: 'Domain is required' });
  }
  
  const parsedLimit = limit ? parseInt(limit) : 10;
  
  // Mock response for now
  res.status(200).json({
    success: true,
    data: {
      domain,
      backlinks: [
        {
          sourceDomain: 'example.com',
          sourceUrl: 'https://example.com/page1',
          targetUrl: `https://${domain}/target-page`,
          anchorText: 'Example anchor text',
          domainAuthority: 75,
          pageAuthority: 65,
          isDoFollow: true
        },
        {
          sourceDomain: 'another-example.com',
          sourceUrl: 'https://another-example.com/page2',
          targetUrl: `https://${domain}/another-target`,
          anchorText: 'Another anchor text',
          domainAuthority: 68,
          pageAuthority: 60,
          isDoFollow: false
        }
      ],
      count: 2,
      limit: parsedLimit,
      totalBacklinks: 15000
    }
  });
});

export default router;
```

--------------------------------------------------------------------------------
/scripts/get_refresh_token.js:
--------------------------------------------------------------------------------

```javascript
import fs from 'fs';
import { OAuth2Client } from 'google-auth-library';
import http from 'http';
import url from 'url';
import open from 'open';
import dotenv from 'dotenv';

dotenv.config();

// Configuration
const PORT = 8080;
const SCOPES = ['https://www.googleapis.com/auth/adwords'];

// Get client ID and client secret from environment variables
const CLIENT_ID = process.env.GOOGLE_ADS_CLIENT_ID;
const CLIENT_SECRET = process.env.GOOGLE_ADS_CLIENT_SECRET;

if (!CLIENT_ID || !CLIENT_SECRET) {
  console.error('Error: GOOGLE_ADS_CLIENT_ID and GOOGLE_ADS_CLIENT_SECRET must be set in .env file');
  process.exit(1);
}

// Create OAuth2 client
const oauth2Client = new OAuth2Client(
  CLIENT_ID,
  CLIENT_SECRET,
  `http://localhost:${PORT}`
);

// Generate authorization URL
const authorizeUrl = oauth2Client.generateAuthUrl({
  access_type: 'offline',
  scope: SCOPES,
  prompt: 'consent', // Force to get refresh token
});

// Create HTTP server to handle the OAuth2 callback
const server = http.createServer(async (req, res) => {
  try {
    // Parse the URL and get the code parameter
    const queryParams = url.parse(req.url, true).query;
    const code = queryParams.code;

    if (code) {
      // Exchange the authorization code for tokens
      const { tokens } = await oauth2Client.getToken(code);
      oauth2Client.setCredentials(tokens);

      // Display the refresh token
      console.log('\nRefresh Token:', tokens.refresh_token);
      console.log('\nAccess Token:', tokens.access_token);
      console.log('\nExpiry Date:', tokens.expiry_date);

      // Save the refresh token to .env file
      if (tokens.refresh_token) {
        try {
          let envContent = fs.readFileSync('.env', 'utf8');
          
          // Check if GOOGLE_ADS_REFRESH_TOKEN already exists in .env
          if (envContent.includes('GOOGLE_ADS_REFRESH_TOKEN=')) {
            // Replace existing refresh token
            envContent = envContent.replace(
              /GOOGLE_ADS_REFRESH_TOKEN=.*/,
              `GOOGLE_ADS_REFRESH_TOKEN=${tokens.refresh_token}`
            );
          } else {
            // Add new refresh token
            envContent += `\nGOOGLE_ADS_REFRESH_TOKEN=${tokens.refresh_token}`;
          }
          
          fs.writeFileSync('.env', envContent);
          console.log('\nRefresh token saved to .env file');
        } catch (error) {
          console.error('Error saving refresh token to .env file:', error);
        }
      } else {
        console.warn('\nWarning: No refresh token received. Try again with prompt=consent parameter.');
      }

      // Send success response to browser
      res.writeHead(200, { 'Content-Type': 'text/html' });
      res.end(`
        <html>
          <body>
            <h1>Authentication Successful</h1>
            <p>You have successfully authenticated with Google Ads API.</p>
            <p>The refresh token has been saved to your .env file.</p>
            <p>You can close this window now.</p>
          </body>
        </html>
      `);

      // Close the server after a short delay
      setTimeout(() => {
        server.close();
        console.log('\nServer closed. You can close this terminal window.');
      }, 2000);
    } else {
      // Handle error case
      res.writeHead(400, { 'Content-Type': 'text/html' });
      res.end(`
        <html>
          <body>
            <h1>Authentication Failed</h1>
            <p>No authorization code received from Google.</p>
            <p>Please try again.</p>
          </body>
        </html>
      `);
    }
  } catch (error) {
    console.error('Error handling OAuth callback:', error);
    res.writeHead(500, { 'Content-Type': 'text/html' });
    res.end(`
      <html>
        <body>
          <h1>Authentication Error</h1>
          <p>An error occurred during authentication: ${error.message}</p>
        </body>
      </html>
    `);
  }
});

// Start the server
server.listen(PORT, () => {
  console.log(`\nOAuth2 callback server listening on http://localhost:${PORT}`);
  console.log('\nOpening browser to authorize application...');
  
  // Open the authorization URL in the default browser
  open(authorizeUrl);
  
  console.log('\nIf the browser does not open automatically, please visit:');
  console.log(authorizeUrl);
});

// Handle server errors
server.on('error', (error) => {
  console.error('Server error:', error);
  process.exit(1);
});
```

--------------------------------------------------------------------------------
/mcp.json:
--------------------------------------------------------------------------------

```json
{
  "name": "SEO AI Tools",
  "description": "SEO automation and AI-powered optimization with Google Ads Keyword Planner integration",
  "version": "1.0.0",
  "tools": [
    {
      "name": "research_keywords",
      "description": "Research keywords related to a given topic or seed keyword",
      "parameters": {
        "type": "object",
        "required": ["keyword"],
        "properties": {
          "keyword": {
            "type": "string",
            "description": "Seed keyword to generate ideas from"
          },
          "language": {
            "type": "string",
            "description": "Language code (e.g., 'en', 'fr', 'es')"
          },
          "locations": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "Array of location IDs (e.g., [2250] for US)"
          },
          "include_questions": {
            "type": "boolean",
            "default": false,
            "description": "Whether to include question-based keywords"
          },
          "include_related": {
            "type": "boolean",
            "default": false,
            "description": "Whether to include related keywords"
          },
          "include_suggestions": {
            "type": "boolean",
            "default": false,
            "description": "Whether to include Google's keyword suggestions"
          }
        }
      },
      "handler": {
        "type": "http",
        "method": "GET",
        "url": "/api/keywords/ideas",
        "query": {
          "keyword": "{keyword}",
          "language": "{language}",
          "locations": "{locations}",
          "include_questions": "{include_questions}",
          "include_related": "{include_related}",
          "include_suggestions": "{include_suggestions}"
        }
      }
    },
    {
      "name": "analyze_serp",
      "description": "Analyze a SERP (Search Engine Results Page) for a given query",
      "parameters": {
        "type": "object",
        "required": ["query"],
        "properties": {
          "query": {
            "type": "string",
            "description": "Search query to analyze"
          },
          "location": {
            "type": "string",
            "description": "Location for the search (e.g., 'United States')"
          },
          "language": {
            "type": "string",
            "description": "Language code (e.g., 'en')"
          },
          "device": {
            "type": "string",
            "enum": ["desktop", "mobile"],
            "default": "desktop",
            "description": "Device type for the search"
          },
          "num": {
            "type": "number",
            "default": 10,
            "minimum": 1,
            "maximum": 100,
            "description": "Number of results to return"
          }
        }
      },
      "handler": {
        "type": "http",
        "method": "GET",
        "url": "/api/serp/analyze",
        "query": {
          "query": "{query}",
          "location": "{location}",
          "language": "{language}",
          "device": "{device}",
          "num": "{num}"
        }
      }
    },
    {
      "name": "analyze_competitors",
      "description": "Analyze competitors for a given keyword or domain",
      "parameters": {
        "type": "object",
        "properties": {
          "keyword": {
            "type": "string",
            "description": "Keyword to analyze competitors for"
          },
          "domain": {
            "type": "string",
            "description": "Domain to analyze competitors for"
          },
          "include_features": {
            "type": "boolean",
            "default": false,
            "description": "Whether to include detailed features in the analysis"
          },
          "num_results": {
            "type": "number",
            "minimum": 1,
            "maximum": 100,
            "default": 10,
            "description": "Number of competitor results to return"
          }
        }
      },
      "handler": {
        "type": "http",
        "method": "GET",
        "url": "/api/competitors/analyze",
        "query": {
          "keyword": "{keyword}",
          "domain": "{domain}",
          "include_features": "{include_features}",
          "limit": "{num_results}"
        }
      }
    },
    {
      "name": "_health",
      "description": "Health check endpoint",
      "parameters": {
        "type": "object",
        "properties": {
          "random_string": {
            "type": "string",
            "description": "Dummy parameter for no-parameter tools"
          }
        },
        "required": ["random_string"]
      },
      "handler": {
        "type": "http",
        "method": "GET",
        "url": "/health"
      }
    }
  ]
}
```

--------------------------------------------------------------------------------
/src/services/keywordPlannerService.js:
--------------------------------------------------------------------------------

```javascript
import { GoogleAdsApi } from 'google-ads-api';
import googleAdsConfig from '../config/googleAdsConfig.js';

// Initialize Google Ads API client
const client = new GoogleAdsApi({
  client_id: googleAdsConfig.client_id,
  client_secret: googleAdsConfig.client_secret,
  developer_token: googleAdsConfig.developer_token,
  refresh_token: googleAdsConfig.refresh_token,
});

// Get customer instance
const customer = client.Customer({
  customer_id: googleAdsConfig.login_customer_id,
});

/**
 * Generate keyword ideas based on a seed keyword
 * @param {string} keyword - Seed keyword
 * @param {string} language - Language code (e.g., 'en')
 * @param {Array} locations - Array of location IDs
 * @param {number} limit - Maximum number of keyword ideas to return
 * @returns {Promise<Array>} - Array of keyword ideas with metrics
 */
export const generateKeywordIdeas = async (keyword, language = 'en', locations = [2250], limit = 50) => {
  try {
    // Create keyword text list
    const keywordTexts = [keyword];

    // Generate keyword ideas
    const keywordIdeas = await customer.keywordPlanner.generateKeywordIdeas({
      keywordTexts,
      language,
      geoTargetConstants: locations,
      keywordPlanNetwork: 'GOOGLE_SEARCH_AND_PARTNERS',
    });

    // Process and format the results
    const formattedResults = keywordIdeas.slice(0, limit).map((idea) => {
      return {
        keyword: idea.text,
        avgMonthlySearches: idea.keywordIdeaMetrics?.avgMonthlySearches || 0,
        competition: idea.keywordIdeaMetrics?.competition || 'UNKNOWN',
        competitionIndex: idea.keywordIdeaMetrics?.competitionIndex || 0,
        lowTopOfPageBidMicros: idea.keywordIdeaMetrics?.lowTopOfPageBidMicros
          ? parseFloat(idea.keywordIdeaMetrics.lowTopOfPageBidMicros) / 1000000
          : 0,
        highTopOfPageBidMicros: idea.keywordIdeaMetrics?.highTopOfPageBidMicros
          ? parseFloat(idea.keywordIdeaMetrics.highTopOfPageBidMicros) / 1000000
          : 0,
      };
    });

    return formattedResults;
  } catch (error) {
    console.error('Error generating keyword ideas:', error);
    throw new Error(`Failed to generate keyword ideas: ${error.message}`);
  }
};

/**
 * Get keyword volume and metrics for a list of keywords
 * @param {Array} keywords - Array of keywords to get metrics for
 * @param {string} language - Language code (e.g., 'en')
 * @param {Array} locations - Array of location IDs
 * @returns {Promise<Array>} - Array of keywords with metrics
 */
export const getKeywordMetrics = async (keywords, language = 'en', locations = [2250]) => {
  try {
    // Generate keyword ideas for the provided keywords
    const keywordIdeas = await customer.keywordPlanner.generateKeywordIdeas({
      keywordTexts: keywords,
      language,
      geoTargetConstants: locations,
      keywordPlanNetwork: 'GOOGLE_SEARCH_AND_PARTNERS',
    });

    // Process and format the results
    const formattedResults = keywordIdeas.map((idea) => {
      return {
        keyword: idea.text,
        avgMonthlySearches: idea.keywordIdeaMetrics?.avgMonthlySearches || 0,
        competition: idea.keywordIdeaMetrics?.competition || 'UNKNOWN',
        competitionIndex: idea.keywordIdeaMetrics?.competitionIndex || 0,
        lowTopOfPageBidMicros: idea.keywordIdeaMetrics?.lowTopOfPageBidMicros
          ? parseFloat(idea.keywordIdeaMetrics.lowTopOfPageBidMicros) / 1000000
          : 0,
        highTopOfPageBidMicros: idea.keywordIdeaMetrics?.highTopOfPageBidMicros
          ? parseFloat(idea.keywordIdeaMetrics.highTopOfPageBidMicros) / 1000000
          : 0,
      };
    });

    return formattedResults;
  } catch (error) {
    console.error('Error getting keyword metrics:', error);
    throw new Error(`Failed to get keyword metrics: ${error.message}`);
  }
};

/**
 * Get historical metrics for keywords
 * @param {Array} keywords - Array of keywords to get historical metrics for
 * @param {string} language - Language code (e.g., 'en')
 * @param {Array} locations - Array of location IDs
 * @returns {Promise<Array>} - Array of keywords with historical metrics
 */
export const getHistoricalMetrics = async (keywords, language = 'en', locations = [2250]) => {
  try {
    // Create a keyword plan
    const keywordPlan = await customer.keywordPlans.create({
      name: `Keyword Plan ${Date.now()}`,
    });

    // Create a keyword plan campaign
    const keywordPlanCampaign = await customer.keywordPlanCampaigns.create({
      keywordPlan: keywordPlan.resource_name,
      name: 'Keyword Plan Campaign',
      cpcBidMicros: 1000000, // $1.00
      geoTargets: locations.map(locationId => ({
        geoTargetConstant: `geoTargetConstants/${locationId}`,
      })),
      languageConstant: `languageConstants/${language}`,
    });

    // Create a keyword plan ad group
    const keywordPlanAdGroup = await customer.keywordPlanAdGroups.create({
      keywordPlanCampaign: keywordPlanCampaign.resource_name,
      name: 'Keyword Plan Ad Group',
      cpcBidMicros: 1000000, // $1.00
    });

    // Create keyword plan keywords
    await Promise.all(
      keywords.map(keyword =>
        customer.keywordPlanKeywords.create({
          keywordPlanAdGroup: keywordPlanAdGroup.resource_name,
          text: keyword,
          cpcBidMicros: 1000000, // $1.00
        })
      )
    );

    // Generate forecast metrics
    const forecastMetrics = await customer.keywordPlans.generateForecastMetrics({
      keywordPlan: keywordPlan.resource_name,
    });

    // Clean up by removing the keyword plan
    await customer.keywordPlans.delete({
      resource_name: keywordPlan.resource_name,
    });

    return forecastMetrics;
  } catch (error) {
    console.error('Error getting historical metrics:', error);
    throw new Error(`Failed to get historical metrics: ${error.message}`);
  }
};

export default {
  generateKeywordIdeas,
  getKeywordMetrics,
  getHistoricalMetrics,
};
```