@tejasanik/postgres-mcp-server 1.5.0 → 1.6.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 +46 -13
- package/dist/__tests__/mcp-server.test.js +20 -5
- package/dist/__tests__/mcp-server.test.js.map +1 -1
- package/dist/__tests__/server-tools.test.js +38 -14
- package/dist/__tests__/server-tools.test.js.map +1 -1
- package/dist/index.js +259 -382
- package/dist/index.js.map +1 -1
- package/dist/tools/server-tools.d.ts +36 -17
- package/dist/tools/server-tools.d.ts.map +1 -1
- package/dist/tools/server-tools.js +93 -85
- package/dist/tools/server-tools.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,418 +1,295 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { StdioServerTransport } from
|
|
4
|
-
import {
|
|
5
|
-
import { resetDbManager } from
|
|
6
|
-
import {
|
|
7
|
-
//
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
description: 'Lists all configured PostgreSQL servers and their databases. Use fetchDatabases=true to list databases. Use searchAllServers=true to fetch databases from all servers (not just the connected one).',
|
|
12
|
-
inputSchema: {
|
|
13
|
-
type: 'object',
|
|
14
|
-
properties: {
|
|
15
|
-
serverFilter: {
|
|
16
|
-
type: 'string',
|
|
17
|
-
description: 'Filter servers by name or host (case-insensitive partial match)'
|
|
18
|
-
},
|
|
19
|
-
databaseFilter: {
|
|
20
|
-
type: 'string',
|
|
21
|
-
description: 'Filter databases by name (case-insensitive partial match)'
|
|
22
|
-
},
|
|
23
|
-
includeSystemDbs: {
|
|
24
|
-
type: 'boolean',
|
|
25
|
-
description: 'Include system databases (template0, template1)',
|
|
26
|
-
default: false
|
|
27
|
-
},
|
|
28
|
-
fetchDatabases: {
|
|
29
|
-
type: 'boolean',
|
|
30
|
-
description: 'Fetch list of databases from servers',
|
|
31
|
-
default: false
|
|
32
|
-
},
|
|
33
|
-
searchAllServers: {
|
|
34
|
-
type: 'boolean',
|
|
35
|
-
description: 'When true, fetch databases from all configured servers (temporarily connects to each). When false, only fetch from currently connected server.',
|
|
36
|
-
default: false
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
name: 'switch_server_db',
|
|
43
|
-
description: 'Switch to a different PostgreSQL server and optionally a specific database and schema. Must be called before using other database tools. Uses server defaults if not specified.',
|
|
44
|
-
inputSchema: {
|
|
45
|
-
type: 'object',
|
|
46
|
-
properties: {
|
|
47
|
-
server: {
|
|
48
|
-
type: 'string',
|
|
49
|
-
description: 'Name of the server to connect to (from POSTGRES_SERVERS config)'
|
|
50
|
-
},
|
|
51
|
-
database: {
|
|
52
|
-
type: 'string',
|
|
53
|
-
description: 'Name of the database to connect to (uses server default or "postgres")'
|
|
54
|
-
},
|
|
55
|
-
schema: {
|
|
56
|
-
type: 'string',
|
|
57
|
-
description: 'Name of the default schema (uses server default or "public")'
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
required: ['server']
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
name: 'get_current_connection',
|
|
65
|
-
description: 'Returns details about the current database connection including server, database, schema, host, port, and access mode (readonly/full).',
|
|
66
|
-
inputSchema: {
|
|
67
|
-
type: 'object',
|
|
68
|
-
properties: {}
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
name: 'list_schemas',
|
|
73
|
-
description: 'Lists all database schemas available in the current PostgreSQL database.',
|
|
74
|
-
inputSchema: {
|
|
75
|
-
type: 'object',
|
|
76
|
-
properties: {
|
|
77
|
-
includeSystemSchemas: {
|
|
78
|
-
type: 'boolean',
|
|
79
|
-
description: 'Include system schemas (pg_catalog, information_schema, etc.)',
|
|
80
|
-
default: false
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
name: 'list_objects',
|
|
87
|
-
description: 'Lists database objects (tables, views, sequences, extensions) within a specified schema.',
|
|
88
|
-
inputSchema: {
|
|
89
|
-
type: 'object',
|
|
90
|
-
properties: {
|
|
91
|
-
schema: {
|
|
92
|
-
type: 'string',
|
|
93
|
-
description: 'Schema name to list objects from'
|
|
94
|
-
},
|
|
95
|
-
objectType: {
|
|
96
|
-
type: 'string',
|
|
97
|
-
enum: ['table', 'view', 'sequence', 'extension', 'all'],
|
|
98
|
-
description: 'Type of objects to list',
|
|
99
|
-
default: 'all'
|
|
100
|
-
},
|
|
101
|
-
filter: {
|
|
102
|
-
type: 'string',
|
|
103
|
-
description: 'Filter objects by name (case-insensitive partial match)'
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
required: ['schema']
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
name: 'get_object_details',
|
|
111
|
-
description: 'Provides detailed information about a specific database object including columns, constraints, indexes, size, and row count.',
|
|
112
|
-
inputSchema: {
|
|
113
|
-
type: 'object',
|
|
114
|
-
properties: {
|
|
115
|
-
schema: {
|
|
116
|
-
type: 'string',
|
|
117
|
-
description: 'Schema name containing the object'
|
|
118
|
-
},
|
|
119
|
-
objectName: {
|
|
120
|
-
type: 'string',
|
|
121
|
-
description: 'Name of the object to get details for'
|
|
122
|
-
},
|
|
123
|
-
objectType: {
|
|
124
|
-
type: 'string',
|
|
125
|
-
enum: ['table', 'view', 'sequence'],
|
|
126
|
-
description: 'Type of the object'
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
required: ['schema', 'objectName']
|
|
130
|
-
}
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
name: 'execute_sql',
|
|
134
|
-
description: 'Executes SQL statements on the database. Supports pagination with offset/maxRows. Returns execution time. Use params for parameterized queries (e.g., sql: "SELECT * FROM users WHERE id = $1", params: [123]). Read-only mode prevents write operations.',
|
|
135
|
-
inputSchema: {
|
|
136
|
-
type: 'object',
|
|
137
|
-
properties: {
|
|
138
|
-
sql: {
|
|
139
|
-
type: 'string',
|
|
140
|
-
description: 'SQL statement to execute. Use $1, $2, etc. for parameterized queries.'
|
|
141
|
-
},
|
|
142
|
-
params: {
|
|
143
|
-
type: 'array',
|
|
144
|
-
description: 'Parameters for parameterized queries (e.g., [123, "value"]). Prevents SQL injection.',
|
|
145
|
-
items: {}
|
|
146
|
-
},
|
|
147
|
-
maxRows: {
|
|
148
|
-
type: 'number',
|
|
149
|
-
description: 'Maximum rows to return (default: 1000, max: 100000). Use with offset for pagination.',
|
|
150
|
-
default: 1000
|
|
151
|
-
},
|
|
152
|
-
offset: {
|
|
153
|
-
type: 'number',
|
|
154
|
-
description: 'Number of rows to skip (for pagination). Use with maxRows to paginate through large results.',
|
|
155
|
-
default: 0
|
|
156
|
-
},
|
|
157
|
-
allowLargeScript: {
|
|
158
|
-
type: 'boolean',
|
|
159
|
-
description: 'Set to true to bypass the 100KB SQL length limit for large deployment scripts.',
|
|
160
|
-
default: false
|
|
161
|
-
}
|
|
162
|
-
},
|
|
163
|
-
required: ['sql']
|
|
164
|
-
}
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
name: 'explain_query',
|
|
168
|
-
description: 'Gets the execution plan for a SQL query, showing how PostgreSQL will process it. Can simulate hypothetical indexes if hypopg extension is installed.',
|
|
169
|
-
inputSchema: {
|
|
170
|
-
type: 'object',
|
|
171
|
-
properties: {
|
|
172
|
-
sql: {
|
|
173
|
-
type: 'string',
|
|
174
|
-
description: 'SQL query to explain'
|
|
175
|
-
},
|
|
176
|
-
analyze: {
|
|
177
|
-
type: 'boolean',
|
|
178
|
-
description: 'Actually execute the query to get real timing (default: false). Only allowed for SELECT queries.',
|
|
179
|
-
default: false
|
|
180
|
-
},
|
|
181
|
-
buffers: {
|
|
182
|
-
type: 'boolean',
|
|
183
|
-
description: 'Include buffer usage statistics',
|
|
184
|
-
default: false
|
|
185
|
-
},
|
|
186
|
-
format: {
|
|
187
|
-
type: 'string',
|
|
188
|
-
enum: ['text', 'json', 'yaml', 'xml'],
|
|
189
|
-
description: 'Output format for the plan',
|
|
190
|
-
default: 'json'
|
|
191
|
-
},
|
|
192
|
-
hypotheticalIndexes: {
|
|
193
|
-
type: 'array',
|
|
194
|
-
description: 'Hypothetical indexes to simulate (requires hypopg extension)',
|
|
195
|
-
items: {
|
|
196
|
-
type: 'object',
|
|
197
|
-
properties: {
|
|
198
|
-
table: {
|
|
199
|
-
type: 'string',
|
|
200
|
-
description: 'Table name for the hypothetical index'
|
|
201
|
-
},
|
|
202
|
-
columns: {
|
|
203
|
-
type: 'array',
|
|
204
|
-
items: { type: 'string' },
|
|
205
|
-
description: 'Columns to include in the index'
|
|
206
|
-
},
|
|
207
|
-
indexType: {
|
|
208
|
-
type: 'string',
|
|
209
|
-
description: 'Index type (btree, hash, gist, etc.)',
|
|
210
|
-
default: 'btree'
|
|
211
|
-
}
|
|
212
|
-
},
|
|
213
|
-
required: ['table', 'columns']
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
},
|
|
217
|
-
required: ['sql']
|
|
218
|
-
}
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
name: 'get_top_queries',
|
|
222
|
-
description: 'Reports the slowest SQL queries based on total execution time using pg_stat_statements data. Requires pg_stat_statements extension.',
|
|
223
|
-
inputSchema: {
|
|
224
|
-
type: 'object',
|
|
225
|
-
properties: {
|
|
226
|
-
limit: {
|
|
227
|
-
type: 'number',
|
|
228
|
-
description: 'Number of queries to return (1-100)',
|
|
229
|
-
default: 10
|
|
230
|
-
},
|
|
231
|
-
orderBy: {
|
|
232
|
-
type: 'string',
|
|
233
|
-
enum: ['total_time', 'mean_time', 'calls'],
|
|
234
|
-
description: 'How to order the results',
|
|
235
|
-
default: 'total_time'
|
|
236
|
-
},
|
|
237
|
-
minCalls: {
|
|
238
|
-
type: 'number',
|
|
239
|
-
description: 'Minimum number of calls to include a query',
|
|
240
|
-
default: 1
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
},
|
|
245
|
-
{
|
|
246
|
-
name: 'analyze_workload_indexes',
|
|
247
|
-
description: 'Analyzes the database workload (using pg_stat_statements) to identify resource-intensive queries and recommends optimal indexes for them.',
|
|
248
|
-
inputSchema: {
|
|
249
|
-
type: 'object',
|
|
250
|
-
properties: {
|
|
251
|
-
topQueriesCount: {
|
|
252
|
-
type: 'number',
|
|
253
|
-
description: 'Number of top queries to analyze (1-50)',
|
|
254
|
-
default: 20
|
|
255
|
-
},
|
|
256
|
-
includeHypothetical: {
|
|
257
|
-
type: 'boolean',
|
|
258
|
-
description: 'Include hypothetical index analysis (requires hypopg)',
|
|
259
|
-
default: false
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
},
|
|
264
|
-
{
|
|
265
|
-
name: 'analyze_query_indexes',
|
|
266
|
-
description: 'Analyzes specific SQL queries (up to 10) and recommends optimal indexes for them.',
|
|
267
|
-
inputSchema: {
|
|
268
|
-
type: 'object',
|
|
269
|
-
properties: {
|
|
270
|
-
queries: {
|
|
271
|
-
type: 'array',
|
|
272
|
-
items: { type: 'string' },
|
|
273
|
-
description: 'List of SQL queries to analyze (max 10)',
|
|
274
|
-
maxItems: 10
|
|
275
|
-
}
|
|
276
|
-
},
|
|
277
|
-
required: ['queries']
|
|
278
|
-
}
|
|
279
|
-
},
|
|
280
|
-
{
|
|
281
|
-
name: 'analyze_db_health',
|
|
282
|
-
description: 'Performs comprehensive database health checks including: buffer cache hit rates, connection health, constraint validation, index health (duplicate/unused/invalid), sequence limits, and vacuum health.',
|
|
283
|
-
inputSchema: {
|
|
284
|
-
type: 'object',
|
|
285
|
-
properties: {}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
];
|
|
289
|
-
// Create MCP server
|
|
290
|
-
const server = new Server({
|
|
291
|
-
name: 'postgres-mcp-server',
|
|
292
|
-
version: '1.0.0',
|
|
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 { resetDbManager } from "./db-manager.js";
|
|
6
|
+
import { listServers, listDatabases, switchServerDb, getCurrentConnection, listSchemas, listObjects, getObjectDetails, executeSql, explainQuery, getTopQueries, analyzeWorkloadIndexes, analyzeQueryIndexes, analyzeDbHealth, } from "./tools/index.js";
|
|
7
|
+
// Create MCP server using the new high-level API
|
|
8
|
+
const server = new McpServer({
|
|
9
|
+
name: "postgres-mcp-server",
|
|
10
|
+
version: "1.6.0",
|
|
293
11
|
}, {
|
|
294
12
|
capabilities: {
|
|
295
13
|
tools: {},
|
|
296
14
|
},
|
|
297
15
|
});
|
|
298
|
-
//
|
|
299
|
-
server.
|
|
300
|
-
|
|
16
|
+
// Register tools with improved descriptions
|
|
17
|
+
server.registerTool("list_servers", {
|
|
18
|
+
description: "List all configured PostgreSQL servers. Call this FIRST to discover available server names before using list_databases or switch_server_db. Returns server names, hosts, ports, and connection status.",
|
|
19
|
+
inputSchema: z.object({
|
|
20
|
+
filter: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Filter servers by name or host (case-insensitive partial match)"),
|
|
24
|
+
}),
|
|
25
|
+
}, async (args) => {
|
|
26
|
+
const result = await listServers(args);
|
|
27
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
301
28
|
});
|
|
302
|
-
|
|
303
|
-
server.
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
case
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
29
|
+
server.registerTool("list_databases", {
|
|
30
|
+
description: "List databases in a specific PostgreSQL server. REQUIRES serverName parameter - use list_servers first to get valid server names. Do NOT guess server names.",
|
|
31
|
+
inputSchema: z.object({
|
|
32
|
+
serverName: z
|
|
33
|
+
.string()
|
|
34
|
+
.describe("REQUIRED: Server name from list_servers. Do NOT use database names here."),
|
|
35
|
+
filter: z
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("Filter databases by name (case-insensitive partial match)"),
|
|
39
|
+
includeSystemDbs: z
|
|
40
|
+
.boolean()
|
|
41
|
+
.optional()
|
|
42
|
+
.default(false)
|
|
43
|
+
.describe("Include system databases (template0, template1)"),
|
|
44
|
+
maxResults: z
|
|
45
|
+
.number()
|
|
46
|
+
.optional()
|
|
47
|
+
.default(50)
|
|
48
|
+
.describe("Maximum databases to return (default: 50, max: 200)"),
|
|
49
|
+
}),
|
|
50
|
+
}, async (args) => {
|
|
51
|
+
const result = await listDatabases(args);
|
|
52
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
53
|
+
});
|
|
54
|
+
server.registerTool("switch_server_db", {
|
|
55
|
+
description: "Connect to a PostgreSQL server and database. MUST be called before executing queries. Use list_servers to find server names, list_databases to find database names.",
|
|
56
|
+
inputSchema: z.object({
|
|
57
|
+
server: z.string().describe("Server name from list_servers (NOT the host)"),
|
|
58
|
+
database: z
|
|
59
|
+
.string()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe("Database name from list_databases (defaults to server's default or 'postgres')"),
|
|
62
|
+
schema: z
|
|
63
|
+
.string()
|
|
64
|
+
.optional()
|
|
65
|
+
.describe("Schema name (defaults to 'public')"),
|
|
66
|
+
}),
|
|
67
|
+
}, async (args) => {
|
|
68
|
+
const result = await switchServerDb(args);
|
|
69
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
70
|
+
});
|
|
71
|
+
server.registerTool("get_current_connection", {
|
|
72
|
+
description: "Get current connection status. Returns server, database, schema, host, port, and access mode (readonly/full). Call this to verify your connection before running queries.",
|
|
73
|
+
inputSchema: z.object({}),
|
|
74
|
+
}, async () => {
|
|
75
|
+
const result = await getCurrentConnection();
|
|
76
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
77
|
+
});
|
|
78
|
+
server.registerTool("list_schemas", {
|
|
79
|
+
description: "List all schemas in the current database. Requires active connection (use switch_server_db first).",
|
|
80
|
+
inputSchema: z.object({
|
|
81
|
+
includeSystemSchemas: z
|
|
82
|
+
.boolean()
|
|
83
|
+
.optional()
|
|
84
|
+
.default(false)
|
|
85
|
+
.describe("Include system schemas (pg_catalog, information_schema, etc.)"),
|
|
86
|
+
}),
|
|
87
|
+
}, async (args) => {
|
|
88
|
+
const result = await listSchemas(args);
|
|
89
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
90
|
+
});
|
|
91
|
+
server.registerTool("list_objects", {
|
|
92
|
+
description: "List tables, views, sequences, or extensions in a schema. Requires active connection.",
|
|
93
|
+
inputSchema: z.object({
|
|
94
|
+
schema: z.string().describe("Schema name (e.g., 'public')"),
|
|
95
|
+
objectType: z
|
|
96
|
+
.enum(["table", "view", "sequence", "extension", "all"])
|
|
97
|
+
.optional()
|
|
98
|
+
.default("all")
|
|
99
|
+
.describe("Type of objects to list"),
|
|
100
|
+
filter: z
|
|
101
|
+
.string()
|
|
102
|
+
.optional()
|
|
103
|
+
.describe("Filter objects by name (case-insensitive partial match)"),
|
|
104
|
+
}),
|
|
105
|
+
}, async (args) => {
|
|
106
|
+
const result = await listObjects(args);
|
|
107
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
108
|
+
});
|
|
109
|
+
server.registerTool("get_object_details", {
|
|
110
|
+
description: "Get detailed info about a table/view/sequence: columns, data types, constraints, indexes, size, row count. Use this to understand table structure before writing queries.",
|
|
111
|
+
inputSchema: z.object({
|
|
112
|
+
schema: z.string().describe("Schema name containing the object"),
|
|
113
|
+
objectName: z.string().describe("Name of the table, view, or sequence"),
|
|
114
|
+
objectType: z
|
|
115
|
+
.enum(["table", "view", "sequence"])
|
|
116
|
+
.optional()
|
|
117
|
+
.describe("Object type (auto-detected if not specified)"),
|
|
118
|
+
}),
|
|
119
|
+
}, async (args) => {
|
|
120
|
+
const result = await getObjectDetails(args);
|
|
121
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
122
|
+
});
|
|
123
|
+
server.registerTool("execute_sql", {
|
|
124
|
+
description: "Execute SQL queries. Supports SELECT, INSERT, UPDATE, DELETE (if not in readonly mode). Use $1, $2 placeholders with params array to prevent SQL injection. Returns rows, execution time, and pagination info.",
|
|
125
|
+
inputSchema: z.object({
|
|
126
|
+
sql: z
|
|
127
|
+
.string()
|
|
128
|
+
.describe("SQL statement. Use $1, $2, etc. for parameterized queries."),
|
|
129
|
+
params: z
|
|
130
|
+
.array(z.any())
|
|
131
|
+
.optional()
|
|
132
|
+
.describe("Parameters for $1, $2, etc. placeholders (e.g., [123, 'value'])"),
|
|
133
|
+
maxRows: z
|
|
134
|
+
.number()
|
|
135
|
+
.optional()
|
|
136
|
+
.default(1000)
|
|
137
|
+
.describe("Max rows to return (default: 1000, max: 100000)"),
|
|
138
|
+
offset: z
|
|
139
|
+
.number()
|
|
140
|
+
.optional()
|
|
141
|
+
.default(0)
|
|
142
|
+
.describe("Skip rows for pagination"),
|
|
143
|
+
allowLargeScript: z
|
|
144
|
+
.boolean()
|
|
145
|
+
.optional()
|
|
146
|
+
.default(false)
|
|
147
|
+
.describe("Bypass 100KB SQL limit for deployment scripts"),
|
|
148
|
+
}),
|
|
149
|
+
}, async (args) => {
|
|
150
|
+
const result = await executeSql(args);
|
|
151
|
+
// Special handling for large output
|
|
152
|
+
if (result.outputFile) {
|
|
375
153
|
return {
|
|
376
154
|
content: [
|
|
377
155
|
{
|
|
378
|
-
type:
|
|
379
|
-
text: JSON.stringify({
|
|
380
|
-
|
|
156
|
+
type: "text",
|
|
157
|
+
text: JSON.stringify({
|
|
158
|
+
message: `Output too large (${result.rowCount} rows). Results written to file.`,
|
|
159
|
+
outputFile: result.outputFile,
|
|
160
|
+
rowCount: result.rowCount,
|
|
161
|
+
fields: result.fields,
|
|
162
|
+
hint: "Use offset/maxRows to paginate, or add WHERE clauses to reduce results.",
|
|
163
|
+
}, null, 2),
|
|
164
|
+
},
|
|
381
165
|
],
|
|
382
|
-
isError: true
|
|
383
166
|
};
|
|
384
167
|
}
|
|
168
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
169
|
+
});
|
|
170
|
+
server.registerTool("explain_query", {
|
|
171
|
+
description: "Show PostgreSQL's execution plan for a query. Use this to understand query performance and identify missing indexes. analyze=true runs the query to get actual timings (SELECT only).",
|
|
172
|
+
inputSchema: z.object({
|
|
173
|
+
sql: z.string().describe("SQL query to explain"),
|
|
174
|
+
analyze: z
|
|
175
|
+
.boolean()
|
|
176
|
+
.optional()
|
|
177
|
+
.default(false)
|
|
178
|
+
.describe("Execute query for real timing (SELECT only, blocked for writes)"),
|
|
179
|
+
buffers: z
|
|
180
|
+
.boolean()
|
|
181
|
+
.optional()
|
|
182
|
+
.default(false)
|
|
183
|
+
.describe("Include buffer/cache statistics"),
|
|
184
|
+
format: z
|
|
185
|
+
.enum(["text", "json", "yaml", "xml"])
|
|
186
|
+
.optional()
|
|
187
|
+
.default("json")
|
|
188
|
+
.describe("Output format"),
|
|
189
|
+
hypotheticalIndexes: z
|
|
190
|
+
.array(z.object({
|
|
191
|
+
table: z.string().describe("Table name"),
|
|
192
|
+
columns: z.array(z.string()).describe("Columns for the index"),
|
|
193
|
+
indexType: z.string().optional().default("btree").describe("Index type"),
|
|
194
|
+
}))
|
|
195
|
+
.optional()
|
|
196
|
+
.describe("Test hypothetical indexes (requires hypopg extension)"),
|
|
197
|
+
}),
|
|
198
|
+
}, async (args) => {
|
|
199
|
+
const result = await explainQuery(args);
|
|
200
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
201
|
+
});
|
|
202
|
+
server.registerTool("get_top_queries", {
|
|
203
|
+
description: "Find slowest queries from pg_stat_statements. Requires pg_stat_statements extension enabled. Use this to identify performance bottlenecks.",
|
|
204
|
+
inputSchema: z.object({
|
|
205
|
+
limit: z
|
|
206
|
+
.number()
|
|
207
|
+
.optional()
|
|
208
|
+
.default(10)
|
|
209
|
+
.describe("Number of queries to return (1-100)"),
|
|
210
|
+
orderBy: z
|
|
211
|
+
.enum(["total_time", "mean_time", "calls"])
|
|
212
|
+
.optional()
|
|
213
|
+
.default("total_time")
|
|
214
|
+
.describe("Sort by total time, average time, or call count"),
|
|
215
|
+
minCalls: z
|
|
216
|
+
.number()
|
|
217
|
+
.optional()
|
|
218
|
+
.default(1)
|
|
219
|
+
.describe("Minimum call count to include"),
|
|
220
|
+
}),
|
|
221
|
+
}, async (args) => {
|
|
222
|
+
const result = await getTopQueries(args);
|
|
223
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
224
|
+
});
|
|
225
|
+
server.registerTool("analyze_workload_indexes", {
|
|
226
|
+
description: "Analyze database workload and recommend indexes. Uses pg_stat_statements to find slow queries and suggests indexes to improve them.",
|
|
227
|
+
inputSchema: z.object({
|
|
228
|
+
topQueriesCount: z
|
|
229
|
+
.number()
|
|
230
|
+
.optional()
|
|
231
|
+
.default(20)
|
|
232
|
+
.describe("Number of top queries to analyze (1-50)"),
|
|
233
|
+
includeHypothetical: z
|
|
234
|
+
.boolean()
|
|
235
|
+
.optional()
|
|
236
|
+
.default(false)
|
|
237
|
+
.describe("Test recommendations with hypothetical indexes (requires hypopg)"),
|
|
238
|
+
}),
|
|
239
|
+
}, async (args) => {
|
|
240
|
+
const result = await analyzeWorkloadIndexes(args);
|
|
241
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
242
|
+
});
|
|
243
|
+
server.registerTool("analyze_query_indexes", {
|
|
244
|
+
description: "Recommend indexes for specific SQL queries. Provide up to 10 SELECT queries and get index recommendations.",
|
|
245
|
+
inputSchema: z.object({
|
|
246
|
+
queries: z
|
|
247
|
+
.array(z.string())
|
|
248
|
+
.max(10)
|
|
249
|
+
.describe("SQL SELECT queries to analyze (max 10)"),
|
|
250
|
+
}),
|
|
251
|
+
}, async (args) => {
|
|
252
|
+
const result = await analyzeQueryIndexes(args);
|
|
253
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
254
|
+
});
|
|
255
|
+
server.registerTool("analyze_db_health", {
|
|
256
|
+
description: "Run comprehensive database health checks: cache hit rates, connection usage, index health (invalid/unused/duplicate), vacuum status, sequence limits, unvalidated constraints. Returns issues with severity levels.",
|
|
257
|
+
inputSchema: z.object({}),
|
|
258
|
+
}, async () => {
|
|
259
|
+
const result = await analyzeDbHealth();
|
|
260
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
385
261
|
});
|
|
386
262
|
// Graceful shutdown handling
|
|
387
263
|
async function shutdown() {
|
|
388
|
-
console.error(
|
|
264
|
+
console.error("Shutting down PostgreSQL MCP Server...");
|
|
389
265
|
try {
|
|
390
266
|
resetDbManager();
|
|
267
|
+
await server.close();
|
|
391
268
|
}
|
|
392
269
|
catch (error) {
|
|
393
|
-
console.error(
|
|
270
|
+
console.error("Error during shutdown:", error);
|
|
394
271
|
}
|
|
395
272
|
process.exit(0);
|
|
396
273
|
}
|
|
397
|
-
process.on(
|
|
398
|
-
process.on(
|
|
399
|
-
process.on(
|
|
274
|
+
process.on("SIGINT", shutdown);
|
|
275
|
+
process.on("SIGTERM", shutdown);
|
|
276
|
+
process.on("SIGHUP", shutdown);
|
|
400
277
|
// Handle uncaught errors
|
|
401
|
-
process.on(
|
|
402
|
-
console.error(
|
|
278
|
+
process.on("uncaughtException", (error) => {
|
|
279
|
+
console.error("Uncaught exception:", error);
|
|
403
280
|
shutdown();
|
|
404
281
|
});
|
|
405
|
-
process.on(
|
|
406
|
-
console.error(
|
|
282
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
283
|
+
console.error("Unhandled rejection at:", promise, "reason:", reason);
|
|
407
284
|
});
|
|
408
285
|
// Start server
|
|
409
286
|
async function main() {
|
|
410
287
|
const transport = new StdioServerTransport();
|
|
411
288
|
await server.connect(transport);
|
|
412
|
-
console.error(
|
|
289
|
+
console.error("PostgreSQL MCP Server started");
|
|
413
290
|
}
|
|
414
291
|
main().catch((error) => {
|
|
415
|
-
console.error(
|
|
292
|
+
console.error("Fatal error:", error);
|
|
416
293
|
process.exit(1);
|
|
417
294
|
});
|
|
418
295
|
//# sourceMappingURL=index.js.map
|