UNPKG

marketing-post-generator-mcp

Version:

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

1,787 lines (1,515 loc) 68.5 kB
# Marketing Post Generator MCP - Architecture Document ## Executive Summary The Marketing Post Generator MCP is a sophisticated AI-powered content generation system designed as a Model Context Protocol (MCP) server. It provides comprehensive tools for sampling blog content, extracting positioning and tone, creating content plans, and generating complete marketing blog posts. The system leverages Claude Code's agentic capabilities to deliver high-quality, contextually-aware content generation while maintaining strict separation of concerns through the MCP protocol. ### Key Architectural Decisions 1. **MCP-First Design**: Native implementation as an MCP server supporting both local (stdio) and remote (HTTP) modes 2. **Dual-Mode Operation**: Seamless support for both interactive and programmatic usage through Claude Code integration 3. **Dependency Injection Architecture**: Full dependency injection container for testing, modularity, and maintainability 4. **File-Based State Management**: Structured `.postgen` directory for persistent state with JSON-based data storage 5. **Strategy Pattern Implementation**: Pluggable content generation strategies and transport mechanisms 6. **Factory-Based Tool Registration**: Dynamic tool and prompt creation with automated MCP registration 7. **Clean Architecture Principles**: Separation of concerns with distinct layers for protocol, business logic, and data access ## System Overview The Marketing Post Generator MCP operates as a sophisticated content generation pipeline that transforms domain analysis into high-quality marketing content. The system consists of five primary components that work together to deliver comprehensive content generation capabilities. ```text ┌─────────────────────────────────────────────────────────────────┐ │ MCP CLIENT LAYER │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ Claude Code │ │ Claude Desktop │ │ Custom Client │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ │ │ MCP Protocol (JSON-RPC) │ ┌─────────────────────────────────────────────────────────────────┐ │ MCP SERVER LAYER │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Transport Layer │ │ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ │ │ Stdio Transport │ │ HTTP Transport │ │ │ │ │ └─────────────────┘ └─────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Protocol Handler Layer │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ │ │ Tool & Prompt Registry │ │ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ │ │ Dependency Injection │ ┌─────────────────────────────────────────────────────────────────┐ │ BUSINESS LOGIC LAYER │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ Content Tools │ │ Analysis Tools │ │ Planning Tools │ │ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ │ │ │ Write Post │ │ │ │ Sample │ │ │ │Content Plan │ │ │ │ │ │ Narrative │ │ │ │ Summarize │ │ │ │ Init │ │ │ │ │ │ │ │ │ │Generate Tone│ │ │ │ │ │ │ │ │ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ │ │ Service Interfaces │ ┌─────────────────────────────────────────────────────────────────┐ │ SERVICE LAYER │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ Claude Service │ │ Content Service│ │ Storage Service │ │ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ │ │ │AI Generation│ │ │ │Web Scraping │ │ │ │File Manager │ │ │ │ │ │Rate Limiting│ │ │ │Content Parse│ │ │ │Data Persist │ │ │ │ │ │Error Retry │ │ │ │URL Validate │ │ │ │Cache Layer │ │ │ │ │ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ │ │ File System Interface │ ┌─────────────────────────────────────────────────────────────────┐ │ DATA PERSISTENCE LAYER │ │ .postgen Directory Structure │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │ │ │ config/ │ │ samples/ │ │ summaries/ │ │ posts/ │ │ │ │ summaries/ │ │content-plans/│ │tone-analysis/│ │narratives/│ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └───────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` ### Core Data Flow 1. **Initialization**: Client invokes `init` prompt to establish `.postgen` directory structure 2. **Analysis Phase**: Tools sample domain content and extract positioning/tone characteristics 3. **Planning Phase**: Content plan generation based on analysis and strategic requirements 4. **Generation Phase**: Narrative creation and full blog post generation from plans 5. **Persistence**: All artifacts stored in structured `.postgen` directory for future reference ## Technical Architecture ### Core Technology Stack - **Runtime**: Node.js 18+ with TypeScript 5.0+ for type safety and modern JavaScript features - **MCP Implementation**: `@modelcontextprotocol/sdk` for native MCP server capabilities - **AI Integration**: `@anthropic-ai/claude-code` for agentic content generation operations - **Module System**: ES Modules (ESM) for modern JavaScript compatibility - **Build System**: TypeScript compiler with `target: ES2022` and `moduleResolution: Node16` - **Container Support**: Docker with multi-stage builds for production deployment ### Architectural Patterns Applied #### 1. Dependency Injection Container ```typescript // Core DI container implementation export class DIContainer { private services = new Map<string, any>(); private factories = new Map<string, () => any>(); register<T>(token: string, factory: () => T): void { this.factories.set(token, factory); } resolve<T>(token: string): T { if (this.services.has(token)) { return this.services.get(token); } const factory = this.factories.get(token); if (!factory) { throw new Error(`Service not registered: ${token}`); } const instance = factory(); this.services.set(token, instance); return instance; } } // Service registration container.register('ClaudeService', () => new ClaudeService(config.claude.apiKey)); container.register('StorageService', () => new StorageService(config.postgen.dataDir)); container.register('ContentService', () => new ContentService()); ``` #### 2. Factory Pattern for Tool Creation ```typescript // Tool factory interface export interface ToolFactory { createTool(): MCPTool; getToolName(): string; getToolDescription(): string; } // Concrete tool factory export class SampleToolFactory implements ToolFactory { constructor( private claudeService: ClaudeService, private storageService: StorageService, private contentService: ContentService ) {} createTool(): MCPTool { return { name: 'marketing_post_generator_mcp__sample', description: 'Sample blog posts from a domain to extract positioning and tone', parameters: this.getParameterSchema(), handler: this.createHandler() }; } private createHandler() { return async (params: any, context: any) => { // Implementation with injected dependencies }; } } ``` #### 3. Strategy Pattern for Content Generation ```typescript // Content generation strategy interface export interface ContentGenerationStrategy { generate(context: GenerationContext): Promise<GeneratedContent>; supports(contentType: ContentType): boolean; } // Concrete strategies export class BlogPostStrategy implements ContentGenerationStrategy { async generate(context: GenerationContext): Promise<GeneratedContent> { // Blog post generation logic } supports(contentType: ContentType): boolean { return contentType === ContentType.BLOG_POST; } } export class NarrativeStrategy implements ContentGenerationStrategy { async generate(context: GenerationContext): Promise<GeneratedContent> { // Narrative generation logic } supports(contentType: ContentType): boolean { return contentType === ContentType.NARRATIVE; } } // Strategy selector export class ContentGenerationEngine { constructor(private strategies: ContentGenerationStrategy[]) {} async generate(contentType: ContentType, context: GenerationContext): Promise<GeneratedContent> { const strategy = this.strategies.find(s => s.supports(contentType)); if (!strategy) { throw new Error(`No strategy found for content type: ${contentType}`); } return strategy.generate(context); } } ``` #### 4. Repository Pattern for Data Access ```typescript // Repository interface export interface Repository<T> { save(entity: T): Promise<void>; findById(id: string): Promise<T | null>; findAll(): Promise<T[]>; delete(id: string): Promise<void>; } // Concrete repository implementation export class FileSystemRepository<T> implements Repository<T> { constructor( private baseDirectory: string, private serializer: Serializer<T> ) {} async save(entity: T): Promise<void> { const filePath = this.getFilePath(entity.id); const data = this.serializer.serialize(entity); await fs.writeFile(filePath, data, 'utf-8'); } async findById(id: string): Promise<T | null> { try { const filePath = this.getFilePath(id); const data = await fs.readFile(filePath, 'utf-8'); return this.serializer.deserialize(data); } catch (error) { if (error.code === 'ENOENT') { return null; } throw error; } } } ``` #### 5. Observer Pattern for Event Handling ```typescript // Event system for cross-component communication export interface Event { type: string; payload: any; timestamp: Date; } export interface EventSubscriber<T = any> { handle(event: Event<T>): Promise<void>; } export class EventBus { private subscribers = new Map<string, EventSubscriber[]>(); subscribe<T>(eventType: string, subscriber: EventSubscriber<T>): void { if (!this.subscribers.has(eventType)) { this.subscribers.set(eventType, []); } this.subscribers.get(eventType)!.push(subscriber); } async publish<T>(event: Event<T>): Promise<void> { const subscribers = this.subscribers.get(event.type) || []; await Promise.all(subscribers.map(subscriber => subscriber.handle(event))); } } ``` ## Component Design ### MCP Server Core The MCP server core implements the Model Context Protocol specification and manages the lifecycle of tools and prompts. ```typescript export class MarketingPostGeneratorServer { private mcpServer: MCPServer; private toolRegistry: ToolRegistry; private promptRegistry: PromptRegistry; private container: DIContainer; private eventBus: EventBus; constructor(private config: ServerConfig) { this.container = new DIContainer(); this.eventBus = new EventBus(); this.initializeDependencies(); this.initializeMCPServer(); } private initializeDependencies(): void { // Register all services with the DI container this.container.register('ClaudeService', () => new ClaudeService(this.config.claude.apiKey, this.eventBus) ); this.container.register('StorageService', () => new StorageService(this.config.postgen.dataDir, this.eventBus) ); this.container.register('ContentService', () => new ContentService(this.eventBus) ); } private initializeMCPServer(): void { const transport = this.createTransport(); this.mcpServer = new MCPServer(transport); this.toolRegistry = new ToolRegistry(this.mcpServer, this.container); this.promptRegistry = new PromptRegistry(this.mcpServer, this.container); this.registerToolsAndPrompts(); } private createTransport(): Transport { switch (this.config.server.mode) { case 'local': return new StdioServerTransport(); case 'remote': return new StreamableHTTPServerTransport({ port: this.config.server.port, cors: { allowedOrigins: this.config.cors.allowedOrigins, allowedHeaders: ['mcp-session-id', 'content-type'] } }); default: throw new Error(`Unsupported server mode: ${this.config.server.mode}`); } } } ``` ### Claude Integration Service The Claude service manages all interactions with the Claude Code SDK, implementing proper error handling, rate limiting, and retry logic. ```typescript export class ClaudeService { private claude: Claude; private rateLimiter: RateLimiter; private circuitBreaker: CircuitBreaker; constructor( apiKey: string, private eventBus: EventBus, private config: ClaudeConfig = DEFAULT_CLAUDE_CONFIG ) { this.claude = new Claude({ apiKey }); this.rateLimiter = new RateLimiter(config.rateLimits); this.circuitBreaker = new CircuitBreaker(config.circuitBreaker); } async generateContent( prompt: string, options: ContentGenerationOptions = {} ): Promise<GeneratedContent> { // Rate limiting check if (!this.rateLimiter.checkLimit()) { throw new RateLimitError('Rate limit exceeded'); } // Circuit breaker check if (this.circuitBreaker.isOpen()) { throw new ServiceUnavailableError('Claude service temporarily unavailable'); } try { const startTime = Date.now(); // Publish generation started event await this.eventBus.publish({ type: 'content.generation.started', payload: { prompt: prompt.substring(0, 100) + '...', options }, timestamp: new Date() }); const response = await this.claude.complete({ prompt, max_tokens: options.maxTokens || 2000, temperature: options.temperature || 0.7, model: options.model || 'claude-3-5-sonnet-latest' }); const duration = Date.now() - startTime; // Publish generation completed event await this.eventBus.publish({ type: 'content.generation.completed', payload: { tokenCount: response.usage?.total_tokens, duration, success: true }, timestamp: new Date() }); this.circuitBreaker.recordSuccess(); return { content: response.completion, metadata: { model: response.model, usage: response.usage, duration } }; } catch (error) { this.circuitBreaker.recordFailure(); await this.eventBus.publish({ type: 'content.generation.failed', payload: { error: error.message }, timestamp: new Date() }); throw new ContentGenerationError(`Failed to generate content: ${error.message}`, error); } } } ``` ### Storage Service Implementation The storage service provides abstracted access to the file system with caching, validation, and atomic operations. ```typescript export class StorageService { private cache: LRUCache<string, any>; private lockManager: LockManager; constructor( private baseDirectory: string, private eventBus: EventBus, private config: StorageConfig = DEFAULT_STORAGE_CONFIG ) { this.cache = new LRUCache({ maxSize: config.cacheSize, ttl: config.cacheTtl }); this.lockManager = new LockManager(); } async ensureInitialized(): Promise<void> { try { await fs.access(this.baseDirectory); } catch (error) { throw new StorageError(`Storage directory not initialized: ${this.baseDirectory}`); } } async writeData<T>( category: string, filename: string, data: T, options: WriteOptions = {} ): Promise<void> { const filePath = path.join(this.baseDirectory, category, filename); const lockKey = `write:${filePath}`; await this.lockManager.acquire(lockKey); try { await this.ensureDirectoryExists(path.dirname(filePath)); // Atomic write operation const tempPath = `${filePath}.tmp`; const serializedData = JSON.stringify(data, null, 2); await fs.writeFile(tempPath, serializedData, 'utf-8'); await fs.rename(tempPath, filePath); // Update cache const cacheKey = `${category}:${filename}`; this.cache.set(cacheKey, data); // Publish event await this.eventBus.publish({ type: 'storage.data.written', payload: { category, filename, size: serializedData.length }, timestamp: new Date() }); } finally { this.lockManager.release(lockKey); } } async readData<T>( category: string, filename: string, options: ReadOptions = {} ): Promise<T | null> { const cacheKey = `${category}:${filename}`; // Check cache first if (!options.bypassCache && this.cache.has(cacheKey)) { return this.cache.get(cacheKey); } const filePath = path.join(this.baseDirectory, category, filename); try { const data = await fs.readFile(filePath, 'utf-8'); const parsed = JSON.parse(data); // Update cache this.cache.set(cacheKey, parsed); return parsed; } catch (error) { if (error.code === 'ENOENT') { return null; } throw new StorageError(`Failed to read data: ${error.message}`, error); } } } ``` ### Tool and Prompt Registration System The registration system provides centralized management of all MCP tools and prompts with validation and lifecycle management. ```typescript export class ToolRegistry { private tools = new Map<string, RegisteredTool>(); private factories = new Map<string, ToolFactory>(); constructor( private mcpServer: MCPServer, private container: DIContainer ) {} registerFactory(factory: ToolFactory): void { const toolName = factory.getToolName(); this.factories.set(toolName, factory); // Create and register the tool const tool = factory.createTool(); this.registerTool(tool, factory); } private registerTool(tool: MCPTool, factory: ToolFactory): void { // Validate tool structure this.validateTool(tool); // Wrap handler with middleware const wrappedTool = { ...tool, handler: this.wrapHandler(tool.handler, tool.name) }; // Register with MCP server this.mcpServer.registerTool(wrappedTool); // Store in registry this.tools.set(tool.name, { tool: wrappedTool, factory, registeredAt: new Date() }); logger.info(`Tool registered: ${tool.name}`); } private wrapHandler(originalHandler: Function, toolName: string) { return async (params: any, context: any) => { const startTime = Date.now(); const requestId = crypto.randomUUID(); logger.info(`Tool execution started: ${toolName}`, { requestId, params }); try { // Inject services from container const enhancedContext = { ...context, claudeService: this.container.resolve('ClaudeService'), storageService: this.container.resolve('StorageService'), contentService: this.container.resolve('ContentService'), requestId }; const result = await originalHandler(params, enhancedContext); const duration = Date.now() - startTime; logger.info(`Tool execution completed: ${toolName}`, { requestId, duration }); return result; } catch (error) { const duration = Date.now() - startTime; logger.error(`Tool execution failed: ${toolName}`, { requestId, duration, error: error.message }); throw new ToolExecutionError( `Tool ${toolName} failed: ${error.message}`, toolName, error ); } }; } private validateTool(tool: MCPTool): void { if (!tool.name || !tool.description || !tool.handler) { throw new ValidationError('Tool missing required properties'); } if (!tool.name.startsWith('marketing_post_generator_mcp__')) { throw new ValidationError(`Tool name must start with 'marketing_post_generator_mcp__': ${tool.name}`); } if (this.tools.has(tool.name)) { throw new ValidationError(`Tool already registered: ${tool.name}`); } } } ``` ## Data Architecture ### File Structure Design The `.postgen` directory provides a structured approach to data persistence with clear separation of concerns: ```text .postgen/ ├── config.json # Domain configuration and metadata ├── samples/ # Domain sampling results │ ├── {domain}-{timestamp}.json # Sample analysis per domain │ └── cache/ # Cached sample data ├── summaries/ # Individual post summaries │ ├── {url-hash}.json # Summary per URL │ └── index.json # Summary index ├── tone-analysis/ # Tone and positioning analysis │ ├── domain-{timestamp}.json # Domain-wide tone analysis │ └── post-{timestamp}.json # Individual post tone analysis ├── content-plans/ # Strategic content planning │ ├── {domain}-{plan-id}.json # Content plan per domain │ └── templates/ # Plan templates ├── narratives/ # Post narrative outlines │ ├── {post-id}-{timestamp}.json # Narrative per planned post │ └── drafts/ # Draft narratives └── posts/ # Generated blog posts ├── {post-id}.md # Final blog post content ├── {post-id}.json # Post metadata └── archive/ # Archived posts ``` ### Data Models ```typescript // Core domain configuration export interface DomainConfig { domain: string; initialized: string; lastSampled?: string; samplingPreferences: { sampleSize: number; excludePatterns: string[]; contentTypes: string[]; }; generationSettings: { defaultWordCount: number; preferredStyle: string; keywordStrategy: string; }; } // Content sampling result export interface SampleResult { domain: string; sampleSize: number; posts: Array<{ title: string; url: string; publishDate?: string; wordCount?: number; excerpt: string; }>; analysis: { positioning: string; toneOfVoice: string; contentStrategy: string; keywordPatterns: string[]; structuralPatterns: string[]; }; metadata: { sampledAt: string; processingDuration: number; confidence: number; }; } // Content plan structure export interface ContentPlan { domain: string; timeframe: 'week' | 'month' | 'quarter'; posts: Array<{ id: string; title: string; topic: string; keywords: string[]; estimatedWordCount: number; targetPublishDate?: string; priority: 'high' | 'medium' | 'low'; dependencies: string[]; }>; metadata: { createdAt: string; updatedAt: string; version: number; }; } // Generated content structure export interface GeneratedPost { id: string; title: string; content: string; metadata: { wordCount: number; readingTime: number; keywords: string[]; tone: string; style: string; generatedAt: string; model: string; revisionCount: number; }; frontmatter: { title: string; description: string; tags: string[]; publishDate?: string; lastModified: string; }; } ``` ### Caching Strategy ```typescript export class CacheManager { private memoryCache: LRUCache<string, any>; private diskCache: DiskCache; constructor(config: CacheConfig) { this.memoryCache = new LRUCache({ maxSize: config.memoryCacheSize, ttl: config.memoryCacheTtl }); this.diskCache = new DiskCache(config.diskCacheDirectory); } async get<T>(key: string, loader: () => Promise<T>): Promise<T> { // Check memory cache first if (this.memoryCache.has(key)) { return this.memoryCache.get(key); } // Check disk cache const diskResult = await this.diskCache.get<T>(key); if (diskResult) { this.memoryCache.set(key, diskResult.data); return diskResult.data; } // Load from source const data = await loader(); // Store in both caches this.memoryCache.set(key, data); await this.diskCache.set(key, data); return data; } async invalidate(pattern: string): Promise<void> { // Invalidate memory cache for (const key of this.memoryCache.keys()) { if (key.match(pattern)) { this.memoryCache.delete(key); } } // Invalidate disk cache await this.diskCache.invalidatePattern(pattern); } } ``` ## Security Architecture ### API Key Management ```typescript export class ApiKeyManager { private keyStore: Map<string, ApiKey> = new Map(); private rotationScheduler: RotationScheduler; constructor(private config: SecurityConfig) { this.rotationScheduler = new RotationScheduler(config.keyRotation); } async getApiKey(service: string): Promise<string> { const apiKey = this.keyStore.get(service); if (!apiKey) { throw new SecurityError(`API key not found for service: ${service}`); } if (this.isKeyExpired(apiKey)) { throw new SecurityError(`API key expired for service: ${service}`); } return apiKey.value; } async rotateKey(service: string): Promise<void> { const newKey = await this.generateNewKey(service); this.keyStore.set(service, newKey); logger.info(`API key rotated for service: ${service}`); } private isKeyExpired(apiKey: ApiKey): boolean { return Date.now() > apiKey.expiresAt; } } ``` ### Rate Limiting Implementation ```typescript export class RateLimiter { private windows = new Map<string, RateLimitWindow>(); constructor(private config: RateLimitConfig) {} async checkLimit(key: string, action: string): Promise<RateLimitResult> { const windowKey = `${key}:${action}`; const now = Date.now(); let window = this.windows.get(windowKey); if (!window || now > window.resetAt) { window = { count: 0, resetAt: now + this.config.windowSize, maxRequests: this.config.actions[action]?.maxRequests || this.config.default.maxRequests }; this.windows.set(windowKey, window); } if (window.count >= window.maxRequests) { return { allowed: false, remainingRequests: 0, resetAt: window.resetAt, retryAfter: window.resetAt - now }; } window.count++; return { allowed: true, remainingRequests: window.maxRequests - window.count, resetAt: window.resetAt, retryAfter: 0 }; } } ``` ### Input Validation ```typescript export class InputValidator { static validateDomain(domain: string): void { try { const url = new URL(domain.startsWith('http') ? domain : `https://${domain}`); if (!url.hostname) { throw new ValidationError('Invalid domain: missing hostname'); } if (url.hostname.length > 253) { throw new ValidationError('Invalid domain: hostname too long'); } if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(url.hostname)) { throw new ValidationError('Invalid domain: invalid characters'); } } catch (error) { if (error instanceof ValidationError) { throw error; } throw new ValidationError(`Invalid domain format: ${domain}`); } } static validateContentParameters(params: ContentParameters): void { if (params.wordCount && (params.wordCount < 100 || params.wordCount > 5000)) { throw new ValidationError('Word count must be between 100 and 5000'); } if (params.keywords && params.keywords.length > 20) { throw new ValidationError('Maximum 20 keywords allowed'); } if (params.title && params.title.length > 200) { throw new ValidationError('Title must be 200 characters or less'); } } } ``` ## Deployment Architecture ### Local Deployment Local deployment utilizes stdio transport for direct integration with Claude Code and other MCP-compatible clients. ```typescript // Local configuration export const localConfig: ServerConfig = { server: { mode: 'local', transport: 'stdio' }, claude: { apiKey: process.env.CLAUDE_API_KEY!, model: 'claude-3-5-sonnet-latest', rateLimits: { requestsPerMinute: 60, tokensPerHour: 100000 } }, postgen: { dataDir: '.postgen', cacheEnabled: true, cacheTtl: 3600000 // 1 hour }, logging: { level: 'info', format: 'simple' } }; ``` ### Remote Deployment Remote deployment supports HTTP transport with SSE for real-time communication and CORS configuration for web clients. ```typescript // Remote configuration export const remoteConfig: ServerConfig = { server: { mode: 'remote', transport: 'http', port: parseInt(process.env.PORT || '3000', 10), host: process.env.HOST || '0.0.0.0' }, cors: { allowedOrigins: process.env.ALLOWED_ORIGINS?.split(',') || ['*'], allowedHeaders: ['mcp-session-id', 'content-type', 'authorization'], credentials: true }, security: { enableRateLimit: true, rateLimits: { windowMs: 60000, // 1 minute maxRequests: 100, keyGenerator: (req) => req.ip }, enableCors: true, trustProxy: true } }; ``` ### Container Deployment ```dockerfile # Multi-stage Docker build for optimized production images FROM node:18-alpine AS builder WORKDIR /app # Install dependencies COPY package*.json ./ RUN npm ci --only=production && npm cache clean --force # Copy source and build COPY . . RUN npm run build # Production stage FROM node:18-alpine AS production # Security hardening RUN addgroup -g 1001 -S nodejs && \ adduser -S mcpserver -u 1001 -G nodejs WORKDIR /app # Copy production dependencies and built application COPY --from=builder --chown=mcpserver:nodejs /app/node_modules ./node_modules COPY --from=builder --chown=mcpserver:nodejs /app/dist ./dist COPY --from=builder --chown=mcpserver:nodejs /app/package*.json ./ # Create data directory RUN mkdir -p /data/.postgen && \ chown -R mcpserver:nodejs /data # Security: run as non-root user USER mcpserver # Environment configuration ENV NODE_ENV=production ENV MCP_MODE=remote ENV MCP_PORT=3000 ENV POSTGEN_DATA_DIR=/data/.postgen # Health check HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 EXPOSE 3000 CMD ["node", "dist/index.js"] ``` ### Docker Compose Configuration ```yaml version: '3.8' services: marketing-post-generator: build: context: . target: production ports: - "${PORT:-3000}:3000" environment: - NODE_ENV=production - MCP_MODE=remote - MCP_PORT=3000 - CLAUDE_API_KEY=${CLAUDE_API_KEY} - POSTGEN_DATA_DIR=/data/.postgen - LOG_LEVEL=${LOG_LEVEL:-info} volumes: - postgen-data:/data/.postgen - logs:/app/logs healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s restart: unless-stopped networks: - mcp-network deploy: resources: limits: memory: 512M cpus: '0.5' reservations: memory: 256M cpus: '0.25' # Optional: Monitoring stack prometheus: image: prom/prometheus:latest ports: - "9090:9090" volumes: - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.path=/prometheus' - '--web.console.libraries=/etc/prometheus/console_libraries' - '--web.console.templates=/etc/prometheus/consoles' networks: - mcp-network volumes: postgen-data: driver: local logs: driver: local prometheus-data: driver: local networks: mcp-network: driver: bridge ``` ## API Design ### MCP Tools Specification #### Initialize Tool ```typescript { name: 'marketing_post_generator_mcp__init', description: 'Initialize the Marketing Post Generator with a blog domain', parameters: { type: 'object', properties: { domain: { type: 'string', description: 'The domain/URL of the main blog page', pattern: '^https?://[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*' } }, required: ['domain'] } } ``` #### Sample Tool ```typescript { name: 'marketing_post_generator_mcp__sample', description: 'Sample blog posts from a domain to extract positioning, tone, and content strategy', parameters: { type: 'object', properties: { domain: { type: 'string', description: 'The domain to sample from' }, sampleSize: { type: 'number', description: 'Number of posts to sample', minimum: 1, maximum: 20, default: 5 }, contentTypes: { type: 'array', items: { type: 'string' }, description: 'Types of content to include', default: ['blog', 'article'] } }, required: ['domain'] } } ``` #### Content Plan Tool ```typescript { name: 'marketing_post_generator_mcp__content_plan', description: 'Create or update a content plan for future blog posts', parameters: { type: 'object', properties: { domain: { type: 'string', description: 'The domain to create a content plan for' }, timeframe: { type: 'string', enum: ['week', 'month', 'quarter'], description: 'Timeframe for the content plan', default: 'month' }, postCount: { type: 'number', description: 'Number of posts to plan', minimum: 1, maximum: 50, default: 4 }, updateExisting: { type: 'boolean', description: 'Whether to update an existing plan', default: false }, focusAreas: { type: 'array', items: { type: 'string' }, description: 'Specific focus areas or topics' } }, required: ['domain'] } } ``` #### Write Post Tool ```typescript { name: 'marketing_post_generator_mcp__write_post', description: 'Write a complete blog post from scratch or based on a narrative', parameters: { type: 'object', properties: { narrativeId: { type: 'string', description: 'ID of the narrative to base the post on' }, title: { type: 'string', description: 'Title of the blog post', maxLength: 200 }, topic: { type: 'string', description: 'Topic of the blog post' }, keywords: { type: 'array', items: { type: 'string' }, description: 'Keywords for the blog post', maxItems: 20 }, wordCount: { type: 'number', description: 'Target word count for the post', minimum: 100, maximum: 5000, default: 1000 }, style: { type: 'string', enum: ['informative', 'persuasive', 'storytelling', 'technical', 'conversational'], description: 'Writing style for the post', default: 'informative' } }, anyOf: [ { required: ['narrativeId'] }, { required: ['title', 'topic'] } ] } } ``` ### MCP Prompts Specification #### Initialize Prompt ```typescript { name: 'marketing_post_generator_mcp__init', description: 'Initialize the Marketing Post Generator workspace', arguments: [ { name: 'domain', description: 'The blog domain to initialize with', required: true } ] } ``` #### Quick Sample Prompt ```typescript { name: 'marketing_post_generator_mcp__quick_sample', description: 'Quickly sample a domain and provide analysis summary', arguments: [ { name: 'domain', description: 'The domain to sample', required: true }, { name: 'count', description: 'Number of posts to sample', required: false } ] } ``` ### Response Formats ```typescript // Standard success response interface SuccessResponse<T = any> { success: true; data: T; metadata: { requestId: string; processingTime: number; timestamp: string; }; } // Standard error response interface ErrorResponse { success: false; error: { code: string; message: string; details?: any; }; metadata: { requestId: string; timestamp: string; }; } // Streaming response (for long-running operations) interface StreamResponse { type: 'progress' | 'data' | 'complete' | 'error'; payload: any; metadata: { requestId: string; timestamp: string; progress?: number; }; } ``` ## Error Handling & Resilience ### Error Hierarchy ```typescript // Base error class export abstract class BaseError extends Error { abstract readonly code: string; abstract readonly statusCode: number; constructor( message: string, public readonly context?: any, public readonly cause?: Error ) { super(message); this.name = this.constructor.name; Error.captureStackTrace(this, this.constructor); } toJSON() { return { name: this.name, code: this.code, message: this.message, context: this.context, stack: this.stack }; } } // Specific error types export class ValidationError extends BaseError { readonly code = 'VALIDATION_ERROR'; readonly statusCode = 400; } export class ContentGenerationError extends BaseError { readonly code = 'CONTENT_GENERATION_ERROR'; readonly statusCode = 500; } export class StorageError extends BaseError { readonly code = 'STORAGE_ERROR'; readonly statusCode = 500; } export class RateLimitError extends BaseError { readonly code = 'RATE_LIMIT_ERROR'; readonly statusCode = 429; } export class ServiceUnavailableError extends BaseError { readonly code = 'SERVICE_UNAVAILABLE'; readonly statusCode = 503; } ``` ### Circuit Breaker Implementation ```typescript export class CircuitBreaker { private state: CircuitBreakerState = CircuitBreakerState.CLOSED; private failureCount = 0; private lastFailureTime = 0; private nextAttemptTime = 0; constructor(private config: CircuitBreakerConfig) {} async execute<T>(operation: () => Promise<T>): Promise<T> { if (this.state === CircuitBreakerState.OPEN) { if (Date.now() < this.nextAttemptTime) { throw new ServiceUnavailableError('Circuit breaker is open'); } this.state = CircuitBreakerState.HALF_OPEN; } try { const result = await operation(); this.onSuccess(); return result; } catch (error) { this.onFailure(); throw error; } } private onSuccess(): void { this.failureCount = 0; this.state = CircuitBreakerState.CLOSED; } private onFailure(): void { this.failureCount++; this.lastFailureTime = Date.now(); if (this.failureCount >= this.config.failureThreshold) { this.state = CircuitBreakerState.OPEN; this.nextAttemptTime = Date.now() + this.config.timeout; } } isOpen(): boolean { return this.state === CircuitBreakerState.OPEN; } } ``` ### Retry Logic with Exponential Backoff ```typescript export class RetryHandler { async execute<T>( operation: () => Promise<T>, config: RetryConfig = DEFAULT_RETRY_CONFIG ): Promise<T> { let lastError: Error; for (let attempt = 1; attempt <= config.maxAttempts; attempt++) { try { return await operation(); } catch (error) { lastError = error; // Don't retry for certain error types if (!this.shouldRetry(error, attempt, config)) { throw error; } if (attempt < config.maxAttempts) { const delay = this.calculateDelay(attempt, config); await this.sleep(delay); } } } throw new Error(`Operation failed after ${config.maxAttempts} attempts: ${lastError.message}`); } private shouldRetry(error: Error, attempt: number, config: RetryConfig): boolean { // Don't retry validation errors if (error instanceof ValidationError) { return false; } // Don't retry rate limit errors (handle them differently) if (error instanceof RateLimitError) { return false; } // Don't retry if we've hit max attempts if (attempt >= config.maxAttempts) { return false; } // Check if error type is retryable return config.retryableErrors.some(errorType => error instanceof errorType); } private calculateDelay(attempt: number, config: RetryConfig): number { const exponentialDelay = config.baseDelay * Math.pow(2, attempt - 1); const jitteredDelay = exponentialDelay * (0.5 + Math.random() * 0.5); return Math.min(jitteredDelay, config.maxDelay); } private sleep(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } } ``` ### Comprehensive Error Handling Middleware ```typescript export class ErrorHandlingMiddleware { constructor( private logger: Logger, private metrics: MetricsCollector ) {} async handleError(error: Error, context: ErrorContext): Promise<ErrorResponse> { const requestId = context.requestId || crypto.randomUUID(); const timestamp = new Date().toISOString(); // Log the error this.logger.error('Error occurred', { requestId, error: error.message, stack: error.stack, context }); // Update metrics this.metrics.incrementErrorCounter({ errorType: error.constructor.name, tool: context.toolName, statusCode: this.getStatusCode(error) }); // Determine response based on error type if (error instanceof BaseError) { return { success: false, error: { code: error.code, message: this.sanitizeErrorMessage(error.message), details: error.context }, metadata: { requestId, timestamp } }; } // Handle unexpected errors this.logger.error('Unexpected error', { requestId, error: error.message, stack: error.stack }); return { success: false, error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred' }, metadata: { requestId, timestamp } }; } private sanitizeErrorMessage(message: string): string { // Remove sensitive information from error messages return message .replace(/api[_-]?key[s]?[:\s]*[a-zA-Z0-9]+/gi, 'api_key:***') .replace(/token[s]?[:\s]*[a-zA-Z0-9]+/gi, 'token:***') .replace(/password[s]?[:\s]*[^\s]+/gi, 'password:***'); } private getStatusCode(error: Error): number { if (error instanceof BaseError) { return error.statusCode; } return 500; } } ``` ## Performance & Scalability ### Caching Strategy ```typescript export class PerformanceOptimizer { private memoryCache: LRUCache<string, any>; private compressionCache: CompressionCache; constructor(config: PerformanceConfig) { this.memoryCache = new LRUCache({ maxSize: config.memoryCacheSize, ttl: config.memoryCacheTtl, updateAgeOnGet: true }); this.compressionCache = new CompressionCache({ compressionLevel: config.compressionLevel, threshold: config.compressionThreshold }); } async getCachedContent( key: string, generator: () => Promise<any>, options: CacheOptions = {} ): Promise<any> { const cacheKey = this.buildCacheKey(key, options); // Check memory cache first const memoryResult = this.memoryCache.get(cacheKey); if (memoryResult) { return memoryResult; } // Check compression cache for larger items if (options.useCompression) { const compressedResult = await this.compressionCache.get(cacheKey); if (compressedResult) { this.memoryCache.set(cacheKey, compressedResult); return compressedResult; } } // Generate content const content = await generator(); // Store in appropriate cache this.memoryCache.set(cacheKey, content); if (options.useCompression && this.shouldCompress(content)) { await this.compressionCache.set(cacheKey, content); } return content; } private buildCacheKey(key: string, options: CacheOptions): string { const hash = crypto.createHash('md5'); hash.update(key); hash.update(JSON.stringify(options)); return hash.digest('hex'); } private shouldCompress(content: any): boolean { const size = JSON.stringify(content).length; return size > this.compressionCache.threshold; } } ``` ### Connection Pooling ```typescript export class ConnectionPool { private pool: Connection[] = []; private activeConnections = 0; private waitingQueue: Array<{ resolve: (connection: Connection) => void; reject: (error: Error) => void; }> = []; constructor(private config: PoolConfig) { this.initializePool(); } async acquire(): Promise<Connection> { // Check for available connection const availableConnection = this.pool.find(conn => !conn.inUse); if (availableConnection) { availableConnection.inUse = true; this.activeConnections++; return availableConnection; } // Create new connection if under limit if (this.pool.length < this.config.maxConnections) { const connection = await this.createConnection(); this.pool.push(connection); connection.inUse = true; this.activeConnections++; return connection; } // Wait for available connection return new Promise((resolve, reject) => { const timeout = setTimeout(() => { const index = this.waitingQueue.findIndex(item => item.resolve === resolve); if (index !== -1) { this.waitingQueue.splice(index, 1); } reject(new Error('Connection acquisition timeout')); }, this.config.acquireTimeout); this.waitingQueue.push({ resolve: (connection) => { clearTimeout(timeout); resolve(connection); }, reject: (error) => { clearTimeout(timeout); reject(error); } }); }); } release(connection: Connection): void { connection.inUse = false; this.activeConnections--; // Serve waiting requests if (this.waitingQueue.length > 0) { const waiter = this.waitingQueue.shift()!; connection.inUse = true; this.activeConnections++; waiter.resolve(connection); } } async destroy(): Promise<void> { // Close all connections await Promise.all(this.pool.map(conn => conn.close())); this.pool = []; this.activeConnections = 0; // Reject waiting requests this.waitingQueue.forEach(waiter => { waiter.reject(new Error('Connection pool destroyed')); }); this.waitingQueue = []; } getStats(): PoolStats { return { totalConnections: this.pool.length, activeConnections: this.activeConnections, availableConnections: this.pool.length - this.activeConnections, waitingRequests: this.waitingQueue.length }; } } ``` ### Memory Management ```typescript export class MemoryManager { private gcScheduler: NodeJS.Timeout; private memoryThreshold: number; constructor(config: MemoryConfig) { this.memoryThreshold = config.threshold; this.scheduleGarbageCollection(config.gcInterval); this.setupMemoryMonitoring(); } private scheduleGarbageCollection(interval: number): void { this.gcScheduler = setInterval(() => { const usage = process.memoryUsage(); const heapUsedMB = usage.heapUsed / 1024 / 1024; if (heapUsedMB > this.memoryThreshold) { logger.i