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/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 +} 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");