Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
121 changes: 70 additions & 51 deletions src/db/postgresql-adapter.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<void> {
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}`);
Expand All @@ -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<any[]> {
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}`);
Expand All @@ -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}`);
Expand All @@ -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<void> {
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<void> {
if (this.client) {
await this.client.end();
this.client = null;
if (this.pool) {
await this.pool.end();
this.pool = null;
}
}

Expand All @@ -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 `
Expand All @@ -194,4 +213,4 @@ export class PostgresqlAdapter implements DbAdapter {
c.ordinal_position
`;
}
}
}
15 changes: 12 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ if (args.length === 0) {
logger.error("Please provide database connection information");
logger.error("Usage for SQLite: node index.js <database_file_path>");
logger.error("Usage for SQL Server: node index.js --sqlserver --server <server> --database <database> [--user <user> --password <password>]");
logger.error("Usage for PostgreSQL: node index.js --postgresql --host <host> --database <database> [--user <user> --password <password> --port <port>]");
logger.error("Usage for PostgreSQL: node index.js --postgresql --host <host> --database <database> [--user <user> --password <password> --port <port> --ssl true --ssl-reject-unauthorized false]");
logger.error("Usage for MySQL: node index.js --mysql --host <host> --database <database> [--user <user> --password <password> --port <port>]");
logger.error("Usage for MySQL with AWS IAM: node index.js --mysql --aws-iam-auth --host <rds-endpoint> --database <database> --user <aws-username> --aws-region <region>");
process.exit(1);
Expand Down Expand Up @@ -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) {
Expand All @@ -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");
Expand Down