easy-pg-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/LICENSE.md +21 -0
- package/README.md +228 -0
- package/README.zh-TW.md +228 -0
- package/build/approvalStore.js +58 -0
- package/build/config.js +46 -0
- package/build/csv.js +109 -0
- package/build/csvTools.js +97 -0
- package/build/db.js +206 -0
- package/build/index.js +196 -0
- package/build/logs.js +28 -0
- package/build/policyHook.js +66 -0
- package/build/sqlPolicy.js +186 -0
- package/build/toolHandlers.js +249 -0
- package/package.json +52 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { config, isReadOnlyMode, normalizeTableName } from './config.js';
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const { Parser } = require('node-sql-parser/build/postgresql.js');
|
|
5
|
+
const parser = new Parser();
|
|
6
|
+
const parserOptions = { database: 'Postgresql' };
|
|
7
|
+
const readQueryTypes = new Set(['select', 'show', 'explain']);
|
|
8
|
+
const executeTypes = new Set(['insert', 'update', 'delete']);
|
|
9
|
+
export class SqlPolicyError extends Error {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'SqlPolicyError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function assertReadQueryAllowed(sql) {
|
|
16
|
+
const parsed = parseSingleStatement(sql);
|
|
17
|
+
if (!readQueryTypes.has(parsed.type)) {
|
|
18
|
+
throw new SqlPolicyError(`SQL rejected: pg_query only allows SELECT, SHOW, and EXPLAIN statements. Received ${parsed.type.toUpperCase()}.`);
|
|
19
|
+
}
|
|
20
|
+
assertNoUnsafeReadOptions(parsed.ast);
|
|
21
|
+
assertTablePolicy(parsed.tables);
|
|
22
|
+
}
|
|
23
|
+
export function analyzeSql(sql) {
|
|
24
|
+
const parsed = parseSingleStatement(sql);
|
|
25
|
+
return {
|
|
26
|
+
statementType: parsed.type,
|
|
27
|
+
tableNames: parsed.tables,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function assertExplainQueryAllowed(sql) {
|
|
31
|
+
const parsed = parseSingleStatement(sql);
|
|
32
|
+
if (parsed.type !== 'select') {
|
|
33
|
+
throw new SqlPolicyError(`SQL rejected: explain_query only accepts a SELECT statement. Received ${parsed.type.toUpperCase()}.`);
|
|
34
|
+
}
|
|
35
|
+
assertNoUnsafeReadOptions(parsed.ast);
|
|
36
|
+
assertTablePolicy(parsed.tables);
|
|
37
|
+
}
|
|
38
|
+
export function assertExecuteAllowed(sql) {
|
|
39
|
+
if (isReadOnlyMode()) {
|
|
40
|
+
throw new SqlPolicyError('SQL rejected: write execution is disabled because PG_READ_ONLY=true or PG_MCP_MODE=readonly.');
|
|
41
|
+
}
|
|
42
|
+
const parsed = parseSingleStatement(sql);
|
|
43
|
+
if (!executeTypes.has(parsed.type)) {
|
|
44
|
+
throw new SqlPolicyError(`SQL rejected: pg_execute only allows INSERT, UPDATE, and DELETE statements. Received ${parsed.type.toUpperCase()}.`);
|
|
45
|
+
}
|
|
46
|
+
assertTablePolicy(parsed.tables);
|
|
47
|
+
}
|
|
48
|
+
export function assertTablesAllowed(tables) {
|
|
49
|
+
assertTablePolicy(tables.map((table) => normalizeTableName(table)));
|
|
50
|
+
}
|
|
51
|
+
export function isTableAllowed(table) {
|
|
52
|
+
try {
|
|
53
|
+
assertTablesAllowed([table]);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error instanceof SqlPolicyError) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function parseSingleStatement(sql) {
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = parser.parse(sql, parserOptions);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
70
|
+
throw new SqlPolicyError(`SQL rejected: unable to parse statement as PostgreSQL SQL. ${message}`);
|
|
71
|
+
}
|
|
72
|
+
const ast = parsed.ast;
|
|
73
|
+
if (Array.isArray(ast)) {
|
|
74
|
+
if (ast.length !== 1) {
|
|
75
|
+
throw new SqlPolicyError('SQL rejected: multiple statements are not allowed.');
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
ast: ast[0],
|
|
79
|
+
type: getStatementType(ast[0]),
|
|
80
|
+
tables: getVisitedTables(parsed, ast[0]),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
ast,
|
|
85
|
+
type: getStatementType(ast),
|
|
86
|
+
tables: getVisitedTables(parsed, ast),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function getStatementType(ast) {
|
|
90
|
+
const type = typeof ast?.type === 'string' ? ast.type.toLowerCase() : 'unknown';
|
|
91
|
+
return type === 'desc' ? 'describe' : type;
|
|
92
|
+
}
|
|
93
|
+
function assertNoUnsafeReadOptions(ast) {
|
|
94
|
+
if (Array.isArray(ast)) {
|
|
95
|
+
for (const statement of ast) {
|
|
96
|
+
assertNoUnsafeReadOptions(statement);
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (ast?.type === 'select' && ast.into?.keyword) {
|
|
101
|
+
throw new SqlPolicyError('SQL rejected: SELECT ... INTO is not allowed.');
|
|
102
|
+
}
|
|
103
|
+
if (ast?.locking_read) {
|
|
104
|
+
throw new SqlPolicyError('SQL rejected: locking reads are not allowed in pg_query.');
|
|
105
|
+
}
|
|
106
|
+
if (Array.isArray(ast?.with)) {
|
|
107
|
+
for (const cte of ast.with) {
|
|
108
|
+
assertNoUnsafeReadOptions(cte?.stmt?.ast);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (ast?.type === 'explain') {
|
|
112
|
+
assertNoUnsafeReadOptions(ast.expr);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function assertTablePolicy(tables) {
|
|
116
|
+
for (const table of tables) {
|
|
117
|
+
if (matchesTableList(table, config.denyTables)) {
|
|
118
|
+
throw new SqlPolicyError(`SQL rejected: table "${table}" is denied by PG_MCP_DENY_TABLES.`);
|
|
119
|
+
}
|
|
120
|
+
if (config.allowTables.length > 0 && !matchesTableList(table, config.allowTables)) {
|
|
121
|
+
throw new SqlPolicyError(`SQL rejected: table "${table}" is not included in PG_MCP_ALLOW_TABLES.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function matchesTableList(table, configuredTables) {
|
|
126
|
+
if (configuredTables.includes(table)) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
const tableOnly = table.split('.').at(-1);
|
|
130
|
+
return tableOnly ? configuredTables.includes(tableOnly) : false;
|
|
131
|
+
}
|
|
132
|
+
function getVisitedTables(parsed, ast) {
|
|
133
|
+
const cteNames = collectCteNames(ast);
|
|
134
|
+
const tables = new Set();
|
|
135
|
+
for (const tableRef of parsed.tableList) {
|
|
136
|
+
const table = normalizeTableRef(tableRef);
|
|
137
|
+
if (table && !cteNames.has(table)) {
|
|
138
|
+
tables.add(table);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
for (const table of collectTablesFromAst(ast)) {
|
|
142
|
+
const normalized = normalizeTableName(table);
|
|
143
|
+
if (normalized && !cteNames.has(normalized)) {
|
|
144
|
+
tables.add(normalized);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return [...tables];
|
|
148
|
+
}
|
|
149
|
+
function normalizeTableRef(tableRef) {
|
|
150
|
+
const [, dbName, tableName] = tableRef.split('::');
|
|
151
|
+
if (!tableName || tableName === 'null') {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const normalizedTable = normalizeTableName(tableName);
|
|
155
|
+
if (dbName && dbName !== 'null') {
|
|
156
|
+
return `${normalizeTableName(dbName)}.${normalizedTable}`;
|
|
157
|
+
}
|
|
158
|
+
return normalizedTable;
|
|
159
|
+
}
|
|
160
|
+
function collectCteNames(ast) {
|
|
161
|
+
const names = new Set();
|
|
162
|
+
if (!Array.isArray(ast?.with)) {
|
|
163
|
+
return names;
|
|
164
|
+
}
|
|
165
|
+
for (const cte of ast.with) {
|
|
166
|
+
const value = cte?.name?.value;
|
|
167
|
+
if (typeof value === 'string') {
|
|
168
|
+
names.add(normalizeTableName(value));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return names;
|
|
172
|
+
}
|
|
173
|
+
function collectTablesFromAst(ast) {
|
|
174
|
+
if (!ast) {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
switch (ast.type) {
|
|
178
|
+
case 'desc':
|
|
179
|
+
case 'describe':
|
|
180
|
+
return typeof ast.table === 'string' ? [ast.table] : [];
|
|
181
|
+
case 'explain':
|
|
182
|
+
return collectTablesFromAst(ast.expr);
|
|
183
|
+
default:
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { config } from './config.js';
|
|
2
|
+
import { exportCsv, importCsv } from './csvTools.js';
|
|
3
|
+
import * as db from './db.js';
|
|
4
|
+
import { writeBatchExecuteLog } from './logs.js';
|
|
5
|
+
import { runWithPolicy } from './policyHook.js';
|
|
6
|
+
import { analyzeSql, assertExecuteAllowed, assertExplainQueryAllowed, assertReadQueryAllowed, assertTablesAllowed, isTableAllowed, } from './sqlPolicy.js';
|
|
7
|
+
export async function pgQuery(sql) {
|
|
8
|
+
assertReadQueryAllowed(sql);
|
|
9
|
+
const analysis = analyzeSql(sql);
|
|
10
|
+
return runWithPolicy({
|
|
11
|
+
functionName: 'pg_query',
|
|
12
|
+
sql,
|
|
13
|
+
statementType: analysis.statementType,
|
|
14
|
+
tableNames: analysis.tableNames,
|
|
15
|
+
}, () => db.query(sql));
|
|
16
|
+
}
|
|
17
|
+
export async function pgExecute(sql, params) {
|
|
18
|
+
assertExecuteAllowed(sql);
|
|
19
|
+
const analysis = analyzeSql(sql);
|
|
20
|
+
return runWithPolicy({
|
|
21
|
+
functionName: 'pg_execute',
|
|
22
|
+
sql,
|
|
23
|
+
statementType: analysis.statementType,
|
|
24
|
+
tableNames: analysis.tableNames,
|
|
25
|
+
paramsPreview: params ?? null,
|
|
26
|
+
summary: { sql, paramsPreview: params ?? null },
|
|
27
|
+
}, () => db.execute(sql, params));
|
|
28
|
+
}
|
|
29
|
+
export async function pgBatchExecute(sql, paramsList, transaction) {
|
|
30
|
+
assertExecuteAllowed(sql);
|
|
31
|
+
const analysis = analyzeSql(sql);
|
|
32
|
+
return runWithPolicy({
|
|
33
|
+
functionName: 'pg_batch_execute',
|
|
34
|
+
sql,
|
|
35
|
+
statementType: analysis.statementType,
|
|
36
|
+
tableNames: analysis.tableNames,
|
|
37
|
+
paramsPreview: { rows: paramsList.length, firstParams: paramsList[0] ?? null },
|
|
38
|
+
summary: { sql, rows: paramsList.length, transaction },
|
|
39
|
+
}, async () => {
|
|
40
|
+
const summary = await db.batchExecute(sql, paramsList, {
|
|
41
|
+
batchSize: config.batchMaxSize,
|
|
42
|
+
transaction,
|
|
43
|
+
});
|
|
44
|
+
const logPath = await writeBatchExecuteLog({
|
|
45
|
+
tool: 'pg_batch_execute',
|
|
46
|
+
success: true,
|
|
47
|
+
sql,
|
|
48
|
+
summary,
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
totalRows: summary.totalRows,
|
|
52
|
+
batches: summary.batches,
|
|
53
|
+
batchSize: summary.batchSize,
|
|
54
|
+
transaction: summary.transaction,
|
|
55
|
+
affectedRows: summary.affectedRows,
|
|
56
|
+
changedRows: summary.changedRows,
|
|
57
|
+
logPath,
|
|
58
|
+
};
|
|
59
|
+
}).catch(async (error) => {
|
|
60
|
+
if (!(error instanceof db.BatchExecuteError)) {
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
const logPath = await writeBatchExecuteLog({
|
|
64
|
+
tool: 'pg_batch_execute',
|
|
65
|
+
success: false,
|
|
66
|
+
sql,
|
|
67
|
+
summary: error.summary,
|
|
68
|
+
error: error.message,
|
|
69
|
+
});
|
|
70
|
+
throw new Error(`Batch execution failed: ${error.message}. Detailed log: ${logPath}`);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export async function pgImportCsv(tableName, filePath, transaction) {
|
|
74
|
+
assertTablesAllowed([tableName]);
|
|
75
|
+
return runWithPolicy({
|
|
76
|
+
functionName: 'pg_import_csv',
|
|
77
|
+
sql: null,
|
|
78
|
+
statementType: 'insert',
|
|
79
|
+
tableNames: [tableName],
|
|
80
|
+
paramsPreview: { filePath, transaction },
|
|
81
|
+
summary: { tableName, filePath, transaction },
|
|
82
|
+
}, () => importCsv(tableName, filePath, transaction));
|
|
83
|
+
}
|
|
84
|
+
export async function pgExportCsv(tableName, filePath) {
|
|
85
|
+
assertTablesAllowed([tableName]);
|
|
86
|
+
return runWithPolicy({
|
|
87
|
+
functionName: 'pg_export_csv',
|
|
88
|
+
sql: null,
|
|
89
|
+
statementType: 'export',
|
|
90
|
+
tableNames: [tableName],
|
|
91
|
+
paramsPreview: { filePath },
|
|
92
|
+
summary: { tableName, filePath },
|
|
93
|
+
}, () => exportCsv(tableName, filePath));
|
|
94
|
+
}
|
|
95
|
+
export async function explainQuery(sql) {
|
|
96
|
+
assertExplainQueryAllowed(sql);
|
|
97
|
+
const analysis = analyzeSql(sql);
|
|
98
|
+
return runWithPolicy({
|
|
99
|
+
functionName: 'explain_query',
|
|
100
|
+
sql,
|
|
101
|
+
statementType: 'explain',
|
|
102
|
+
tableNames: analysis.tableNames,
|
|
103
|
+
}, () => db.query(`EXPLAIN ${sql}`));
|
|
104
|
+
}
|
|
105
|
+
export async function listTables() {
|
|
106
|
+
return runWithPolicy({
|
|
107
|
+
functionName: 'list_tables',
|
|
108
|
+
sql: null,
|
|
109
|
+
statementType: 'schema',
|
|
110
|
+
tableNames: [],
|
|
111
|
+
}, async () => {
|
|
112
|
+
const results = await db.query(`
|
|
113
|
+
SELECT
|
|
114
|
+
table_name,
|
|
115
|
+
COALESCE(pg_class.reltuples::bigint, 0) AS table_rows,
|
|
116
|
+
obj_description(pg_class.oid, 'pg_class') AS table_comment
|
|
117
|
+
FROM information_schema.tables
|
|
118
|
+
LEFT JOIN pg_namespace
|
|
119
|
+
ON pg_namespace.nspname = information_schema.tables.table_schema
|
|
120
|
+
LEFT JOIN pg_class
|
|
121
|
+
ON pg_class.relname = information_schema.tables.table_name
|
|
122
|
+
AND pg_class.relnamespace = pg_namespace.oid
|
|
123
|
+
WHERE table_schema = current_schema()
|
|
124
|
+
AND table_type = 'BASE TABLE'
|
|
125
|
+
ORDER BY table_name
|
|
126
|
+
`);
|
|
127
|
+
return Array.isArray(results)
|
|
128
|
+
? results.filter((row) => typeof row.table_name === 'string' && isTableAllowed(row.table_name))
|
|
129
|
+
: results;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
export async function listViews() {
|
|
133
|
+
return runWithPolicy({
|
|
134
|
+
functionName: 'list_views',
|
|
135
|
+
sql: null,
|
|
136
|
+
statementType: 'schema',
|
|
137
|
+
tableNames: [],
|
|
138
|
+
}, async () => {
|
|
139
|
+
const results = await db.query(`
|
|
140
|
+
SELECT table_name, view_definition
|
|
141
|
+
FROM information_schema.views
|
|
142
|
+
WHERE table_schema = current_schema()
|
|
143
|
+
ORDER BY table_name
|
|
144
|
+
`);
|
|
145
|
+
return Array.isArray(results)
|
|
146
|
+
? results.filter((row) => typeof row.table_name === 'string' && isTableAllowed(row.table_name))
|
|
147
|
+
: results;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
export async function describeTable(tables) {
|
|
151
|
+
assertTablesAllowed(tables);
|
|
152
|
+
return runWithPolicy({
|
|
153
|
+
functionName: 'describe_table',
|
|
154
|
+
sql: null,
|
|
155
|
+
statementType: 'schema',
|
|
156
|
+
tableNames: tables,
|
|
157
|
+
summary: { tables },
|
|
158
|
+
}, async () => {
|
|
159
|
+
const results = {};
|
|
160
|
+
for (const table of tables) {
|
|
161
|
+
const { schema, name } = splitTableName(table);
|
|
162
|
+
results[table] = await db.query(`
|
|
163
|
+
SELECT
|
|
164
|
+
column_name,
|
|
165
|
+
data_type,
|
|
166
|
+
is_nullable,
|
|
167
|
+
column_default,
|
|
168
|
+
character_maximum_length,
|
|
169
|
+
numeric_precision,
|
|
170
|
+
numeric_scale
|
|
171
|
+
FROM information_schema.columns
|
|
172
|
+
WHERE table_schema = COALESCE($1, current_schema())
|
|
173
|
+
AND table_name = $2
|
|
174
|
+
ORDER BY ordinal_position
|
|
175
|
+
`, [schema, name]);
|
|
176
|
+
}
|
|
177
|
+
return results;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
export async function describeIndex(table) {
|
|
181
|
+
assertTablesAllowed([table]);
|
|
182
|
+
return runWithPolicy({
|
|
183
|
+
functionName: 'describe_index',
|
|
184
|
+
sql: null,
|
|
185
|
+
statementType: 'schema',
|
|
186
|
+
tableNames: [table],
|
|
187
|
+
summary: { table },
|
|
188
|
+
}, () => {
|
|
189
|
+
const { schema, name } = splitTableName(table);
|
|
190
|
+
return db.query(`
|
|
191
|
+
SELECT
|
|
192
|
+
schemaname AS schema_name,
|
|
193
|
+
tablename AS table_name,
|
|
194
|
+
indexname AS index_name,
|
|
195
|
+
indexdef AS index_definition
|
|
196
|
+
FROM pg_indexes
|
|
197
|
+
WHERE schemaname = COALESCE($1, current_schema())
|
|
198
|
+
AND tablename = $2
|
|
199
|
+
ORDER BY indexname
|
|
200
|
+
`, [schema, name]);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
export async function listTriggers() {
|
|
204
|
+
return runWithPolicy({
|
|
205
|
+
functionName: 'list_triggers',
|
|
206
|
+
sql: null,
|
|
207
|
+
statementType: 'schema',
|
|
208
|
+
tableNames: [],
|
|
209
|
+
}, async () => {
|
|
210
|
+
const results = await db.query(`
|
|
211
|
+
SELECT
|
|
212
|
+
trigger_name,
|
|
213
|
+
event_object_table AS table_name,
|
|
214
|
+
event_manipulation,
|
|
215
|
+
action_timing,
|
|
216
|
+
action_statement
|
|
217
|
+
FROM information_schema.triggers
|
|
218
|
+
WHERE trigger_schema = current_schema()
|
|
219
|
+
ORDER BY trigger_name, event_manipulation
|
|
220
|
+
`);
|
|
221
|
+
return Array.isArray(results)
|
|
222
|
+
? results.filter((row) => typeof row.table_name === 'string' && isTableAllowed(row.table_name))
|
|
223
|
+
: results;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
export async function getCurrentPrivileges() {
|
|
227
|
+
return runWithPolicy({
|
|
228
|
+
functionName: 'get_current_privileges',
|
|
229
|
+
sql: null,
|
|
230
|
+
statementType: 'privileges',
|
|
231
|
+
tableNames: [],
|
|
232
|
+
}, async () => {
|
|
233
|
+
const user = await db.query('SELECT current_user AS user');
|
|
234
|
+
const grants = await db.query(`
|
|
235
|
+
SELECT grantee, table_schema, table_name, privilege_type, is_grantable
|
|
236
|
+
FROM information_schema.role_table_grants
|
|
237
|
+
WHERE grantee = current_user
|
|
238
|
+
ORDER BY table_schema, table_name, privilege_type
|
|
239
|
+
`);
|
|
240
|
+
return { currentUser: user, grants };
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
function splitTableName(table) {
|
|
244
|
+
const parts = table.replace(/"/g, '').split('.');
|
|
245
|
+
if (parts.length > 1) {
|
|
246
|
+
return { schema: parts[0], name: parts.at(-1) ?? table };
|
|
247
|
+
}
|
|
248
|
+
return { schema: null, name: table.replace(/"/g, '') };
|
|
249
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "easy-pg-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "High performance PostgreSQL MCP Server using node-postgres",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"easy-pg-mcp": "build/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"build",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"model-context-protocol",
|
|
20
|
+
"postgresql",
|
|
21
|
+
"postgres",
|
|
22
|
+
"pg",
|
|
23
|
+
"node-postgres",
|
|
24
|
+
"claude"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc",
|
|
32
|
+
"test": "npm run build && node --test test/*.test.mjs",
|
|
33
|
+
"start": "node build/index.js",
|
|
34
|
+
"dev": "tsc --watch",
|
|
35
|
+
"prepack": "npm run build"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
39
|
+
"node-sql-parser": "^5.4.0",
|
|
40
|
+
"pg": "^8.21.0",
|
|
41
|
+
"zod": "^4.4.3"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^25.6.0",
|
|
45
|
+
"@types/pg": "^8.15.6",
|
|
46
|
+
"typescript": "^6.0.3"
|
|
47
|
+
},
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "https://github.com/chenkumi/easy-pg-mcp.git"
|
|
51
|
+
}
|
|
52
|
+
}
|