marketing-post-generator-mcp
Version:
A powerful MCP server for AI-powered marketing blog post generation with Claude integration
552 lines (470 loc) • 16.2 kB
JavaScript
/**
* Batch Generation Example - Marketing Post Generator MCP
*
* This example demonstrates efficient batch processing for generating
* multiple blog posts from a content plan with progress tracking,
* error handling, and result aggregation.
*
* Features:
* - Bulk post generation from content plans
* - Progress tracking with ETA calculations
* - Intelligent rate limiting and retry logic
* - Result aggregation and reporting
* - Resumable operations from checkpoint files
*
* Usage:
* node examples/batch-generation.js --domain blog.example.com --count 10
* node examples/batch-generation.js --resume checkpoint.json
* node examples/batch-generation.js --plan content-plan.json
*/
import { spawn } from 'child_process';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import fs from 'fs/promises';
import { performance } from 'perf_hooks';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Configuration
const DEFAULT_CONFIG = {
domain: 'blog.stripe.com',
postCount: 5,
concurrency: 2,
retryAttempts: 3,
delayBetweenRequests: 2000,
checkpointInterval: 5,
outputDir: './batch-output',
resumeFromCheckpoint: false
};
class ProgressTracker {
constructor(total) {
this.total = total;
this.completed = 0;
this.failed = 0;
this.startTime = performance.now();
this.lastUpdate = this.startTime;
}
update(success = true) {
if (success) {
this.completed++;
} else {
this.failed++;
}
this.display();
}
display() {
const now = performance.now();
const elapsed = (now - this.startTime) / 1000;
const processed = this.completed + this.failed;
const rate = processed / elapsed;
const eta = rate > 0 ? (this.total - processed) / rate : 0;
const percentage = ((processed / this.total) * 100).toFixed(1);
const progressBar = this.createProgressBar(processed, this.total);
console.log(`\\r${progressBar} ${percentage}% | ${this.completed}✅ ${this.failed}❌ | ETA: ${eta.toFixed(0)}s`);
}
createProgressBar(current, total, length = 30) {
const filled = Math.floor((current / total) * length);
const bar = '█'.repeat(filled) + '░'.repeat(length - filled);
return `[${bar}]`;
}
getStats() {
const elapsed = (performance.now() - this.startTime) / 1000;
return {
total: this.total,
completed: this.completed,
failed: this.failed,
successRate: ((this.completed / this.total) * 100).toFixed(1),
totalTime: elapsed.toFixed(2),
averageTime: (elapsed / (this.completed + this.failed)).toFixed(2)
};
}
}
class BatchProcessor {
constructor(config = DEFAULT_CONFIG) {
this.config = { ...DEFAULT_CONFIG, ...config };
this.mcpProcess = null;
this.requestId = 1;
this.activeRequests = new Map();
this.queue = [];
this.results = [];
this.checkpoint = null;
}
async initialize() {
console.log('🚀 Initializing Batch Processor...');
// Create output directory
await fs.mkdir(this.config.outputDir, { recursive: true });
// Start MCP process
this.mcpProcess = spawn('node', [join(__dirname, '../dist/index.js')], {
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
MCP_MODE: 'local',
LOG_LEVEL: 'error'
}
});
// Set up response handling
this.mcpProcess.stdout.on('data', (data) => {
this.handleMCPResponse(data.toString());
});
this.mcpProcess.stderr.on('data', (data) => {
const message = data.toString();
if (message.includes('ERROR')) {
console.error('MCP Error:', message);
}
});
// Wait for MCP to be ready
await new Promise(resolve => setTimeout(resolve, 3000));
console.log('✅ MCP Process ready');
}
handleMCPResponse(data) {
const lines = data.trim().split('\\n');
for (const line of lines) {
if (!line.trim()) continue;
try {
const response = JSON.parse(line);
const request = this.activeRequests.get(response.id);
if (request) {
this.activeRequests.delete(response.id);
if (response.error) {
request.reject(new Error(response.error.message));
} else {
request.resolve(response.result);
}
}
} catch (error) {
// Ignore parse errors
}
}
}
async sendMCPRequest(method, params) {
const id = this.requestId++;
const request = {
jsonrpc: '2.0',
id,
method,
params
};
return new Promise((resolve, reject) => {
// Store request handler
this.activeRequests.set(id, { resolve, reject });
// Set timeout
setTimeout(() => {
if (this.activeRequests.has(id)) {
this.activeRequests.delete(id);
reject(new Error('Request timeout'));
}
}, 120000); // 2 minute timeout
// Send request
this.mcpProcess.stdin.write(JSON.stringify(request) + '\\n');
});
}
async initializeDomain() {
console.log(`🏗️ Initializing domain: ${this.config.domain}`);
try {
await this.sendMCPRequest('prompts/get', {
name: 'init',
arguments: { domain: this.config.domain }
});
console.log('✅ Domain initialized');
} catch (error) {
throw new Error(`Domain initialization failed: ${error.message}`);
}
}
async createContentPlan() {
console.log(`📋 Creating content plan for ${this.config.postCount} posts...`);
try {
const result = await this.sendMCPRequest('tools/call', {
name: 'content_plan',
arguments: {
domain: this.config.domain,
timeframe: 'month',
postCount: this.config.postCount
}
});
console.log(`✅ Content plan created with ${result.plan?.length || 0} posts`);
return result.plan || [];
} catch (error) {
throw new Error(`Content plan creation failed: ${error.message}`);
}
}
async loadCheckpoint() {
if (!this.config.resumeFromCheckpoint) return null;
try {
const checkpointPath = this.config.checkpointFile || join(this.config.outputDir, 'checkpoint.json');
const data = await fs.readFile(checkpointPath, 'utf-8');
this.checkpoint = JSON.parse(data);
console.log(`📄 Resumed from checkpoint: ${this.checkpoint.completed}/${this.checkpoint.total} completed`);
return this.checkpoint;
} catch (error) {
console.log('ℹ️ No checkpoint found, starting fresh');
return null;
}
}
async saveCheckpoint(postPlan, completed, results) {
const checkpoint = {
timestamp: new Date().toISOString(),
domain: this.config.domain,
total: postPlan.length,
completed: completed.length,
remaining: postPlan.filter(p => !completed.includes(p.id)),
results: results.map(r => ({
postId: r.postId,
success: r.success,
filePath: r.filePath,
error: r.error
}))
};
const checkpointPath = join(this.config.outputDir, 'checkpoint.json');
await fs.writeFile(checkpointPath, JSON.stringify(checkpoint, null, 2));
}
async generatePost(post, retryCount = 0) {
const startTime = performance.now();
try {
// Generate narrative first
const narrative = await this.sendMCPRequest('tools/call', {
name: 'generate_narrative',
arguments: {
postId: post.id,
style: 'detailed'
}
});
// Generate blog post
const blogPost = await this.sendMCPRequest('tools/call', {
name: 'write_post',
arguments: {
narrativeId: post.id,
wordCount: post.estimatedWordCount || 1200,
style: 'informative'
}
});
const generationTime = (performance.now() - startTime) / 1000;
return {
postId: post.id,
success: true,
title: blogPost.title,
filePath: blogPost.filePath,
wordCount: blogPost.metadata?.wordCount,
generationTime: generationTime.toFixed(2),
narrative: narrative.narrative
};
} catch (error) {
if (retryCount < this.config.retryAttempts) {
console.log(`⏳ Retrying post ${post.id} (attempt ${retryCount + 1}/${this.config.retryAttempts})...`);
await new Promise(resolve => setTimeout(resolve, 1000 * (retryCount + 1)));
return this.generatePost(post, retryCount + 1);
}
return {
postId: post.id,
success: false,
title: post.title,
error: error.message,
generationTime: (performance.now() - startTime) / 1000
};
}
}
async processBatch(postPlan) {
const checkpoint = await this.loadCheckpoint();
const completed = checkpoint ? checkpoint.results.map(r => r.postId) : [];
const remaining = postPlan.filter(p => !completed.includes(p.id));
console.log(`\\n📊 Batch Processing Summary:`);
console.log(` Total posts: ${postPlan.length}`);
console.log(` Already completed: ${completed.length}`);
console.log(` Remaining: ${remaining.length}`);
console.log(` Concurrency: ${this.config.concurrency}`);
console.log(` Delay between requests: ${this.config.delayBetweenRequests}ms\\n`);
const progress = new ProgressTracker(remaining.length);
const results = checkpoint ? checkpoint.results : [];
let processedCount = 0;
// Process posts with controlled concurrency
const semaphore = new Array(this.config.concurrency).fill(null);
const promises = [];
for (const post of remaining) {
const promise = this.processWithSemaphore(semaphore, async () => {
const result = await this.generatePost(post);
results.push(result);
progress.update(result.success);
processedCount++;
// Save checkpoint periodically
if (processedCount % this.config.checkpointInterval === 0) {
await this.saveCheckpoint(postPlan, completed.concat(results.map(r => r.postId)), results);
}
// Rate limiting delay
if (processedCount < remaining.length) {
await new Promise(resolve => setTimeout(resolve, this.config.delayBetweenRequests));
}
return result;
});
promises.push(promise);
}
// Wait for all posts to complete
await Promise.allSettled(promises);
// Final checkpoint
await this.saveCheckpoint(postPlan, completed.concat(results.map(r => r.postId)), results);
console.log('\\n✅ Batch processing completed!');
return results;
}
async processWithSemaphore(semaphore, task) {
// Wait for available slot
while (semaphore.every(slot => slot !== null)) {
await new Promise(resolve => setTimeout(resolve, 100));
}
// Acquire slot
const slotIndex = semaphore.findIndex(slot => slot === null);
const promise = task();
semaphore[slotIndex] = promise;
try {
const result = await promise;
return result;
} finally {
// Release slot
semaphore[slotIndex] = null;
}
}
async generateReport(results, stats) {
const successful = results.filter(r => r.success);
const failed = results.filter(r => !r.success);
const report = {
summary: {
domain: this.config.domain,
timestamp: new Date().toISOString(),
...stats,
posts: {
total: results.length,
successful: successful.length,
failed: failed.length
}
},
successful: successful.map(r => ({
postId: r.postId,
title: r.title,
filePath: r.filePath,
wordCount: r.wordCount,
generationTime: r.generationTime
})),
failed: failed.map(r => ({
postId: r.postId,
title: r.title,
error: r.error,
generationTime: r.generationTime
})),
metrics: {
averageWordCount: successful.reduce((sum, r) => sum + (r.wordCount || 0), 0) / successful.length || 0,
averageGenerationTime: results.reduce((sum, r) => sum + parseFloat(r.generationTime), 0) / results.length,
totalGenerationTime: results.reduce((sum, r) => sum + parseFloat(r.generationTime), 0)
}
};
// Save detailed report
const reportPath = join(this.config.outputDir, 'batch-report.json');
await fs.writeFile(reportPath, JSON.stringify(report, null, 2));
// Display summary
console.log('\\n📊 Batch Generation Report');
console.log('==========================');
console.log(`Domain: ${this.config.domain}`);
console.log(`Success Rate: ${stats.successRate}%`);
console.log(`Total Time: ${stats.totalTime}s`);
console.log(`Average per Post: ${stats.averageTime}s`);
console.log(`Average Word Count: ${Math.round(report.metrics.averageWordCount)}`);
console.log(`\\n📄 Detailed report: ${reportPath}`);
if (failed.length > 0) {
console.log('\\n❌ Failed Posts:');
failed.forEach(f => console.log(` - ${f.title}: ${f.error}`));
}
return report;
}
async run() {
try {
await this.initialize();
await this.initializeDomain();
const postPlan = await this.createContentPlan();
if (postPlan.length === 0) {
throw new Error('No posts in content plan');
}
const results = await this.processBatch(postPlan);
const progressStats = new ProgressTracker(results.length);
// Update progress with final results
results.forEach(r => progressStats.update(r.success));
const stats = progressStats.getStats();
await this.generateReport(results, stats);
} catch (error) {
console.error('❌ Batch processing failed:', error);
throw error;
} finally {
if (this.mcpProcess) {
this.mcpProcess.kill('SIGTERM');
}
}
}
async cleanup() {
if (this.mcpProcess) {
this.mcpProcess.kill('SIGTERM');
console.log('🛑 MCP process stopped');
}
}
}
// CLI argument parsing
function parseArgs() {
const args = process.argv.slice(2);
const config = { ...DEFAULT_CONFIG };
for (let i = 0; i < args.length; i += 2) {
const flag = args[i];
const value = args[i + 1];
switch (flag) {
case '--domain':
config.domain = value;
break;
case '--count':
config.postCount = parseInt(value);
break;
case '--concurrency':
config.concurrency = parseInt(value);
break;
case '--output':
config.outputDir = value;
break;
case '--resume':
config.resumeFromCheckpoint = true;
config.checkpointFile = value;
break;
case '--delay':
config.delayBetweenRequests = parseInt(value);
break;
case '--retries':
config.retryAttempts = parseInt(value);
break;
}
}
return config;
}
// Main execution
async function main() {
console.log('Marketing Post Generator - Batch Processing');
console.log('==========================================\\n');
const config = parseArgs();
console.log('Configuration:');
console.log(` Domain: ${config.domain}`);
console.log(` Post Count: ${config.postCount}`);
console.log(` Concurrency: ${config.concurrency}`);
console.log(` Output Directory: ${config.outputDir}`);
console.log(` Resume from Checkpoint: ${config.resumeFromCheckpoint}`);
const processor = new BatchProcessor(config);
// Handle graceful shutdown
const cleanup = async () => {
console.log('\\n👋 Shutting down gracefully...');
await processor.cleanup();
process.exit(0);
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
try {
await processor.run();
console.log('\\n🎉 Batch processing completed successfully!');
} catch (error) {
console.error('💥 Batch processing failed:', error.message);
process.exit(1);
}
}
// Run if executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}
export default BatchProcessor;