@sarmadparvez/postgresql-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +100 -0
  2. package/index.js +308 -0
  3. package/package.json +40 -0
package/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # postgresql-mcp
2
+
3
+ A reusable [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server for PostgreSQL with full read-write support.
4
+
5
+ ## Why this exists
6
+
7
+ There are two other PostgreSQL MCP servers worth knowing about, and neither fully covers the general-purpose use case:
8
+
9
+ **Anthropic's official [`@modelcontextprotocol/server-postgres`](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/postgres)** is strictly read-only — it exposes only a single `query` tool that runs inside a `READ ONLY` transaction. No writes, no DDL, no transactions. It was also deprecated and archived in July 2025.
10
+
11
+ **Microsoft's [`azure-postgresql-mcp`](https://github.com/Azure-Samples/azure-postgresql-mcp)** does support writes and DDL, but it is built specifically for Azure Database for PostgreSQL Flexible Server. While it technically accepts standard `PG*` environment variables (so a local connection may work), local use is untested and unsupported. Several tools require Microsoft Entra authentication and Azure-specific APIs that won't function outside Azure. It also pulls in Azure SDK dependencies you don't need for a non-Azure setup. It is currently in Preview.
12
+
13
+ This server is the alternative for everything else: local PostgreSQL, self-hosted, Supabase, RDS, or any standard PostgreSQL instance. It is a single dependency-light file with no cloud lock-in, full read-write support, atomic multi-statement transactions, and an optional `?mode=readonly` flag when you want to restrict access.
14
+
15
+ ## Requirements
16
+
17
+ - Node.js 18+
18
+ - A running PostgreSQL database
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install -g @sarmadparvez/postgresql-mcp
24
+ ```
25
+
26
+ Or run directly without installing:
27
+
28
+ ```bash
29
+ npx @sarmadparvez/postgresql-mcp <postgresql-connection-string>
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```bash
35
+ postgresql-mcp <postgresql-connection-string>
36
+ ```
37
+
38
+ **Examples:**
39
+
40
+ ```bash
41
+ # Read-write access
42
+ postgresql-mcp postgresql://user:pass@localhost:5432/mydb
43
+
44
+ # Read-only mode (disables execute and transaction tools)
45
+ postgresql-mcp postgresql://user:pass@localhost:5432/mydb?mode=readonly
46
+ ```
47
+
48
+ ## Tools
49
+
50
+ | Tool | Available in | Description |
51
+ |------|-------------|-------------|
52
+ | `query` | Always | Execute a read-only `SELECT` query. Runs inside a `READ ONLY` transaction. Returns rows as JSON. |
53
+ | `execute` | Read-write mode | Execute a write SQL statement (`INSERT`, `UPDATE`, `DELETE`, `CREATE`, `DROP`, etc.). Returns rows affected. |
54
+ | `schema` | Always | List columns, types, nullability, defaults, and primary keys for tables in a given schema. Optionally filter to a specific table. |
55
+ | `list_tables` | Always | List all base tables in a schema with their disk size. |
56
+ | `transaction` | Read-write mode | Execute multiple SQL statements atomically. Rolls back all statements if any one fails. |
57
+
58
+ ## Read-Only Mode
59
+
60
+ Append `?mode=readonly` to the connection string to start the server in read-only mode. This disables the `execute` and `transaction` tools, leaving only `query`, `schema`, and `list_tables`.
61
+
62
+ ## Claude Desktop Configuration
63
+
64
+ Add this to your `claude_desktop_config.json`:
65
+
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "postgres": {
70
+ "command": "npx",
71
+ "args": [
72
+ "@sarmadparvez/postgresql-mcp",
73
+ "postgresql://user:pass@localhost:5432/mydb"
74
+ ]
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ For read-only access:
81
+
82
+ ```json
83
+ {
84
+ "mcpServers": {
85
+ "postgres-readonly": {
86
+ "command": "npx",
87
+ "args": [
88
+ "@sarmadparvez/postgresql-mcp",
89
+ "postgresql://user:pass@localhost:5432/mydb?mode=readonly"
90
+ ]
91
+ }
92
+ }
93
+ }
94
+ ```
95
+
96
+ ## Dependencies
97
+
98
+ - [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) — MCP server framework
99
+ - [`pg`](https://node-postgres.com) — PostgreSQL client
100
+ - [`zod`](https://zod.dev) — Schema validation for tool inputs
package/index.js ADDED
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Reusable PostgreSQL MCP Server
5
+ * Supports any PostgreSQL database via connection string argument
6
+ *
7
+ * Usage:
8
+ * node index.js <connection-string>
9
+ *
10
+ * Example:
11
+ * node index.js postgresql://user:pass@localhost:5432/mydb
12
+ * node index.js postgresql://user:pass@localhost:5432/mydb?mode=readonly
13
+ */
14
+
15
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
17
+ import { z } from "zod";
18
+ import pg from "pg";
19
+
20
+ // ── Config from CLI args ──────────────────────────────────────────────────────
21
+
22
+ const connectionString = process.argv[2];
23
+
24
+ if (!connectionString) {
25
+ console.error("Usage: node index.js <postgresql-connection-string>");
26
+ console.error("Example: node index.js postgresql://user:pass@localhost:5432/mydb");
27
+ process.exit(1);
28
+ }
29
+
30
+ // Parse optional ?mode=readonly from connection string
31
+ const url = new URL(connectionString);
32
+ const isReadOnly = url.searchParams.get("mode") === "readonly";
33
+ url.searchParams.delete("mode");
34
+ const cleanConnectionString = url.toString();
35
+
36
+ // ── Database pool ─────────────────────────────────────────────────────────────
37
+
38
+ const pool = new pg.Pool({
39
+ connectionString: cleanConnectionString,
40
+ max: 5,
41
+ idleTimeoutMillis: 30000,
42
+ });
43
+
44
+ pool.on("error", (err) => {
45
+ console.error("Unexpected DB error:", err.message);
46
+ });
47
+
48
+ // ── MCP Server ────────────────────────────────────────────────────────────────
49
+
50
+ const dbName = url.pathname.replace("/", "") || "postgres";
51
+
52
+ const server = new McpServer({
53
+ name: `postgres-mcp-server-${dbName}`,
54
+ version: "1.0.0",
55
+ });
56
+
57
+ // ── Tool: query (read-only SELECT) ────────────────────────────────────────────
58
+
59
+ server.registerTool(
60
+ "query",
61
+ {
62
+ title: "Run SQL Query",
63
+ description: "Execute a read-only SQL SELECT query and return results as JSON",
64
+ inputSchema: {
65
+ sql: z.string().describe("The SQL SELECT query to execute"),
66
+ },
67
+ },
68
+ async ({ sql }) => {
69
+ const client = await pool.connect();
70
+ try {
71
+ await client.query("BEGIN TRANSACTION READ ONLY");
72
+ const result = await client.query(sql);
73
+ await client.query("COMMIT");
74
+ return {
75
+ content: [
76
+ {
77
+ type: "text",
78
+ text: JSON.stringify(result.rows, null, 2),
79
+ },
80
+ ],
81
+ };
82
+ } catch (err) {
83
+ try { await client.query("ROLLBACK"); } catch (_) {}
84
+ return {
85
+ content: [{ type: "text", text: `Query error: ${err.message}` }],
86
+ isError: true,
87
+ };
88
+ } finally {
89
+ client.release();
90
+ }
91
+ }
92
+ );
93
+
94
+ // ── Tool: execute (write operations) ─────────────────────────────────────────
95
+
96
+ if (!isReadOnly) {
97
+ server.registerTool(
98
+ "execute",
99
+ {
100
+ title: "Execute SQL",
101
+ description:
102
+ "Execute a write SQL statement (INSERT, UPDATE, DELETE, TRUNCATE, CREATE, DROP). Returns rows affected.",
103
+ inputSchema: {
104
+ sql: z.string().describe("The SQL statement to execute"),
105
+ },
106
+ },
107
+ async ({ sql }) => {
108
+ const client = await pool.connect();
109
+ try {
110
+ const result = await client.query(sql);
111
+ const message =
112
+ result.rows && result.rows.length > 0
113
+ ? `Success. ${result.rowCount} row(s) affected.\nReturned: ${JSON.stringify(result.rows, null, 2)}`
114
+ : `Success. ${result.rowCount ?? 0} row(s) affected.`;
115
+ return {
116
+ content: [{ type: "text", text: message }],
117
+ };
118
+ } catch (err) {
119
+ return {
120
+ content: [{ type: "text", text: `Execute error: ${err.message}` }],
121
+ isError: true,
122
+ };
123
+ } finally {
124
+ client.release();
125
+ }
126
+ }
127
+ );
128
+ }
129
+
130
+ // ── Tool: schema ──────────────────────────────────────────────────────────────
131
+
132
+ server.registerTool(
133
+ "schema",
134
+ {
135
+ title: "Get Database Schema",
136
+ description:
137
+ "List all tables with their columns, types, and constraints for a given schema (default: public)",
138
+ inputSchema: {
139
+ schema_name: z
140
+ .string()
141
+ .optional()
142
+ .default("public")
143
+ .describe("PostgreSQL schema name (default: public)"),
144
+ table_name: z
145
+ .string()
146
+ .optional()
147
+ .describe("Optional: filter to a specific table"),
148
+ },
149
+ },
150
+ async ({ schema_name = "public", table_name }) => {
151
+ const client = await pool.connect();
152
+ try {
153
+ const tableFilter = table_name ? `AND c.table_name = $2` : "";
154
+ const params = table_name ? [schema_name, table_name] : [schema_name];
155
+
156
+ const result = await client.query(
157
+ `SELECT
158
+ c.table_name,
159
+ c.column_name,
160
+ c.data_type,
161
+ c.is_nullable,
162
+ c.column_default,
163
+ tc.constraint_type
164
+ FROM information_schema.columns c
165
+ LEFT JOIN information_schema.key_column_usage kcu
166
+ ON c.table_name = kcu.table_name
167
+ AND c.column_name = kcu.column_name
168
+ AND c.table_schema = kcu.table_schema
169
+ LEFT JOIN information_schema.table_constraints tc
170
+ ON kcu.constraint_name = tc.constraint_name
171
+ AND tc.table_schema = kcu.table_schema
172
+ AND tc.constraint_type = 'PRIMARY KEY'
173
+ WHERE c.table_schema = $1
174
+ AND c.table_name NOT IN (SELECT table_name FROM information_schema.views)
175
+ ${tableFilter}
176
+ ORDER BY c.table_name, c.ordinal_position`,
177
+ params
178
+ );
179
+
180
+ // Group by table
181
+ const tables = {};
182
+ for (const row of result.rows) {
183
+ if (!tables[row.table_name]) tables[row.table_name] = [];
184
+ tables[row.table_name].push({
185
+ column: row.column_name,
186
+ type: row.data_type,
187
+ nullable: row.is_nullable === "YES",
188
+ default: row.column_default,
189
+ primaryKey: row.constraint_type === "PRIMARY KEY",
190
+ });
191
+ }
192
+
193
+ return {
194
+ content: [{ type: "text", text: JSON.stringify(tables, null, 2) }],
195
+ };
196
+ } catch (err) {
197
+ return {
198
+ content: [{ type: "text", text: `Schema error: ${err.message}` }],
199
+ isError: true,
200
+ };
201
+ } finally {
202
+ client.release();
203
+ }
204
+ }
205
+ );
206
+
207
+ // ── Tool: list_tables ─────────────────────────────────────────────────────────
208
+
209
+ server.registerTool(
210
+ "list_tables",
211
+ {
212
+ title: "List Tables",
213
+ description: "List all tables in the database with row counts",
214
+ inputSchema: {
215
+ schema_name: z
216
+ .string()
217
+ .optional()
218
+ .default("public")
219
+ .describe("PostgreSQL schema name (default: public)"),
220
+ },
221
+ },
222
+ async ({ schema_name = "public" }) => {
223
+ const client = await pool.connect();
224
+ try {
225
+ const result = await client.query(
226
+ `SELECT
227
+ t.table_name,
228
+ t.table_schema,
229
+ pg_size_pretty(pg_total_relation_size(quote_ident(t.table_schema)||'.'||quote_ident(t.table_name))) AS size
230
+ FROM information_schema.tables t
231
+ WHERE t.table_schema = $1
232
+ AND t.table_type = 'BASE TABLE'
233
+ ORDER BY t.table_name`,
234
+ [schema_name]
235
+ );
236
+ return {
237
+ content: [{ type: "text", text: JSON.stringify(result.rows, null, 2) }],
238
+ };
239
+ } catch (err) {
240
+ return {
241
+ content: [{ type: "text", text: `Error: ${err.message}` }],
242
+ isError: true,
243
+ };
244
+ } finally {
245
+ client.release();
246
+ }
247
+ }
248
+ );
249
+
250
+ // ── Tool: transaction (multi-statement) ───────────────────────────────────────
251
+
252
+ if (!isReadOnly) {
253
+ server.registerTool(
254
+ "transaction",
255
+ {
256
+ title: "Run SQL Transaction",
257
+ description:
258
+ "Execute multiple SQL statements as a single atomic transaction. Rolls back all on any error.",
259
+ inputSchema: {
260
+ statements: z
261
+ .array(z.string())
262
+ .describe("Array of SQL statements to execute in order, atomically"),
263
+ },
264
+ },
265
+ async ({ statements }) => {
266
+ const client = await pool.connect();
267
+ try {
268
+ await client.query("BEGIN");
269
+ const results = [];
270
+ for (const sql of statements) {
271
+ const result = await client.query(sql);
272
+ results.push({
273
+ sql: sql.slice(0, 80) + (sql.length > 80 ? "..." : ""),
274
+ rowsAffected: result.rowCount ?? 0,
275
+ returned: result.rows?.length > 0 ? result.rows : undefined,
276
+ });
277
+ }
278
+ await client.query("COMMIT");
279
+ return {
280
+ content: [
281
+ {
282
+ type: "text",
283
+ text: `Transaction committed successfully.\n${JSON.stringify(results, null, 2)}`,
284
+ },
285
+ ],
286
+ };
287
+ } catch (err) {
288
+ await client.query("ROLLBACK");
289
+ return {
290
+ content: [
291
+ {
292
+ type: "text",
293
+ text: `Transaction rolled back due to error: ${err.message}`,
294
+ },
295
+ ],
296
+ isError: true,
297
+ };
298
+ } finally {
299
+ client.release();
300
+ }
301
+ }
302
+ );
303
+ }
304
+
305
+ // ── Start server ──────────────────────────────────────────────────────────────
306
+
307
+ const transport = new StdioServerTransport();
308
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@sarmadparvez/postgresql-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for PostgreSQL with full read-write support. Works with any PostgreSQL instance — local, Supabase, RDS, or self-hosted.",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "postgresql-mcp": "./index.js"
8
+ },
9
+ "files": [
10
+ "index.js"
11
+ ],
12
+ "scripts": {
13
+ "test": "echo \"Error: no test specified\" && exit 1"
14
+ },
15
+ "keywords": [
16
+ "mcp",
17
+ "model-context-protocol",
18
+ "postgresql",
19
+ "postgres",
20
+ "ai",
21
+ "claude",
22
+ "llm"
23
+ ],
24
+ "author": "sarmadparvez",
25
+ "license": "ISC",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/sarmadparvez/postgresql-mcp.git"
29
+ },
30
+ "homepage": "https://github.com/sarmadparvez/postgresql-mcp#readme",
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "type": "module",
35
+ "dependencies": {
36
+ "@modelcontextprotocol/sdk": "^1.27.1",
37
+ "pg": "^8.20.0",
38
+ "zod": "^4.3.6"
39
+ }
40
+ }