@tejasanik/postgres-mcp-server 2.1.1 → 2.2.1

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 (79) hide show
  1. package/README.md +163 -95
  2. package/dist/db-manager/index.js +7 -0
  3. package/dist/db-manager/validation.js +54 -0
  4. package/dist/db-manager.js +589 -26
  5. package/dist/index.js +141 -11
  6. package/dist/tools/analysis-tools.js +53 -49
  7. package/dist/tools/schema-tools.js +174 -92
  8. package/dist/tools/server-tools.js +5 -2
  9. package/dist/tools/sql/utils/connection-utils.js +129 -0
  10. package/dist/tools/sql/utils/constants.js +55 -0
  11. package/dist/tools/sql/utils/dry-run-utils.js +173 -0
  12. package/dist/tools/sql/utils/file-handler.js +150 -0
  13. package/dist/tools/sql/utils/index.js +12 -0
  14. package/dist/tools/sql/utils/result-formatter.js +154 -0
  15. package/dist/tools/sql/utils/sql-parser.js +468 -0
  16. package/dist/tools/sql-tools.js +383 -532
  17. package/dist/utils/validation.js +335 -72
  18. package/package.json +10 -4
  19. package/dist/__tests__/analysis-tools.test.d.ts +0 -2
  20. package/dist/__tests__/analysis-tools.test.d.ts.map +0 -1
  21. package/dist/__tests__/analysis-tools.test.js +0 -294
  22. package/dist/__tests__/analysis-tools.test.js.map +0 -1
  23. package/dist/__tests__/db-manager.test.d.ts +0 -2
  24. package/dist/__tests__/db-manager.test.d.ts.map +0 -1
  25. package/dist/__tests__/db-manager.test.js +0 -410
  26. package/dist/__tests__/db-manager.test.js.map +0 -1
  27. package/dist/__tests__/mcp-server.test.d.ts +0 -13
  28. package/dist/__tests__/mcp-server.test.d.ts.map +0 -1
  29. package/dist/__tests__/mcp-server.test.js +0 -146
  30. package/dist/__tests__/mcp-server.test.js.map +0 -1
  31. package/dist/__tests__/schema-tools.test.d.ts +0 -2
  32. package/dist/__tests__/schema-tools.test.d.ts.map +0 -1
  33. package/dist/__tests__/schema-tools.test.js +0 -171
  34. package/dist/__tests__/schema-tools.test.js.map +0 -1
  35. package/dist/__tests__/server-tools.test.d.ts +0 -2
  36. package/dist/__tests__/server-tools.test.d.ts.map +0 -1
  37. package/dist/__tests__/server-tools.test.js +0 -137
  38. package/dist/__tests__/server-tools.test.js.map +0 -1
  39. package/dist/__tests__/sql-tools.test.d.ts +0 -2
  40. package/dist/__tests__/sql-tools.test.d.ts.map +0 -1
  41. package/dist/__tests__/sql-tools.test.js +0 -1912
  42. package/dist/__tests__/sql-tools.test.js.map +0 -1
  43. package/dist/__tests__/validation.test.d.ts +0 -2
  44. package/dist/__tests__/validation.test.d.ts.map +0 -1
  45. package/dist/__tests__/validation.test.js +0 -203
  46. package/dist/__tests__/validation.test.js.map +0 -1
  47. package/dist/db-manager.d.ts +0 -83
  48. package/dist/db-manager.d.ts.map +0 -1
  49. package/dist/db-manager.js.map +0 -1
  50. package/dist/index.d.ts +0 -3
  51. package/dist/index.d.ts.map +0 -1
  52. package/dist/index.js.map +0 -1
  53. package/dist/tools/analysis-tools.d.ts +0 -25
  54. package/dist/tools/analysis-tools.d.ts.map +0 -1
  55. package/dist/tools/analysis-tools.js.map +0 -1
  56. package/dist/tools/index.d.ts +0 -5
  57. package/dist/tools/index.d.ts.map +0 -1
  58. package/dist/tools/index.js.map +0 -1
  59. package/dist/tools/schema-tools.d.ts +0 -22
  60. package/dist/tools/schema-tools.d.ts.map +0 -1
  61. package/dist/tools/schema-tools.js.map +0 -1
  62. package/dist/tools/server-tools.d.ts +0 -61
  63. package/dist/tools/server-tools.d.ts.map +0 -1
  64. package/dist/tools/server-tools.js.map +0 -1
  65. package/dist/tools/sql-tools.d.ts +0 -194
  66. package/dist/tools/sql-tools.d.ts.map +0 -1
  67. package/dist/tools/sql-tools.js.map +0 -1
  68. package/dist/types.d.ts +0 -394
  69. package/dist/types.d.ts.map +0 -1
  70. package/dist/types.js.map +0 -1
  71. package/dist/utils/index.d.ts +0 -3
  72. package/dist/utils/index.d.ts.map +0 -1
  73. package/dist/utils/index.js.map +0 -1
  74. package/dist/utils/retry.d.ts +0 -21
  75. package/dist/utils/retry.d.ts.map +0 -1
  76. package/dist/utils/retry.js.map +0 -1
  77. package/dist/utils/validation.d.ts +0 -27
  78. package/dist/utils/validation.d.ts.map +0 -1
  79. package/dist/utils/validation.js.map +0 -1
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Dry-Run Utilities
3
+ *
4
+ * Helper functions for transaction-based dry-run operations.
5
+ * Handles error extraction and detection of non-rollbackable operations.
6
+ */
7
+ /**
8
+ * Non-rollbackable operation patterns.
9
+ * Operations that cannot run inside a transaction or have permanent side effects.
10
+ */
11
+ const NON_ROLLBACKABLE_PATTERNS = [
12
+ // Operations that cannot run inside a transaction at all
13
+ {
14
+ pattern: /\bVACUUM\b/,
15
+ operation: 'VACUUM',
16
+ message: 'VACUUM cannot run inside a transaction block. Statement skipped.',
17
+ mustSkip: true,
18
+ },
19
+ {
20
+ pattern: /\bCLUSTER\b(?!.*CREATE)/,
21
+ operation: 'CLUSTER',
22
+ message: 'CLUSTER cannot run inside a transaction block. Statement skipped.',
23
+ mustSkip: true,
24
+ },
25
+ {
26
+ pattern: /\bREINDEX\b.*\bCONCURRENTLY\b/,
27
+ operation: 'REINDEX_CONCURRENTLY',
28
+ message: 'REINDEX CONCURRENTLY cannot run inside a transaction block. Statement skipped.',
29
+ mustSkip: true,
30
+ },
31
+ {
32
+ pattern: /\bCREATE\s+INDEX\b.*\bCONCURRENTLY\b/,
33
+ operation: 'CREATE_INDEX_CONCURRENTLY',
34
+ message: 'CREATE INDEX CONCURRENTLY cannot run inside a transaction block. Statement skipped.',
35
+ mustSkip: true,
36
+ },
37
+ {
38
+ pattern: /\bCREATE\s+DATABASE\b/,
39
+ operation: 'CREATE_DATABASE',
40
+ message: 'CREATE DATABASE cannot run inside a transaction block. Statement skipped.',
41
+ mustSkip: true,
42
+ },
43
+ {
44
+ pattern: /\bDROP\s+DATABASE\b/,
45
+ operation: 'DROP_DATABASE',
46
+ message: 'DROP DATABASE cannot run inside a transaction block. Statement skipped.',
47
+ mustSkip: true,
48
+ },
49
+ // Operations with permanent side effects
50
+ {
51
+ pattern: /\bNEXTVAL\s*\(/,
52
+ operation: 'SEQUENCE',
53
+ message: 'NEXTVAL increments sequence even when transaction is rolled back. Statement skipped to prevent sequence consumption.',
54
+ mustSkip: true,
55
+ },
56
+ {
57
+ pattern: /\bSETVAL\s*\(/,
58
+ operation: 'SEQUENCE',
59
+ message: 'SETVAL modifies sequence. Statement skipped to prevent side effects.',
60
+ mustSkip: true,
61
+ },
62
+ // Warning-only operations (still executed)
63
+ {
64
+ pattern: /\bINSERT\s+INTO\b/,
65
+ operation: 'SEQUENCE',
66
+ message: 'INSERT may consume sequence values (for SERIAL/BIGSERIAL columns) even when rolled back.',
67
+ mustSkip: false,
68
+ },
69
+ {
70
+ pattern: /\bNOTIFY\b/,
71
+ operation: 'NOTIFY',
72
+ message: 'NOTIFY sends notifications on commit. Since dry-run rolls back, notifications will NOT be sent.',
73
+ mustSkip: false,
74
+ },
75
+ ];
76
+ /**
77
+ * Extract detailed error information from a PostgreSQL error.
78
+ * Captures all available fields to help AI quickly identify and fix issues.
79
+ */
80
+ export function extractDryRunError(error) {
81
+ // Extract message - prioritize Error.message, then object.message, then String conversion
82
+ let message;
83
+ if (error instanceof Error) {
84
+ message = error.message;
85
+ }
86
+ else if (error && typeof error === 'object' && 'message' in error) {
87
+ message = String(error.message);
88
+ }
89
+ else {
90
+ message = String(error);
91
+ }
92
+ const result = { message };
93
+ if (error && typeof error === 'object') {
94
+ const pgError = error;
95
+ // Extract string fields
96
+ if (pgError.code)
97
+ result.code = String(pgError.code);
98
+ if (pgError.severity)
99
+ result.severity = String(pgError.severity);
100
+ if (pgError.detail)
101
+ result.detail = String(pgError.detail);
102
+ if (pgError.hint)
103
+ result.hint = String(pgError.hint);
104
+ if (pgError.internalQuery)
105
+ result.internalQuery = String(pgError.internalQuery);
106
+ if (pgError.where)
107
+ result.where = String(pgError.where);
108
+ if (pgError.schema)
109
+ result.schema = String(pgError.schema);
110
+ if (pgError.table)
111
+ result.table = String(pgError.table);
112
+ if (pgError.column)
113
+ result.column = String(pgError.column);
114
+ if (pgError.dataType)
115
+ result.dataType = String(pgError.dataType);
116
+ if (pgError.constraint)
117
+ result.constraint = String(pgError.constraint);
118
+ if (pgError.file)
119
+ result.file = String(pgError.file);
120
+ if (pgError.line)
121
+ result.line = String(pgError.line);
122
+ if (pgError.routine)
123
+ result.routine = String(pgError.routine);
124
+ // Extract number fields
125
+ if (pgError.position !== undefined)
126
+ result.position = Number(pgError.position);
127
+ if (pgError.internalPosition !== undefined)
128
+ result.internalPosition = Number(pgError.internalPosition);
129
+ }
130
+ return result;
131
+ }
132
+ /**
133
+ * Check if a SQL statement contains operations that cannot be fully rolled back
134
+ * or have side effects even within a transaction.
135
+ *
136
+ * @param sql - The SQL statement to check
137
+ * @param statementIndex - Optional statement index for error reporting
138
+ * @param lineNumber - Optional line number for error reporting
139
+ * @returns Array of warnings about non-rollbackable operations
140
+ */
141
+ export function detectNonRollbackableOperations(sql, statementIndex, lineNumber) {
142
+ const warnings = [];
143
+ const upperSql = sql.toUpperCase().trim();
144
+ const clusterPattern = /\bCLUSTER\b/;
145
+ for (const { pattern, operation, message, mustSkip } of NON_ROLLBACKABLE_PATTERNS) {
146
+ // Special handling for CLUSTER (must not be part of CREATE)
147
+ if (operation === 'CLUSTER') {
148
+ if (clusterPattern.test(upperSql) && !upperSql.includes('CREATE')) {
149
+ warnings.push({ operation, message, statementIndex, lineNumber, mustSkip });
150
+ }
151
+ }
152
+ else if (pattern.test(upperSql)) {
153
+ warnings.push({ operation, message, statementIndex, lineNumber, mustSkip });
154
+ }
155
+ }
156
+ return warnings;
157
+ }
158
+ /**
159
+ * Check if any warning requires skipping the statement.
160
+ */
161
+ export function hasMustSkipWarning(warnings) {
162
+ return warnings.some((w) => w.mustSkip);
163
+ }
164
+ /**
165
+ * Get skip reason from must-skip warnings.
166
+ */
167
+ export function getSkipReason(warnings) {
168
+ return warnings
169
+ .filter((w) => w.mustSkip)
170
+ .map((w) => w.message)
171
+ .join('; ');
172
+ }
173
+ //# sourceMappingURL=dry-run-utils.js.map
@@ -0,0 +1,150 @@
1
+ /**
2
+ * File Handler Utilities
3
+ *
4
+ * Functions for validating and processing SQL files.
5
+ * Handles file validation, reading, and preprocessing.
6
+ */
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import { MAX_SQL_FILE_SIZE } from './constants.js';
10
+ /**
11
+ * Validates a SQL file path and returns file information.
12
+ *
13
+ * @param filePath - Path to the SQL file
14
+ * @returns Validation result with resolved path and file size
15
+ */
16
+ export function validateSqlFile(filePath) {
17
+ // Check file extension
18
+ const ext = path.extname(filePath).toLowerCase();
19
+ if (ext !== '.sql') {
20
+ return {
21
+ isValid: false,
22
+ resolvedPath: '',
23
+ fileSize: 0,
24
+ error: 'Only .sql files are allowed. Received file extension: ' + ext,
25
+ };
26
+ }
27
+ // Resolve path
28
+ const resolvedPath = path.resolve(filePath);
29
+ // Check file exists
30
+ if (!fs.existsSync(resolvedPath)) {
31
+ return {
32
+ isValid: false,
33
+ resolvedPath,
34
+ fileSize: 0,
35
+ error: `File not found: ${filePath}`,
36
+ };
37
+ }
38
+ // Get file stats
39
+ const stats = fs.statSync(resolvedPath);
40
+ // Check it's a file
41
+ if (!stats.isFile()) {
42
+ return {
43
+ isValid: false,
44
+ resolvedPath,
45
+ fileSize: 0,
46
+ error: `Not a file: ${filePath}`,
47
+ };
48
+ }
49
+ // Check file size
50
+ if (stats.size > MAX_SQL_FILE_SIZE) {
51
+ return {
52
+ isValid: false,
53
+ resolvedPath,
54
+ fileSize: stats.size,
55
+ error: `File too large: ${formatFileSize(stats.size)}. Maximum allowed: ${formatFileSize(MAX_SQL_FILE_SIZE)}`,
56
+ };
57
+ }
58
+ // Check file not empty
59
+ if (stats.size === 0) {
60
+ return {
61
+ isValid: false,
62
+ resolvedPath,
63
+ fileSize: 0,
64
+ error: 'File is empty',
65
+ };
66
+ }
67
+ return {
68
+ isValid: true,
69
+ resolvedPath,
70
+ fileSize: stats.size,
71
+ };
72
+ }
73
+ /**
74
+ * Reads a SQL file and optionally preprocesses it.
75
+ *
76
+ * @param resolvedPath - Resolved path to the SQL file
77
+ * @param stripPatterns - Optional patterns to remove from content
78
+ * @param stripAsRegex - If true, patterns are regex; if false, literal strings
79
+ * @returns Preprocessed SQL content
80
+ */
81
+ export function readSqlFile(resolvedPath, stripPatterns, stripAsRegex = false) {
82
+ let content = fs.readFileSync(resolvedPath, 'utf-8');
83
+ if (stripPatterns && stripPatterns.length > 0) {
84
+ content = preprocessSqlContent(content, stripPatterns, stripAsRegex);
85
+ }
86
+ return content;
87
+ }
88
+ /**
89
+ * Preprocess SQL content by removing patterns.
90
+ * Supports both literal string matching and regex patterns.
91
+ *
92
+ * @param sql - The SQL content to preprocess
93
+ * @param patterns - Array of patterns to remove from SQL content
94
+ * @param isRegex - If true, patterns are treated as regex; if false, as literal strings
95
+ * @returns Preprocessed SQL content
96
+ */
97
+ export function preprocessSqlContent(sql, patterns, isRegex = false) {
98
+ let result = sql;
99
+ for (const pattern of patterns) {
100
+ try {
101
+ if (isRegex) {
102
+ // Treat as regex pattern (multiline by default)
103
+ const regex = new RegExp(pattern, 'gm');
104
+ result = result.replace(regex, '');
105
+ }
106
+ else {
107
+ // Treat as literal string - escape and match on its own line
108
+ const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
109
+ const regex = new RegExp(`^\\s*${escapedPattern}\\s*$`, 'gm');
110
+ result = result.replace(regex, '');
111
+ }
112
+ }
113
+ catch (error) {
114
+ // Invalid regex - skip this pattern silently in production
115
+ // Log only in non-production for debugging
116
+ if (process.env.NODE_ENV !== 'production') {
117
+ console.error(`Warning: Invalid pattern "${pattern}": ${error instanceof Error ? error.message : String(error)}`);
118
+ }
119
+ }
120
+ }
121
+ return result;
122
+ }
123
+ /**
124
+ * Format file size in human-readable format.
125
+ *
126
+ * @param bytes - File size in bytes
127
+ * @returns Human-readable file size (e.g., "1.5 MB")
128
+ */
129
+ export function formatFileSize(bytes) {
130
+ if (bytes === 0)
131
+ return '0 B';
132
+ const units = ['B', 'KB', 'MB', 'GB'];
133
+ const base = 1024;
134
+ const unitIndex = Math.min(Math.floor(Math.log(bytes) / Math.log(base)), units.length - 1);
135
+ const size = bytes / Math.pow(base, unitIndex);
136
+ return `${size.toFixed(unitIndex > 0 ? 1 : 0)} ${units[unitIndex]}`;
137
+ }
138
+ /**
139
+ * Ensure a file path is safe (no path traversal attacks).
140
+ *
141
+ * @param basePath - Base directory path
142
+ * @param filePath - File path to validate
143
+ * @returns True if the file path is within the base path
144
+ */
145
+ export function isPathSafe(basePath, filePath) {
146
+ const resolvedBase = path.resolve(basePath);
147
+ const resolvedFile = path.resolve(filePath);
148
+ return resolvedFile.startsWith(resolvedBase);
149
+ }
150
+ //# sourceMappingURL=file-handler.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * SQL Tools Utilities
3
+ *
4
+ * Re-exports all utility modules for convenient importing.
5
+ */
6
+ export * from './constants.js';
7
+ export * from './connection-utils.js';
8
+ export * from './dry-run-utils.js';
9
+ export * from './file-handler.js';
10
+ export * from './result-formatter.js';
11
+ export * from './sql-parser.js';
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Result Formatter Utilities
3
+ *
4
+ * Functions for formatting query results, handling large outputs,
5
+ * and timing measurements.
6
+ */
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import * as os from 'os';
10
+ import { v4 as uuidv4 } from 'uuid';
11
+ import { MAX_OUTPUT_CHARS } from './constants.js';
12
+ /**
13
+ * Calculate execution time from hrtime bigints.
14
+ *
15
+ * @param startTime - Start time from process.hrtime.bigint()
16
+ * @param endTime - End time from process.hrtime.bigint()
17
+ * @returns Execution time in milliseconds (rounded to 2 decimal places)
18
+ */
19
+ export function calculateExecutionTime(startTime, endTime) {
20
+ return Math.round((Number(endTime - startTime) / 1_000_000) * 100) / 100;
21
+ }
22
+ /**
23
+ * Get current time for timing measurements.
24
+ *
25
+ * @returns Current high-resolution time as bigint
26
+ */
27
+ export function getStartTime() {
28
+ return process.hrtime.bigint();
29
+ }
30
+ /**
31
+ * Handle potentially large output by writing to file if necessary.
32
+ *
33
+ * @param rows - Result rows to check
34
+ * @param maxChars - Maximum characters before writing to file
35
+ * @returns Object with truncated flag, optional file path, and rows
36
+ */
37
+ export function handleLargeOutput(rows, maxChars = MAX_OUTPUT_CHARS) {
38
+ const output = JSON.stringify(rows);
39
+ if (output.length <= maxChars) {
40
+ return { truncated: false, rows };
41
+ }
42
+ // Write to temp file
43
+ const tempDir = os.tmpdir();
44
+ const fileName = `sql-result-${uuidv4()}.json`;
45
+ const filePath = path.join(tempDir, fileName);
46
+ fs.writeFileSync(filePath, output, { mode: 0o600 });
47
+ return {
48
+ truncated: true,
49
+ outputFile: filePath,
50
+ rows: [], // Return empty rows since output is in file
51
+ };
52
+ }
53
+ /**
54
+ * Paginate result rows.
55
+ *
56
+ * @param rows - All result rows
57
+ * @param offset - Number of rows to skip
58
+ * @param maxRows - Maximum rows to return
59
+ * @returns Paginated rows and metadata
60
+ */
61
+ export function paginateRows(rows, offset, maxRows) {
62
+ const totalCount = rows.length;
63
+ const paginatedRows = rows.slice(offset, offset + maxRows);
64
+ const hasMore = offset + paginatedRows.length < totalCount;
65
+ return {
66
+ rows: paginatedRows,
67
+ offset,
68
+ hasMore,
69
+ totalCount,
70
+ };
71
+ }
72
+ /**
73
+ * Truncate SQL for display in error messages or previews.
74
+ *
75
+ * @param sql - SQL string to truncate
76
+ * @param maxLength - Maximum length (default: 200)
77
+ * @returns Truncated SQL with ellipsis if needed
78
+ */
79
+ export function truncateSql(sql, maxLength = 200) {
80
+ const trimmed = sql.trim();
81
+ if (trimmed.length <= maxLength) {
82
+ return trimmed;
83
+ }
84
+ return trimmed.substring(0, maxLength) + '...';
85
+ }
86
+ /**
87
+ * Format field names from query result.
88
+ *
89
+ * @param fields - Array of field objects from pg result
90
+ * @returns Array of field names
91
+ */
92
+ export function formatFieldNames(fields) {
93
+ return fields.map((f) => f.name);
94
+ }
95
+ /**
96
+ * Create a summary message for statement execution.
97
+ *
98
+ * @param totalStatements - Total number of statements
99
+ * @param successCount - Number of successful statements
100
+ * @param failureCount - Number of failed statements
101
+ * @param skippedCount - Number of skipped statements
102
+ * @param rolledBack - Whether changes were rolled back
103
+ * @returns Summary message string
104
+ */
105
+ export function createExecutionSummary(totalStatements, successCount, failureCount, skippedCount, rolledBack) {
106
+ const parts = [];
107
+ if (rolledBack) {
108
+ parts.push(`Dry-run of ${totalStatements} statements:`);
109
+ }
110
+ else {
111
+ parts.push(`Executed ${totalStatements} statements:`);
112
+ }
113
+ if (successCount > 0) {
114
+ parts.push(`${successCount} succeeded`);
115
+ }
116
+ if (failureCount > 0) {
117
+ parts.push(`${failureCount} failed`);
118
+ }
119
+ if (skippedCount > 0) {
120
+ parts.push(`${skippedCount} skipped (non-rollbackable)`);
121
+ }
122
+ if (rolledBack) {
123
+ parts.push('All changes rolled back.');
124
+ }
125
+ return parts.join(', ').replace('statements:,', 'statements:');
126
+ }
127
+ /**
128
+ * Count statements by type.
129
+ *
130
+ * @param types - Array of statement types
131
+ * @returns Object with counts by type
132
+ */
133
+ export function countStatementsByType(types) {
134
+ const counts = {};
135
+ for (const type of types) {
136
+ counts[type] = (counts[type] || 0) + 1;
137
+ }
138
+ return counts;
139
+ }
140
+ /**
141
+ * Create a human-readable file summary.
142
+ *
143
+ * @param statementsByType - Statement counts by type
144
+ * @param totalStatements - Total statement count
145
+ * @returns Summary string
146
+ */
147
+ export function createFileSummary(statementsByType, totalStatements) {
148
+ const parts = Object.entries(statementsByType)
149
+ .sort(([, a], [, b]) => b - a)
150
+ .map(([type, count]) => `${count} ${type}`)
151
+ .join(', ');
152
+ return `File contains ${totalStatements} statements: ${parts}`;
153
+ }
154
+ //# sourceMappingURL=result-formatter.js.map