From f69dc557a39695ece600001ee0e54319ae5154ba Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Thu, 25 Dec 2025 07:08:14 -0500 Subject: [PATCH 1/2] Add --ssl-reject-unauthorized flag for trusting self signed certs --- readme.md | 1 + src/index.ts | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 24e8991..cd5cc7c 100644 --- a/readme.md +++ b/readme.md @@ -90,6 +90,7 @@ Optional parameters: - `--password`: Password for PostgreSQL authentication - `--port`: Port number (default: 5432) - `--ssl`: Enable SSL connection (true/false) +- `--ssl-reject-unauthorized`: Reject unauthorized SSL certificates (true/false, default: true). Set to `false` to accept self-signed certificates. - `--connection-timeout`: Connection timeout in milliseconds (default: 30000) ### MySQL Database diff --git a/src/index.ts b/src/index.ts index 9bf7fda..d955456 100644 --- a/src/index.ts +++ b/src/index.ts @@ -44,7 +44,7 @@ if (args.length === 0) { logger.error("Please provide database connection information"); logger.error("Usage for SQLite: node index.js "); logger.error("Usage for SQL Server: node index.js --sqlserver --server --database [--user --password ]"); - logger.error("Usage for PostgreSQL: node index.js --postgresql --host --database [--user --password --port ]"); + logger.error("Usage for PostgreSQL: node index.js --postgresql --host --database [--user --password --port --ssl true --ssl-reject-unauthorized false]"); logger.error("Usage for MySQL: node index.js --mysql --host --database [--user --password --port ]"); logger.error("Usage for MySQL with AWS IAM: node index.js --mysql --aws-iam-auth --host --database --user --aws-region "); process.exit(1); @@ -95,9 +95,10 @@ else if (args.includes('--postgresql') || args.includes('--postgres')) { password: undefined, port: undefined, ssl: undefined, + sslRejectUnauthorized: undefined, connectionTimeout: undefined }; - + // Parse PostgreSQL connection parameters for (let i = 0; i < args.length; i++) { if (args[i] === '--host' && i + 1 < args.length) { @@ -112,11 +113,19 @@ else if (args.includes('--postgresql') || args.includes('--postgres')) { connectionInfo.port = parseInt(args[i + 1], 10); } else if (args[i] === '--ssl' && i + 1 < args.length) { connectionInfo.ssl = args[i + 1] === 'true'; + } else if (args[i] === '--ssl-reject-unauthorized' && i + 1 < args.length) { + connectionInfo.sslRejectUnauthorized = args[i + 1] === 'true'; } else if (args[i] === '--connection-timeout' && i + 1 < args.length) { connectionInfo.connectionTimeout = parseInt(args[i + 1], 10); } } - + + // Build SSL configuration object if needed + if (connectionInfo.ssl && connectionInfo.sslRejectUnauthorized === false) { + connectionInfo.ssl = { rejectUnauthorized: false }; + logger.info("SSL enabled with self-signed certificate support (rejectUnauthorized: false)"); + } + // Validate PostgreSQL connection info if (!connectionInfo.host || !connectionInfo.database) { logger.error("Error: PostgreSQL requires --host and --database parameters"); From 40f6ba66ac0816e762aa9ff524ac3dc0b04d8329 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Sun, 8 Mar 2026 01:03:02 -0500 Subject: [PATCH 2/2] Fix PostgreSQL connection leak: switch Client to Pool with timeouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: pg.Client holds a single connection open forever with no idle timeout, no statement timeout, and no lifecycle management. Every MCP tool invocation that touches the DB opens a connection that never closes, eventually exhausting all server connection slots. Fix: - Replace pg.Client with pg.Pool (max: 1) so idle connections are reaped after 10s via idleTimeoutMillis - Add statement_timeout (30s) to kill runaway queries — prevents the multi-hour zombie SELECT/INSERT queries seen in production - Add idle_in_transaction_session_timeout (60s) to kill abandoned transactions (stuck ROLLBACK/COMMIT) - Add pool error handler to prevent silent crashes - Verify connectivity on init with SELECT 1 + proper client.release() --- src/db/postgresql-adapter.ts | 121 ++++++++++++++++++++--------------- 1 file changed, 70 insertions(+), 51 deletions(-) diff --git a/src/db/postgresql-adapter.ts b/src/db/postgresql-adapter.ts index 6e95cf1..138ca46 100644 --- a/src/db/postgresql-adapter.ts +++ b/src/db/postgresql-adapter.ts @@ -1,14 +1,25 @@ import { DbAdapter } from "./adapter.js"; import pg from 'pg'; +// Default timeouts (in milliseconds) +const DEFAULT_STATEMENT_TIMEOUT_MS = 30_000; // 30 s — kills runaway queries +const DEFAULT_IDLE_IN_TRANSACTION_TIMEOUT_MS = 60_000; // 60 s — kills forgotten transactions +const DEFAULT_IDLE_TIMEOUT_MS = 10_000; // 10 s — release idle pool connections +const DEFAULT_CONNECTION_TIMEOUT_MS = 30_000; // 30 s — give up connecting + /** * PostgreSQL database adapter implementation + * + * Uses pg.Pool (max 1) instead of a bare pg.Client so that: + * - idle connections are reaped after idleTimeoutMillis + * - statement_timeout prevents queries from running forever + * - idle_in_transaction_session_timeout kills abandoned transactions */ export class PostgresqlAdapter implements DbAdapter { - private client: pg.Client | null = null; - private config: pg.ClientConfig; + private pool: pg.Pool | null = null; private host: string; private database: string; + private poolConfig: pg.PoolConfig; constructor(connectionInfo: { host: string; @@ -19,41 +30,63 @@ export class PostgresqlAdapter implements DbAdapter { ssl?: boolean | object; options?: any; connectionTimeout?: number; + statementTimeout?: number; + idleTimeout?: number; }) { this.host = connectionInfo.host; this.database = connectionInfo.database; - - // Create PostgreSQL connection config - this.config = { + + const statementTimeout = connectionInfo.statementTimeout || DEFAULT_STATEMENT_TIMEOUT_MS; + + this.poolConfig = { host: connectionInfo.host, database: connectionInfo.database, port: connectionInfo.port || 5432, user: connectionInfo.user, password: connectionInfo.password, ssl: connectionInfo.ssl, - // Add connection timeout if provided (in milliseconds) - connectionTimeoutMillis: connectionInfo.connectionTimeout || 30000, - }; + // Single connection — MCP server is single-threaded + max: 1, + connectionTimeoutMillis: connectionInfo.connectionTimeout || DEFAULT_CONNECTION_TIMEOUT_MS, + idleTimeoutMillis: connectionInfo.idleTimeout || DEFAULT_IDLE_TIMEOUT_MS, + // Server-side timeouts applied to every connection + statement_timeout: statementTimeout, + idle_in_transaction_session_timeout: DEFAULT_IDLE_IN_TRANSACTION_TIMEOUT_MS, + } as pg.PoolConfig; } /** - * Initialize PostgreSQL connection + * Initialize PostgreSQL connection pool */ async init(): Promise { try { console.error(`[INFO] Connecting to PostgreSQL: ${this.host}, Database: ${this.database}`); - console.error(`[DEBUG] Connection details:`, { - host: this.host, + console.error(`[DEBUG] Pool config:`, { + host: this.host, database: this.database, - port: this.config.port, - user: this.config.user, - connectionTimeoutMillis: this.config.connectionTimeoutMillis, - ssl: !!this.config.ssl + port: this.poolConfig.port, + user: this.poolConfig.user, + max: this.poolConfig.max, + connectionTimeoutMillis: this.poolConfig.connectionTimeoutMillis, + idleTimeoutMillis: this.poolConfig.idleTimeoutMillis, + ssl: !!this.poolConfig.ssl, + }); + + this.pool = new pg.Pool(this.poolConfig); + + // Log pool errors instead of crashing + this.pool.on('error', (err) => { + console.error('[ERROR] Unexpected pool client error:', err.message); }); - - this.client = new pg.Client(this.config); - await this.client.connect(); - console.error(`[INFO] PostgreSQL connection established successfully`); + + // Verify connectivity + const client = await this.pool.connect(); + try { + await client.query('SELECT 1'); + } finally { + client.release(); + } + console.error(`[INFO] PostgreSQL connection pool ready`); } catch (err) { console.error(`[ERROR] PostgreSQL connection error: ${(err as Error).message}`); throw new Error(`Failed to connect to PostgreSQL: ${(err as Error).message}`); @@ -62,20 +95,15 @@ export class PostgresqlAdapter implements DbAdapter { /** * Execute a SQL query and get all results - * @param query SQL query to execute - * @param params Query parameters - * @returns Promise with query results */ async all(query: string, params: any[] = []): Promise { - if (!this.client) { + if (!this.pool) { throw new Error("Database not initialized"); } try { - // PostgreSQL uses $1, $2, etc. for parameterized queries const preparedQuery = query.replace(/\?/g, (_, i) => `$${i + 1}`); - - const result = await this.client.query(preparedQuery, params); + const result = await this.pool.query(preparedQuery, params); return result.rows; } catch (err) { throw new Error(`PostgreSQL query error: ${(err as Error).message}`); @@ -84,37 +112,31 @@ export class PostgresqlAdapter implements DbAdapter { /** * Execute a SQL query that modifies data - * @param query SQL query to execute - * @param params Query parameters - * @returns Promise with result info */ async run(query: string, params: any[] = []): Promise<{ changes: number, lastID: number }> { - if (!this.client) { + if (!this.pool) { throw new Error("Database not initialized"); } try { - // Replace ? with numbered parameters const preparedQuery = query.replace(/\?/g, (_, i) => `$${i + 1}`); - + let lastID = 0; let changes = 0; - - // For INSERT queries, try to get the inserted ID + if (query.trim().toUpperCase().startsWith('INSERT')) { - // Add RETURNING clause to get the inserted ID if it doesn't already have one - const returningQuery = preparedQuery.includes('RETURNING') - ? preparedQuery + const returningQuery = preparedQuery.includes('RETURNING') + ? preparedQuery : `${preparedQuery} RETURNING id`; - - const result = await this.client.query(returningQuery, params); + + const result = await this.pool.query(returningQuery, params); changes = result.rowCount || 0; lastID = result.rows[0]?.id || 0; } else { - const result = await this.client.query(preparedQuery, params); + const result = await this.pool.query(preparedQuery, params); changes = result.rowCount || 0; } - + return { changes, lastID }; } catch (err) { throw new Error(`PostgreSQL query error: ${(err as Error).message}`); @@ -123,28 +145,26 @@ export class PostgresqlAdapter implements DbAdapter { /** * Execute multiple SQL statements - * @param query SQL statements to execute - * @returns Promise that resolves when execution completes */ async exec(query: string): Promise { - if (!this.client) { + if (!this.pool) { throw new Error("Database not initialized"); } try { - await this.client.query(query); + await this.pool.query(query); } catch (err) { throw new Error(`PostgreSQL batch error: ${(err as Error).message}`); } } /** - * Close the database connection + * Close the connection pool — releases all connections back to the server */ async close(): Promise { - if (this.client) { - await this.client.end(); - this.client = null; + if (this.pool) { + await this.pool.end(); + this.pool = null; } } @@ -169,7 +189,6 @@ export class PostgresqlAdapter implements DbAdapter { /** * Get database-specific query for describing a table - * @param tableName Table name */ getDescribeTableQuery(tableName: string): string { return ` @@ -194,4 +213,4 @@ export class PostgresqlAdapter implements DbAdapter { c.ordinal_position `; } -} \ No newline at end of file +}