marketing-post-generator-mcp
Version:
A powerful MCP server for AI-powered marketing blog post generation with Claude integration
453 lines • 19.9 kB
JavaScript
// Core MCP Server implementation for Marketing Post Generator
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { ListPromptsRequestSchema, GetPromptRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
import { DIContainer } from './container/DIContainer.js';
import { createLogger } from '../utils/logger.js';
import { ClaudeService } from '../services/claude/index.js';
import { createSearchService, registerBuiltInAdapters, } from '../services/search/index.js';
import { InitPrompt } from '../prompts/index.js';
import { SampleTool, SummarizeTool, GenerateToneTool, ContentPlanTool, NarrativeGeneratorTool, BlogPostGeneratorTool, } from '../tools/index.js';
import { RegistryFactory, } from './registry/index.js';
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import { randomUUID } from 'crypto';
export class MarketingPostGeneratorServer {
config;
mcpServer;
container;
logger;
httpServer;
httpTransport;
registry;
registryFactory;
prompts = new Map();
tools = new Map();
constructor(config) {
this.config = config;
this.logger = createLogger(config.logging);
this.container = new DIContainer();
}
async initialize() {
await this.initializeDependencies();
await this.initializeMCPServer();
}
async initializeDependencies() {
// Register core services with the DI container
this.container.register('Logger', () => this.logger);
this.container.register('Config', () => this.config);
// Register Claude service
this.container.register('ClaudeService', () => {
return new ClaudeService(this.config.claude);
});
// Register built-in search adapters
await registerBuiltInAdapters();
// Register SearchService
this.container.register('SearchService', () => {
// Create SearchService from configuration - this will be initialized asynchronously
throw new Error('SearchService must be initialized asynchronously');
});
// Initialize SearchService asynchronously
const searchService = await createSearchService(this.config.search, this.logger);
this.container.register('SearchService', () => searchService);
// Initialize Registry Factory and Registry
await this.initializeRegistry();
this.logger.info('Dependencies initialized', {
registeredServices: this.container.getRegisteredTokens(),
});
}
async initializeRegistry() {
try {
// Create registry factory configuration
const registryConfig = {
registryConfig: {
validateOnRegister: true,
allowDuplicateNames: false,
enforceVersioning: true,
maxRetries: 3,
enableLogging: true,
namePrefix: 'marketing_post_generator_mcp__',
},
loggerConfig: {
level: this.config.logging?.level || 'info',
},
enableValidation: true,
enableVersioning: true,
enableDiscovery: true,
};
// Initialize registry factory
this.registryFactory = await RegistryFactory.createWithConfig(this.container, registryConfig);
this.registry = this.registryFactory.getRegistry();
this.logger.info('Registry system initialized successfully');
}
catch (error) {
this.logger.error('Failed to initialize registry system', { error });
throw error;
}
}
getClaudeService() {
return this.container.resolve('ClaudeService');
}
getRegistry() {
return this.registry;
}
async initializeMCPServer() {
// Create transport based on configuration
const transport = this.createTransport();
// Initialize MCP server
this.mcpServer = new Server({
name: 'marketing-post-generator-mcp',
version: process.env.npm_package_version || '1.0.0',
}, {
capabilities: {
tools: {},
prompts: {},
},
});
// Register prompts and tools
await this.registerPrompts();
await this.registerTools();
// Connect transport
void this.mcpServer.connect(transport);
this.logger.info('MCP Server initialized', {
mode: this.config.server.mode,
transport: this.config.server.transport,
registryEnabled: !!this.registry,
});
}
createTransport() {
switch (this.config.server.mode) {
case 'local':
this.logger.info('Creating stdio transport for local mode');
return new StdioServerTransport();
case 'remote':
this.logger.info('Creating HTTP transport for remote mode');
return this.createHttpTransport();
default:
throw new Error(`Unsupported server mode: ${this.config.server.mode}`);
}
}
createHttpTransport() {
const httpConfig = this.config.server.http || {};
// Create the HTTP transport with configuration
const transportOptions = {
sessionIdGenerator: httpConfig.sessionIdGenerator || (() => randomUUID()),
enableJsonResponse: httpConfig.enableJsonResponse || false,
enableDnsRebindingProtection: httpConfig.enableDnsRebindingProtection || false,
onsessioninitialized: (sessionId) => {
this.logger.info('New MCP session initialized', { sessionId });
},
};
// Only add allowedHosts and allowedOrigins if they are defined
if (httpConfig.allowedHosts) {
transportOptions.allowedHosts = httpConfig.allowedHosts;
}
if (httpConfig.allowedOrigins) {
transportOptions.allowedOrigins = httpConfig.allowedOrigins;
}
this.httpTransport = new StreamableHTTPServerTransport(transportOptions);
// Set up Express server for HTTP transport
this.setupHttpServer();
return this.httpTransport;
}
setupHttpServer() {
this.httpServer = express();
// Security middleware
this.httpServer.use(helmet({
contentSecurityPolicy: false, // Disable CSP for MCP compatibility
}));
// CORS configuration
if (this.config.cors) {
this.httpServer.use(cors({
origin: this.config.cors.allowedOrigins,
allowedHeaders: this.config.cors.allowedHeaders,
credentials: this.config.cors.credentials,
}));
}
// Body parsing middleware
this.httpServer.use(express.json({ limit: '10mb' }));
// Health check endpoint
this.httpServer.get('/health', (_req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
server: 'marketing-post-generator-mcp',
version: process.env.npm_package_version || '1.0.0',
});
});
// MCP endpoint - handle all MCP requests
this.httpServer.all('/mcp', async (req, res) => {
if (!this.httpTransport) {
res.status(500).json({ error: 'HTTP transport not initialized' });
return;
}
try {
await this.httpTransport.handleRequest(req, res, req.body);
}
catch (error) {
this.logger.error('Error handling MCP request', {
error: error instanceof Error ? error.message : String(error),
});
if (!res.headersSent) {
res.status(500).json({ error: 'Internal server error' });
}
}
});
this.logger.info('HTTP server configured with MCP endpoint');
}
async start() {
try {
this.logger.info('Starting Marketing Post Generator MCP Server...');
if (this.config.server.mode === 'remote' && this.httpServer) {
// Start HTTP server for remote mode
const port = this.config.server.port || 3000;
const host = this.config.server.host || '0.0.0.0';
await new Promise((resolve, reject) => {
const server = this.httpServer.listen(port, host, () => {
this.logger.info('HTTP server started', {
port,
host,
healthCheck: `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}/health`,
mcpEndpoint: `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}/mcp`,
});
resolve();
});
server.on('error', (error) => {
this.logger.error('Failed to start HTTP server', { error });
reject(error);
});
});
}
else {
// Local mode with stdio transport starts automatically when connected
this.logger.info('Server is ready to accept connections via stdio');
}
this.logger.info('Marketing Post Generator MCP Server started successfully', {
mode: this.config.server.mode,
transport: this.config.server.transport,
});
}
catch (error) {
this.logger.error('Failed to start server', { error });
throw error;
}
}
async registerPrompts() {
try {
// Register all prompt instances
const promptInstances = [
new InitPrompt(),
// Add more prompts here as they're implemented
];
// Store prompts in legacy registry for O(1) lookup (backward compatibility)
promptInstances.forEach((prompt) => {
this.prompts.set(prompt.getPromptName(), prompt);
});
// Register prompts with the new registry system
if (this.registry) {
for (const prompt of promptInstances) {
const promptDefinition = prompt.createPrompt();
await this.registry.registerPrompt(promptDefinition, async (args) => await this.executePrompt(prompt, promptDefinition.name, args), {
version: { major: 1, minor: 0, patch: 0 },
author: 'Marketing Post Generator MCP',
tags: ['prompt', 'marketing', 'content'],
createdAt: new Date(),
updatedAt: new Date(),
});
}
}
// Create prompt definitions for MCP registration (use registry if available)
const promptDefinitions = this.registry
? this.registry.getPromptDefinitions()
: promptInstances.map((prompt) => prompt.createPrompt());
this.mcpServer.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: promptDefinitions,
};
});
this.mcpServer.setRequestHandler(GetPromptRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
// Try registry first, fallback to legacy method
if (this.registry) {
const registryEntry = this.registry.getPrompt(name);
if (registryEntry) {
const result = await this.registry.executePrompt(name, args);
return {
description: registryEntry.description,
arguments: registryEntry.promptDefinition.parameters,
messages: [
{
role: 'user',
content: {
type: 'text',
text: result,
},
},
],
};
}
}
// Fallback to legacy method
const promptFactory = this.prompts.get(name);
if (!promptFactory) {
throw new Error(`Unknown prompt: ${name}`);
}
const promptDefinition = promptFactory.createPrompt();
// Validate arguments based on prompt requirements
this.validatePromptArguments(name, args);
// Execute the prompt
const result = await this.executePrompt(promptFactory, name, args);
return {
description: promptDefinition.description,
arguments: promptDefinition.parameters,
messages: [
{
role: 'user',
content: {
type: 'text',
text: result,
},
},
],
};
});
this.logger.info('Prompts registered successfully', {
registeredPrompts: promptDefinitions.map((p) => p.name),
});
}
catch (error) {
this.logger.error('Failed to register prompts', { error });
throw error;
}
}
async registerTools() {
try {
// Get services from DI container
const searchService = this.container.resolve('SearchService');
const logger = this.container.resolve('Logger');
// Register all tool instances with dependency injection
const toolInstances = [
new SampleTool(searchService, logger),
new SummarizeTool(searchService, logger),
new GenerateToneTool(searchService, logger),
new ContentPlanTool(searchService, logger),
new NarrativeGeneratorTool(searchService, logger),
new BlogPostGeneratorTool(searchService, logger),
// Add more tools here as they're implemented
];
// Store tools in legacy registry for O(1) lookup (backward compatibility)
toolInstances.forEach((tool) => {
this.tools.set(tool.getToolDefinition().name, tool);
});
// Register tools with the new registry system
if (this.registry) {
for (const tool of toolInstances) {
const toolDefinition = tool.getToolDefinition();
await this.registry.registerTool(toolDefinition, async (args) => {
const claudeService = this.getClaudeService();
return await tool.execute(args, claudeService);
}, {
version: { major: 1, minor: 0, patch: 0 },
author: 'Marketing Post Generator MCP',
tags: ['tool', 'marketing', 'content'],
createdAt: new Date(),
updatedAt: new Date(),
});
}
}
// Create tool definitions for MCP registration (use registry if available)
const toolDefinitions = this.registry
? this.registry.getToolDefinitions()
: toolInstances.map((tool) => tool.getToolDefinition());
this.mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: toolDefinitions,
};
});
this.mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
// Try registry first, fallback to legacy method
if (this.registry) {
const registryEntry = this.registry.getTool(name);
if (registryEntry) {
const result = await this.registry.executeTool(name, args);
return {
content: [
{
type: 'text',
text: result,
},
],
};
}
}
// Fallback to legacy method
const tool = this.tools.get(name);
if (!tool) {
throw new Error(`Unknown tool: ${name}`);
}
// Get Claude service for tool execution
const claudeService = this.getClaudeService();
// TODO: Add runtime validation using the tool's input schema
// const toolDefinition = tool.getToolDefinition();
// Execute the tool with validated arguments
const result = await tool.execute(args, claudeService);
return {
content: [
{
type: 'text',
text: result,
},
],
};
});
this.logger.info('Tools registered successfully', {
registeredTools: toolDefinitions.map((t) => t.name),
});
}
catch (error) {
this.logger.error('Failed to register tools', { error });
throw error;
}
}
validatePromptArguments(name, args) {
if (name === 'init') {
if (!args ||
typeof args !== 'object' ||
!('domain' in args) ||
typeof args.domain !== 'string') {
throw new Error('Init prompt requires a "domain" argument');
}
}
// Add validation for other prompts as they're implemented
}
async executePrompt(promptFactory, name, args) {
if (name === 'init') {
const initPrompt = promptFactory;
return await initPrompt.executePrompt(args);
}
// Add execution logic for other prompts as they're implemented
throw new Error(`Prompt execution not implemented for: ${name}`);
}
async stop() {
try {
this.logger.info('Stopping Marketing Post Generator MCP Server...');
// Close MCP server
await this.mcpServer.close();
// Close HTTP transport if it exists
if (this.httpTransport) {
await this.httpTransport.close();
}
// Note: Express server doesn't need explicit shutdown for our use case
// since we're using the listen() callback pattern
this.logger.info('Server stopped successfully');
}
catch (error) {
this.logger.error('Error during server shutdown', { error });
throw error;
}
}
}
//# sourceMappingURL=MarketingPostGeneratorServer.js.map