marketing-post-generator-mcp
Version:
A powerful MCP server for AI-powered marketing blog post generation with Claude integration
329 lines (310 loc) • 14.7 kB
JavaScript
import { createLogger } from '../utils/logger.js';
import * as fs from 'fs/promises';
import * as path from 'path';
import * as crypto from 'crypto';
export class GenerateToneTool {
logger;
searchService;
constructor(searchService, logger) {
this.logger = logger || createLogger({ level: 'info', format: 'simple' });
this.searchService = searchService;
}
getToolDefinition() {
return {
name: 'marketing_post_generator_mcp__generate_tone',
description: 'Analyze content to determine the tone of voice used in a blog or specific post. Can analyze either a specific post URL or sample multiple posts from a domain to extract comprehensive tone characteristics.',
inputSchema: {
type: 'object',
properties: {
source: {
type: 'string',
description: 'URL of a specific post or domain name to analyze (e.g., "https://example.com/post" or "example.com")',
},
detailLevel: {
type: 'string',
description: 'Level of detail in the tone analysis',
enum: ['basic', 'detailed', 'comprehensive'],
default: 'detailed',
},
},
required: ['source'],
},
};
}
async execute(args, claudeService) {
const { source, detailLevel = 'detailed' } = args;
try {
this.logger.info('Starting tone generation tool execution', { source, detailLevel });
// Validate .postgen directory exists
await this.validatePostgenDirectory();
// Generate source hash for caching
const sourceHash = this.generateSourceHash(source, detailLevel);
const tonePath = path.join(process.cwd(), '.postgen', 'analysis', 'tone-analysis', `${sourceHash}.json`);
// Check for existing tone analysis (cache hit)
const existingTone = await this.checkExistingTone(tonePath);
if (existingTone) {
this.logger.info('Returning cached tone analysis', { source, cached: true });
return this.formatResponse(existingTone);
}
// Determine source type and fetch content
const { sourceType, content } = await this.determineSourceAndFetchContent(source);
this.logger.info('Content fetched successfully', {
source,
sourceType,
contentLength: content.length,
});
// Generate tone analysis with Claude
this.logger.info('Generating tone analysis with Claude', { source, detailLevel });
const toneAnalysis = await this.generateToneAnalysis(content, detailLevel, claudeService);
// Prepare result
const result = {
source,
sourceType,
detailLevel,
toneAnalysis,
timestamp: new Date().toISOString(),
};
// Save result
await this.saveResult(result, tonePath);
this.logger.info('Tone generation tool execution completed successfully', { source });
return this.formatResponse(result);
}
catch (error) {
this.logger.error('Tone generation tool execution failed', {
source,
error: error instanceof Error ? error.message : String(error),
});
throw new Error(`Failed to generate tone analysis for ${source}: ${error instanceof Error ? error.message : String(error)}`);
}
}
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 tone-analysis directory exists
const toneAnalysisDir = path.join(postgenDir, 'analysis', 'tone-analysis');
try {
await fs.access(toneAnalysisDir);
}
catch (error) {
await fs.mkdir(toneAnalysisDir, { recursive: true });
this.logger.info('Created tone-analysis directory', { toneAnalysisDir });
}
}
generateSourceHash(source, detailLevel) {
return crypto.createHash('md5').update(`${source}-${detailLevel}`).digest('hex');
}
async checkExistingTone(tonePath) {
try {
const existingContent = await fs.readFile(tonePath, 'utf-8');
return JSON.parse(existingContent);
}
catch (error) {
// No existing tone analysis found
return null;
}
}
async determineSourceAndFetchContent(source) {
let sourceType;
let content;
// Try to determine if source is a URL or domain
try {
const url = new URL(source.startsWith('http') ? source : `https://${source}`);
// Check if it's a domain root or has a path
if (url.pathname === '/' || url.pathname === '') {
sourceType = 'domain';
// Get samples from the domain
this.logger.info('Fetching domain samples for tone analysis', { domain: url.hostname });
const posts = await this.searchService.sampleDomain(url.hostname, 3);
if (posts.length === 0) {
throw new Error(`No blog posts found for domain: ${url.hostname}. Please verify the domain is accessible and contains blog content.`);
}
content = posts
.map((post) => `Title: ${post.title}\nContent: ${post.content}`)
.join('\n\n---\n\n');
}
else {
sourceType = 'url';
// Fetch specific post
this.logger.info('Fetching specific post for tone analysis', { url: source });
content = await this.searchService.fetchContent(source);
if (!content) {
throw new Error(`Unable to fetch content from URL: ${source}. Please verify the URL is accessible and contains blog content.`);
}
}
}
catch (error) {
if (error instanceof Error && error.message.includes('No blog posts found')) {
throw error;
}
// Assume it's a domain if URL parsing fails
sourceType = 'domain';
this.logger.info('Treating as domain and fetching samples', { domain: source });
const posts = await this.searchService.sampleDomain(source, 3);
if (posts.length === 0) {
throw new Error(`No blog posts found for domain: ${source}. Please verify the domain is accessible and contains blog content.`);
}
content = posts
.map((post) => `Title: ${post.title}\nContent: ${post.content}`)
.join('\n\n---\n\n');
}
return { sourceType, content };
}
async generateToneAnalysis(content, detailLevel, claudeService) {
const tonePrompt = this.buildTonePrompt(content, detailLevel);
const response = await claudeService.generateContent(tonePrompt, {
maxTokens: detailLevel === 'comprehensive' ? 2048 : detailLevel === 'detailed' ? 1024 : 512,
temperature: 0.3,
});
return this.parseToneResponse(response.content, detailLevel);
}
buildTonePrompt(content, detailLevel) {
// Truncate content if too long to fit in prompt
const maxContentLength = 6000;
const truncatedContent = content.length > maxContentLength ? content.substring(0, maxContentLength) + '...' : content;
const basePrompt = `You are a professional content analyst specialized in analyzing tone of voice and writing style.
Please analyze the following content and provide a tone analysis in the following JSON format:
{
"formality": "Description of formality level (formal, informal, conversational, etc.)",
"emotion": "Description of emotional tone (friendly, professional, enthusiastic, etc.)",
"style": "Description of writing style (authoritative, accessible, technical, etc.)",
"overallTone": "Overall tone description combining all aspects"`;
const detailedFields = `,
"wordChoicePatterns": "Analysis of word choice patterns and vocabulary preferences",
"sentenceStructure": "Analysis of sentence structure and paragraph organization"`;
const comprehensiveFields = `,
"uniqueCharacteristics": "Unique voice characteristics and distinguishing features"`;
let promptFields = basePrompt;
if (detailLevel === 'detailed' || detailLevel === 'comprehensive') {
promptFields += detailedFields;
}
if (detailLevel === 'comprehensive') {
promptFields += comprehensiveFields;
}
promptFields += `
}
## Content to Analyze:
${truncatedContent}
## Analysis Instructions:
1. **Formality**: Assess the level of formality in the writing (formal, semi-formal, informal, conversational)
2. **Emotion**: Identify the emotional tone (friendly, professional, enthusiastic, serious, playful, etc.)
3. **Style**: Analyze the writing style (authoritative, accessible, technical, narrative, instructional, etc.)`;
if (detailLevel === 'detailed' || detailLevel === 'comprehensive') {
promptFields += `
4. **Word Choice Patterns**: Examine vocabulary preferences, technical terms usage, and language complexity
5. **Sentence Structure**: Analyze sentence length, complexity, and paragraph organization`;
}
if (detailLevel === 'comprehensive') {
promptFields += `
6. **Unique Characteristics**: Identify distinctive voice elements that make this content unique`;
}
promptFields += `
Please respond with ONLY the JSON object, no additional text or formatting.`;
return promptFields;
}
parseToneResponse(response, detailLevel) {
try {
// Try to extract JSON from the response
const jsonMatch = response.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
throw new Error('No JSON found in tone analysis response');
}
const parsed = JSON.parse(jsonMatch[0]);
// Validate required fields
const requiredFields = ['formality', 'emotion', 'style', 'overallTone'];
for (const field of requiredFields) {
if (!(field in parsed)) {
throw new Error(`Missing required field: ${field}`);
}
}
const result = {
formality: parsed.formality,
emotion: parsed.emotion,
style: parsed.style,
overallTone: parsed.overallTone,
};
// Add optional fields based on detail level
if (detailLevel === 'detailed' || detailLevel === 'comprehensive') {
result.wordChoicePatterns = parsed.wordChoicePatterns || 'Not analyzed';
result.sentenceStructure = parsed.sentenceStructure || 'Not analyzed';
}
if (detailLevel === 'comprehensive') {
result.uniqueCharacteristics = parsed.uniqueCharacteristics || 'Not analyzed';
}
return result;
}
catch (error) {
this.logger.warn('Failed to parse tone analysis response as JSON, using fallback', {
error: error instanceof Error ? error.message : String(error),
});
// Fallback: Create a basic analysis from the raw response
const fallbackResult = {
formality: 'Unable to extract formality analysis from AI response',
emotion: 'Unable to extract emotion analysis from AI response',
style: 'Unable to extract style analysis from AI response',
overallTone: 'Unable to extract overall tone analysis from AI response',
};
if (detailLevel === 'detailed' || detailLevel === 'comprehensive') {
fallbackResult.wordChoicePatterns =
'Unable to extract word choice patterns - manual review recommended';
fallbackResult.sentenceStructure =
'Unable to extract sentence structure analysis - manual review recommended';
}
if (detailLevel === 'comprehensive') {
fallbackResult.uniqueCharacteristics =
'Unable to extract unique characteristics - manual review recommended';
}
return fallbackResult;
}
}
async saveResult(result, tonePath) {
await fs.writeFile(tonePath, JSON.stringify(result, null, 2));
this.logger.info('Tone analysis result saved', { tonePath, source: result.source });
}
formatResponse(result) {
const sourceInfo = result.sourceType === 'url' ? 'Specific Post' : 'Domain Sample';
let response = `# Tone of Voice Analysis
## Source Information
- **Type**: ${sourceInfo}
- **Source**: ${result.source}
- **Detail Level**: ${result.detailLevel}
- **Analyzed**: ${new Date(result.timestamp).toLocaleDateString()}
## Tone Analysis
### Formality Level
${result.toneAnalysis.formality}
### Emotional Tone
${result.toneAnalysis.emotion}
### Writing Style
${result.toneAnalysis.style}
### Overall Tone
${result.toneAnalysis.overallTone}`;
// Add detailed analysis if available
if (result.toneAnalysis.wordChoicePatterns) {
response += `
### Word Choice Patterns
${result.toneAnalysis.wordChoicePatterns}`;
}
if (result.toneAnalysis.sentenceStructure) {
response += `
### Sentence Structure
${result.toneAnalysis.sentenceStructure}`;
}
if (result.toneAnalysis.uniqueCharacteristics) {
response += `
### Unique Characteristics
${result.toneAnalysis.uniqueCharacteristics}`;
}
const sourceHash = this.generateSourceHash(result.source, result.detailLevel);
response += `
---
**Note**: This tone analysis has been cached and can be accessed again without re-processing the content.
**Storage**: \`.postgen/analysis/tone-analysis/${sourceHash}.json\`
This analysis can be used to maintain consistent tone across future content creation.`;
return response;
}
}
//# sourceMappingURL=GenerateToneTool.js.map