From bd819d923f6a710a0cbc221c69157709fc35afaf Mon Sep 17 00:00:00 2001 From: Karthik KK Date: Tue, 22 Apr 2025 08:16:20 +1200 Subject: [PATCH 1/2] Add PostgreSQL support to MCP Database Server Updated package.json and package-lock.json to include PostgreSQL dependencies. Enhanced README with PostgreSQL usage instructions and configuration details. Modified index.ts to handle PostgreSQL connection parameters and logging. Added PostgresqlAdapter for database interactions. --- docs/postgresql-setup.md | 107 ++++++++++++++++ package-lock.json | 238 +++++++++++++++++++++++++++++++++++ package.json | 2 + readme.md | 45 ++++++- src/db/adapter.ts | 4 + src/db/postgresql-adapter.ts | 185 +++++++++++++++++++++++++++ src/index.ts | 38 ++++++ 7 files changed, 618 insertions(+), 1 deletion(-) create mode 100644 docs/postgresql-setup.md create mode 100644 src/db/postgresql-adapter.ts diff --git a/docs/postgresql-setup.md b/docs/postgresql-setup.md new file mode 100644 index 0000000..fc1691c --- /dev/null +++ b/docs/postgresql-setup.md @@ -0,0 +1,107 @@ +# PostgreSQL Setup for MCP Database Server + +This document describes how to set up and use the PostgreSQL adapter with the MCP Database Server. + +## Prerequisites + +1. You need to have PostgreSQL installed and running on your system or on a remote server. +2. Ensure the pg package is installed: + +``` +npm install pg +npm install @types/pg --save-dev +``` + +## Running the Server with PostgreSQL + +To connect to a PostgreSQL database, use the following command-line arguments: + +```bash +# Basic connection +node dist/src/index.js --postgresql --host localhost --database yourdb --user postgres --password yourpassword + +# With custom port (default is 5432) +node dist/src/index.js --postgresql --host localhost --database yourdb --user postgres --password yourpassword --port 5433 + +# With SSL enabled +node dist/src/index.js --postgresql --host localhost --database yourdb --user postgres --password yourpassword --ssl true +``` + +## Command Line Arguments + +- `--postgresql` or `--postgres`: Specifies that you want to connect to a PostgreSQL database. +- `--host`: The hostname or IP address of the PostgreSQL server (required). +- `--database`: The name of the database to connect to (required). +- `--user`: The PostgreSQL user to authenticate as. +- `--password`: The password for the PostgreSQL user. +- `--port`: The port the PostgreSQL server is listening on (default: 5432). +- `--ssl`: Whether to use SSL for the connection (true/false). + +## Usage from MCP Client + +The MCP client can interact with a PostgreSQL database using the same tools that are available for SQLite and SQL Server. The server automatically translates the generic SQL queries to PostgreSQL-specific formats. + +## Supported Features + +- Full SQL query support for SELECT, INSERT, UPDATE, and DELETE operations. +- Table management (CREATE TABLE, ALTER TABLE, DROP TABLE). +- Schema introspection. +- Connection pooling for efficient database access. +- SSL support for secure connections. + +## Examples + +### Create a Table + +```sql +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + username VARCHAR(50) NOT NULL, + email VARCHAR(100) UNIQUE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +### Insert Data + +```sql +INSERT INTO users (username, email) VALUES ('johndoe', 'john@example.com'); +``` + +### Query Data + +```sql +SELECT * FROM users WHERE username = 'johndoe'; +``` + +## Limitations + +- For the `run` method with INSERT statements, the adapter attempts to retrieve the last inserted ID by adding a RETURNING clause. This assumes your tables have an 'id' column. +- Complex stored procedures or PostgreSQL-specific features might require custom implementation. + +## Troubleshooting + +### Connection Issues + +If you're having trouble connecting to your PostgreSQL database: + +1. Verify that PostgreSQL is running: `pg_isready -h localhost -p 5432` +2. Check that your credentials are correct. +3. Ensure that the database exists and the user has appropriate permissions. +4. Check firewall settings if connecting to a remote database. + +### Query Errors + +If your queries are failing: + +1. Check the syntax against PostgreSQL's SQL dialect. +2. Verify table and column names. +3. Check that the user has proper permissions for the operations. + +## Performance Considerations + +For optimal performance: + +1. Use parameterized queries to prevent SQL injection and improve query caching. +2. Consider indexing frequently queried columns. +3. For large result sets, use LIMIT and OFFSET for pagination. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d8cf92b..b2e3e04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@modelcontextprotocol/sdk": "1.9.0", "mssql": "11.0.1", + "pg": "^8.11.3", "sqlite3": "5.1.7" }, "bin": { @@ -18,6 +19,7 @@ }, "devDependencies": { "@types/mssql": "^9.1.5", + "@types/pg": "^8.11.13", "@types/sqlite3": "5.1.0", "rimraf": "^5.0.5", "shx": "0.4.0", @@ -492,6 +494,17 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/pg": { + "version": "8.11.13", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.13.tgz", + "integrity": "sha512-6kXByGkvRvwXLuyaWzsebs2du6+XuAB2CuMsuzP7uaihQahshVgSmB22Pmh0vQMkQ1h5+PZU0d+Di1o+WpVWJg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + } + }, "node_modules/@types/readable-stream": { "version": "4.0.18", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.18.tgz", @@ -737,6 +750,14 @@ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "engines": { + "node": ">=4" + } + }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -2718,6 +2739,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -2784,6 +2811,11 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2839,6 +2871,151 @@ "node": ">=16" } }, + "node_modules/pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-pool": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.8.0.tgz", + "integrity": "sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.8.0.tgz", + "integrity": "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==" + }, + "node_modules/pg-types": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", + "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", + "dev": true, + "dependencies": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.1.0", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pg/node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg/node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/pg/node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pg/node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pg/node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -2859,6 +3036,51 @@ "node": ">=16.20.0" } }, + "node_modules/postgres-array": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", + "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dev": true, + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postgres-date": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", + "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-range": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", + "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", + "dev": true + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -3430,6 +3652,14 @@ "node": ">= 10" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -4038,6 +4268,14 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index 7e40462..4903364 100644 --- a/package.json +++ b/package.json @@ -25,10 +25,12 @@ "dependencies": { "@modelcontextprotocol/sdk": "1.9.0", "mssql": "11.0.1", + "pg": "^8.11.3", "sqlite3": "5.1.7" }, "devDependencies": { "@types/mssql": "^9.1.5", + "@types/pg": "^8.11.13", "@types/sqlite3": "5.1.0", "rimraf": "^5.0.5", "shx": "0.4.0", diff --git a/readme.md b/readme.md index 7c96f9a..6eacbba 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # MCP Database Server -This MCP (Model Context Protocol) server provides database access capabilities to Claude, supporting both SQLite and SQL Server databases. +This MCP (Model Context Protocol) server provides database access capabilities to Claude, supporting SQLite, SQL Server, and PostgreSQL databases. ## Installation @@ -71,6 +71,24 @@ Optional parameters: - `--password`: Password for SQL Server authentication - `--port`: Port number (default: 1433) +### PostgreSQL Database + +To use with a PostgreSQL database: + +``` +node dist/src/index.js --postgresql --host --database [--user --password ] +``` + +Required parameters: +- `--host`: PostgreSQL host name or IP address +- `--database`: Name of the database + +Optional parameters: +- `--user`: Username for PostgreSQL authentication +- `--password`: Password for PostgreSQL authentication +- `--port`: Port number (default: 5432) +- `--ssl`: Enable SSL connection (true/false) + ## Configuring Claude Desktop ### Direct Usage Configuration @@ -99,6 +117,18 @@ If you installed the package globally, configure Claude Desktop with: "--user", "your-username", "--password", "your-password" ] + }, + "postgresql": { + "command": "npx", + "args": [ + "-y", + "@executeautomation/database-server", + "--postgresql", + "--host", "your-host-name", + "--database", "your-database-name", + "--user", "your-username", + "--password", "your-password" + ] } } } @@ -128,6 +158,17 @@ For local development, configure Claude Desktop to use your locally built versio "--user", "your-username", "--password", "your-password" ] + }, + "postgresql": { + "command": "node", + "args": [ + "/absolute/path/to/mcp-database-server/dist/src/index.js", + "--postgresql", + "--host", "your-host-name", + "--database", "your-database-name", + "--user", "your-username", + "--password", "your-password" + ] } } } @@ -160,6 +201,7 @@ For practical examples of how to use these tools with Claude, see [Usage Example ## Additional Documentation - [SQL Server Setup Guide](docs/sql-server-setup.md): Details on connecting to SQL Server databases +- [PostgreSQL Setup Guide](docs/postgresql-setup.md): Details on connecting to PostgreSQL databases - [Usage Examples](docs/usage-examples.md): Example queries and commands to use with Claude ## Development @@ -180,6 +222,7 @@ npm run watch - Node.js 18+ - For SQL Server connectivity: SQL Server 2012 or later +- For PostgreSQL connectivity: PostgreSQL 9.5 or later ## License diff --git a/src/db/adapter.ts b/src/db/adapter.ts index 280c0c0..78fbb4e 100644 --- a/src/db/adapter.ts +++ b/src/db/adapter.ts @@ -53,6 +53,7 @@ export interface DbAdapter { // Import adapters using dynamic imports import { SqliteAdapter } from './sqlite-adapter.js'; import { SqlServerAdapter } from './sqlserver-adapter.js'; +import { PostgresqlAdapter } from './postgresql-adapter.js'; /** * Factory function to create the appropriate database adapter @@ -68,6 +69,9 @@ export function createDbAdapter(type: string, connectionInfo: any): DbAdapter { } case 'sqlserver': return new SqlServerAdapter(connectionInfo); + case 'postgresql': + case 'postgres': + return new PostgresqlAdapter(connectionInfo); default: throw new Error(`Unsupported database type: ${type}`); } diff --git a/src/db/postgresql-adapter.ts b/src/db/postgresql-adapter.ts new file mode 100644 index 0000000..1b68297 --- /dev/null +++ b/src/db/postgresql-adapter.ts @@ -0,0 +1,185 @@ +import { DbAdapter } from "./adapter.js"; +import pg from 'pg'; + +/** + * PostgreSQL database adapter implementation + */ +export class PostgresqlAdapter implements DbAdapter { + private client: pg.Client | null = null; + private config: pg.ClientConfig; + private host: string; + private database: string; + + constructor(connectionInfo: { + host: string; + database: string; + user?: string; + password?: string; + port?: number; + ssl?: boolean | object; + options?: any; + }) { + this.host = connectionInfo.host; + this.database = connectionInfo.database; + + // Create PostgreSQL connection config + this.config = { + host: connectionInfo.host, + database: connectionInfo.database, + port: connectionInfo.port || 5432, + user: connectionInfo.user, + password: connectionInfo.password, + ssl: connectionInfo.ssl + }; + } + + /** + * Initialize PostgreSQL connection + */ + async init(): Promise { + try { + console.error(`[INFO] Connecting to PostgreSQL: ${this.host}, Database: ${this.database}`); + this.client = new pg.Client(this.config); + await this.client.connect(); + console.error(`[INFO] PostgreSQL connection established successfully`); + } catch (err) { + console.error(`[ERROR] PostgreSQL connection error: ${(err as Error).message}`); + throw new Error(`Failed to connect to PostgreSQL: ${(err as Error).message}`); + } + } + + /** + * 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) { + 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); + return result.rows; + } catch (err) { + throw new Error(`PostgreSQL query error: ${(err as Error).message}`); + } + } + + /** + * 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) { + 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 + : `${preparedQuery} RETURNING id`; + + const result = await this.client.query(returningQuery, params); + changes = result.rowCount || 0; + lastID = result.rows[0]?.id || 0; + } else { + const result = await this.client.query(preparedQuery, params); + changes = result.rowCount || 0; + } + + return { changes, lastID }; + } catch (err) { + throw new Error(`PostgreSQL query error: ${(err as Error).message}`); + } + } + + /** + * 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) { + throw new Error("Database not initialized"); + } + + try { + await this.client.query(query); + } catch (err) { + throw new Error(`PostgreSQL batch error: ${(err as Error).message}`); + } + } + + /** + * Close the database connection + */ + async close(): Promise { + if (this.client) { + await this.client.end(); + this.client = null; + } + } + + /** + * Get database metadata + */ + getMetadata(): { name: string, type: string, server: string, database: string } { + return { + name: "PostgreSQL", + type: "postgresql", + server: this.host, + database: this.database + }; + } + + /** + * Get database-specific query for listing tables + */ + getListTablesQuery(): string { + return "SELECT table_name as name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name"; + } + + /** + * Get database-specific query for describing a table + * @param tableName Table name + */ + getDescribeTableQuery(tableName: string): string { + return ` + SELECT + c.column_name as name, + c.data_type as type, + CASE WHEN c.is_nullable = 'NO' THEN 1 ELSE 0 END as notnull, + CASE WHEN pk.constraint_name IS NOT NULL THEN 1 ELSE 0 END as pk, + c.column_default as dflt_value + FROM + information_schema.columns c + LEFT JOIN + information_schema.key_column_usage kcu + ON c.table_name = kcu.table_name AND c.column_name = kcu.column_name + LEFT JOIN + information_schema.table_constraints pk + ON kcu.constraint_name = pk.constraint_name AND pk.constraint_type = 'PRIMARY KEY' + WHERE + c.table_name = '${tableName}' + AND c.table_schema = 'public' + ORDER BY + c.ordinal_position + `; + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 6f0048c..5da0fee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -44,6 +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 ]"); process.exit(1); } @@ -81,6 +82,41 @@ if (args.includes('--sqlserver')) { logger.error("Error: SQL Server requires --server and --database parameters"); process.exit(1); } +} +// Check if using PostgreSQL +else if (args.includes('--postgresql') || args.includes('--postgres')) { + dbType = 'postgresql'; + connectionInfo = { + host: '', + database: '', + user: undefined, + password: undefined, + port: undefined, + ssl: undefined + }; + + // Parse PostgreSQL connection parameters + for (let i = 0; i < args.length; i++) { + if (args[i] === '--host' && i + 1 < args.length) { + connectionInfo.host = args[i + 1]; + } else if (args[i] === '--database' && i + 1 < args.length) { + connectionInfo.database = args[i + 1]; + } else if (args[i] === '--user' && i + 1 < args.length) { + connectionInfo.user = args[i + 1]; + } else if (args[i] === '--password' && i + 1 < args.length) { + connectionInfo.password = args[i + 1]; + } else if (args[i] === '--port' && i + 1 < args.length) { + connectionInfo.port = parseInt(args[i + 1], 10); + } else if (args[i] === '--ssl' && i + 1 < args.length) { + connectionInfo.ssl = args[i + 1] === 'true'; + } + } + + // Validate PostgreSQL connection info + if (!connectionInfo.host || !connectionInfo.database) { + logger.error("Error: PostgreSQL requires --host and --database parameters"); + process.exit(1); + } } else { // SQLite mode (default) dbType = 'sqlite'; @@ -137,6 +173,8 @@ async function runServer() { logger.info(`Database path: ${connectionInfo}`); } else if (dbType === 'sqlserver') { logger.info(`Server: ${connectionInfo.server}, Database: ${connectionInfo.database}`); + } else if (dbType === 'postgresql') { + logger.info(`Host: ${connectionInfo.host}, Database: ${connectionInfo.database}`); } // Initialize the database From c71779fcb52565b3228adb6702dc1680f1cc8ae9 Mon Sep 17 00:00:00 2001 From: Karthik KK Date: Tue, 22 Apr 2025 16:44:36 +1200 Subject: [PATCH 2/2] Add connection timeout support for PostgreSQL in MCP Database Server Enhanced the PostgreSQL connection handling by introducing a `--connection-timeout` parameter, allowing users to specify a custom timeout in milliseconds. Updated relevant documentation in README and postgresql-setup.md to reflect this new option. Modified the PostgresqlAdapter to utilize the connection timeout setting during database connections. --- .DS_Store | Bin 8196 -> 8196 bytes docs/postgresql-setup.md | 4 ++++ readme.md | 1 + src/.DS_Store | Bin 6148 -> 6148 bytes src/db/postgresql-adapter.ts | 14 +++++++++++++- src/index.ts | 5 ++++- 6 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.DS_Store b/.DS_Store index 615e03ba58b7bdaca96b2d0a9785bbdc197533a5..2cccfe87c9540f8c1af1eae5f18b0fba6669b55e 100644 GIT binary patch delta 493 zcmZp1XmQxELx6SPU3Sm7$$JH)A*{`J1-KYv@AWeJOa(KpF0|yFR7GW%=EV9`Qj0&5RL|3xY(C|e}0Es?` A2><{9 delta 493 zcmZp1XmQxELx8nIs`j(ju1+)MvfiM|af*;vPwmlCf ytB5=gUm6Z{G&3+riWriSO>~p_xYY*YWVkHCSWH=Dvl$o}HYbU$WT&Cwi*xDCguPD delta 127 zcmZoMXffFEj)loeaPkKh5o<-&g}>dd9dl%0U|?cMVMxl&PjN}g$xj0EInd<_G7N*0 z^K%P;azGF;*`76$?eZVClh>|Jp2w<$V2Vw?!|K82!Wy{{D5J$D17&XZVcQ`H0A-yl A5dZ)H diff --git a/src/db/postgresql-adapter.ts b/src/db/postgresql-adapter.ts index 1b68297..6e95cf1 100644 --- a/src/db/postgresql-adapter.ts +++ b/src/db/postgresql-adapter.ts @@ -18,6 +18,7 @@ export class PostgresqlAdapter implements DbAdapter { port?: number; ssl?: boolean | object; options?: any; + connectionTimeout?: number; }) { this.host = connectionInfo.host; this.database = connectionInfo.database; @@ -29,7 +30,9 @@ export class PostgresqlAdapter implements DbAdapter { port: connectionInfo.port || 5432, user: connectionInfo.user, password: connectionInfo.password, - ssl: connectionInfo.ssl + ssl: connectionInfo.ssl, + // Add connection timeout if provided (in milliseconds) + connectionTimeoutMillis: connectionInfo.connectionTimeout || 30000, }; } @@ -39,6 +42,15 @@ export class PostgresqlAdapter implements DbAdapter { async init(): Promise { try { console.error(`[INFO] Connecting to PostgreSQL: ${this.host}, Database: ${this.database}`); + console.error(`[DEBUG] Connection details:`, { + host: this.host, + database: this.database, + port: this.config.port, + user: this.config.user, + connectionTimeoutMillis: this.config.connectionTimeoutMillis, + ssl: !!this.config.ssl + }); + this.client = new pg.Client(this.config); await this.client.connect(); console.error(`[INFO] PostgreSQL connection established successfully`); diff --git a/src/index.ts b/src/index.ts index 5da0fee..6b29b06 100644 --- a/src/index.ts +++ b/src/index.ts @@ -92,7 +92,8 @@ else if (args.includes('--postgresql') || args.includes('--postgres')) { user: undefined, password: undefined, port: undefined, - ssl: undefined + ssl: undefined, + connectionTimeout: undefined }; // Parse PostgreSQL connection parameters @@ -109,6 +110,8 @@ 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] === '--connection-timeout' && i + 1 < args.length) { + connectionInfo.connectionTimeout = parseInt(args[i + 1], 10); } }