UNPKG

marketing-post-generator-mcp

Version:

A powerful MCP server for AI-powered marketing blog post generation with Claude integration

478 lines (444 loc) 20.5 kB
import { createLogger } from '../utils/logger.js'; import * as fs from 'fs/promises'; import * as path from 'path'; export class ContentPlanTool { logger; // TODO: Future enhancement - integrate SearchService for real-time content research and trend analysis constructor(_searchService, logger) { this.logger = logger || createLogger({ level: 'info', format: 'simple' }); // Note: _searchService parameter maintained for consistency with other tools but not currently used // TODO: Future enhancement - integrate SearchService for real-time content research and trend analysis } getToolDefinition() { return { name: 'marketing_post_generator_mcp__content_plan', description: 'Create or update a comprehensive content plan for future blog posts. Analyzes existing content, identifies gaps, and generates strategic post recommendations based on domain expertise, content variety, and emerging trends.', inputSchema: { type: 'object', properties: { domain: { type: 'string', description: 'The domain to create a content plan for (e.g., "example.com")', }, timeframe: { type: 'string', description: 'Timeframe for the content plan', enum: ['week', 'month', 'quarter'], default: 'month', }, postCount: { type: 'number', description: 'Number of posts to plan', minimum: 1, maximum: 50, default: 8, }, updateExisting: { type: 'boolean', description: 'Whether to update an existing plan or create a new one', default: false, }, }, required: ['domain'], }, }; } async execute(args, claudeService) { const { domain, timeframe = 'month', postCount = 8, updateExisting = false } = args; try { this.logger.info('Starting content plan tool execution', { domain, timeframe, postCount, updateExisting, }); // Validate .postgen directory exists await this.validatePostgenDirectory(); // Gather existing data const samples = await this.getExistingSamples(domain); const toneAnalysis = await this.getExistingToneAnalysis(domain); // Check for existing content plan if updating let existingPlan = null; if (updateExisting) { existingPlan = await this.getLatestContentPlan(domain); } this.logger.info('Data gathering completed', { samplesCount: samples.length, hasToneAnalysis: !!toneAnalysis, hasExistingPlan: !!existingPlan, }); // Generate content plan with Claude const contentPlan = await this.generateContentPlan({ domain, timeframe, postCount, samples, toneAnalysis, existingPlan, claudeService, }); // Save the content plan const planPath = await this.saveContentPlan(contentPlan, domain); this.logger.info('Content plan tool execution completed successfully', { domain, postsGenerated: contentPlan.posts.length, planPath, }); return this.formatResponse(contentPlan, planPath); } catch (error) { this.logger.error('Content plan tool execution failed', { domain, error: error instanceof Error ? error.message : String(error), }); throw new Error(`Failed to generate content plan for ${domain}: ${error instanceof Error ? error.message : String(error)}`); } } createDomainMatcher(domain) { const normalizedDomain = domain.replace(/^www\./, '').toLowerCase(); return (filename) => { const normalizedFile = filename.toLowerCase(); return ((normalizedFile.includes(normalizedDomain.replace(/\./g, '-')) || normalizedFile.includes(normalizedDomain)) && filename.endsWith('.json')); }; } async validatePostgenDirectory() { const postgenDir = path.join(process.cwd(), '.postgen'); try { await fs.access(postgenDir); } catch (error) { throw new Error('The .postgen directory does not exist. Please run the init tool first to set up the project structure.'); } // Ensure content-plans directory exists const contentPlansDir = path.join(postgenDir, 'content-plans'); try { await fs.access(contentPlansDir); } catch (error) { await fs.mkdir(contentPlansDir, { recursive: true }); this.logger.info('Created content-plans directory', { contentPlansDir }); } } async getExistingSamples(domain) { try { const samplesDir = path.join(process.cwd(), '.postgen', 'samples'); // Look for domain-specific sample files const files = await fs.readdir(samplesDir); const domainMatcher = this.createDomainMatcher(domain); const domainFiles = files.filter(domainMatcher); const samples = []; for (const file of domainFiles) { try { const content = await fs.readFile(path.join(samplesDir, file), 'utf-8'); const sampleData = JSON.parse(content); // Extract posts from sample data structure if (sampleData.posts && Array.isArray(sampleData.posts)) { samples.push(...sampleData.posts.map((post) => ({ title: post.title || 'Untitled', content: post.content || '', url: post.url || '', categories: post.categories || [], }))); } } catch (error) { this.logger.warn('Failed to parse sample file', { file, error }); } } this.logger.info('Loaded existing samples', { domain, samplesCount: samples.length }); return samples; } catch (error) { this.logger.warn('No existing samples found for domain', { domain }); return []; } } async getExistingToneAnalysis(domain) { try { const toneAnalysisDir = path.join(process.cwd(), '.postgen', 'analysis', 'tone-analysis'); const files = await fs.readdir(toneAnalysisDir); // Look for domain-related tone analysis files const domainFiles = []; for (const file of files) { try { const content = await fs.readFile(path.join(toneAnalysisDir, file), 'utf-8'); const toneData = JSON.parse(content); // Use more specific domain matching to prevent false positives if (toneData.source && this.createDomainMatcher(domain)(file)) { domainFiles.push(file); } } catch { // Skip files that can't be read or parsed } } if (domainFiles.length > 0) { // Use the most recent tone analysis by modification time const filesWithStats = await Promise.all(domainFiles.map(async (file) => { const stat = await fs.stat(path.join(toneAnalysisDir, file)); return { file, mtime: stat.mtime }; })); const latestFile = filesWithStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())[0]; if (!latestFile) { return null; } const content = await fs.readFile(path.join(toneAnalysisDir, latestFile.file), 'utf-8'); const toneData = JSON.parse(content); return { formality: toneData.toneAnalysis.formality || 'Not specified', emotion: toneData.toneAnalysis.emotion || 'Not specified', style: toneData.toneAnalysis.style || 'Not specified', overallTone: toneData.toneAnalysis.overallTone || 'Not specified', }; } return null; } catch (error) { this.logger.warn('No existing tone analysis found for domain', { domain }); return null; } } async getLatestContentPlan(domain) { try { const contentPlansDir = path.join(process.cwd(), '.postgen', 'content-plans'); const files = await fs.readdir(contentPlansDir); // Look for domain-specific content plan files const domainMatcher = this.createDomainMatcher(domain); const domainFiles = files.filter(domainMatcher); if (domainFiles.length > 0) { // Get the most recent file by modification time const filesWithStats = await Promise.all(domainFiles.map(async (file) => { const stat = await fs.stat(path.join(contentPlansDir, file)); return { file, mtime: stat.mtime }; })); const latestFile = filesWithStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())[0]; if (!latestFile) { return null; } const content = await fs.readFile(path.join(contentPlansDir, latestFile.file), 'utf-8'); return JSON.parse(content); } return null; } catch (error) { this.logger.warn('No existing content plan found for domain', { domain }); return null; } } async generateContentPlan(options) { const prompt = this.buildContentPlanPrompt(options); this.logger.info('Generating content plan with Claude', { domain: options.domain, promptLength: prompt.length, }); const response = await options.claudeService.generateContent(prompt, { maxTokens: 4096, temperature: 0.4, }); const parsedPlan = this.parseContentPlanResponse(response.content, options); return { domain: options.domain, timeframe: options.timeframe, postCount: options.postCount, posts: parsedPlan.posts, contentGaps: parsedPlan.contentGaps, trendingTopics: parsedPlan.trendingTopics, recommendations: parsedPlan.recommendations, executionStrategy: parsedPlan.executionStrategy, timestamp: new Date().toISOString(), updatedFrom: options.existingPlan?.timestamp || undefined, }; } buildContentPlanPrompt(options) { const { domain, timeframe, postCount, samples, toneAnalysis, existingPlan } = options; let prompt = `You are a professional content strategist tasked with creating a comprehensive content plan for "${domain}". ## Context Information **Target**: ${postCount} blog posts over ${timeframe === 'week' ? '1 week' : timeframe === 'month' ? '1 month' : '3 months'} **Domain**: ${domain}`; // Add existing content context if (samples.length > 0) { prompt += ` ## Existing Content Analysis The domain currently has ${samples.length} analyzed posts with the following themes: ${samples .slice(0, 5) .map((sample) => `- "${sample.title}": ${sample.content.slice(0, 100)}...`) .join('\n')}`; } // Add tone analysis if available if (toneAnalysis) { prompt += ` ## Brand Voice Guidelines Based on existing content analysis: - **Formality**: ${toneAnalysis.formality} - **Emotion**: ${toneAnalysis.emotion} - **Style**: ${toneAnalysis.style} - **Overall Tone**: ${toneAnalysis.overallTone}`; } // Add existing plan context if updating if (existingPlan) { prompt += ` ## Previous Plan Context This is an update to an existing content plan from ${new Date(existingPlan.timestamp).toLocaleDateString()}. Previous plan included ${existingPlan.posts.length} posts covering these topics: ${existingPlan.posts .slice(0, 3) .map((post) => `- ${post.title} (${post.category})`) .join('\n')} Please consider these previous plans while creating new content that builds upon or complements the existing strategy.`; } prompt += ` ## Content Plan Requirements Please create a comprehensive content plan that includes: 1. **Content Gap Analysis**: Identify missing topics or content types 2. **Trending Topics**: Suggest current industry trends to cover 3. **Strategic Recommendations**: Overall content strategy advice 4. **Execution Strategy**: How to implement this content plan effectively ## Response Format Please respond with a JSON object in the following format: \`\`\`json { "posts": [ { "title": "Specific post title", "description": "Detailed description of the post content and approach", "keywords": ["keyword1", "keyword2", "keyword3"], "category": "Category name", "estimatedLength": "Short (500-800) | Medium (800-1500) | Long (1500+)", "priority": "high | medium | low", "targetAudience": "Description of target audience", "contentType": "educational | promotional | thought-leadership | case-study | tutorial | news | opinion" } ], "contentGaps": [ "Description of content gaps identified" ], "trendingTopics": [ "Current trending topics relevant to the domain" ], "recommendations": [ "Strategic recommendations for content success" ], "executionStrategy": "Detailed strategy for implementing this content plan" } \`\`\` ## Guidelines 1. **Variety**: Include diverse content types and formats 2. **Value**: Focus on providing genuine value to readers 3. **SEO**: Consider search optimization potential 4. **Engagement**: Plan content that encourages interaction 5. **Brand Consistency**: Maintain the established tone and style 6. **Seasonal Relevance**: Consider timing and seasonal factors 7. **Competitive Edge**: Suggest unique angles or perspectives Generate exactly ${postCount} post ideas that form a cohesive content strategy.`; return prompt; } parseContentPlanResponse(response, options) { try { // Extract JSON from response const jsonMatch = response.match(/```json\s*([\s\S]*?)\s*```/) || response.match(/\{[\s\S]*\}/); if (!jsonMatch) { throw new Error('No JSON found in content plan response'); } const jsonText = jsonMatch[jsonMatch.length - 1]; if (!jsonText) { throw new Error('No valid JSON content found in response'); } const parsed = JSON.parse(jsonText); // Validate and structure the response const posts = (parsed.posts || []).map((post) => ({ title: post.title || 'Untitled Post', description: post.description || 'No description provided', keywords: Array.isArray(post.keywords) ? post.keywords : [], category: post.category || 'General', estimatedLength: post.estimatedLength || 'Medium (800-1500)', priority: ['high', 'medium', 'low'].includes(post.priority) ? post.priority : 'medium', targetAudience: post.targetAudience || 'General audience', contentType: [ 'educational', 'promotional', 'thought-leadership', 'case-study', 'tutorial', 'news', 'opinion', ].includes(post.contentType) ? post.contentType : 'educational', })); return { posts: posts.slice(0, options.postCount), // Ensure we don't exceed requested count contentGaps: Array.isArray(parsed.contentGaps) ? parsed.contentGaps : [], trendingTopics: Array.isArray(parsed.trendingTopics) ? parsed.trendingTopics : [], recommendations: Array.isArray(parsed.recommendations) ? parsed.recommendations : [], executionStrategy: parsed.executionStrategy || 'No execution strategy provided', }; } catch (error) { this.logger.error('Failed to parse content plan response', { error }); // Fallback: Create a basic plan return { posts: Array.from({ length: options.postCount }, (_, i) => ({ title: `Content Idea ${i + 1} for ${options.domain}`, description: 'Content plan generation failed - manual planning required', keywords: [options.domain.replace(/\./g, '')], category: 'General', estimatedLength: 'Medium (800-1500)', priority: 'medium', targetAudience: 'General audience', contentType: 'educational', })), contentGaps: ['Unable to analyze content gaps - manual review required'], trendingTopics: ['Unable to identify trending topics - manual research required'], recommendations: ['Content plan generation failed - please review and create manually'], executionStrategy: 'Manual planning required due to generation failure', }; } } async saveContentPlan(contentPlan, domain) { const timestamp = Date.now(); const cleanDomain = domain.replace(/\./g, '-'); const planPath = path.join(process.cwd(), '.postgen', 'content-plans', `${cleanDomain}-${contentPlan.timeframe}-${timestamp}.json`); await fs.writeFile(planPath, JSON.stringify(contentPlan, null, 2)); this.logger.info('Content plan saved', { planPath, domain, postsCount: contentPlan.posts.length, }); return planPath; } formatResponse(contentPlan, planPath) { const response = `# Content Plan for ${contentPlan.domain} ## Plan Overview - **Timeframe**: ${contentPlan.timeframe} - **Posts Planned**: ${contentPlan.posts.length} - **Created**: ${new Date(contentPlan.timestamp).toLocaleDateString()} ${contentPlan.updatedFrom ? `- **Updated From**: ${new Date(contentPlan.updatedFrom).toLocaleDateString()}` : ''} ## Planned Content ${contentPlan.posts .map((post, index) => `### ${index + 1}. ${post.title} **Category**: ${post.category} | **Priority**: ${post.priority} | **Type**: ${post.contentType} **Length**: ${post.estimatedLength} **Target Audience**: ${post.targetAudience} ${post.description} **Keywords**: ${post.keywords.join(', ')} ---`) .join('\n\n')} ## Content Strategy Analysis ### Identified Content Gaps ${contentPlan.contentGaps.map((gap) => `- ${gap}`).join('\n')} ### Trending Topics to Consider ${contentPlan.trendingTopics.map((topic) => `- ${topic}`).join('\n')} ### Strategic Recommendations ${contentPlan.recommendations.map((rec) => `- ${rec}`).join('\n')} ## Execution Strategy ${contentPlan.executionStrategy} --- **Content Plan Storage**: \`${planPath.split('/').slice(-3).join('/')}\` This comprehensive content plan provides a strategic roadmap for creating engaging, valuable content that aligns with your brand voice and audience needs. Each post idea includes specific guidance for implementation and optimization.`; return response; } } //# sourceMappingURL=ContentPlanTool.js.map