UNPKG

@henkey/postgres-mcp-server

Version:

A Model Context Protocol (MCP) server that provides comprehensive PostgreSQL database management capabilities for AI assistants

225 lines 7.69 kB
import pkg from 'pg'; import monitor from 'pg-monitor'; const { Pool } = pkg; // Enable pg-monitor for better debugging in development if (process.env.NODE_ENV !== 'production') { monitor.attach({ query: true, error: true, notice: true, connect: true, disconnect: true }); monitor.setTheme('matrix'); } // Connection pool cache to reuse connections const poolCache = new Map(); export class DatabaseConnection { static instance; pool = null; client = null; connectionString = ''; lastError = null; connectionOptions = {}; constructor() { } static getInstance() { if (!DatabaseConnection.instance) { DatabaseConnection.instance = new DatabaseConnection(); } return DatabaseConnection.instance; } /** * Connect to a PostgreSQL database */ async connect(connectionString, options = {}) { try { // Use environment variable if connection string is not provided const connString = connectionString || process.env.POSTGRES_CONNECTION_STRING; if (!connString) { throw new Error('No connection string provided and POSTGRES_CONNECTION_STRING environment variable is not set'); } // If already connected to this database, reuse the connection if (this.pool && this.connectionString === connString) { return; } // If connected to a different database, disconnect first if (this.pool) { await this.disconnect(); } this.connectionString = connString; this.connectionOptions = options; // Check if we have a cached pool for this connection string if (poolCache.has(connString)) { this.pool = poolCache.get(connString); } else { // Create a new pool const config = { connectionString: connString, max: options.maxConnections || 20, idleTimeoutMillis: options.idleTimeoutMillis || 30000, connectionTimeoutMillis: options.connectionTimeoutMillis || 2000, allowExitOnIdle: true, ssl: options.ssl }; this.pool = new Pool(config); // Set up error handler for the pool this.pool.on('error', (err) => { console.error('Unexpected error on idle client', err); this.lastError = err; }); // Cache the pool for future use poolCache.set(connString, this.pool); } // Test connection this.client = await this.pool.connect(); // Set statement timeout if specified if (options.statementTimeout) { await this.client.query(`SET statement_timeout = ${options.statementTimeout}`); } // Test the connection await this.client.query('SELECT 1'); } catch (error) { this.lastError = error instanceof Error ? error : new Error(String(error)); if (this.client) { this.client.release(); this.client = null; } if (this.pool) { // Remove from cache if connection failed poolCache.delete(this.connectionString); await this.pool.end(); this.pool = null; } throw new Error(`Failed to connect to database: ${this.lastError.message}`); } } /** * Disconnect from the database */ async disconnect() { if (this.client) { this.client.release(); this.client = null; } // Note: We don't end the pool here to allow connection reuse // The pool will be cleaned up when the application exits this.connectionString = ''; } /** * Execute a SQL query */ async query(text, values = [], options = {}) { if (!this.client || !this.pool) { throw new Error('Not connected to database'); } try { const queryConfig = { text, values }; // Set query timeout if specified if (options.timeout || this.connectionOptions.queryTimeout) { // We need to use a type assertion here because the pg types don't include timeout // but the library actually supports it queryConfig.timeout = options.timeout || this.connectionOptions.queryTimeout; } // Use type assertion only for the query call const result = await this.client.query(queryConfig); return result.rows; } catch (error) { this.lastError = error instanceof Error ? error : new Error(String(error)); throw new Error(`Query failed: ${this.lastError.message}`); } } /** * Execute a query that returns a single row */ async queryOne(text, values = [], options = {}) { const rows = await this.query(text, values, options); return rows.length > 0 ? rows[0] : null; } /** * Execute a query that returns a single value */ async queryValue(text, values = [], options = {}) { const rows = await this.query(text, values, options); if (rows.length > 0) { const firstRow = rows[0]; const firstValue = Object.values(firstRow)[0]; return firstValue; } return null; } /** * Execute multiple queries in a transaction */ async transaction(callback) { if (!this.client || !this.pool) { throw new Error('Not connected to database'); } try { await this.client.query('BEGIN'); const result = await callback(this.client); await this.client.query('COMMIT'); return result; } catch (error) { await this.client.query('ROLLBACK'); this.lastError = error instanceof Error ? error : new Error(String(error)); throw new Error(`Transaction failed: ${this.lastError.message}`); } } /** * Get the current connection pool */ getPool() { return this.pool; } /** * Get the current client */ getClient() { return this.client; } /** * Get the last error that occurred */ getLastError() { return this.lastError; } /** * Check if connected to database */ isConnected() { return this.pool !== null && this.client !== null; } /** * Get connection string (with password masked) */ getConnectionInfo() { if (!this.connectionString) { return 'Not connected'; } // Mask password in connection string return this.connectionString.replace(/password=([^&]*)/, 'password=*****'); } /** * Clean up all connection pools * Should be called when the application is shutting down */ static async cleanupPools() { for (const [connectionString, pool] of poolCache.entries()) { try { await pool.end(); poolCache.delete(connectionString); } catch (error) { console.error(`Error closing pool for ${connectionString}:`, error); } } } } //# sourceMappingURL=connection.js.map