marketing-post-generator-mcp
Version:
A powerful MCP server for AI-powered marketing blog post generation with Claude integration
246 lines • 10.3 kB
JavaScript
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