@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.
- package/README.md +100 -0
- package/index.js +308 -0
- 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
|
+
}
|