# 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;
```
--------------------------------------------------------------------------------
/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;
```
--------------------------------------------------------------------------------
/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
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
CMD ["node", "src/mcp.js", "--mcp"]
```
--------------------------------------------------------------------------------
/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,
};
```