import { MCPServer } from './server/mcp-server.js';
import { Logger } from './utils/logger.js';
import { DatabaseConnection } from './database/connection.js';
import { BillingClient } from './billing/billing-client.js';
import { AuthManager } from './auth/auth-manager.js';
interface ServerConfig {
authEnabled: boolean;
logLevel: string;
maxConcurrentRequests: number;
requestTimeoutMs: number;
sessionCleanupIntervalMinutes: number;
healthCheckPort: number;
}
class Application {
private logger: Logger;
private server?: MCPServer;
private db?: DatabaseConnection;
private config: ServerConfig;
private isShuttingDown: boolean = false;
constructor() {
this.logger = Logger.getInstance();
this.config = this.loadConfiguration();
}
private loadConfiguration(): ServerConfig {
const config: ServerConfig = {
authEnabled: process.env.AUTH_ENABLED !== 'false',
logLevel: process.env.LOG_LEVEL || 'info',
maxConcurrentRequests: parseInt(process.env.MAX_CONCURRENT_REQUESTS || '10'),
requestTimeoutMs: parseInt(process.env.REQUEST_TIMEOUT_MS || '30000'),
sessionCleanupIntervalMinutes: parseInt(process.env.SESSION_CLEANUP_INTERVAL_MINUTES || '60'),
healthCheckPort: 0 // Disabled for Claude Desktop use
};
this.logger.info('Configuration loaded', { config: { ...config, authEnabled: config.authEnabled } });
return config;
}
private async validateStartupRequirements(): Promise<void> {
this.logger.info('Validating startup requirements');
// Check environment variables
const requiredEnvVars = [];
const optionalEnvVars = [
'JWT_SECRET',
'GOOGLE_CLIENT_ID',
'GOOGLE_CLIENT_SECRET',
'GOOGLE_REDIRECT_URI',
'ADMIN_EMAILS',
'AWS_ACCESS_KEY_ID',
'AWS_SECRET_ACCESS_KEY',
'AWS_REGION'
];
if (this.config.authEnabled) {
requiredEnvVars.push('GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET');
}
const missingRequired = requiredEnvVars.filter(envVar => !process.env[envVar]);
if (missingRequired.length > 0) {
throw new Error(`Missing required environment variables: ${missingRequired.join(', ')}`);
}
const missingOptional = optionalEnvVars.filter(envVar => !process.env[envVar]);
if (missingOptional.length > 0) {
this.logger.warn('Missing optional environment variables', { missing: missingOptional });
}
// Test database connection
try {
this.db = await DatabaseConnection.getInstance();
await this.db.get('SELECT 1 as test');
this.logger.info('Database connection validated');
} catch (error: any) {
throw new Error(`Database connection failed: ${error.message}`);
}
// Validate AWS credentials if provided (for Claude Desktop use case)
this.logger.debug('Checking for AWS credentials in environment', {
hasAccessKey: !!process.env.AWS_ACCESS_KEY_ID,
hasSecretKey: !!process.env.AWS_SECRET_ACCESS_KEY
});
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
try {
// Simple validation using STS GetCallerIdentity
const { STSClient, GetCallerIdentityCommand } = await import('@aws-sdk/client-sts');
const stsClient = new STSClient({
region: process.env.AWS_REGION || 'us-east-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
...(process.env.AWS_SESSION_TOKEN && { sessionToken: process.env.AWS_SESSION_TOKEN })
}
});
const identityCommand = new GetCallerIdentityCommand({});
const identityResponse = await stsClient.send(identityCommand);
this.logger.warn('AWS credentials validated successfully', {
accountId: identityResponse.Account,
userId: identityResponse.UserId
});
} catch (error: any) {
this.logger.warn('AWS credentials validation failed - will use mock data', {
error: error.message
});
}
} else {
this.logger.warn('No AWS credentials provided - will use mock data only');
}
// Test billing client initialization
try {
await BillingClient.getInstance();
this.logger.info('Billing client initialized');
} catch (error: any) {
this.logger.warn('Billing client initialization warning', { error: error.message });
}
// Test auth manager if enabled
if (this.config.authEnabled) {
try {
await AuthManager.getInstance();
this.logger.info('Authentication manager initialized');
} catch (error: any) {
throw new Error(`Authentication manager initialization failed: ${error.message}`);
}
}
this.logger.info('All startup requirements validated');
}
// Health check endpoint removed for Claude Desktop use case
// MCP servers communicate via stdio, no HTTP endpoint needed
public async start(): Promise<void> {
try {
this.logger.info('Starting AWS Billing MCP Server', {
version: '1.0.0',
nodeVersion: process.version,
config: this.config
});
// Validate startup requirements
await this.validateStartupRequirements();
// Initialize and start MCP server (no health check endpoint needed for Claude Desktop)
this.server = new MCPServer(this.config.authEnabled);
await this.server.start();
this.logger.info('AWS Billing MCP Server started successfully', {
authEnabled: this.config.authEnabled,
mode: 'Claude Desktop MCP'
});
} catch (error: any) {
this.logger.error('Failed to start server', { error: error.message, stack: error.stack });
throw error;
}
}
public async shutdown(): Promise<void> {
if (this.isShuttingDown) {
return;
}
this.isShuttingDown = true;
this.logger.info('Initiating graceful shutdown');
try {
// Close database connection
if (this.db) {
await this.db.close();
this.logger.info('Database connection closed');
}
// Clean up any other resources
this.logger.info('Graceful shutdown completed');
} catch (error: any) {
this.logger.error('Error during shutdown', { error: error.message });
}
}
}
const app = new Application();
async function main(): Promise<void> {
try {
await app.start();
} catch (error: any) {
// Don't use console.error as it interferes with MCP protocol
process.stderr.write(`Failed to start application: ${error.message}\n`);
process.exit(1);
}
}
// Handle graceful shutdown
const handleShutdown = async (signal: string) => {
// Don't use console.log as it interferes with MCP protocol
process.stderr.write(`Received ${signal}, shutting down gracefully\n`);
try {
await app.shutdown();
process.exit(0);
} catch (error: any) {
process.stderr.write(`Error during shutdown: ${error.message}\n`);
process.exit(1);
}
};
process.on('SIGINT', () => handleShutdown('SIGINT'));
process.on('SIGTERM', () => handleShutdown('SIGTERM'));
// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
process.stderr.write(`Unhandled Rejection at: ${promise}, reason: ${reason}\n`);
process.exit(1);
});
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
process.stderr.write(`Uncaught Exception: ${error}\n`);
process.exit(1);
});
main().catch((error) => {
process.stderr.write(`Unhandled error in main: ${error}\n`);
process.exit(1);
});