@spec-this/mcp-postgres-sql 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 +183 -0
  2. package/dist/index.js +394 -0
  3. package/package.json +55 -0
package/README.md ADDED
@@ -0,0 +1,183 @@
1
+ # MCP PostgreSQL
2
+
3
+ A Model Context Protocol (MCP) server that provides PostgreSQL database query capabilities with built-in connection management.
4
+
5
+ ## Features
6
+
7
+ - PostgreSQL database support using the `pg` driver
8
+ - Connection management via MCP tools (add, list, update, remove)
9
+ - Connections stored in `~/.mcp-postgres-sql/connections.json`
10
+ - No environment variables needed — manage connections through the tools
11
+ - Passwords hidden when listing connections
12
+ - SSL support
13
+ - Stdio transport for MCP integration
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install
19
+ npm run build
20
+ ```
21
+
22
+ ## MCP Configuration
23
+
24
+ Add to your Claude Desktop or Claude Code MCP config:
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "postgres": {
30
+ "command": "node",
31
+ "args": ["/path/to/mcp-postgres-sql/dist/index.js"]
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ Or if installed globally:
38
+
39
+ ```json
40
+ {
41
+ "mcpServers": {
42
+ "postgres": {
43
+ "command": "mcp-postgres-sql"
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ ## Tools
50
+
51
+ ### `add_connection`
52
+
53
+ Save a new PostgreSQL connection configuration.
54
+
55
+ | Parameter | Type | Required | Default | Description |
56
+ |------------|---------|----------|---------|--------------------|
57
+ | `name` | string | yes | | Name for this connection |
58
+ | `host` | string | yes | | Database host |
59
+ | `port` | number | no | 5432 | Database port |
60
+ | `database` | string | yes | | Database name |
61
+ | `user` | string | yes | | Database user |
62
+ | `password` | string | yes | | Database password |
63
+ | `ssl` | boolean | no | false | Use SSL connection |
64
+
65
+ ### `list_connections`
66
+
67
+ List all saved connections. Passwords are hidden from the output.
68
+
69
+ ### `update_connection`
70
+
71
+ Update an existing connection. Only the fields you provide will be changed.
72
+
73
+ | Parameter | Type | Required | Description |
74
+ |------------|---------|----------|--------------------------|
75
+ | `name` | string | yes | Connection name to update |
76
+ | `host` | string | no | New host |
77
+ | `port` | number | no | New port |
78
+ | `database` | string | no | New database name |
79
+ | `user` | string | no | New user |
80
+ | `password` | string | no | New password |
81
+ | `ssl` | boolean | no | New SSL setting |
82
+
83
+ ### `remove_connection`
84
+
85
+ Remove a saved connection. If this is the active connection, it will be disconnected first.
86
+
87
+ | Parameter | Type | Required | Description |
88
+ |-----------|--------|----------|--------------------------|
89
+ | `name` | string | yes | Connection name to remove |
90
+
91
+ ### `connect`
92
+
93
+ Connect to a saved connection by name. Disconnects any existing connection first.
94
+
95
+ | Parameter | Type | Required | Description |
96
+ |-----------|--------|----------|------------------------------|
97
+ | `name` | string | yes | Connection name to connect to |
98
+
99
+ ### `disconnect`
100
+
101
+ Disconnect from the current database.
102
+
103
+ ### `query`
104
+
105
+ Execute a SQL query against the connected database.
106
+
107
+ | Parameter | Type | Required | Description |
108
+ |-----------|--------|----------|----------------------|
109
+ | `sql` | string | yes | The SQL query to run |
110
+
111
+ Returns JSON-formatted query results.
112
+
113
+ ## Usage Example
114
+
115
+ Once configured as an MCP server, use the tools in sequence:
116
+
117
+ ```
118
+ 1. add_connection → name: "local-dev", host: "localhost", database: "myapp", user: "postgres", password: "secret"
119
+ 2. connect → name: "local-dev"
120
+ 3. query → sql: "SELECT * FROM users LIMIT 10"
121
+ 4. disconnect
122
+ ```
123
+
124
+ ## Connection Storage
125
+
126
+ Connections are stored at `~/.mcp-postgres-sql/connections.json`:
127
+
128
+ ```json
129
+ {
130
+ "connections": {
131
+ "local-dev": {
132
+ "host": "localhost",
133
+ "port": 5432,
134
+ "database": "myapp",
135
+ "user": "postgres",
136
+ "password": "secret",
137
+ "ssl": false
138
+ }
139
+ }
140
+ }
141
+ ```
142
+
143
+ The directory is created with `0700` permissions and the file with `0600` permissions.
144
+
145
+ ## Development
146
+
147
+ ```bash
148
+ # Install dependencies
149
+ npm install
150
+
151
+ # Run in development mode
152
+ npm run dev
153
+
154
+ # Build for production
155
+ npm run build
156
+
157
+ # Type check
158
+ npx tsc --noEmit
159
+ ```
160
+
161
+ ## Testing
162
+
163
+ Tests require Docker.
164
+
165
+ ```bash
166
+ # Start PostgreSQL test container
167
+ npm run test:up
168
+
169
+ # Run tests
170
+ npm test
171
+
172
+ # Stop and clean up
173
+ npm run test:down
174
+
175
+ # Or run everything at once
176
+ npm run test:ci
177
+ ```
178
+
179
+ The test container runs PostgreSQL 16 on port 5433 to avoid conflicts with any local instance.
180
+
181
+ ## License
182
+
183
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,394 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import pg from "pg";
6
+ import { readFile, writeFile, mkdir } from "fs/promises";
7
+ import { join } from "path";
8
+ import { homedir } from "os";
9
+ import { existsSync } from "fs";
10
+ const { Client } = pg;
11
+ class ConnectionManager {
12
+ configPath;
13
+ constructor() {
14
+ const configDir = join(homedir(), ".mcp-postgres-sql");
15
+ this.configPath = join(configDir, "connections.json");
16
+ }
17
+ async ensureConfigDir() {
18
+ const configDir = join(homedir(), ".mcp-postgres-sql");
19
+ if (!existsSync(configDir)) {
20
+ await mkdir(configDir, { recursive: true, mode: 0o700 });
21
+ }
22
+ }
23
+ async loadConnections() {
24
+ await this.ensureConfigDir();
25
+ if (!existsSync(this.configPath)) {
26
+ return { connections: {} };
27
+ }
28
+ try {
29
+ const data = await readFile(this.configPath, "utf-8");
30
+ return JSON.parse(data);
31
+ }
32
+ catch (error) {
33
+ console.error("Error loading connections:", error);
34
+ return { connections: {} };
35
+ }
36
+ }
37
+ async saveConnections(data) {
38
+ await this.ensureConfigDir();
39
+ await writeFile(this.configPath, JSON.stringify(data, null, 2), {
40
+ encoding: "utf-8",
41
+ mode: 0o600,
42
+ });
43
+ }
44
+ async addConnection(name, config) {
45
+ const data = await this.loadConnections();
46
+ data.connections[name] = config;
47
+ await this.saveConnections(data);
48
+ }
49
+ async updateConnection(name, config) {
50
+ const data = await this.loadConnections();
51
+ if (!data.connections[name]) {
52
+ throw new Error(`Connection '${name}' not found`);
53
+ }
54
+ data.connections[name] = {
55
+ ...data.connections[name],
56
+ ...config,
57
+ };
58
+ await this.saveConnections(data);
59
+ }
60
+ async removeConnection(name) {
61
+ const data = await this.loadConnections();
62
+ if (!data.connections[name]) {
63
+ throw new Error(`Connection '${name}' not found`);
64
+ }
65
+ delete data.connections[name];
66
+ await this.saveConnections(data);
67
+ }
68
+ async getConnection(name) {
69
+ const data = await this.loadConnections();
70
+ if (!data.connections[name]) {
71
+ throw new Error(`Connection '${name}' not found`);
72
+ }
73
+ return data.connections[name];
74
+ }
75
+ async listConnections() {
76
+ const data = await this.loadConnections();
77
+ const result = {};
78
+ for (const [name, config] of Object.entries(data.connections)) {
79
+ const { password, ...safeConfig } = config;
80
+ result[name] = safeConfig;
81
+ }
82
+ return result;
83
+ }
84
+ }
85
+ class PostgresDatabase {
86
+ client = null;
87
+ config;
88
+ constructor(config) {
89
+ this.config = config;
90
+ }
91
+ async connect() {
92
+ this.client = new Client({
93
+ host: this.config.host,
94
+ port: this.config.port,
95
+ database: this.config.database,
96
+ user: this.config.user,
97
+ password: this.config.password,
98
+ ssl: this.config.ssl ? { rejectUnauthorized: false } : false,
99
+ });
100
+ await this.client.connect();
101
+ }
102
+ async query(sql) {
103
+ if (!this.client) {
104
+ throw new Error("Not connected to database");
105
+ }
106
+ const result = await this.client.query(sql);
107
+ return result.rows;
108
+ }
109
+ async disconnect() {
110
+ if (this.client) {
111
+ await this.client.end();
112
+ this.client = null;
113
+ }
114
+ }
115
+ isConnected() {
116
+ return this.client !== null;
117
+ }
118
+ }
119
+ async function main() {
120
+ const connectionManager = new ConnectionManager();
121
+ let database = null;
122
+ let currentConnectionName = null;
123
+ const server = new McpServer({
124
+ name: "mcp-postgres-sql",
125
+ description: "PostgreSQL query server for MCP with connection management",
126
+ version: "1.0.0",
127
+ });
128
+ // Tool: add_connection
129
+ server.tool("add_connection", "Add a new PostgreSQL connection configuration", {
130
+ name: z.string().describe("Name for this connection"),
131
+ host: z.string().describe("Database host"),
132
+ port: z.number().default(5432).describe("Database port"),
133
+ database: z.string().describe("Database name"),
134
+ user: z.string().describe("Database user"),
135
+ password: z.string().describe("Database password"),
136
+ ssl: z.boolean().default(false).describe("Use SSL connection"),
137
+ }, async ({ name, host, port, database, user, password, ssl }) => {
138
+ try {
139
+ await connectionManager.addConnection(name, {
140
+ host,
141
+ port,
142
+ database,
143
+ user,
144
+ password,
145
+ ssl,
146
+ });
147
+ return {
148
+ content: [
149
+ {
150
+ type: "text",
151
+ text: `Connection '${name}' added successfully`,
152
+ },
153
+ ],
154
+ };
155
+ }
156
+ catch (error) {
157
+ return {
158
+ isError: true,
159
+ content: [
160
+ {
161
+ type: "text",
162
+ text: `Error adding connection: ${error instanceof Error ? error.message : String(error)}`,
163
+ },
164
+ ],
165
+ };
166
+ }
167
+ });
168
+ // Tool: list_connections
169
+ server.tool("list_connections", "List all saved PostgreSQL connections (passwords hidden)", {}, async () => {
170
+ try {
171
+ const connections = await connectionManager.listConnections();
172
+ return {
173
+ content: [
174
+ {
175
+ type: "text",
176
+ text: JSON.stringify(connections, null, 2),
177
+ },
178
+ ],
179
+ };
180
+ }
181
+ catch (error) {
182
+ return {
183
+ isError: true,
184
+ content: [
185
+ {
186
+ type: "text",
187
+ text: `Error listing connections: ${error instanceof Error ? error.message : String(error)}`,
188
+ },
189
+ ],
190
+ };
191
+ }
192
+ });
193
+ // Tool: update_connection
194
+ server.tool("update_connection", "Update an existing PostgreSQL connection configuration", {
195
+ name: z.string().describe("Name of the connection to update"),
196
+ host: z.string().optional().describe("Database host"),
197
+ port: z.number().optional().describe("Database port"),
198
+ database: z.string().optional().describe("Database name"),
199
+ user: z.string().optional().describe("Database user"),
200
+ password: z.string().optional().describe("Database password"),
201
+ ssl: z.boolean().optional().describe("Use SSL connection"),
202
+ }, async ({ name, host, port, database, user, password, ssl }) => {
203
+ try {
204
+ const updates = {};
205
+ if (host !== undefined)
206
+ updates.host = host;
207
+ if (port !== undefined)
208
+ updates.port = port;
209
+ if (database !== undefined)
210
+ updates.database = database;
211
+ if (user !== undefined)
212
+ updates.user = user;
213
+ if (password !== undefined)
214
+ updates.password = password;
215
+ if (ssl !== undefined)
216
+ updates.ssl = ssl;
217
+ await connectionManager.updateConnection(name, updates);
218
+ return {
219
+ content: [
220
+ {
221
+ type: "text",
222
+ text: `Connection '${name}' updated successfully`,
223
+ },
224
+ ],
225
+ };
226
+ }
227
+ catch (error) {
228
+ return {
229
+ isError: true,
230
+ content: [
231
+ {
232
+ type: "text",
233
+ text: `Error updating connection: ${error instanceof Error ? error.message : String(error)}`,
234
+ },
235
+ ],
236
+ };
237
+ }
238
+ });
239
+ // Tool: remove_connection
240
+ server.tool("remove_connection", "Remove a saved PostgreSQL connection", {
241
+ name: z.string().describe("Name of the connection to remove"),
242
+ }, async ({ name }) => {
243
+ try {
244
+ // Disconnect if this is the current connection
245
+ if (currentConnectionName === name && database) {
246
+ await database.disconnect();
247
+ database = null;
248
+ currentConnectionName = null;
249
+ }
250
+ await connectionManager.removeConnection(name);
251
+ return {
252
+ content: [
253
+ {
254
+ type: "text",
255
+ text: `Connection '${name}' removed successfully`,
256
+ },
257
+ ],
258
+ };
259
+ }
260
+ catch (error) {
261
+ return {
262
+ isError: true,
263
+ content: [
264
+ {
265
+ type: "text",
266
+ text: `Error removing connection: ${error instanceof Error ? error.message : String(error)}`,
267
+ },
268
+ ],
269
+ };
270
+ }
271
+ });
272
+ // Tool: connect
273
+ server.tool("connect", "Connect to a saved PostgreSQL database connection", {
274
+ name: z.string().describe("Name of the connection to use"),
275
+ }, async ({ name }) => {
276
+ try {
277
+ // Disconnect from current connection if any
278
+ if (database) {
279
+ await database.disconnect();
280
+ database = null;
281
+ currentConnectionName = null;
282
+ }
283
+ const config = await connectionManager.getConnection(name);
284
+ database = new PostgresDatabase(config);
285
+ await database.connect();
286
+ currentConnectionName = name;
287
+ return {
288
+ content: [
289
+ {
290
+ type: "text",
291
+ text: `Connected to '${name}' at ${config.host}:${config.port}/${config.database}`,
292
+ },
293
+ ],
294
+ };
295
+ }
296
+ catch (error) {
297
+ database = null;
298
+ currentConnectionName = null;
299
+ return {
300
+ isError: true,
301
+ content: [
302
+ {
303
+ type: "text",
304
+ text: `Error connecting to database: ${error instanceof Error ? error.message : String(error)}`,
305
+ },
306
+ ],
307
+ };
308
+ }
309
+ });
310
+ // Tool: disconnect
311
+ server.tool("disconnect", "Disconnect from the current PostgreSQL database", {}, async () => {
312
+ try {
313
+ if (!database) {
314
+ return {
315
+ content: [
316
+ {
317
+ type: "text",
318
+ text: "No active database connection",
319
+ },
320
+ ],
321
+ };
322
+ }
323
+ const connectionName = currentConnectionName;
324
+ await database.disconnect();
325
+ database = null;
326
+ currentConnectionName = null;
327
+ return {
328
+ content: [
329
+ {
330
+ type: "text",
331
+ text: `Disconnected from '${connectionName}'`,
332
+ },
333
+ ],
334
+ };
335
+ }
336
+ catch (error) {
337
+ return {
338
+ isError: true,
339
+ content: [
340
+ {
341
+ type: "text",
342
+ text: `Error disconnecting: ${error instanceof Error ? error.message : String(error)}`,
343
+ },
344
+ ],
345
+ };
346
+ }
347
+ });
348
+ // Tool: query
349
+ server.tool("query", "Execute a SQL query against the connected PostgreSQL database", {
350
+ sql: z.string().describe("The SQL query to execute"),
351
+ }, async ({ sql }) => {
352
+ try {
353
+ if (!database || !database.isConnected()) {
354
+ throw new Error("Not connected to any database. Use 'connect' tool first.");
355
+ }
356
+ const results = await database.query(sql);
357
+ return {
358
+ content: [
359
+ {
360
+ type: "text",
361
+ text: JSON.stringify(results, null, 2),
362
+ },
363
+ ],
364
+ };
365
+ }
366
+ catch (error) {
367
+ return {
368
+ isError: true,
369
+ content: [
370
+ {
371
+ type: "text",
372
+ text: `Error executing query: ${error instanceof Error ? error.message : String(error)}`,
373
+ },
374
+ ],
375
+ };
376
+ }
377
+ });
378
+ const transport = new StdioServerTransport();
379
+ await server.connect(transport);
380
+ console.error("MCP PostgreSQL Server running on stdio");
381
+ // Graceful shutdown handlers
382
+ const shutdown = async () => {
383
+ if (database) {
384
+ await database.disconnect();
385
+ }
386
+ process.exit(0);
387
+ };
388
+ process.on("SIGINT", shutdown);
389
+ process.on("SIGTERM", shutdown);
390
+ }
391
+ main().catch((error) => {
392
+ console.error("Fatal error:", error);
393
+ process.exit(1);
394
+ });
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@spec-this/mcp-postgres-sql",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for PostgreSQL database queries",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "mcp-postgres-sql": "dist/index.js"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "scripts": {
14
+ "build": "tsc && chmod 755 dist/index.js",
15
+ "start": "node dist/index.js",
16
+ "dev": "tsx src/index.ts",
17
+ "prepublishOnly": "npm run build",
18
+ "typecheck": "tsc --noEmit",
19
+ "test:up": "docker compose up -d",
20
+ "test:down": "docker compose down -v",
21
+ "test": "tsx --test test/*.test.ts",
22
+ "test:ci": "docker compose up -d && sleep 10 && npm test && docker compose down -v",
23
+ "semantic-release": "semantic-release"
24
+ },
25
+ "keywords": [
26
+ "modelcontextprotocol",
27
+ "mcp",
28
+ "server",
29
+ "postgresql",
30
+ "postgres",
31
+ "database"
32
+ ],
33
+ "author": "",
34
+ "license": "MIT",
35
+ "files": [
36
+ "dist"
37
+ ],
38
+ "dependencies": {
39
+ "@modelcontextprotocol/sdk": "^1.12.0",
40
+ "zod": "^3.24.2",
41
+ "pg": "^8.13.1"
42
+ },
43
+ "devDependencies": {
44
+ "@semantic-release/changelog": "^6.0.3",
45
+ "@semantic-release/git": "^10.0.1",
46
+ "@types/node": "^22.13.14",
47
+ "@types/pg": "^8.11.10",
48
+ "semantic-release": "^24.0.0",
49
+ "tsx": "^4.17.0",
50
+ "typescript": "^5.8.2"
51
+ },
52
+ "engines": {
53
+ "node": ">=18"
54
+ }
55
+ }