easy-pg-mcp 1.0.0 → 1.1.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/MANUAL.md +120 -0
- package/README.md +59 -8
- package/README.zh-TW.md +13 -4
- package/build/config.js +6 -0
- package/build/index.js +25 -3
- package/build/sqlPolicy.js +120 -3
- package/build/toolHandlers.js +11 -1
- package/package.json +7 -5
package/MANUAL.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# easy-pg-mcp Manual
|
|
2
|
+
|
|
3
|
+
`easy-pg-mcp` 是一個用來操作 PostgreSQL 的 MCP server,提供查詢、寫入、批次執行、CSV 匯入,以及 schema / 權限檢查工具。
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
適合用在:
|
|
8
|
+
- PostgreSQL 資料查詢
|
|
9
|
+
- 參數化寫入
|
|
10
|
+
- `RETURNING` 型流程
|
|
11
|
+
- 需要 CTE、複雜 join、聚合查詢的場景
|
|
12
|
+
|
|
13
|
+
不適合用在:
|
|
14
|
+
- 多語句 SQL
|
|
15
|
+
- 直接字串拼接參數
|
|
16
|
+
- 過度依賴隱式型別轉換
|
|
17
|
+
|
|
18
|
+
## When to Consult This Manual
|
|
19
|
+
|
|
20
|
+
如果你遇到以下情況,先看這份手冊:
|
|
21
|
+
- 不確定 `pg_query`、`pg_execute`、`pg_batch_execute` 要怎麼用
|
|
22
|
+
- SQL 被拒絕或回傳錯誤
|
|
23
|
+
- 不確定 `$1, $2, ...` 參數要怎麼綁定
|
|
24
|
+
- 不確定哪些 SQL 寫法是安全且允許的
|
|
25
|
+
- 不確定應該先查 schema 還是直接執行
|
|
26
|
+
|
|
27
|
+
## Modes
|
|
28
|
+
|
|
29
|
+
- `readonly`: 只提供讀取相關工具
|
|
30
|
+
- `readwrite`: 預設模式,允許一般讀寫,但不包含 DDL
|
|
31
|
+
- `advanced`: 允許 schema / DDL 類工具
|
|
32
|
+
|
|
33
|
+
## Tools
|
|
34
|
+
|
|
35
|
+
- `pg_query`: 讀取資料
|
|
36
|
+
- `pg_execute`: 執行單一寫入或變更 SQL
|
|
37
|
+
- `pg_batch_execute`: 以多組參數批次執行同一段 SQL
|
|
38
|
+
- `pg_import_csv`: 匯入 UTF-8 CSV
|
|
39
|
+
- `pg_export_csv`: 匯出表格為 UTF-8 CSV
|
|
40
|
+
- `pg_schema_execute`: 執行 schema / DDL 變更,僅 advanced 模式可用
|
|
41
|
+
- `pg_list_pending_approvals`: 列出待審核命令
|
|
42
|
+
- `pg_run_approved_command`: 執行已核准命令
|
|
43
|
+
- `pg_cancel_approval`: 取消審核
|
|
44
|
+
- `explain_query`: 檢查 query plan
|
|
45
|
+
- `list_tables`, `list_views`, `describe_table`, `describe_index`, `list_triggers`
|
|
46
|
+
- `get_current_privileges`
|
|
47
|
+
|
|
48
|
+
## Execute Usage
|
|
49
|
+
|
|
50
|
+
`pg_execute` 應使用 PostgreSQL placeholder:
|
|
51
|
+
- `"$1", "$2", "$3"...`
|
|
52
|
+
|
|
53
|
+
規則:
|
|
54
|
+
- SQL 只描述結構
|
|
55
|
+
- 值由 `params` 陣列提供
|
|
56
|
+
- `$n` 與參數順序必須完全對應
|
|
57
|
+
- 不要用 `?`
|
|
58
|
+
- 不要把使用者輸入直接插入 SQL 字串
|
|
59
|
+
|
|
60
|
+
`pg_batch_execute` 適合:
|
|
61
|
+
- 同一個 SQL 搭配多組參數重複執行
|
|
62
|
+
- 大量資料寫入
|
|
63
|
+
|
|
64
|
+
參數寫法補充:
|
|
65
|
+
- SQL 內使用 `$1`, `$2`, ... 代表參數位置
|
|
66
|
+
- `params` 陣列的第 1 個值會綁定到 `$1`
|
|
67
|
+
- `params` 陣列的第 2 個值會綁定到 `$2`
|
|
68
|
+
- 依此類推,順序必須完全一致
|
|
69
|
+
|
|
70
|
+
## SQL Algebra / Composition Rules
|
|
71
|
+
|
|
72
|
+
PostgreSQL 查詢組裝可視為一個由上而下的代數式:
|
|
73
|
+
|
|
74
|
+
1. 先定義中間結果:`WITH`
|
|
75
|
+
2. 選擇欄位:`SELECT`
|
|
76
|
+
3. 指定資料來源:`FROM`
|
|
77
|
+
4. 關聯資料:`JOIN ... ON`
|
|
78
|
+
5. 篩選資料:`WHERE`
|
|
79
|
+
6. 聚合:`GROUP BY`
|
|
80
|
+
7. 聚合後篩選:`HAVING`
|
|
81
|
+
8. 排序:`ORDER BY`
|
|
82
|
+
9. 限制筆數:`LIMIT` / `OFFSET`
|
|
83
|
+
|
|
84
|
+
PostgreSQL 常用特性:
|
|
85
|
+
- `RETURNING`:寫入後直接取回欄位
|
|
86
|
+
- `::type`:明確型別轉換
|
|
87
|
+
- CTE:提升複雜查詢可讀性
|
|
88
|
+
- 參數化日期區間查詢:搭配 `$1`, `$2`
|
|
89
|
+
|
|
90
|
+
值與型別的規則:
|
|
91
|
+
- 值優先用參數
|
|
92
|
+
- 型別轉換要明確
|
|
93
|
+
- 時區與日期欄位要一致處理
|
|
94
|
+
|
|
95
|
+
## Safety Rules
|
|
96
|
+
|
|
97
|
+
- 不支援 multi-statement
|
|
98
|
+
- 不要把使用者輸入直接嵌入 SQL
|
|
99
|
+
- schema execute 不支援參數化
|
|
100
|
+
- 某些 DDL 解析與授權限制需特別注意
|
|
101
|
+
|
|
102
|
+
## Examples
|
|
103
|
+
|
|
104
|
+
- 參數化查詢:
|
|
105
|
+
- `SELECT * FROM users WHERE id = $1`
|
|
106
|
+
- 寫入後取回 id:
|
|
107
|
+
- `INSERT INTO users (name) VALUES ($1) RETURNING id`
|
|
108
|
+
- CTE:
|
|
109
|
+
- `WITH recent_orders AS (...) SELECT * FROM recent_orders`
|
|
110
|
+
- 聚合:
|
|
111
|
+
- `SELECT status, COUNT(*) FROM orders GROUP BY status`
|
|
112
|
+
- 日期區間查詢:
|
|
113
|
+
- `SELECT * FROM logs WHERE created_at >= $1 AND created_at < $2`
|
|
114
|
+
|
|
115
|
+
## Troubleshooting
|
|
116
|
+
|
|
117
|
+
- 如果參數沒生效,先檢查 `$n` 與 `params` 順序
|
|
118
|
+
- 如果欄位型別不對,補上 `::type`
|
|
119
|
+
- 如果查詢太複雜,先改寫成 `WITH`
|
|
120
|
+
- 如果執行失敗,先用 `describe_table` 確認 schema
|
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`
|
|
@@ -17,7 +17,7 @@ This project uses Node.js, TypeScript, the official MCP SDK, and [`pg`](https://
|
|
|
17
17
|
|
|
18
18
|
## Requirements
|
|
19
19
|
|
|
20
|
-
- Node.js
|
|
20
|
+
- Node.js 20 or newer
|
|
21
21
|
- npm
|
|
22
22
|
- A reachable PostgreSQL database
|
|
23
23
|
|
|
@@ -29,7 +29,7 @@ Run the server directly with `npx`:
|
|
|
29
29
|
npx -y easy-pg-mcp
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
For local development:
|
|
32
|
+
For local development after cloning the repository:
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
35
|
cd easy-pg-mcp
|
|
@@ -39,6 +39,8 @@ npm run build
|
|
|
39
39
|
|
|
40
40
|
## Configuration
|
|
41
41
|
|
|
42
|
+
Configure the server with environment variables. You can provide them through your MCP client configuration or by creating a local `.env` file.
|
|
43
|
+
|
|
42
44
|
| Variable | Required | Default | Description |
|
|
43
45
|
| --- | --- | --- | --- |
|
|
44
46
|
| `PG_CONNECTION_STRING` | No | - | PostgreSQL connection string. When set, host/user/password/database fields are optional |
|
|
@@ -53,7 +55,7 @@ npm run build
|
|
|
53
55
|
| `PG_KEEP_ALIVE_INITIAL_DELAY` | No | `0` | Initial TCP keep-alive delay in milliseconds |
|
|
54
56
|
| `PG_SSL` | No | - | Set `true` to enable SSL, or `no-verify` to disable certificate verification |
|
|
55
57
|
| `PG_READ_ONLY` | No | `false` | When `true`, write tools are not registered |
|
|
56
|
-
| `PG_MCP_MODE` | No | `readwrite` | Use `readonly` to disable write execution |
|
|
58
|
+
| `PG_MCP_MODE` | No | `readwrite` | Use `readonly` to disable write execution, or `advanced` to enable schema execution |
|
|
57
59
|
| `PG_MCP_ALLOW_TABLES` | No | - | Comma-separated table allowlist, such as `users,orders` |
|
|
58
60
|
| `PG_MCP_DENY_TABLES` | No | - | Comma-separated table denylist, such as `payments,secrets` |
|
|
59
61
|
| `PG_BATCH_MAX_SIZE` | No | `100` | Maximum number of parameter sets per internal batch for `pg_batch_execute` |
|
|
@@ -73,9 +75,27 @@ PG_PASSWORD=your_password
|
|
|
73
75
|
PG_DATABASE=your_database
|
|
74
76
|
```
|
|
75
77
|
|
|
76
|
-
##
|
|
78
|
+
## Usage
|
|
79
|
+
|
|
80
|
+
Configure your MCP client to launch the package through `npx`.
|
|
81
|
+
|
|
82
|
+
For local development, build the TypeScript source first:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npm run build
|
|
86
|
+
```
|
|
77
87
|
|
|
78
|
-
|
|
88
|
+
Start the MCP server:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm start
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The server communicates over stdio and is normally launched by an MCP client rather than run manually.
|
|
95
|
+
|
|
96
|
+
If you are unsure how a tool should be used, or an operation fails, call `pg_manual` first. It returns the built-in manual with safe usage rules, placeholder guidance, and SQL composition notes.
|
|
97
|
+
|
|
98
|
+
## Claude Desktop Example
|
|
79
99
|
|
|
80
100
|
```json
|
|
81
101
|
{
|
|
@@ -95,7 +115,9 @@ Claude Desktop:
|
|
|
95
115
|
}
|
|
96
116
|
```
|
|
97
117
|
|
|
98
|
-
|
|
118
|
+
Restart Claude Desktop after updating the configuration.
|
|
119
|
+
|
|
120
|
+
## Codex config.toml Example
|
|
99
121
|
|
|
100
122
|
```toml
|
|
101
123
|
[mcp_servers.easy-pg-mcp]
|
|
@@ -111,12 +133,36 @@ PG_PASSWORD = "YOUR PASSWORD"
|
|
|
111
133
|
PG_DATABASE = "YOUR DB NAME"
|
|
112
134
|
```
|
|
113
135
|
|
|
136
|
+
## OpenCode opencode.jsonc Example
|
|
137
|
+
|
|
138
|
+
```jsonc
|
|
139
|
+
{
|
|
140
|
+
"$schema": "https://opencode.ai/config.json",
|
|
141
|
+
"mcp": {
|
|
142
|
+
"easy-pg-mcp": {
|
|
143
|
+
"type": "local",
|
|
144
|
+
"command": ["npx", "-y", "easy-pg-mcp"],
|
|
145
|
+
"enabled": true,
|
|
146
|
+
"environment": {
|
|
147
|
+
"PG_HOST": "localhost",
|
|
148
|
+
"PG_PORT": "5432",
|
|
149
|
+
"PG_USER": "YOUR USERNAME",
|
|
150
|
+
"PG_PASSWORD": "YOUR PASSWORD",
|
|
151
|
+
"PG_DATABASE": "YOUR DB NAME"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
114
158
|
## Available Tools
|
|
115
159
|
|
|
116
160
|
| Tool | Description |
|
|
117
161
|
| --- | --- |
|
|
162
|
+
| `pg_manual` | Return the built-in manual. Use this first when you are unsure how to use PostgreSQL tools or need help diagnosing an operation error |
|
|
118
163
|
| `pg_query` | Execute a read-only SQL query, such as `SELECT` |
|
|
119
164
|
| `pg_execute` | Execute a data modification statement, such as `INSERT`, `UPDATE`, or `DELETE` |
|
|
165
|
+
| `pg_schema_execute` | Execute advanced schema statements, such as table, view, index, trigger, or function DDL. Only registered when `PG_MCP_MODE=advanced` |
|
|
120
166
|
| `pg_batch_execute` | Execute one data modification statement repeatedly with multiple parameter sets |
|
|
121
167
|
| `pg_import_csv` | Import a UTF-8 CSV file into a table using the header row as column names |
|
|
122
168
|
| `pg_export_csv` | Export all rows from a table to a UTF-8 CSV file |
|
|
@@ -131,7 +177,9 @@ PG_DATABASE = "YOUR DB NAME"
|
|
|
131
177
|
| `pg_list_pending_approvals` | List pending approval requests, only registered when `PG_POLICY_HOOK` is set |
|
|
132
178
|
| `pg_cancel_approval` | Cancel a pending approval request, only registered when `PG_POLICY_HOOK` is set |
|
|
133
179
|
|
|
134
|
-
When `PG_READ_ONLY=true` or `PG_MCP_MODE=readonly`, `pg_execute`, `pg_batch_execute`, and `pg_import_csv` are not registered.
|
|
180
|
+
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.
|
|
181
|
+
|
|
182
|
+
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
183
|
|
|
136
184
|
## Parameter Syntax
|
|
137
185
|
|
|
@@ -177,6 +225,7 @@ Detailed per-row execution results are written to a timestamped `.log` file unde
|
|
|
177
225
|
- `pg_query` allows only single-statement `SELECT`, `SHOW`, and `EXPLAIN` queries.
|
|
178
226
|
- `explain_query` accepts only a single `SELECT` statement and runs `EXPLAIN` for it.
|
|
179
227
|
- `pg_execute` allows only single-statement `INSERT`, `UPDATE`, and `DELETE` statements when write mode is enabled.
|
|
228
|
+
- `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
229
|
- `pg_batch_execute` and `pg_import_csv` use the same write policy as `pg_execute`.
|
|
181
230
|
- `pg_export_csv` checks table policy before exporting table data.
|
|
182
231
|
- Multi-statement SQL is rejected.
|
|
@@ -186,6 +235,8 @@ Detailed per-row execution results are written to a timestamped `.log` file unde
|
|
|
186
235
|
|
|
187
236
|
Table policy matching is best-effort and based on SQL parsing. PostgreSQL grants remain the final security boundary.
|
|
188
237
|
|
|
238
|
+
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.
|
|
239
|
+
|
|
189
240
|
## Policy Hook and Approvals
|
|
190
241
|
|
|
191
242
|
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` 查詢計畫分析
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
## 需求
|
|
19
19
|
|
|
20
|
-
- Node.js
|
|
20
|
+
- Node.js 20 以上
|
|
21
21
|
- npm
|
|
22
22
|
- 可連線的 PostgreSQL 資料庫
|
|
23
23
|
|
|
@@ -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` 每批最大參數組數 |
|
|
@@ -113,10 +113,14 @@ PG_DATABASE = "YOUR DB NAME"
|
|
|
113
113
|
|
|
114
114
|
## 可用工具
|
|
115
115
|
|
|
116
|
+
如果你不確定某個工具該怎麼用,或操作失敗,請先呼叫 `pg_manual`。它會回傳內建手冊,包含安全使用規則、placeholder 指引,以及 SQL 組合方式。
|
|
117
|
+
|
|
116
118
|
| 工具 | 說明 |
|
|
117
119
|
| --- | --- |
|
|
120
|
+
| `pg_manual` | 回傳內建手冊。當你不確定如何使用 PostgreSQL 工具,或需要排查操作錯誤時,請先查這個工具 |
|
|
118
121
|
| `pg_query` | 執行唯讀 SQL,例如 `SELECT` |
|
|
119
122
|
| `pg_execute` | 執行資料修改 SQL,例如 `INSERT`、`UPDATE`、`DELETE` |
|
|
123
|
+
| `pg_schema_execute` | 執行進階 schema SQL,例如 table、view、index、trigger、function DDL。僅在 `PG_MCP_MODE=advanced` 時註冊 |
|
|
120
124
|
| `pg_batch_execute` | 使用多組參數重複執行同一個資料修改 SQL |
|
|
121
125
|
| `pg_import_csv` | 依 CSV header 將 UTF-8 CSV 匯入資料表 |
|
|
122
126
|
| `pg_export_csv` | 將資料表匯出成 UTF-8 CSV |
|
|
@@ -131,7 +135,9 @@ PG_DATABASE = "YOUR DB NAME"
|
|
|
131
135
|
| `pg_list_pending_approvals` | 列出 pending approvals,僅在 `PG_POLICY_HOOK` 設定時註冊 |
|
|
132
136
|
| `pg_cancel_approval` | 取消 pending approval,僅在 `PG_POLICY_HOOK` 設定時註冊 |
|
|
133
137
|
|
|
134
|
-
當 `PG_READ_ONLY=true` 或 `PG_MCP_MODE=readonly` 時,`pg_execute`、`pg_batch_execute`、`pg_import_csv` 不會被註冊。
|
|
138
|
+
當 `PG_READ_ONLY=true` 或 `PG_MCP_MODE=readonly` 時,`pg_execute`、`pg_schema_execute`、`pg_batch_execute`、`pg_import_csv` 不會被註冊。
|
|
139
|
+
|
|
140
|
+
當 `PG_MCP_MODE=advanced` 時,會註冊 `pg_schema_execute`,可執行通過 policy 的單一 statement DDL,用於修改 tables、views、indexes、triggers、functions。
|
|
135
141
|
|
|
136
142
|
## 參數語法
|
|
137
143
|
|
|
@@ -177,6 +183,7 @@ node-postgres 使用 `$1`、`$2`、`$3` 這種 positional placeholders。不要
|
|
|
177
183
|
- `pg_query` 只允許單一 statement 的 `SELECT`、`SHOW`、`EXPLAIN`。
|
|
178
184
|
- `explain_query` 只接受單一 `SELECT`,並在前方加上 `EXPLAIN` 執行。
|
|
179
185
|
- `pg_execute` 在 write mode 下只允許單一 statement 的 `INSERT`、`UPDATE`、`DELETE`。
|
|
186
|
+
- `pg_schema_execute` 僅在 `PG_MCP_MODE=advanced` 時可用,允許單一 statement 的 table、view、index、trigger、function `CREATE` / `ALTER` / `DROP` DDL,以及 `TRUNCATE`。
|
|
180
187
|
- `pg_batch_execute` 與 `pg_import_csv` 使用和 `pg_execute` 相同的寫入規則。
|
|
181
188
|
- `pg_export_csv` 會先檢查 table policy。
|
|
182
189
|
- 禁止 multi-statement SQL。
|
|
@@ -186,6 +193,8 @@ node-postgres 使用 `$1`、`$2`、`$3` 這種 positional placeholders。不要
|
|
|
186
193
|
|
|
187
194
|
Table policy 是根據 SQL parser 的 best-effort 檢查;PostgreSQL grants 仍然是最後的安全邊界。
|
|
188
195
|
|
|
196
|
+
部分 PostgreSQL DDL 無法永遠透過靜態解析對應回 table,例如 `DROP INDEX` 或 `DROP FUNCTION`。在 advanced mode 下若需要更嚴格控管,建議搭配 PostgreSQL grants 與 `PG_POLICY_HOOK`。
|
|
197
|
+
|
|
189
198
|
## Policy Hook 與 Approval
|
|
190
199
|
|
|
191
200
|
設定 `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
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
2
4
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
5
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
6
|
import { z } from 'zod';
|
|
5
|
-
import { isPolicyHookEnabled, isReadOnlyMode } from './config.js';
|
|
7
|
+
import { isAdvancedMode, isPolicyHookEnabled, isReadOnlyMode } from './config.js';
|
|
6
8
|
import { cleanupOldLogs } from './logs.js';
|
|
7
9
|
import { cancelApproval, listPendingApprovals, runApprovedCommand } from './approvalStore.js';
|
|
8
|
-
import { describeIndex, describeTable, explainQuery, getCurrentPrivileges, listTables, listTriggers, listViews, pgBatchExecute, pgExecute, pgExportCsv, pgImportCsv, pgQuery, } from './toolHandlers.js';
|
|
10
|
+
import { describeIndex, describeTable, explainQuery, getCurrentPrivileges, listTables, listTriggers, listViews, pgBatchExecute, pgExecute, pgExportCsv, pgImportCsv, pgSchemaExecute, pgQuery, } from './toolHandlers.js';
|
|
9
11
|
const { PG_HOST, PG_PORT, PG_DATABASE, } = process.env;
|
|
10
12
|
// Initialize MCP Server/mcp
|
|
11
13
|
const server = new McpServer({
|
|
12
14
|
name: 'easy-pg-mcp',
|
|
13
|
-
version: '1.0.
|
|
15
|
+
version: '1.0.1',
|
|
14
16
|
description: `PostgreSQL Database: ${PG_HOST}:${PG_PORT ?? 5432}/${PG_DATABASE}`,
|
|
15
17
|
});
|
|
18
|
+
const manualText = readFileSync(fileURLToPath(new URL('../MANUAL.md', import.meta.url)), 'utf8');
|
|
16
19
|
// --- Register Tools ---
|
|
17
20
|
const transactionModeSchema = z.enum(['all', 'batch', 'each', 'none']);
|
|
21
|
+
server.registerTool('pg_manual', {
|
|
22
|
+
description: 'Return the PostgreSQL MCP manual. Use this first when you are unsure how to use pg_query, pg_execute, pg_batch_execute, or when an operation fails and you need the safe usage rules, placeholder rules, or SQL composition guidance.',
|
|
23
|
+
inputSchema: z.object({}),
|
|
24
|
+
}, async () => ({
|
|
25
|
+
content: [{ type: 'text', text: manualText }],
|
|
26
|
+
}));
|
|
18
27
|
server.registerTool('pg_query', {
|
|
19
28
|
description: 'Execute a read-only SQL query (e.g., SELECT). Use this for data retrieval.',
|
|
20
29
|
inputSchema: z.object({
|
|
@@ -66,6 +75,19 @@ if (!isReadOnlyMode()) {
|
|
|
66
75
|
};
|
|
67
76
|
});
|
|
68
77
|
}
|
|
78
|
+
if (isAdvancedMode()) {
|
|
79
|
+
server.registerTool('pg_schema_execute', {
|
|
80
|
+
description: 'Execute an advanced PostgreSQL schema statement such as CREATE/ALTER/DROP table, view, index, trigger, or function. Requires PG_MCP_MODE=advanced.',
|
|
81
|
+
inputSchema: z.object({
|
|
82
|
+
sql: z.string().describe('The single PostgreSQL schema statement to execute. Parameters are not supported for schema execution.'),
|
|
83
|
+
}),
|
|
84
|
+
}, async ({ sql }) => {
|
|
85
|
+
const result = await pgSchemaExecute(sql);
|
|
86
|
+
return {
|
|
87
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
}
|
|
69
91
|
if (isPolicyHookEnabled()) {
|
|
70
92
|
server.registerTool('pg_run_approved_command', {
|
|
71
93
|
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);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "easy-pg-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "High performance PostgreSQL MCP Server using node-postgres",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"build",
|
|
12
|
-
"README.md"
|
|
12
|
+
"README.md",
|
|
13
|
+
"README.zh-TW.md",
|
|
14
|
+
"MANUAL.md"
|
|
13
15
|
],
|
|
14
16
|
"engines": {
|
|
15
|
-
"node": ">=
|
|
17
|
+
"node": ">=20"
|
|
16
18
|
},
|
|
17
19
|
"keywords": [
|
|
18
20
|
"mcp",
|
|
@@ -41,8 +43,8 @@
|
|
|
41
43
|
"zod": "^4.4.3"
|
|
42
44
|
},
|
|
43
45
|
"devDependencies": {
|
|
44
|
-
"@types/node": "^25.
|
|
45
|
-
"@types/pg": "^8.
|
|
46
|
+
"@types/node": "^25.9.3",
|
|
47
|
+
"@types/pg": "^8.20.0",
|
|
46
48
|
"typescript": "^6.0.3"
|
|
47
49
|
},
|
|
48
50
|
"repository": {
|