easy-pg-mcp 1.0.0 → 1.0.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.
- package/README.md +9 -3
- package/README.zh-TW.md +9 -3
- package/build/config.js +6 -0
- package/build/index.js +16 -3
- package/build/sqlPolicy.js +120 -3
- package/build/toolHandlers.js +11 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ This project uses Node.js, TypeScript, the official MCP SDK, and [`pg`](https://
|
|
|
8
8
|
|
|
9
9
|
- PostgreSQL connection pooling powered by node-postgres
|
|
10
10
|
- Read-only query tool for data retrieval
|
|
11
|
-
- Execute
|
|
11
|
+
- Execute tools for data modification and optional advanced schema changes
|
|
12
12
|
- Batch execution and CSV import/export helpers
|
|
13
13
|
- Schema discovery tools for tables, views, indexes, and triggers
|
|
14
14
|
- Query plan inspection with `EXPLAIN`
|
|
@@ -53,7 +53,7 @@ npm run build
|
|
|
53
53
|
| `PG_KEEP_ALIVE_INITIAL_DELAY` | No | `0` | Initial TCP keep-alive delay in milliseconds |
|
|
54
54
|
| `PG_SSL` | No | - | Set `true` to enable SSL, or `no-verify` to disable certificate verification |
|
|
55
55
|
| `PG_READ_ONLY` | No | `false` | When `true`, write tools are not registered |
|
|
56
|
-
| `PG_MCP_MODE` | No | `readwrite` | Use `readonly` to disable write execution |
|
|
56
|
+
| `PG_MCP_MODE` | No | `readwrite` | Use `readonly` to disable write execution, or `advanced` to enable schema execution |
|
|
57
57
|
| `PG_MCP_ALLOW_TABLES` | No | - | Comma-separated table allowlist, such as `users,orders` |
|
|
58
58
|
| `PG_MCP_DENY_TABLES` | No | - | Comma-separated table denylist, such as `payments,secrets` |
|
|
59
59
|
| `PG_BATCH_MAX_SIZE` | No | `100` | Maximum number of parameter sets per internal batch for `pg_batch_execute` |
|
|
@@ -117,6 +117,7 @@ PG_DATABASE = "YOUR DB NAME"
|
|
|
117
117
|
| --- | --- |
|
|
118
118
|
| `pg_query` | Execute a read-only SQL query, such as `SELECT` |
|
|
119
119
|
| `pg_execute` | Execute a data modification statement, such as `INSERT`, `UPDATE`, or `DELETE` |
|
|
120
|
+
| `pg_schema_execute` | Execute advanced schema statements, such as table, view, index, trigger, or function DDL. Only registered when `PG_MCP_MODE=advanced` |
|
|
120
121
|
| `pg_batch_execute` | Execute one data modification statement repeatedly with multiple parameter sets |
|
|
121
122
|
| `pg_import_csv` | Import a UTF-8 CSV file into a table using the header row as column names |
|
|
122
123
|
| `pg_export_csv` | Export all rows from a table to a UTF-8 CSV file |
|
|
@@ -131,7 +132,9 @@ PG_DATABASE = "YOUR DB NAME"
|
|
|
131
132
|
| `pg_list_pending_approvals` | List pending approval requests, only registered when `PG_POLICY_HOOK` is set |
|
|
132
133
|
| `pg_cancel_approval` | Cancel a pending approval request, only registered when `PG_POLICY_HOOK` is set |
|
|
133
134
|
|
|
134
|
-
When `PG_READ_ONLY=true` or `PG_MCP_MODE=readonly`, `pg_execute`, `pg_batch_execute`, and `pg_import_csv` are not registered.
|
|
135
|
+
When `PG_READ_ONLY=true` or `PG_MCP_MODE=readonly`, `pg_execute`, `pg_schema_execute`, `pg_batch_execute`, and `pg_import_csv` are not registered.
|
|
136
|
+
|
|
137
|
+
When `PG_MCP_MODE=advanced`, `pg_schema_execute` is registered and can run approved single-statement DDL for tables, views, indexes, triggers, and functions.
|
|
135
138
|
|
|
136
139
|
## Parameter Syntax
|
|
137
140
|
|
|
@@ -177,6 +180,7 @@ Detailed per-row execution results are written to a timestamped `.log` file unde
|
|
|
177
180
|
- `pg_query` allows only single-statement `SELECT`, `SHOW`, and `EXPLAIN` queries.
|
|
178
181
|
- `explain_query` accepts only a single `SELECT` statement and runs `EXPLAIN` for it.
|
|
179
182
|
- `pg_execute` allows only single-statement `INSERT`, `UPDATE`, and `DELETE` statements when write mode is enabled.
|
|
183
|
+
- `pg_schema_execute` is only available when `PG_MCP_MODE=advanced`. It allows single-statement DDL for `CREATE` / `ALTER` / `DROP` table, view, index, trigger, and function operations, plus `TRUNCATE`.
|
|
180
184
|
- `pg_batch_execute` and `pg_import_csv` use the same write policy as `pg_execute`.
|
|
181
185
|
- `pg_export_csv` checks table policy before exporting table data.
|
|
182
186
|
- Multi-statement SQL is rejected.
|
|
@@ -186,6 +190,8 @@ Detailed per-row execution results are written to a timestamped `.log` file unde
|
|
|
186
190
|
|
|
187
191
|
Table policy matching is best-effort and based on SQL parsing. PostgreSQL grants remain the final security boundary.
|
|
188
192
|
|
|
193
|
+
Some PostgreSQL DDL cannot always be mapped back to a table by static parsing, such as `DROP INDEX` or `DROP FUNCTION`. Use PostgreSQL grants and `PG_POLICY_HOOK` for stricter control in advanced mode.
|
|
194
|
+
|
|
189
195
|
## Policy Hook and Approvals
|
|
190
196
|
|
|
191
197
|
When `PG_POLICY_HOOK` is configured, the server posts each tool action to the hook after built-in policy checks pass and before the command runs. The hook must return one of:
|
package/README.zh-TW.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
- 使用 node-postgres 的 PostgreSQL connection pool
|
|
10
10
|
- 唯讀查詢工具
|
|
11
|
-
- INSERT / UPDATE / DELETE
|
|
11
|
+
- INSERT / UPDATE / DELETE 執行工具,以及可選的進階 schema 修改工具
|
|
12
12
|
- 批次執行與 CSV 匯入 / 匯出
|
|
13
13
|
- 資料表、view、index、trigger 結構查詢
|
|
14
14
|
- `EXPLAIN` 查詢計畫分析
|
|
@@ -53,7 +53,7 @@ npm run build
|
|
|
53
53
|
| `PG_KEEP_ALIVE_INITIAL_DELAY` | 否 | `0` | keep-alive 初始延遲,單位毫秒 |
|
|
54
54
|
| `PG_SSL` | 否 | - | 設為 `true` 啟用 SSL,或 `no-verify` 關閉憑證驗證 |
|
|
55
55
|
| `PG_READ_ONLY` | 否 | `false` | 設為 `true` 時不註冊寫入工具 |
|
|
56
|
-
| `PG_MCP_MODE` | 否 | `readwrite` | 設為 `readonly`
|
|
56
|
+
| `PG_MCP_MODE` | 否 | `readwrite` | 設為 `readonly` 可停用寫入工具;設為 `advanced` 可啟用 schema 執行工具 |
|
|
57
57
|
| `PG_MCP_ALLOW_TABLES` | 否 | - | table allowlist,例如 `users,orders` |
|
|
58
58
|
| `PG_MCP_DENY_TABLES` | 否 | - | table denylist,例如 `payments,secrets` |
|
|
59
59
|
| `PG_BATCH_MAX_SIZE` | 否 | `100` | `pg_batch_execute` 每批最大參數組數 |
|
|
@@ -117,6 +117,7 @@ PG_DATABASE = "YOUR DB NAME"
|
|
|
117
117
|
| --- | --- |
|
|
118
118
|
| `pg_query` | 執行唯讀 SQL,例如 `SELECT` |
|
|
119
119
|
| `pg_execute` | 執行資料修改 SQL,例如 `INSERT`、`UPDATE`、`DELETE` |
|
|
120
|
+
| `pg_schema_execute` | 執行進階 schema SQL,例如 table、view、index、trigger、function DDL。僅在 `PG_MCP_MODE=advanced` 時註冊 |
|
|
120
121
|
| `pg_batch_execute` | 使用多組參數重複執行同一個資料修改 SQL |
|
|
121
122
|
| `pg_import_csv` | 依 CSV header 將 UTF-8 CSV 匯入資料表 |
|
|
122
123
|
| `pg_export_csv` | 將資料表匯出成 UTF-8 CSV |
|
|
@@ -131,7 +132,9 @@ PG_DATABASE = "YOUR DB NAME"
|
|
|
131
132
|
| `pg_list_pending_approvals` | 列出 pending approvals,僅在 `PG_POLICY_HOOK` 設定時註冊 |
|
|
132
133
|
| `pg_cancel_approval` | 取消 pending approval,僅在 `PG_POLICY_HOOK` 設定時註冊 |
|
|
133
134
|
|
|
134
|
-
當 `PG_READ_ONLY=true` 或 `PG_MCP_MODE=readonly` 時,`pg_execute`、`pg_batch_execute`、`pg_import_csv` 不會被註冊。
|
|
135
|
+
當 `PG_READ_ONLY=true` 或 `PG_MCP_MODE=readonly` 時,`pg_execute`、`pg_schema_execute`、`pg_batch_execute`、`pg_import_csv` 不會被註冊。
|
|
136
|
+
|
|
137
|
+
當 `PG_MCP_MODE=advanced` 時,會註冊 `pg_schema_execute`,可執行通過 policy 的單一 statement DDL,用於修改 tables、views、indexes、triggers、functions。
|
|
135
138
|
|
|
136
139
|
## 參數語法
|
|
137
140
|
|
|
@@ -177,6 +180,7 @@ node-postgres 使用 `$1`、`$2`、`$3` 這種 positional placeholders。不要
|
|
|
177
180
|
- `pg_query` 只允許單一 statement 的 `SELECT`、`SHOW`、`EXPLAIN`。
|
|
178
181
|
- `explain_query` 只接受單一 `SELECT`,並在前方加上 `EXPLAIN` 執行。
|
|
179
182
|
- `pg_execute` 在 write mode 下只允許單一 statement 的 `INSERT`、`UPDATE`、`DELETE`。
|
|
183
|
+
- `pg_schema_execute` 僅在 `PG_MCP_MODE=advanced` 時可用,允許單一 statement 的 table、view、index、trigger、function `CREATE` / `ALTER` / `DROP` DDL,以及 `TRUNCATE`。
|
|
180
184
|
- `pg_batch_execute` 與 `pg_import_csv` 使用和 `pg_execute` 相同的寫入規則。
|
|
181
185
|
- `pg_export_csv` 會先檢查 table policy。
|
|
182
186
|
- 禁止 multi-statement SQL。
|
|
@@ -186,6 +190,8 @@ node-postgres 使用 `$1`、`$2`、`$3` 這種 positional placeholders。不要
|
|
|
186
190
|
|
|
187
191
|
Table policy 是根據 SQL parser 的 best-effort 檢查;PostgreSQL grants 仍然是最後的安全邊界。
|
|
188
192
|
|
|
193
|
+
部分 PostgreSQL DDL 無法永遠透過靜態解析對應回 table,例如 `DROP INDEX` 或 `DROP FUNCTION`。在 advanced mode 下若需要更嚴格控管,建議搭配 PostgreSQL grants 與 `PG_POLICY_HOOK`。
|
|
194
|
+
|
|
189
195
|
## Policy Hook 與 Approval
|
|
190
196
|
|
|
191
197
|
設定 `PG_POLICY_HOOK` 後,server 會在內建 policy 檢查通過、但命令實際執行前,將每次工具操作 POST 到外部 hook。Hook 必須回傳以下其中一種結果:
|
package/build/config.js
CHANGED
|
@@ -27,6 +27,9 @@ function resolveMode() {
|
|
|
27
27
|
if (readOnly || explicitMode === 'readonly' || explicitMode === 'read-only') {
|
|
28
28
|
return 'readonly';
|
|
29
29
|
}
|
|
30
|
+
if (explicitMode === 'advanced') {
|
|
31
|
+
return 'advanced';
|
|
32
|
+
}
|
|
30
33
|
return 'readwrite';
|
|
31
34
|
}
|
|
32
35
|
export const config = {
|
|
@@ -41,6 +44,9 @@ export const config = {
|
|
|
41
44
|
export function isReadOnlyMode() {
|
|
42
45
|
return config.mode === 'readonly';
|
|
43
46
|
}
|
|
47
|
+
export function isAdvancedMode() {
|
|
48
|
+
return config.mode === 'advanced';
|
|
49
|
+
}
|
|
44
50
|
export function isPolicyHookEnabled() {
|
|
45
51
|
return Boolean(config.policyHookUrl);
|
|
46
52
|
}
|
package/build/index.js
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import { isPolicyHookEnabled, isReadOnlyMode } from './config.js';
|
|
5
|
+
import { isAdvancedMode, isPolicyHookEnabled, isReadOnlyMode } from './config.js';
|
|
6
6
|
import { cleanupOldLogs } from './logs.js';
|
|
7
7
|
import { cancelApproval, listPendingApprovals, runApprovedCommand } from './approvalStore.js';
|
|
8
|
-
import { describeIndex, describeTable, explainQuery, getCurrentPrivileges, listTables, listTriggers, listViews, pgBatchExecute, pgExecute, pgExportCsv, pgImportCsv, pgQuery, } from './toolHandlers.js';
|
|
8
|
+
import { describeIndex, describeTable, explainQuery, getCurrentPrivileges, listTables, listTriggers, listViews, pgBatchExecute, pgExecute, pgExportCsv, pgImportCsv, pgSchemaExecute, pgQuery, } from './toolHandlers.js';
|
|
9
9
|
const { PG_HOST, PG_PORT, PG_DATABASE, } = process.env;
|
|
10
10
|
// Initialize MCP Server/mcp
|
|
11
11
|
const server = new McpServer({
|
|
12
12
|
name: 'easy-pg-mcp',
|
|
13
|
-
version: '1.0.
|
|
13
|
+
version: '1.0.1',
|
|
14
14
|
description: `PostgreSQL Database: ${PG_HOST}:${PG_PORT ?? 5432}/${PG_DATABASE}`,
|
|
15
15
|
});
|
|
16
16
|
// --- Register Tools ---
|
|
@@ -66,6 +66,19 @@ if (!isReadOnlyMode()) {
|
|
|
66
66
|
};
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
|
+
if (isAdvancedMode()) {
|
|
70
|
+
server.registerTool('pg_schema_execute', {
|
|
71
|
+
description: 'Execute an advanced PostgreSQL schema statement such as CREATE/ALTER/DROP table, view, index, trigger, or function. Requires PG_MCP_MODE=advanced.',
|
|
72
|
+
inputSchema: z.object({
|
|
73
|
+
sql: z.string().describe('The single PostgreSQL schema statement to execute. Parameters are not supported for schema execution.'),
|
|
74
|
+
}),
|
|
75
|
+
}, async ({ sql }) => {
|
|
76
|
+
const result = await pgSchemaExecute(sql);
|
|
77
|
+
return {
|
|
78
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
69
82
|
if (isPolicyHookEnabled()) {
|
|
70
83
|
server.registerTool('pg_run_approved_command', {
|
|
71
84
|
description: 'Run a pending command after the host has obtained user approval.',
|
package/build/sqlPolicy.js
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
|
-
import { config, isReadOnlyMode, normalizeTableName } from './config.js';
|
|
2
|
+
import { config, isAdvancedMode, isReadOnlyMode, normalizeTableName } from './config.js';
|
|
3
3
|
const require = createRequire(import.meta.url);
|
|
4
4
|
const { Parser } = require('node-sql-parser/build/postgresql.js');
|
|
5
5
|
const parser = new Parser();
|
|
6
6
|
const parserOptions = { database: 'Postgresql' };
|
|
7
7
|
const readQueryTypes = new Set(['select', 'show', 'explain']);
|
|
8
8
|
const executeTypes = new Set(['insert', 'update', 'delete']);
|
|
9
|
+
const advancedTypes = new Set([
|
|
10
|
+
'alter_function',
|
|
11
|
+
'alter_index',
|
|
12
|
+
'alter_table',
|
|
13
|
+
'alter_trigger',
|
|
14
|
+
'alter_view',
|
|
15
|
+
'create_function',
|
|
16
|
+
'create_index',
|
|
17
|
+
'create_table',
|
|
18
|
+
'create_trigger',
|
|
19
|
+
'create_view',
|
|
20
|
+
'drop_function',
|
|
21
|
+
'drop_index',
|
|
22
|
+
'drop_table',
|
|
23
|
+
'drop_trigger',
|
|
24
|
+
'drop_view',
|
|
25
|
+
'truncate',
|
|
26
|
+
]);
|
|
9
27
|
export class SqlPolicyError extends Error {
|
|
10
28
|
constructor(message) {
|
|
11
29
|
super(message);
|
|
@@ -23,7 +41,7 @@ export function assertReadQueryAllowed(sql) {
|
|
|
23
41
|
export function analyzeSql(sql) {
|
|
24
42
|
const parsed = parseSingleStatement(sql);
|
|
25
43
|
return {
|
|
26
|
-
statementType: parsed.
|
|
44
|
+
statementType: parsed.statementType,
|
|
27
45
|
tableNames: parsed.tables,
|
|
28
46
|
};
|
|
29
47
|
}
|
|
@@ -45,6 +63,23 @@ export function assertExecuteAllowed(sql) {
|
|
|
45
63
|
}
|
|
46
64
|
assertTablePolicy(parsed.tables);
|
|
47
65
|
}
|
|
66
|
+
export function assertSchemaExecuteAllowed(sql) {
|
|
67
|
+
if (isReadOnlyMode()) {
|
|
68
|
+
throw new SqlPolicyError('SQL rejected: schema execution is disabled because PG_READ_ONLY=true or PG_MCP_MODE=readonly.');
|
|
69
|
+
}
|
|
70
|
+
if (!isAdvancedMode()) {
|
|
71
|
+
throw new SqlPolicyError('SQL rejected: pg_schema_execute requires PG_MCP_MODE=advanced.');
|
|
72
|
+
}
|
|
73
|
+
const parsed = parseSingleStatement(sql, { allowAdvancedFallback: true });
|
|
74
|
+
if (!advancedTypes.has(parsed.statementType)) {
|
|
75
|
+
throw new SqlPolicyError(`SQL rejected: pg_schema_execute only allows advanced schema statements. Received ${parsed.statementType.toUpperCase()}.`);
|
|
76
|
+
}
|
|
77
|
+
assertTablePolicy(parsed.tables);
|
|
78
|
+
return {
|
|
79
|
+
statementType: parsed.statementType,
|
|
80
|
+
tableNames: parsed.tables,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
48
83
|
export function assertTablesAllowed(tables) {
|
|
49
84
|
assertTablePolicy(tables.map((table) => normalizeTableName(table)));
|
|
50
85
|
}
|
|
@@ -60,12 +95,18 @@ export function isTableAllowed(table) {
|
|
|
60
95
|
throw error;
|
|
61
96
|
}
|
|
62
97
|
}
|
|
63
|
-
function parseSingleStatement(sql) {
|
|
98
|
+
function parseSingleStatement(sql, options) {
|
|
64
99
|
let parsed;
|
|
65
100
|
try {
|
|
66
101
|
parsed = parser.parse(sql, parserOptions);
|
|
67
102
|
}
|
|
68
103
|
catch (error) {
|
|
104
|
+
if (options?.allowAdvancedFallback) {
|
|
105
|
+
const fallback = parseAdvancedFallback(sql);
|
|
106
|
+
if (fallback) {
|
|
107
|
+
return fallback;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
69
110
|
const message = error instanceof Error ? error.message : String(error);
|
|
70
111
|
throw new SqlPolicyError(`SQL rejected: unable to parse statement as PostgreSQL SQL. ${message}`);
|
|
71
112
|
}
|
|
@@ -77,19 +118,62 @@ function parseSingleStatement(sql) {
|
|
|
77
118
|
return {
|
|
78
119
|
ast: ast[0],
|
|
79
120
|
type: getStatementType(ast[0]),
|
|
121
|
+
statementType: getDetailedStatementType(ast[0]),
|
|
80
122
|
tables: getVisitedTables(parsed, ast[0]),
|
|
81
123
|
};
|
|
82
124
|
}
|
|
83
125
|
return {
|
|
84
126
|
ast,
|
|
85
127
|
type: getStatementType(ast),
|
|
128
|
+
statementType: getDetailedStatementType(ast),
|
|
86
129
|
tables: getVisitedTables(parsed, ast),
|
|
87
130
|
};
|
|
88
131
|
}
|
|
132
|
+
function parseAdvancedFallback(sql) {
|
|
133
|
+
const trimmed = sql.trim();
|
|
134
|
+
const statement = trimmed.endsWith(';') ? trimmed.slice(0, -1).trim() : trimmed;
|
|
135
|
+
if (statement.includes(';')) {
|
|
136
|
+
throw new SqlPolicyError('SQL rejected: multiple statements are not allowed.');
|
|
137
|
+
}
|
|
138
|
+
const dropTriggerMatch = /^drop\s+trigger\s+(?:if\s+exists\s+)?(?<trigger>"[^"]+"|[a-zA-Z_][\w$]*)\s+on\s+(?:only\s+)?(?<table>"[^"]+"|[a-zA-Z_][\w$]*(?:\."[^"]+"|\.[a-zA-Z_][\w$]*)?)(?:\s+(?:cascade|restrict))?$/iu.exec(statement);
|
|
139
|
+
if (dropTriggerMatch?.groups?.table) {
|
|
140
|
+
return {
|
|
141
|
+
ast: null,
|
|
142
|
+
type: 'drop',
|
|
143
|
+
statementType: 'drop_trigger',
|
|
144
|
+
tables: [normalizeTableName(dropTriggerMatch.groups.table)],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const dropFunctionMatch = /^drop\s+function\s+(?:if\s+exists\s+)?(?<function>"[^"]+"|[a-zA-Z_][\w$]*(?:\."[^"]+"|\.[a-zA-Z_][\w$]*)?)\s*\([^)]*\)(?:\s+(?:cascade|restrict))?$/iu.exec(statement);
|
|
148
|
+
if (dropFunctionMatch) {
|
|
149
|
+
return {
|
|
150
|
+
ast: null,
|
|
151
|
+
type: 'drop',
|
|
152
|
+
statementType: 'drop_function',
|
|
153
|
+
tables: [],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
89
158
|
function getStatementType(ast) {
|
|
90
159
|
const type = typeof ast?.type === 'string' ? ast.type.toLowerCase() : 'unknown';
|
|
91
160
|
return type === 'desc' ? 'describe' : type;
|
|
92
161
|
}
|
|
162
|
+
function getDetailedStatementType(ast) {
|
|
163
|
+
const type = getStatementType(ast);
|
|
164
|
+
if (type === 'truncate') {
|
|
165
|
+
return 'truncate';
|
|
166
|
+
}
|
|
167
|
+
const keyword = typeof ast?.keyword === 'string' ? ast.keyword.toLowerCase() : undefined;
|
|
168
|
+
const constraintType = typeof ast?.constraint_type === 'string' ? ast.constraint_type.toLowerCase() : undefined;
|
|
169
|
+
if (type === 'create' && constraintType === 'trigger') {
|
|
170
|
+
return 'create_trigger';
|
|
171
|
+
}
|
|
172
|
+
if ((type === 'create' || type === 'alter' || type === 'drop') && keyword) {
|
|
173
|
+
return `${type}_${keyword.replace(/\s+/g, '_')}`;
|
|
174
|
+
}
|
|
175
|
+
return type;
|
|
176
|
+
}
|
|
93
177
|
function assertNoUnsafeReadOptions(ast) {
|
|
94
178
|
if (Array.isArray(ast)) {
|
|
95
179
|
for (const statement of ast) {
|
|
@@ -175,12 +259,45 @@ function collectTablesFromAst(ast) {
|
|
|
175
259
|
return [];
|
|
176
260
|
}
|
|
177
261
|
switch (ast.type) {
|
|
262
|
+
case 'alter':
|
|
263
|
+
return normalizeAstTableList(ast.table);
|
|
264
|
+
case 'create':
|
|
265
|
+
return collectCreateTables(ast);
|
|
178
266
|
case 'desc':
|
|
179
267
|
case 'describe':
|
|
180
268
|
return typeof ast.table === 'string' ? [ast.table] : [];
|
|
269
|
+
case 'drop':
|
|
270
|
+
return normalizeAstTableList(ast.name);
|
|
181
271
|
case 'explain':
|
|
182
272
|
return collectTablesFromAst(ast.expr);
|
|
273
|
+
case 'truncate':
|
|
274
|
+
return normalizeAstTableList(ast.name);
|
|
183
275
|
default:
|
|
184
276
|
return [];
|
|
185
277
|
}
|
|
186
278
|
}
|
|
279
|
+
function collectCreateTables(ast) {
|
|
280
|
+
const tables = normalizeAstTableList(ast.table);
|
|
281
|
+
if (ast.keyword === 'view') {
|
|
282
|
+
tables.push(...normalizeAstTableList(ast.view));
|
|
283
|
+
}
|
|
284
|
+
return tables;
|
|
285
|
+
}
|
|
286
|
+
function normalizeAstTableList(value) {
|
|
287
|
+
if (!value) {
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
if (Array.isArray(value)) {
|
|
291
|
+
return value.flatMap((item) => normalizeAstTableList(item));
|
|
292
|
+
}
|
|
293
|
+
if (typeof value === 'string') {
|
|
294
|
+
return [value];
|
|
295
|
+
}
|
|
296
|
+
if (typeof value.table === 'string') {
|
|
297
|
+
return [value.db ? `${value.db}.${value.table}` : value.table];
|
|
298
|
+
}
|
|
299
|
+
if (typeof value.view === 'string') {
|
|
300
|
+
return [value.db ? `${value.db}.${value.view}` : value.view];
|
|
301
|
+
}
|
|
302
|
+
return [];
|
|
303
|
+
}
|
package/build/toolHandlers.js
CHANGED
|
@@ -3,7 +3,7 @@ import { exportCsv, importCsv } from './csvTools.js';
|
|
|
3
3
|
import * as db from './db.js';
|
|
4
4
|
import { writeBatchExecuteLog } from './logs.js';
|
|
5
5
|
import { runWithPolicy } from './policyHook.js';
|
|
6
|
-
import { analyzeSql, assertExecuteAllowed, assertExplainQueryAllowed, assertReadQueryAllowed, assertTablesAllowed, isTableAllowed, } from './sqlPolicy.js';
|
|
6
|
+
import { analyzeSql, assertExecuteAllowed, assertExplainQueryAllowed, assertReadQueryAllowed, assertSchemaExecuteAllowed, assertTablesAllowed, isTableAllowed, } from './sqlPolicy.js';
|
|
7
7
|
export async function pgQuery(sql) {
|
|
8
8
|
assertReadQueryAllowed(sql);
|
|
9
9
|
const analysis = analyzeSql(sql);
|
|
@@ -26,6 +26,16 @@ export async function pgExecute(sql, params) {
|
|
|
26
26
|
summary: { sql, paramsPreview: params ?? null },
|
|
27
27
|
}, () => db.execute(sql, params));
|
|
28
28
|
}
|
|
29
|
+
export async function pgSchemaExecute(sql) {
|
|
30
|
+
const analysis = assertSchemaExecuteAllowed(sql);
|
|
31
|
+
return runWithPolicy({
|
|
32
|
+
functionName: 'pg_schema_execute',
|
|
33
|
+
sql,
|
|
34
|
+
statementType: analysis.statementType,
|
|
35
|
+
tableNames: analysis.tableNames,
|
|
36
|
+
summary: { sql },
|
|
37
|
+
}, () => db.execute(sql));
|
|
38
|
+
}
|
|
29
39
|
export async function pgBatchExecute(sql, paramsList, transaction) {
|
|
30
40
|
assertExecuteAllowed(sql);
|
|
31
41
|
const analysis = analyzeSql(sql);
|