/**
* Main entry point for PostgreSQL MCP Server Actor
*/
import { Actor, log } from 'apify';
import pg from 'pg';
import { ActorInput, NormalizedInput, DatabaseConfig } from './types.js';
import { createPool, testConnection, closePool } from './database.js';
import { startMCPServer } from './mcp-server.js';
/**
* Validate and normalize Actor input
*/
function normalizeInput(input: ActorInput | null): NormalizedInput {
if (!input) {
throw new Error('No input provided');
}
if (!input.connectionString) {
throw new Error('connectionString is required');
}
// Validate connection string format
if (!input.connectionString.startsWith('postgres://') &&
!input.connectionString.startsWith('postgresql://')) {
throw new Error(
'Invalid connection string format. Must start with postgresql:// or postgres://'
);
}
// Set defaults
const normalized: NormalizedInput = {
connectionString: input.connectionString,
allowedSchemas: input.allowedSchemas || ['public'],
maxQueryResults: input.maxQueryResults || 1000,
readOnly: input.readOnly !== undefined ? input.readOnly : true,
timeout: input.timeout || 30,
sslMode: input.sslMode || 'prefer',
};
// Validate maxQueryResults
if (normalized.maxQueryResults < 1) {
throw new Error('maxQueryResults must be at least 1');
}
if (normalized.maxQueryResults > 10000) {
throw new Error('maxQueryResults cannot exceed 10000');
}
// Validate timeout
if (normalized.timeout < 1) {
throw new Error('timeout must be at least 1 second');
}
if (normalized.timeout > 300) {
throw new Error('timeout cannot exceed 300 seconds');
}
// Validate allowedSchemas
if (!normalized.allowedSchemas || normalized.allowedSchemas.length === 0) {
throw new Error('allowedSchemas must contain at least one schema');
}
return normalized;
}
/**
* Create database configuration from normalized input
*/
function createDatabaseConfig(input: NormalizedInput): DatabaseConfig {
// Configure SSL based on sslMode
let ssl: boolean | { rejectUnauthorized: boolean };
switch (input.sslMode) {
case 'disable':
ssl = false;
break;
case 'require':
ssl = { rejectUnauthorized: true };
break;
case 'prefer':
default:
ssl = { rejectUnauthorized: false };
break;
}
return {
connectionString: input.connectionString,
allowedSchemas: input.allowedSchemas,
maxQueryResults: input.maxQueryResults,
readOnly: input.readOnly,
timeout: input.timeout,
ssl,
};
}
/**
* Main Actor function
*/
async function main(): Promise<void> {
// Initialize Actor
await Actor.init();
let pool: pg.Pool | null = null;
try {
log.info('PostgreSQL MCP Server Actor starting...');
// Get and validate input
const input = await Actor.getInput<ActorInput>();
log.info('Input received, validating...');
const normalizedInput = normalizeInput(input);
log.info('Input validated successfully', {
allowedSchemas: normalizedInput.allowedSchemas,
maxQueryResults: normalizedInput.maxQueryResults,
readOnly: normalizedInput.readOnly,
timeout: normalizedInput.timeout,
sslMode: normalizedInput.sslMode,
});
// Create database configuration
const dbConfig = createDatabaseConfig(normalizedInput);
// Create connection pool
log.info('Creating database connection pool...');
pool = createPool(dbConfig);
// Test database connection
log.info('Testing database connection...');
await testConnection(pool);
log.info('Database connection successful');
// Log initial configuration to dataset
await Actor.pushData({
event: 'actor_started',
timestamp: new Date().toISOString(),
configuration: {
allowedSchemas: normalizedInput.allowedSchemas,
maxQueryResults: normalizedInput.maxQueryResults,
readOnly: normalizedInput.readOnly,
timeout: normalizedInput.timeout,
sslMode: normalizedInput.sslMode,
},
});
// Start MCP server
log.info('Starting MCP server...');
await startMCPServer(pool, dbConfig);
// The server will run indefinitely handling stdio requests
// It will exit when stdin is closed
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
log.error('Actor failed:', { error: errorMessage });
// Log error to dataset
await Actor.pushData({
event: 'actor_error',
timestamp: new Date().toISOString(),
error: errorMessage,
success: false,
});
throw error;
} finally {
// Clean up: close database connection
if (pool) {
log.info('Closing database connection...');
try {
await closePool(pool);
log.info('Database connection closed');
} catch (error) {
const closeError = error instanceof Error ? error.message : 'Unknown error';
log.error('Error closing database connection:', { error: closeError });
}
}
// Exit Actor
await Actor.exit();
}
}
// Run the Actor
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});