UNPKG

marketing-post-generator-mcp

Version:

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

246 lines 10.3 kB
import Anthropic from '@anthropic-ai/sdk'; import { createLogger } from '../../utils/logger.js'; const CLAUDE_MODELS = { DEFAULT: 'claude-3-sonnet-20240229', CHEAP: 'claude-3-haiku-20240307', }; export class ClaudeService { client; config; logger; rateLimiter; constructor(config) { this.config = config; this.logger = createLogger({ level: 'info', format: 'simple' }); this.client = new Anthropic({ apiKey: config.apiKey, ...(config.baseUrl && { baseURL: config.baseUrl }), ...(config.timeout && { timeout: config.timeout }), ...(config.maxRetries && { maxRetries: config.maxRetries }), }); this.rateLimiter = { requests: { count: 0, resetTime: Date.now() + 60000 }, tokens: { count: 0, resetTime: Date.now() + 60000 }, }; this.logger.info('Claude service initialized', { baseUrl: config.baseUrl, maxRetries: config.maxRetries, timeout: config.timeout, rateLimit: config.rateLimit, }); } async generateContent(prompt, options = {}) { const startTime = Date.now(); try { await this.checkRateLimit(); const createParams = { model: options.model || CLAUDE_MODELS.DEFAULT, max_tokens: options.maxTokens || 4096, messages: [ { role: 'user', content: prompt, }, ], }; if (options.temperature !== undefined) createParams.temperature = options.temperature; if (options.topK !== undefined) createParams.top_k = options.topK; if (options.topP !== undefined) createParams.top_p = options.topP; if (options.stopSequences !== undefined) createParams.stop_sequences = options.stopSequences; if (options.metadata !== undefined) createParams.metadata = options.metadata; const response = await this.client.messages.create(createParams); this.updateRateLimit(response.usage?.input_tokens || 0, response.usage?.output_tokens || 0); const duration = Date.now() - startTime; const content = response.content[0]?.type === 'text' ? response.content[0].text : ''; this.logger.debug('Content generated successfully', { model: response.model, inputTokens: response.usage?.input_tokens, outputTokens: response.usage?.output_tokens, duration, }); return { content, metadata: { model: response.model, usage: { promptTokens: response.usage?.input_tokens, completionTokens: response.usage?.output_tokens, totalTokens: (response.usage?.input_tokens || 0) + (response.usage?.output_tokens || 0), }, duration, }, }; } catch (error) { const duration = Date.now() - startTime; this.logger.error('Failed to generate content', { error: error instanceof Error ? error.message : String(error), duration, prompt: prompt.substring(0, 100) + '...', }); if (error instanceof Anthropic.APIError) { throw new Error(`Claude API error (${error.status}): ${error.message}`); } else if (error instanceof Anthropic.RateLimitError) { throw new Error('Claude API rate limit exceeded. Please try again later.'); } else if (error instanceof Anthropic.AuthenticationError) { throw new Error('Claude API authentication failed. Please check your API key.'); } else { throw new Error(`Claude service error: ${error instanceof Error ? error.message : String(error)}`); } } } async *streamContent(prompt, options = {}) { try { await this.checkRateLimit(); const streamParams = { model: options.model || CLAUDE_MODELS.DEFAULT, max_tokens: options.maxTokens || 4096, messages: [ { role: 'user', content: prompt, }, ], stream: true, }; if (options.temperature !== undefined) streamParams.temperature = options.temperature; if (options.topK !== undefined) streamParams.top_k = options.topK; if (options.topP !== undefined) streamParams.top_p = options.topP; if (options.stopSequences !== undefined) streamParams.stop_sequences = options.stopSequences; if (options.metadata !== undefined) streamParams.metadata = options.metadata; const stream = this.client.messages.stream(streamParams); let outputTokenCount = 0; try { for await (const chunk of stream) { if (chunk.type === 'content_block_delta' && chunk.delta.type === 'text_delta') { // Track output tokens using rough estimation (4 chars per token) outputTokenCount += Math.ceil(chunk.delta.text.length / 4); yield chunk.delta.text; } } } finally { // Estimate input tokens using rough heuristic (4 chars per token) const estimatedInputTokens = Math.ceil(prompt.length / 4); // Use actual tracked output tokens from stream this.updateRateLimit(estimatedInputTokens, outputTokenCount); } } catch (error) { this.logger.error('Failed to stream content', { error: error instanceof Error ? error.message : String(error), prompt: prompt.substring(0, 100) + '...', }); throw new Error(`Claude stream error: ${error instanceof Error ? error.message : String(error)}`); } } async isHealthy() { try { const response = await this.client.messages.create({ model: CLAUDE_MODELS.CHEAP, max_tokens: 10, messages: [ { role: 'user', content: 'Hello', }, ], }); return response.content.length > 0; } catch (error) { this.logger.warn('Claude health check failed', { error: error instanceof Error ? error.message : String(error), }); return false; } } async getRemainingQuota() { try { const now = Date.now(); if (now > this.rateLimiter.requests.resetTime) { this.rateLimiter.requests = { count: 0, resetTime: now + 60000 }; } if (now > this.rateLimiter.tokens.resetTime) { this.rateLimiter.tokens = { count: 0, resetTime: now + 60000 }; } return { requests: Math.max(0, (this.config.rateLimit?.requestsPerMinute || 60) - this.rateLimiter.requests.count), tokens: Math.max(0, (this.config.rateLimit?.tokensPerMinute || 50000) - this.rateLimiter.tokens.count), }; } catch (error) { this.logger.warn('Failed to get remaining quota', { error: error instanceof Error ? error.message : String(error), }); return null; } } async validateApiKey() { try { const response = await this.client.messages.create({ model: CLAUDE_MODELS.CHEAP, max_tokens: 5, messages: [ { role: 'user', content: 'Test', }, ], }); return response.content.length > 0; } catch (error) { if (error instanceof Anthropic.AuthenticationError) { return false; } this.logger.warn('API key validation failed with non-auth error', { error: error instanceof Error ? error.message : String(error), }); return false; } } async checkRateLimit() { const now = Date.now(); if (now > this.rateLimiter.requests.resetTime) { this.rateLimiter.requests = { count: 0, resetTime: now + 60000 }; } if (now > this.rateLimiter.tokens.resetTime) { this.rateLimiter.tokens = { count: 0, resetTime: now + 60000 }; } const maxRequests = this.config.rateLimit?.requestsPerMinute || 60; const maxTokens = this.config.rateLimit?.tokensPerMinute || 50_000; if (this.rateLimiter.requests.count >= maxRequests) { const waitTime = this.rateLimiter.requests.resetTime - now; throw new Error(`Rate limit exceeded. Please wait ${Math.ceil(waitTime / 1000)} seconds.`); } if (this.rateLimiter.tokens.count >= maxTokens) { const waitTime = this.rateLimiter.tokens.resetTime - now; throw new Error(`Token quota exceeded. Please wait ${Math.ceil(waitTime / 1000)} seconds.`); } } updateRateLimit(inputTokens, outputTokens) { this.rateLimiter.requests.count += 1; this.rateLimiter.tokens.count += inputTokens + outputTokens; this.logger.debug('Rate limit updated', { requestCount: this.rateLimiter.requests.count, tokenCount: this.rateLimiter.tokens.count, inputTokens, outputTokens, }); } } //# sourceMappingURL=ClaudeService.js.map