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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 chenkumi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,228 @@
1
+ # easy-pg-mcp
2
+
3
+ A lightweight [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that lets AI assistants inspect and query a PostgreSQL database through a safe, structured tool interface.
4
+
5
+ This project uses Node.js, TypeScript, the official MCP SDK, and [`pg`](https://www.npmjs.com/package/pg) / node-postgres. It runs over stdio, so it can be used directly by MCP clients such as Claude Desktop, Codex, and OpenCode.
6
+
7
+ ## Features
8
+
9
+ - PostgreSQL connection pooling powered by node-postgres
10
+ - Read-only query tool for data retrieval
11
+ - Execute tool for data modification statements
12
+ - Batch execution and CSV import/export helpers
13
+ - Schema discovery tools for tables, views, indexes, and triggers
14
+ - Query plan inspection with `EXPLAIN`
15
+ - Current user and table grant inspection
16
+ - Optional policy hook and approval workflow
17
+
18
+ ## Requirements
19
+
20
+ - Node.js 18 or newer
21
+ - npm
22
+ - A reachable PostgreSQL database
23
+
24
+ ## Installation
25
+
26
+ Run the server directly with `npx`:
27
+
28
+ ```bash
29
+ npx -y easy-pg-mcp
30
+ ```
31
+
32
+ For local development:
33
+
34
+ ```bash
35
+ cd easy-pg-mcp
36
+ npm install
37
+ npm run build
38
+ ```
39
+
40
+ ## Configuration
41
+
42
+ | Variable | Required | Default | Description |
43
+ | --- | --- | --- | --- |
44
+ | `PG_CONNECTION_STRING` | No | - | PostgreSQL connection string. When set, host/user/password/database fields are optional |
45
+ | `PG_HOST` | Yes* | - | PostgreSQL host name or IP address |
46
+ | `PG_PORT` | No | `5432` | PostgreSQL port |
47
+ | `PG_USER` | Yes* | - | PostgreSQL user name |
48
+ | `PG_PASSWORD` | Yes* | - | PostgreSQL password |
49
+ | `PG_DATABASE` | Yes* | - | Database name |
50
+ | `PG_CONNECTION_LIMIT` | No | `10` | Maximum number of active pool connections |
51
+ | `PG_IDLE_TIMEOUT` | No | `30000` | Idle connection timeout in milliseconds |
52
+ | `PG_ENABLE_KEEP_ALIVE` | No | `true` | Whether TCP keep-alive is enabled |
53
+ | `PG_KEEP_ALIVE_INITIAL_DELAY` | No | `0` | Initial TCP keep-alive delay in milliseconds |
54
+ | `PG_SSL` | No | - | Set `true` to enable SSL, or `no-verify` to disable certificate verification |
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 |
57
+ | `PG_MCP_ALLOW_TABLES` | No | - | Comma-separated table allowlist, such as `users,orders` |
58
+ | `PG_MCP_DENY_TABLES` | No | - | Comma-separated table denylist, such as `payments,secrets` |
59
+ | `PG_BATCH_MAX_SIZE` | No | `100` | Maximum number of parameter sets per internal batch for `pg_batch_execute` |
60
+ | `PG_LOG_PATH` | No | `logs` | Directory used for batch execution and CSV import log files |
61
+ | `PG_POLICY_HOOK` | No | - | HTTP POST URL for external accept/reject/approval policy decisions |
62
+ | `PG_APPROVAL_TTL_SECONDS` | No | `300` | Number of seconds a pending approval remains valid |
63
+
64
+ `*` Required unless `PG_CONNECTION_STRING` is set.
65
+
66
+ Example `.env`:
67
+
68
+ ```env
69
+ PG_HOST=localhost
70
+ PG_PORT=5432
71
+ PG_USER=postgres
72
+ PG_PASSWORD=your_password
73
+ PG_DATABASE=your_database
74
+ ```
75
+
76
+ ## MCP Client Examples
77
+
78
+ Claude Desktop:
79
+
80
+ ```json
81
+ {
82
+ "mcpServers": {
83
+ "easy-pg-mcp": {
84
+ "command": "npx",
85
+ "args": ["-y", "easy-pg-mcp"],
86
+ "env": {
87
+ "PG_HOST": "localhost",
88
+ "PG_PORT": "5432",
89
+ "PG_USER": "YOUR USERNAME",
90
+ "PG_PASSWORD": "YOUR PASSWORD",
91
+ "PG_DATABASE": "YOUR DB NAME"
92
+ }
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ Codex `config.toml`:
99
+
100
+ ```toml
101
+ [mcp_servers.easy-pg-mcp]
102
+ args = ["-y", "easy-pg-mcp"]
103
+ command = "npx"
104
+ enabled = true
105
+
106
+ [mcp_servers.easy-pg-mcp.env]
107
+ PG_HOST = "localhost"
108
+ PG_PORT = "5432"
109
+ PG_USER = "YOUR USERNAME"
110
+ PG_PASSWORD = "YOUR PASSWORD"
111
+ PG_DATABASE = "YOUR DB NAME"
112
+ ```
113
+
114
+ ## Available Tools
115
+
116
+ | Tool | Description |
117
+ | --- | --- |
118
+ | `pg_query` | Execute a read-only SQL query, such as `SELECT` |
119
+ | `pg_execute` | Execute a data modification statement, such as `INSERT`, `UPDATE`, or `DELETE` |
120
+ | `pg_batch_execute` | Execute one data modification statement repeatedly with multiple parameter sets |
121
+ | `pg_import_csv` | Import a UTF-8 CSV file into a table using the header row as column names |
122
+ | `pg_export_csv` | Export all rows from a table to a UTF-8 CSV file |
123
+ | `explain_query` | Run `EXPLAIN` for a `SELECT` query |
124
+ | `list_tables` | List base tables in the current schema, including approximate row counts and comments |
125
+ | `list_views` | List views in the current schema |
126
+ | `describe_table` | Show column information for one or more tables |
127
+ | `describe_index` | Show indexes for a table |
128
+ | `list_triggers` | List triggers in the current schema |
129
+ | `get_current_privileges` | Show the current PostgreSQL user and table grants |
130
+ | `pg_run_approved_command` | Run a pending command after approval, only registered when `PG_POLICY_HOOK` is set |
131
+ | `pg_list_pending_approvals` | List pending approval requests, only registered when `PG_POLICY_HOOK` is set |
132
+ | `pg_cancel_approval` | Cancel a pending approval request, only registered when `PG_POLICY_HOOK` is set |
133
+
134
+ When `PG_READ_ONLY=true` or `PG_MCP_MODE=readonly`, `pg_execute`, `pg_batch_execute`, and `pg_import_csv` are not registered.
135
+
136
+ ## Parameter Syntax
137
+
138
+ node-postgres uses positional placeholders: `$1`, `$2`, `$3`, and so on. Do not use MySQL-style `?` placeholders.
139
+
140
+ Example `pg_execute` input:
141
+
142
+ ```json
143
+ {
144
+ "sql": "UPDATE users SET email = $1 WHERE id = $2",
145
+ "params": ["new@example.com", 123]
146
+ }
147
+ ```
148
+
149
+ Example `pg_batch_execute` input:
150
+
151
+ ```json
152
+ {
153
+ "sql": "INSERT INTO users (name, email) VALUES ($1, $2)",
154
+ "paramsList": [
155
+ ["Alice", "alice@example.com"],
156
+ ["Bob", "bob@example.com"]
157
+ ],
158
+ "transaction": "all"
159
+ }
160
+ ```
161
+
162
+ ## Batch Execute
163
+
164
+ `pg_batch_execute` runs the same parameterized write statement with multiple parameter arrays. The `transaction` option controls transaction scope:
165
+
166
+ | Value | Behavior |
167
+ | --- | --- |
168
+ | `all` | Default. Wrap all rows in one transaction |
169
+ | `batch` | Wrap each internal batch in its own transaction |
170
+ | `each` | Wrap each parameter set in its own transaction |
171
+ | `none` | Do not start explicit transactions |
172
+
173
+ Detailed per-row execution results are written to a timestamped `.log` file under `PG_LOG_PATH`, which defaults to `logs/`. Log files older than seven days are cleaned up automatically when the server starts.
174
+
175
+ ## SQL Policy
176
+
177
+ - `pg_query` allows only single-statement `SELECT`, `SHOW`, and `EXPLAIN` queries.
178
+ - `explain_query` accepts only a single `SELECT` statement and runs `EXPLAIN` for it.
179
+ - `pg_execute` allows only single-statement `INSERT`, `UPDATE`, and `DELETE` statements when write mode is enabled.
180
+ - `pg_batch_execute` and `pg_import_csv` use the same write policy as `pg_execute`.
181
+ - `pg_export_csv` checks table policy before exporting table data.
182
+ - Multi-statement SQL is rejected.
183
+ - `SELECT ... INTO` and locking reads are rejected for read-query tools.
184
+ - `PG_MCP_DENY_TABLES` rejects matching tables before `PG_MCP_ALLOW_TABLES` is evaluated.
185
+ - If `PG_MCP_ALLOW_TABLES` is set, every detected table must be included in the allowlist.
186
+
187
+ Table policy matching is best-effort and based on SQL parsing. PostgreSQL grants remain the final security boundary.
188
+
189
+ ## Policy Hook and Approvals
190
+
191
+ 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:
192
+
193
+ ```json
194
+ { "status": "accept" }
195
+ ```
196
+
197
+ ```json
198
+ { "status": "reject", "message": "Writes are blocked outside maintenance windows." }
199
+ ```
200
+
201
+ ```json
202
+ { "status": "approval_required", "message": "User approval is required before updating users." }
203
+ ```
204
+
205
+ For `approval_required`, the server stores the pending command in memory and returns an `approvalId`. The MCP host can later call `pg_run_approved_command` with that approval id. Pending approvals are one-time use and expire after `PG_APPROVAL_TTL_SECONDS`.
206
+
207
+ ## Development
208
+
209
+ ```bash
210
+ npm run build
211
+ npm run test
212
+ ```
213
+
214
+ To run the integration tests against a real PostgreSQL database, create `.env`:
215
+
216
+ ```env
217
+ TEST_HOST=localhost
218
+ TEST_PORT=5432
219
+ TEST_USERNAME=test_user
220
+ TEST_PASSWORD=test_password
221
+ TEST_DB=test_database
222
+ ```
223
+
224
+ The tests create and drop temporary tables, a view, a function, and a trigger in `TEST_DB`. If the `TEST_*` variables are missing, integration tests are skipped.
225
+
226
+ ## License
227
+
228
+ MIT. See [LICENSE.md](LICENSE.md).
@@ -0,0 +1,228 @@
1
+ # easy-pg-mcp
2
+
3
+ `easy-pg-mcp` 是一個輕量的 [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server,讓 AI 助理可以透過安全、結構化的工具介面查詢與檢視 PostgreSQL 資料庫。
4
+
5
+ 本專案使用 Node.js、TypeScript、官方 MCP SDK,以及 [`pg`](https://www.npmjs.com/package/pg) / node-postgres。Server 透過 stdio 運作,可由 Claude Desktop、Codex、OpenCode 等 MCP client 啟動。
6
+
7
+ ## 功能
8
+
9
+ - 使用 node-postgres 的 PostgreSQL connection pool
10
+ - 唯讀查詢工具
11
+ - INSERT / UPDATE / DELETE 執行工具
12
+ - 批次執行與 CSV 匯入 / 匯出
13
+ - 資料表、view、index、trigger 結構查詢
14
+ - `EXPLAIN` 查詢計畫分析
15
+ - 目前使用者與 table grant 查詢
16
+ - 可選的 policy hook 與 approval workflow
17
+
18
+ ## 需求
19
+
20
+ - Node.js 18 以上
21
+ - npm
22
+ - 可連線的 PostgreSQL 資料庫
23
+
24
+ ## 安裝
25
+
26
+ 直接用 `npx` 啟動:
27
+
28
+ ```bash
29
+ npx -y easy-pg-mcp
30
+ ```
31
+
32
+ 本機開發:
33
+
34
+ ```bash
35
+ cd easy-pg-mcp
36
+ npm install
37
+ npm run build
38
+ ```
39
+
40
+ ## 設定
41
+
42
+ | 變數 | 必填 | 預設值 | 說明 |
43
+ | --- | --- | --- | --- |
44
+ | `PG_CONNECTION_STRING` | 否 | - | PostgreSQL connection string。設定後可省略 host/user/password/database |
45
+ | `PG_HOST` | 是* | - | PostgreSQL host |
46
+ | `PG_PORT` | 否 | `5432` | PostgreSQL port |
47
+ | `PG_USER` | 是* | - | PostgreSQL 使用者 |
48
+ | `PG_PASSWORD` | 是* | - | PostgreSQL 密碼 |
49
+ | `PG_DATABASE` | 是* | - | 資料庫名稱 |
50
+ | `PG_CONNECTION_LIMIT` | 否 | `10` | pool 最大連線數 |
51
+ | `PG_IDLE_TIMEOUT` | 否 | `30000` | idle timeout,單位毫秒 |
52
+ | `PG_ENABLE_KEEP_ALIVE` | 否 | `true` | 是否啟用 TCP keep-alive |
53
+ | `PG_KEEP_ALIVE_INITIAL_DELAY` | 否 | `0` | keep-alive 初始延遲,單位毫秒 |
54
+ | `PG_SSL` | 否 | - | 設為 `true` 啟用 SSL,或 `no-verify` 關閉憑證驗證 |
55
+ | `PG_READ_ONLY` | 否 | `false` | 設為 `true` 時不註冊寫入工具 |
56
+ | `PG_MCP_MODE` | 否 | `readwrite` | 設為 `readonly` 可停用寫入工具 |
57
+ | `PG_MCP_ALLOW_TABLES` | 否 | - | table allowlist,例如 `users,orders` |
58
+ | `PG_MCP_DENY_TABLES` | 否 | - | table denylist,例如 `payments,secrets` |
59
+ | `PG_BATCH_MAX_SIZE` | 否 | `100` | `pg_batch_execute` 每批最大參數組數 |
60
+ | `PG_LOG_PATH` | 否 | `logs` | 批次執行與 CSV 匯入 log 目錄 |
61
+ | `PG_POLICY_HOOK` | 否 | - | 外部 policy hook HTTP POST URL |
62
+ | `PG_APPROVAL_TTL_SECONDS` | 否 | `300` | pending approval 有效秒數 |
63
+
64
+ `*` 若已設定 `PG_CONNECTION_STRING`,則不必另外設定。
65
+
66
+ `.env` 範例:
67
+
68
+ ```env
69
+ PG_HOST=localhost
70
+ PG_PORT=5432
71
+ PG_USER=postgres
72
+ PG_PASSWORD=your_password
73
+ PG_DATABASE=your_database
74
+ ```
75
+
76
+ ## MCP Client 範例
77
+
78
+ Claude Desktop:
79
+
80
+ ```json
81
+ {
82
+ "mcpServers": {
83
+ "easy-pg-mcp": {
84
+ "command": "npx",
85
+ "args": ["-y", "easy-pg-mcp"],
86
+ "env": {
87
+ "PG_HOST": "localhost",
88
+ "PG_PORT": "5432",
89
+ "PG_USER": "YOUR USERNAME",
90
+ "PG_PASSWORD": "YOUR PASSWORD",
91
+ "PG_DATABASE": "YOUR DB NAME"
92
+ }
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ Codex `config.toml`:
99
+
100
+ ```toml
101
+ [mcp_servers.easy-pg-mcp]
102
+ args = ["-y", "easy-pg-mcp"]
103
+ command = "npx"
104
+ enabled = true
105
+
106
+ [mcp_servers.easy-pg-mcp.env]
107
+ PG_HOST = "localhost"
108
+ PG_PORT = "5432"
109
+ PG_USER = "YOUR USERNAME"
110
+ PG_PASSWORD = "YOUR PASSWORD"
111
+ PG_DATABASE = "YOUR DB NAME"
112
+ ```
113
+
114
+ ## 可用工具
115
+
116
+ | 工具 | 說明 |
117
+ | --- | --- |
118
+ | `pg_query` | 執行唯讀 SQL,例如 `SELECT` |
119
+ | `pg_execute` | 執行資料修改 SQL,例如 `INSERT`、`UPDATE`、`DELETE` |
120
+ | `pg_batch_execute` | 使用多組參數重複執行同一個資料修改 SQL |
121
+ | `pg_import_csv` | 依 CSV header 將 UTF-8 CSV 匯入資料表 |
122
+ | `pg_export_csv` | 將資料表匯出成 UTF-8 CSV |
123
+ | `explain_query` | 對 `SELECT` 執行 `EXPLAIN` |
124
+ | `list_tables` | 列出目前 schema 的 base tables |
125
+ | `list_views` | 列出目前 schema 的 views |
126
+ | `describe_table` | 顯示一個或多個資料表的欄位資訊 |
127
+ | `describe_index` | 顯示資料表 indexes |
128
+ | `list_triggers` | 列出目前 schema 的 triggers |
129
+ | `get_current_privileges` | 顯示目前 PostgreSQL 使用者與 table grants |
130
+ | `pg_run_approved_command` | 執行已核准的 pending command,僅在 `PG_POLICY_HOOK` 設定時註冊 |
131
+ | `pg_list_pending_approvals` | 列出 pending approvals,僅在 `PG_POLICY_HOOK` 設定時註冊 |
132
+ | `pg_cancel_approval` | 取消 pending approval,僅在 `PG_POLICY_HOOK` 設定時註冊 |
133
+
134
+ 當 `PG_READ_ONLY=true` 或 `PG_MCP_MODE=readonly` 時,`pg_execute`、`pg_batch_execute`、`pg_import_csv` 不會被註冊。
135
+
136
+ ## 參數語法
137
+
138
+ node-postgres 使用 `$1`、`$2`、`$3` 這種 positional placeholders。不要使用 MySQL 的 `?` placeholder。
139
+
140
+ `pg_execute` 範例:
141
+
142
+ ```json
143
+ {
144
+ "sql": "UPDATE users SET email = $1 WHERE id = $2",
145
+ "params": ["new@example.com", 123]
146
+ }
147
+ ```
148
+
149
+ `pg_batch_execute` 範例:
150
+
151
+ ```json
152
+ {
153
+ "sql": "INSERT INTO users (name, email) VALUES ($1, $2)",
154
+ "paramsList": [
155
+ ["Alice", "alice@example.com"],
156
+ ["Bob", "bob@example.com"]
157
+ ],
158
+ "transaction": "all"
159
+ }
160
+ ```
161
+
162
+ ## 批次執行
163
+
164
+ `pg_batch_execute` 會用多組參數重複執行同一個參數化寫入 SQL。`transaction` 可控制交易範圍:
165
+
166
+ | 值 | 行為 |
167
+ | --- | --- |
168
+ | `all` | 預設值。所有資料列包在同一個 transaction |
169
+ | `batch` | 每個內部分批各自使用一個 transaction |
170
+ | `each` | 每組參數各自使用一個 transaction |
171
+ | `none` | 不主動開啟 transaction |
172
+
173
+ 每筆執行結果會寫入 `PG_LOG_PATH` 下帶時間戳的 `.log` 檔,預設路徑為 `logs/`。Server 啟動時也會自動清除七天前的舊 log。
174
+
175
+ ## SQL Policy
176
+
177
+ - `pg_query` 只允許單一 statement 的 `SELECT`、`SHOW`、`EXPLAIN`。
178
+ - `explain_query` 只接受單一 `SELECT`,並在前方加上 `EXPLAIN` 執行。
179
+ - `pg_execute` 在 write mode 下只允許單一 statement 的 `INSERT`、`UPDATE`、`DELETE`。
180
+ - `pg_batch_execute` 與 `pg_import_csv` 使用和 `pg_execute` 相同的寫入規則。
181
+ - `pg_export_csv` 會先檢查 table policy。
182
+ - 禁止 multi-statement SQL。
183
+ - `SELECT ... INTO` 與 locking reads 會被唯讀查詢工具拒絕。
184
+ - `PG_MCP_DENY_TABLES` 優先於 `PG_MCP_ALLOW_TABLES`。
185
+ - 若設定 `PG_MCP_ALLOW_TABLES`,所有偵測到的 table 都必須在 allowlist 內。
186
+
187
+ Table policy 是根據 SQL parser 的 best-effort 檢查;PostgreSQL grants 仍然是最後的安全邊界。
188
+
189
+ ## Policy Hook 與 Approval
190
+
191
+ 設定 `PG_POLICY_HOOK` 後,server 會在內建 policy 檢查通過、但命令實際執行前,將每次工具操作 POST 到外部 hook。Hook 必須回傳以下其中一種結果:
192
+
193
+ ```json
194
+ { "status": "accept" }
195
+ ```
196
+
197
+ ```json
198
+ { "status": "reject", "message": "Writes are blocked outside maintenance windows." }
199
+ ```
200
+
201
+ ```json
202
+ { "status": "approval_required", "message": "User approval is required before updating users." }
203
+ ```
204
+
205
+ 若回傳 `approval_required`,server 會把待執行命令暫存在記憶體中,並回傳 `approvalId`。之後 MCP host 可以透過 `pg_run_approved_command` 搭配該 `approvalId` 執行命令。Pending approvals 只能使用一次,並會在 `PG_APPROVAL_TTL_SECONDS` 到期後失效。
206
+
207
+ ## 開發與測試
208
+
209
+ ```bash
210
+ npm run build
211
+ npm run test
212
+ ```
213
+
214
+ 若要執行 PostgreSQL integration tests,建立 `.env`:
215
+
216
+ ```env
217
+ TEST_HOST=localhost
218
+ TEST_PORT=5432
219
+ TEST_USERNAME=test_user
220
+ TEST_PASSWORD=test_password
221
+ TEST_DB=test_database
222
+ ```
223
+
224
+ 測試會在 `TEST_DB` 建立並刪除暫時資料表、view、function、trigger。若缺少 `TEST_*` 變數,integration tests 會自動 skip。
225
+
226
+ ## 授權
227
+
228
+ MIT。請見 [LICENSE.md](LICENSE.md)。
@@ -0,0 +1,58 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { config } from './config.js';
3
+ const pendingApprovals = new Map();
4
+ export function createPendingApproval(input) {
5
+ cleanupExpiredApprovals();
6
+ const approvalId = `apv_${randomUUID()}`;
7
+ const createdAtMs = Date.now();
8
+ const expiresAtMs = createdAtMs + config.approvalTtlSeconds * 1000;
9
+ const pendingApproval = {
10
+ ...input,
11
+ approvalId,
12
+ createdAtMs,
13
+ expiresAtMs,
14
+ };
15
+ pendingApprovals.set(approvalId, pendingApproval);
16
+ return toSummary(pendingApproval);
17
+ }
18
+ export async function runApprovedCommand(approvalId) {
19
+ cleanupExpiredApprovals();
20
+ const pendingApproval = pendingApprovals.get(approvalId);
21
+ if (!pendingApproval) {
22
+ throw new Error(`Approval not found or expired: ${approvalId}`);
23
+ }
24
+ pendingApprovals.delete(approvalId);
25
+ return pendingApproval.command();
26
+ }
27
+ export function listPendingApprovals() {
28
+ cleanupExpiredApprovals();
29
+ return [...pendingApprovals.values()].map(toSummary);
30
+ }
31
+ export function cancelApproval(approvalId) {
32
+ cleanupExpiredApprovals();
33
+ const pendingApproval = pendingApprovals.get(approvalId);
34
+ if (!pendingApproval) {
35
+ throw new Error(`Approval not found or expired: ${approvalId}`);
36
+ }
37
+ pendingApprovals.delete(approvalId);
38
+ return toSummary(pendingApproval);
39
+ }
40
+ export function cleanupExpiredApprovals(now = Date.now()) {
41
+ for (const [approvalId, pendingApproval] of pendingApprovals.entries()) {
42
+ if (pendingApproval.expiresAtMs <= now) {
43
+ pendingApprovals.delete(approvalId);
44
+ }
45
+ }
46
+ }
47
+ function toSummary(pendingApproval) {
48
+ return {
49
+ approvalId: pendingApproval.approvalId,
50
+ functionName: pendingApproval.functionName,
51
+ statementType: pendingApproval.statementType,
52
+ tableNames: pendingApproval.tableNames,
53
+ message: pendingApproval.message ?? 'Approval required before executing this command.',
54
+ createdAt: new Date(pendingApproval.createdAtMs).toISOString(),
55
+ expiresAt: new Date(pendingApproval.expiresAtMs).toISOString(),
56
+ summary: pendingApproval.summary,
57
+ };
58
+ }
@@ -0,0 +1,46 @@
1
+ const DEFAULT_BATCH_MAX_SIZE = 100;
2
+ const DEFAULT_APPROVAL_TTL_SECONDS = 300;
3
+ const DEFAULT_LOG_PATH = 'logs';
4
+ function parseBoolean(value) {
5
+ return value?.toLowerCase() === 'true';
6
+ }
7
+ function parsePositiveInt(value, defaultValue) {
8
+ const parsed = value ? Number.parseInt(value, 10) : NaN;
9
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : defaultValue;
10
+ }
11
+ function parseList(value) {
12
+ if (!value) {
13
+ return [];
14
+ }
15
+ return value
16
+ .split(',')
17
+ .map((item) => item.trim())
18
+ .filter(Boolean)
19
+ .map((item) => normalizeTableName(item));
20
+ }
21
+ export function normalizeTableName(table) {
22
+ return table.replace(/"/g, '').toLowerCase();
23
+ }
24
+ function resolveMode() {
25
+ const explicitMode = process.env.PG_MCP_MODE?.toLowerCase();
26
+ const readOnly = parseBoolean(process.env.PG_READ_ONLY);
27
+ if (readOnly || explicitMode === 'readonly' || explicitMode === 'read-only') {
28
+ return 'readonly';
29
+ }
30
+ return 'readwrite';
31
+ }
32
+ export const config = {
33
+ mode: resolveMode(),
34
+ allowTables: parseList(process.env.PG_MCP_ALLOW_TABLES),
35
+ denyTables: parseList(process.env.PG_MCP_DENY_TABLES),
36
+ batchMaxSize: parsePositiveInt(process.env.PG_BATCH_MAX_SIZE, DEFAULT_BATCH_MAX_SIZE),
37
+ logPath: process.env.PG_LOG_PATH?.trim() || DEFAULT_LOG_PATH,
38
+ policyHookUrl: process.env.PG_POLICY_HOOK?.trim() || undefined,
39
+ approvalTtlSeconds: parsePositiveInt(process.env.PG_APPROVAL_TTL_SECONDS, DEFAULT_APPROVAL_TTL_SECONDS),
40
+ };
41
+ export function isReadOnlyMode() {
42
+ return config.mode === 'readonly';
43
+ }
44
+ export function isPolicyHookEnabled() {
45
+ return Boolean(config.policyHookUrl);
46
+ }
package/build/csv.js ADDED
@@ -0,0 +1,109 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ export async function readCsvFile(filePath) {
4
+ const content = await readFile(filePath, 'utf8');
5
+ const records = parseCsv(content);
6
+ if (records.length === 0) {
7
+ throw new Error('CSV import failed: file is empty.');
8
+ }
9
+ const headers = records[0].map((header) => header.trim());
10
+ if (headers.length === 0 || headers.some((header) => !header)) {
11
+ throw new Error('CSV import failed: header row contains an empty column name.');
12
+ }
13
+ const uniqueHeaders = new Set(headers.map((header) => header.toLowerCase()));
14
+ if (uniqueHeaders.size !== headers.length) {
15
+ throw new Error('CSV import failed: header row contains duplicate column names.');
16
+ }
17
+ const rows = records.slice(1).filter((row) => row.some((value) => value !== ''));
18
+ for (const [index, row] of rows.entries()) {
19
+ if (row.length !== headers.length) {
20
+ throw new Error(`CSV import failed: row ${index + 2} has ${row.length} columns, expected ${headers.length}.`);
21
+ }
22
+ }
23
+ return { headers, rows };
24
+ }
25
+ export async function writeCsvFile(filePath, headers, rows) {
26
+ const directory = path.dirname(filePath);
27
+ if (directory && directory !== '.') {
28
+ await mkdir(directory, { recursive: true });
29
+ }
30
+ const lines = [
31
+ serializeCsvRow(headers),
32
+ ...rows.map((row) => serializeCsvRow(headers.map((header) => stringifyCsvValue(row[header])))),
33
+ ];
34
+ await writeFile(filePath, `${lines.join('\n')}\n`, 'utf8');
35
+ }
36
+ function parseCsv(content) {
37
+ const rows = [];
38
+ let row = [];
39
+ let field = '';
40
+ let inQuotes = false;
41
+ for (let index = 0; index < content.length; index += 1) {
42
+ const char = content[index];
43
+ const nextChar = content[index + 1];
44
+ if (inQuotes) {
45
+ if (char === '"' && nextChar === '"') {
46
+ field += '"';
47
+ index += 1;
48
+ }
49
+ else if (char === '"') {
50
+ inQuotes = false;
51
+ }
52
+ else {
53
+ field += char;
54
+ }
55
+ continue;
56
+ }
57
+ if (char === '"') {
58
+ inQuotes = true;
59
+ }
60
+ else if (char === ',') {
61
+ row.push(field);
62
+ field = '';
63
+ }
64
+ else if (char === '\n') {
65
+ row.push(field);
66
+ rows.push(row);
67
+ row = [];
68
+ field = '';
69
+ }
70
+ else if (char === '\r') {
71
+ if (nextChar === '\n') {
72
+ continue;
73
+ }
74
+ row.push(field);
75
+ rows.push(row);
76
+ row = [];
77
+ field = '';
78
+ }
79
+ else {
80
+ field += char;
81
+ }
82
+ }
83
+ if (inQuotes) {
84
+ throw new Error('CSV parse failed: unterminated quoted field.');
85
+ }
86
+ if (field !== '' || row.length > 0) {
87
+ row.push(field);
88
+ rows.push(row);
89
+ }
90
+ return rows;
91
+ }
92
+ function serializeCsvRow(values) {
93
+ return values.map(escapeCsvValue).join(',');
94
+ }
95
+ function stringifyCsvValue(value) {
96
+ if (value === null || value === undefined) {
97
+ return '';
98
+ }
99
+ if (value instanceof Date) {
100
+ return value.toISOString();
101
+ }
102
+ return String(value);
103
+ }
104
+ function escapeCsvValue(value) {
105
+ if (!/[",\r\n]/.test(value)) {
106
+ return value;
107
+ }
108
+ return `"${value.replace(/"/g, '""')}"`;
109
+ }