server.ts•5.9 kB
/**
* MCP Server setup for News Aggregator API
*/
// Load environment variables before any other imports
import dotenv from 'dotenv';
dotenv.config();
import express, { Express, Request, Response, NextFunction } from 'express';
import cors from 'cors';
import helmet from 'helmet';
import swaggerUi from 'swagger-ui-express';
import path from 'path';
import fs from 'fs';
import routes from './routes';
import { logger } from './utils/logger';
import { cacheService } from './utils/cache';
import { connectDb, disconnectDb } from './utils/db';
import { swaggerSpec } from './utils/swagger';
export class McpServer {
private app: Express;
private port: number;
public server: any; // HTTP server instance
constructor(port: number = 3000) {
this.app = express();
this.port = port;
this.configureMiddleware();
this.configureRoutes();
this.configureErrorHandling();
}
private configureMiddleware(): void {
// Security middleware
this.app.use(helmet());
// CORS setup
this.app.use(cors());
// Parse JSON bodies
this.app.use(express.json());
// Parse URL-encoded bodies
this.app.use(express.urlencoded({ extended: true }));
// Request logging
this.app.use((req: Request, _res: Response, next: NextFunction) => {
logger.info(`${req.method} ${req.url}`);
next();
});
}
private configureRoutes(): void {
// API routes
this.app.use('/api', routes);
// API documentation endpoints
this.app.use('/docs', swaggerUi.serve);
this.app.get('/docs', swaggerUi.setup(swaggerSpec, {
explorer: true,
customCss: '.swagger-ui .topbar { display: none }',
customSiteTitle: 'News Aggregator API Documentation',
}));
// OpenAPI specification JSON endpoint
this.app.get('/docs.json', (_req: Request, res: Response) => {
res.setHeader('Content-Type', 'application/json');
res.send(swaggerSpec);
});
// Documentation examples endpoint
this.app.get('/examples', (_req: Request, res: Response) => {
const examplesPath = path.join(__dirname, 'docs', 'examples.md');
fs.readFile(examplesPath, 'utf8', (err: NodeJS.ErrnoException | null, data: string) => {
if (err) {
logger.error('Error reading examples file:', err);
return res.status(500).json({
success: false,
error: 'Failed to load examples documentation'
});
}
res.setHeader('Content-Type', 'text/markdown');
res.send(data);
});
});
// Health check endpoint
this.app.get('/health', (_req: Request, res: Response) => {
res.status(200).json({ status: 'ok' });
});
// Handle 404s
this.app.use((_req: Request, res: Response) => {
res.status(404).json({
success: false,
error: 'Resource not found'
});
});
}
private configureErrorHandling(): void {
// Global error handler
this.app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
logger.error('Unhandled error:', err);
res.status(500).json({
success: false,
error: process.env.NODE_ENV === 'production'
? 'An unexpected error occurred'
: err.message
});
});
}
public async start(): Promise<void> {
try {
// Connect to the database
await connectDb();
const port = process.env.PORT || 3000;
this.server = this.app.listen(port, () => {
logger.info(`Server is running on port ${port}`);
});
// Set up graceful shutdown
process.on('SIGTERM', () => this.shutdown());
process.on('SIGINT', () => this.shutdown());
} catch (error) {
logger.error('Failed to start server:', error);
process.exit(1);
}
}
public async shutdown(): Promise<void> {
logger.info('Shutting down server...');
// Properly wait for the server to close using a Promise
if (this.server) {
// First, forcefully close all existing connections to prevent hanging
// This is especially important for tests where connections might still be active
if (this.server.closeAllConnections && typeof this.server.closeAllConnections === 'function') {
try {
this.server.closeAllConnections();
logger.info('Forcibly closed all existing connections');
} catch (error) {
logger.warn('Failed to close all connections:', error);
}
}
// Now wait for the server to fully close
await new Promise<void>((resolve) => {
this.server.close((err: Error | undefined) => {
if (err) {
logger.warn('Error during server close:', err);
} else {
logger.info('HTTP server closed successfully');
}
resolve(); // Resolve regardless of error to prevent hanging
});
});
this.server = null; // Clear the server reference
}
// Disconnect from database
await disconnectDb();
logger.info('Server shutdown complete');
// Only exit process in non-test environments
if (process.env.NODE_ENV !== 'test') {
process.exit(0);
}
}
/**
* Get the Express app instance for testing purposes
*/
public getApp(): Express {
return this.app;
}
}
// Create and export instances for tests
const mcpServer = new McpServer();
const app = mcpServer.getApp();
// Export the HTTP server for testing
import http from 'http';
export const server = http.createServer(app);
// Export the app for testing
export { app };
// Allow direct execution of server
if (require.main === module) {
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
mcpServer.start().then(() => {
// When running directly, mcpServer will create its own HTTP server
});
}