easy-mysql-mcp 1.0.3 → 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/README.md CHANGED
@@ -9,10 +9,10 @@ This project uses Node.js, TypeScript, the official MCP SDK, and `mysql2/promise
9
9
  - MySQL connection pooling powered by `mysql2/promise`
10
10
  - Read-only query tool for data retrieval
11
11
  - Execute tool for data modification statements
12
+ - Batch execution and CSV import/export helpers
12
13
  - Schema discovery tools for tables, views, indexes, and triggers
13
14
  - Query plan inspection with `EXPLAIN`
14
15
  - Current user and privilege inspection
15
- - stdout protection to prevent non-MCP logs from polluting the stdio protocol
16
16
 
17
17
  ## Requirements
18
18
 
@@ -54,6 +54,14 @@ Configure the server with environment variables. You can provide them through yo
54
54
  | `MYSQL_WAIT_FOR_CONNECTIONS` | No | `true` | Whether the pool waits when all connections are busy |
55
55
  | `MYSQL_ENABLE_KEEP_ALIVE` | No | `true` | Whether TCP keep-alive is enabled |
56
56
  | `MYSQL_KEEP_ALIVE_INITIAL_DELAY` | No | `0` | Initial TCP keep-alive delay in milliseconds |
57
+ | `MYSQL_READ_ONLY` | No | `false` | When `true`, enables read-only mode and does not register `mysql_execute` |
58
+ | `MYSQL_MCP_MODE` | No | `readwrite` | MCP policy mode. Use `readonly` to disable write execution |
59
+ | `MYSQL_MCP_ALLOW_TABLES` | No | - | Comma-separated table allowlist, such as `users,orders` |
60
+ | `MYSQL_MCP_DENY_TABLES` | No | - | Comma-separated table denylist, such as `payments,secrets` |
61
+ | `MYSQL_BATCH_MAX_SIZE` | No | `100` | Maximum number of parameter sets per internal batch for `mysql_batch_execute` |
62
+ | `MYSQL_LOG_PATH` | No | `logs` | Directory used for batch execution and CSV import log files |
63
+ | `MYSQL_POLICY_HOOK` | No | - | HTTP POST URL for external accept/reject/approval policy decisions |
64
+ | `MYSQL_APPROVAL_TTL_SECONDS` | No | `300` | Number of seconds a pending approval remains valid |
57
65
 
58
66
  Example `.env`:
59
67
 
@@ -149,6 +157,9 @@ MYSQL_DATABASE = "YOUR DB NAME"
149
157
  | --- | --- |
150
158
  | `mysql_query` | Execute a SQL query intended for data retrieval, such as `SELECT` |
151
159
  | `mysql_execute` | Execute a data modification statement, such as `INSERT`, `UPDATE`, or `DELETE` |
160
+ | `mysql_batch_execute` | Execute one data modification statement repeatedly with multiple parameter sets |
161
+ | `mysql_import_csv` | Import a UTF-8 CSV file into a table using the header row as column names |
162
+ | `mysql_export_csv` | Export all rows from a table to a UTF-8 CSV file |
152
163
  | `explain_query` | Run `EXPLAIN` for a SQL query and return the execution plan |
153
164
  | `list_tables` | List base tables in the current database, including approximate row counts and comments |
154
165
  | `list_views` | List views in the current database |
@@ -156,12 +167,168 @@ MYSQL_DATABASE = "YOUR DB NAME"
156
167
  | `describe_index` | Show indexes for a table |
157
168
  | `list_triggers` | List triggers in the current database |
158
169
  | `get_current_privileges` | Show the current MySQL user and grants |
170
+ | `mysql_run_approved_command` | Run a pending command after approval, only registered when `MYSQL_POLICY_HOOK` is set |
171
+ | `mysql_list_pending_approvals` | List pending approval requests, only registered when `MYSQL_POLICY_HOOK` is set |
172
+ | `mysql_cancel_approval` | Cancel a pending approval request, only registered when `MYSQL_POLICY_HOOK` is set |
173
+
174
+ When `MYSQL_READ_ONLY=true` or `MYSQL_MCP_MODE=readonly`, the `mysql_execute`, `mysql_batch_execute`, and `mysql_import_csv` tools are not registered.
175
+
176
+ ### Batch Execute
177
+
178
+ `mysql_batch_execute` runs the same parameterized write statement with multiple parameter arrays. It is useful for bulk inserts or repeated updates without enabling multi-statement SQL.
179
+
180
+ Example input:
181
+
182
+ ```json
183
+ {
184
+ "sql": "INSERT INTO users (name, email) VALUES (?, ?)",
185
+ "paramsList": [
186
+ ["Alice", "alice@example.com"],
187
+ ["Bob", "bob@example.com"]
188
+ ],
189
+ "transaction": "all"
190
+ }
191
+ ```
192
+
193
+ The `transaction` option controls transaction scope:
194
+
195
+ | Value | Behavior |
196
+ | --- | --- |
197
+ | `all` | Default. Wrap all rows in one transaction |
198
+ | `batch` | Wrap each internal batch in its own transaction |
199
+ | `each` | Wrap each parameter set in its own transaction |
200
+ | `none` | Do not start explicit transactions |
201
+
202
+ The server splits `paramsList` into internal batches using `MYSQL_BATCH_MAX_SIZE`. For example, with the default size of `100`, `250` parameter sets run as `100`, `100`, and `50`.
203
+
204
+ Detailed per-row execution results are written to a timestamped `.log` file under `MYSQL_LOG_PATH`, which defaults to `logs/`. The tool response only returns summary counts and the log file path. Log files older than seven days are cleaned up automatically when the server starts.
205
+
206
+ ### CSV Import and Export
207
+
208
+ `mysql_import_csv` reads a UTF-8 CSV file and inserts rows into a table. The first CSV row must contain column names, and every data row must have the same number of columns. Internally, the tool builds a parameterized `INSERT` statement and executes it through the same batch execution path as `mysql_batch_execute`.
209
+
210
+ Example import input:
211
+
212
+ ```json
213
+ {
214
+ "tableName": "users",
215
+ "filePath": "./data/users.csv",
216
+ "transaction": "all"
217
+ }
218
+ ```
219
+
220
+ `mysql_export_csv` exports all rows from a table to a UTF-8 CSV file. It writes a header row using the table's column names, even when the table has no rows.
221
+
222
+ Example export input:
223
+
224
+ ```json
225
+ {
226
+ "tableName": "users",
227
+ "filePath": "./exports/users.csv"
228
+ }
229
+ ```
230
+
231
+ CSV import/export uses standard comma-separated CSV with double-quote escaping. Empty CSV fields are imported as empty strings.
232
+
233
+ ## SQL Policy
234
+
235
+ The server applies a lightweight SQL policy before executing user-provided SQL:
236
+
237
+ - `mysql_query` allows only single-statement `SELECT`, `SHOW`, `DESCRIBE`, and `EXPLAIN` queries.
238
+ - `explain_query` accepts only a single `SELECT` statement and runs `EXPLAIN` for it.
239
+ - `mysql_execute` allows only single-statement `INSERT`, `UPDATE`, `DELETE`, and `REPLACE` statements when write mode is enabled.
240
+ - `mysql_batch_execute` uses the same SQL policy as `mysql_execute` and applies the statement repeatedly with parameter arrays.
241
+ - `mysql_import_csv` uses table policy and the same batch execution path as `mysql_batch_execute`.
242
+ - `mysql_export_csv` uses table policy before exporting table data.
243
+ - Multi-statement SQL is rejected.
244
+ - `SELECT ... INTO` and locking reads are rejected for read-query tools.
245
+ - `MYSQL_MCP_DENY_TABLES` rejects matching tables before `MYSQL_MCP_ALLOW_TABLES` is evaluated.
246
+ - If `MYSQL_MCP_ALLOW_TABLES` is set, every detected table must be included in the allowlist.
247
+
248
+ Table policy matching is best-effort and based on SQL parsing. You can use either `table` or `database.table` entries. MySQL grants remain the final security boundary.
249
+
250
+ ### Policy Order
251
+
252
+ Policy checks run in this order:
253
+
254
+ 1. Built-in SQL safety checks run first, such as single-statement enforcement and allowed statement types for each tool.
255
+ 2. `MYSQL_MCP_DENY_TABLES` is checked next. If a detected table matches the denylist, the command is rejected immediately.
256
+ 3. `MYSQL_MCP_ALLOW_TABLES` is checked after the denylist. If an allowlist is configured, every detected table must be included in it.
257
+ 4. `MYSQL_POLICY_HOOK` runs only after the built-in SQL policy and table allow/deny policy pass.
258
+
259
+ If both `MYSQL_MCP_ALLOW_TABLES` and `MYSQL_MCP_DENY_TABLES` are configured, the denylist takes precedence. For example:
260
+
261
+ ```env
262
+ MYSQL_MCP_ALLOW_TABLES=users,orders,payments
263
+ MYSQL_MCP_DENY_TABLES=payments
264
+ ```
265
+
266
+ In this configuration, `users` and `orders` are allowed, `payments` is rejected, and all other tables are rejected because they are not in the allowlist.
267
+
268
+ `MYSQL_POLICY_HOOK` cannot override built-in rejections. It can only decide what happens after a command has already passed local policy: `accept`, `reject`, or `approval_required`.
269
+
270
+ ## Policy Hook and Approvals
271
+
272
+ When `MYSQL_POLICY_HOOK` is configured, the server posts each tool action to the hook after built-in policy checks pass and before the command runs.
273
+
274
+ Example hook request:
275
+
276
+ ```json
277
+ {
278
+ "functionName": "mysql_execute",
279
+ "sql": "UPDATE users SET email = ? WHERE id = ?",
280
+ "statementType": "update",
281
+ "tableNames": ["users"],
282
+ "paramsPreview": ["new@example.com", 123],
283
+ "metadata": {
284
+ "database": "app_db",
285
+ "mode": "readwrite",
286
+ "timestamp": "2026-05-20T12:00:00.000Z"
287
+ }
288
+ }
289
+ ```
290
+
291
+ The hook must return one of:
292
+
293
+ ```json
294
+ { "status": "accept" }
295
+ ```
296
+
297
+ ```json
298
+ { "status": "reject", "message": "Writes are blocked outside maintenance windows." }
299
+ ```
300
+
301
+ ```json
302
+ {
303
+ "status": "approval_required",
304
+ "message": "User approval is required before updating users."
305
+ }
306
+ ```
307
+
308
+ For `approval_required`, the server does not execute the command. It stores the original pending command in memory and returns an `approval_required` response with a server-generated `approvalId`. The hook does not provide the approval id. After the MCP host obtains user approval, it can call:
309
+
310
+ ```json
311
+ {
312
+ "approvalId": "apv_..."
313
+ }
314
+ ```
315
+
316
+ with `mysql_run_approved_command`. Pending approvals are one-time use and expire after `MYSQL_APPROVAL_TTL_SECONDS`. `mysql_list_pending_approvals` and `mysql_cancel_approval` are also available while `MYSQL_POLICY_HOOK` is set.
317
+
318
+ This is an approval-friendly protocol. The server cannot verify that a human approved the action; the MCP host or external platform is responsible for presenting the approval request to a user.
159
319
 
160
320
  ## Security Notes
161
321
 
162
322
  - Use a dedicated MySQL user with the minimum permissions your assistant needs.
163
323
  - Prefer read-only database credentials if you only need inspection and reporting.
324
+ - Use `MYSQL_READ_ONLY=true` or `MYSQL_MCP_MODE=readonly` to hide write execution from MCP clients.
325
+ - Use `MYSQL_MCP_ALLOW_TABLES` and `MYSQL_MCP_DENY_TABLES` as MCP-level guardrails, not as a replacement for MySQL grants.
326
+ - Use `MYSQL_POLICY_HOOK` when you need an external policy or approval workflow.
164
327
  - Be careful with `mysql_execute`, because it can modify data.
328
+ - Be careful with `mysql_import_csv`, because it can insert many rows.
329
+ - Batch execution and CSV import logs include parameter values and per-row results. Treat files under `MYSQL_LOG_PATH` as sensitive.
330
+ - CSV export writes table data to the local filesystem. Treat exported files as sensitive.
331
+ - Multi-statement SQL is disabled in the MySQL client configuration.
165
332
  - Do not commit `.env` files or real database credentials to GitHub.
166
333
  - Review generated SQL before running it against production data.
167
334
 
@@ -179,15 +346,40 @@ To create a production build:
179
346
  npm run build
180
347
  ```
181
348
 
349
+ To run the integration test suite, configure a test database in `.env`:
350
+
351
+ ```env
352
+ TEST_HOST=localhost
353
+ TEST_PORT=3306
354
+ TEST_USERNAME=test_user
355
+ TEST_PASSWORD=test_password
356
+ TEST_DB=test_database
357
+ ```
358
+
359
+ Then run:
360
+
361
+ ```bash
362
+ npm run test
363
+ ```
364
+
365
+ The tests create and drop temporary tables, a view, and a trigger in `TEST_DB`. If the `TEST_*` variables are missing, the integration test is skipped.
366
+
182
367
  ## Project Structure
183
368
 
184
369
  ```text
185
370
  src/
371
+ config.ts Environment-driven MCP policy configuration
372
+ csv.ts CSV parsing and writing helpers
373
+ csvTools.ts CSV import/export tool implementations
186
374
  db.ts MySQL pool and query helpers
187
375
  index.ts MCP server and tool registration
188
- proxy.ts stdout protection for stdio-based MCP transport
376
+ logs.ts Batch execution log helpers
377
+ policyHook.ts External policy hook client and approval response helpers
378
+ sqlPolicy.ts SQL parsing and policy enforcement
379
+ toolHandlers.ts Shared tool handler implementations
380
+ approvalStore.ts In-memory pending approval store
189
381
  ```
190
382
 
191
383
  ## License
192
384
 
193
- ISC
385
+ MIT. See [LICENSE.md](LICENSE.md).
@@ -0,0 +1,385 @@
1
+ # easy-mysql-mcp
2
+
3
+ 一個輕量的 [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server,讓 AI assistant 可以透過安全、結構化的工具介面檢查與查詢 MySQL 資料庫。
4
+
5
+ 本專案使用 Node.js、TypeScript、官方 MCP SDK,以及 `mysql2/promise`。它透過 stdio 執行,因此可以直接被 Claude Desktop 等 MCP client 使用。
6
+
7
+ ## 功能
8
+
9
+ - 使用 `mysql2/promise` 的 MySQL connection pool
10
+ - 用於資料讀取的 read-only query tool
11
+ - 用於資料修改的 execute tool
12
+ - 批次執行與 CSV 匯入/匯出工具
13
+ - tables、views、indexes、triggers 的 schema discovery tools
14
+ - 使用 `EXPLAIN` 檢查 query plan
15
+ - 檢查目前資料庫使用者與權限
16
+
17
+ ## 需求
18
+
19
+ - Node.js 18 或更新版本
20
+ - npm
21
+ - 可連線的 MySQL-compatible database
22
+
23
+ ## 安裝
24
+
25
+ 直接使用 `npx` 執行 server:
26
+
27
+ ```bash
28
+ npx -y easy-mysql-mcp
29
+ ```
30
+
31
+ 本機開發:
32
+
33
+ ```bash
34
+ cd easy-mysql-mcp
35
+ npm install
36
+ npm run build
37
+ ```
38
+
39
+ ## 設定
40
+
41
+ 你可以透過 MCP client configuration 或本機 `.env` 檔設定環境變數。
42
+
43
+ | 變數 | 必填 | 預設值 | 說明 |
44
+ | --- | --- | --- | --- |
45
+ | `MYSQL_HOST` | 是 | - | MySQL host name 或 IP address |
46
+ | `MYSQL_PORT` | 否 | `3306` | MySQL port |
47
+ | `MYSQL_USER` | 是 | - | MySQL 使用者名稱 |
48
+ | `MYSQL_PASSWORD` | 是 | - | MySQL 密碼 |
49
+ | `MYSQL_DATABASE` | 是 | - | 預設 database/schema |
50
+ | `MYSQL_CONNECTION_LIMIT` | 否 | `10` | pool 最大 active connections |
51
+ | `MYSQL_MAX_IDLE` | 否 | `10` | pool 最大 idle connections |
52
+ | `MYSQL_IDLE_TIMEOUT` | 否 | `60000` | idle connection timeout,單位毫秒 |
53
+ | `MYSQL_QUEUE_LIMIT` | 否 | `0` | 最大 queued connection requests,`0` 代表無限制 |
54
+ | `MYSQL_WAIT_FOR_CONNECTIONS` | 否 | `true` | connection 滿時是否等待 |
55
+ | `MYSQL_ENABLE_KEEP_ALIVE` | 否 | `true` | 是否啟用 TCP keep-alive |
56
+ | `MYSQL_KEEP_ALIVE_INITIAL_DELAY` | 否 | `0` | TCP keep-alive 初始延遲,單位毫秒 |
57
+ | `MYSQL_READ_ONLY` | 否 | `false` | 設為 `true` 時啟用唯讀模式,且不註冊 `mysql_execute` |
58
+ | `MYSQL_MCP_MODE` | 否 | `readwrite` | MCP policy mode。使用 `readonly` 可停用寫入執行 |
59
+ | `MYSQL_MCP_ALLOW_TABLES` | 否 | - | table allowlist,逗號分隔,例如 `users,orders` |
60
+ | `MYSQL_MCP_DENY_TABLES` | 否 | - | table denylist,逗號分隔,例如 `payments,secrets` |
61
+ | `MYSQL_BATCH_MAX_SIZE` | 否 | `100` | `mysql_batch_execute` 每個內部分批最多處理的參數組數 |
62
+ | `MYSQL_LOG_PATH` | 否 | `logs` | batch execution 與 CSV import log files 使用的目錄 |
63
+ | `MYSQL_POLICY_HOOK` | 否 | - | 外部 accept/reject/approval policy decision 的 HTTP POST URL |
64
+ | `MYSQL_APPROVAL_TTL_SECONDS` | 否 | `300` | pending approval 的有效秒數 |
65
+
66
+ 範例 `.env`:
67
+
68
+ ```env
69
+ MYSQL_HOST=localhost
70
+ MYSQL_PORT=3306
71
+ MYSQL_USER=root
72
+ MYSQL_PASSWORD=your_password
73
+ MYSQL_DATABASE=your_database
74
+ ```
75
+
76
+ ## 使用方式
77
+
78
+ 設定 MCP client,讓它透過 `npx` 啟動 package。
79
+
80
+ 本機開發時,先 build TypeScript source:
81
+
82
+ ```bash
83
+ npm run build
84
+ ```
85
+
86
+ 啟動 MCP server:
87
+
88
+ ```bash
89
+ npm start
90
+ ```
91
+
92
+ 這個 server 使用 stdio 溝通,通常會由 MCP client 啟動,而不是手動直接執行。
93
+
94
+ ## Claude Desktop 範例
95
+
96
+ ```json
97
+ {
98
+ "mcpServers": {
99
+ "easy-mysql-mcp": {
100
+ "command": "npx",
101
+ "args": ["-y", "easy-mysql-mcp"],
102
+ "env": {
103
+ "MYSQL_HOST": "localhost",
104
+ "MYSQL_PORT": "3306",
105
+ "MYSQL_USER": "YOUR USERNAME",
106
+ "MYSQL_PASSWORD": "YOUR PASSWORD",
107
+ "MYSQL_DATABASE": "YOUR DB NAME"
108
+ }
109
+ }
110
+ }
111
+ }
112
+ ```
113
+
114
+ 更新設定後,請重新啟動 Claude Desktop。
115
+
116
+ ## Codex config.toml 範例
117
+
118
+ ```toml
119
+ [mcp_servers.easy-mysql-mcp]
120
+ args = ["-y", "easy-mysql-mcp"]
121
+ command = "npx"
122
+ enabled = true
123
+
124
+ [mcp_servers.easy-mysql-mcp.env]
125
+ MYSQL_HOST = "localhost"
126
+ MYSQL_PORT = "3306"
127
+ MYSQL_USER = "YOUR USERNAME"
128
+ MYSQL_PASSWORD = "YOUR PASSWORD"
129
+ MYSQL_DATABASE = "YOUR DB NAME"
130
+ ```
131
+
132
+ ## OpenCode opencode.jsonc 範例
133
+
134
+ ```json
135
+ {
136
+ "$schema": "https://opencode.ai/config.json",
137
+ "mcp": {
138
+ "easy-mysql-mcp": {
139
+ "type": "local",
140
+ "command": ["npx", "-y", "easy-mysql-mcp"],
141
+ "enabled": true,
142
+ "environment": {
143
+ "MYSQL_HOST": "localhost",
144
+ "MYSQL_PORT": "3306",
145
+ "MYSQL_USER": "YOUR USERNAME",
146
+ "MYSQL_PASSWORD": "YOUR PASSWORD",
147
+ "MYSQL_DATABASE": "YOUR DB NAME",
148
+ },
149
+ },
150
+ },
151
+ }
152
+ ```
153
+
154
+ ## 可用工具
155
+
156
+ | 工具 | 說明 |
157
+ | --- | --- |
158
+ | `mysql_query` | 執行用於資料讀取的 SQL query,例如 `SELECT` |
159
+ | `mysql_execute` | 執行資料修改 statement,例如 `INSERT`、`UPDATE`、`DELETE` |
160
+ | `mysql_batch_execute` | 使用多組參數重複執行同一個資料修改 statement |
161
+ | `mysql_import_csv` | 使用 CSV header row 作為欄位名稱,將 UTF-8 CSV 匯入 table |
162
+ | `mysql_export_csv` | 將 table 的所有 rows 匯出為 UTF-8 CSV |
163
+ | `explain_query` | 對 SQL query 執行 `EXPLAIN` 並回傳 execution plan |
164
+ | `list_tables` | 列出目前 database 的 base tables,包含約略 row count 與 comment |
165
+ | `list_views` | 列出目前 database 的 views |
166
+ | `describe_table` | 顯示一個或多個 tables 的欄位資訊 |
167
+ | `describe_index` | 顯示 table indexes |
168
+ | `list_triggers` | 列出目前 database 的 triggers |
169
+ | `get_current_privileges` | 顯示目前 MySQL 使用者與 grants |
170
+ | `mysql_run_approved_command` | approval 後執行 pending command,只有設定 `MYSQL_POLICY_HOOK` 時註冊 |
171
+ | `mysql_list_pending_approvals` | 列出 pending approval requests,只有設定 `MYSQL_POLICY_HOOK` 時註冊 |
172
+ | `mysql_cancel_approval` | 取消 pending approval request,只有設定 `MYSQL_POLICY_HOOK` 時註冊 |
173
+
174
+ 當 `MYSQL_READ_ONLY=true` 或 `MYSQL_MCP_MODE=readonly` 時,`mysql_execute`、`mysql_batch_execute`、`mysql_import_csv` 不會被註冊。
175
+
176
+ ### Batch Execute
177
+
178
+ `mysql_batch_execute` 會使用多組參數重複執行同一個 parameterized write statement。它適合 bulk insert 或 repeated update,而且不需要啟用 multi-statement SQL。
179
+
180
+ 範例輸入:
181
+
182
+ ```json
183
+ {
184
+ "sql": "INSERT INTO users (name, email) VALUES (?, ?)",
185
+ "paramsList": [
186
+ ["Alice", "alice@example.com"],
187
+ ["Bob", "bob@example.com"]
188
+ ],
189
+ "transaction": "all"
190
+ }
191
+ ```
192
+
193
+ `transaction` 控制 transaction scope:
194
+
195
+ | 值 | 行為 |
196
+ | --- | --- |
197
+ | `all` | 預設值。所有 rows 包在同一個 transaction |
198
+ | `batch` | 每個內部分批各自一個 transaction |
199
+ | `each` | 每一組參數各自一個 transaction |
200
+ | `none` | 不主動開啟 transaction |
201
+
202
+ server 會依照 `MYSQL_BATCH_MAX_SIZE` 將 `paramsList` 切成內部分批。例如預設大小 `100` 時,`250` 組參數會切成 `100`、`100`、`50`。
203
+
204
+ 詳細的逐筆執行結果會寫到 `MYSQL_LOG_PATH` 底下具時間戳記的 `.log` 檔,預設目錄是 `logs/`。tool response 只會回傳摘要數字與 log file path。server 啟動時會自動清理超過七天的 log files。
205
+
206
+ ### CSV 匯入與匯出
207
+
208
+ `mysql_import_csv` 會讀取 UTF-8 CSV file 並插入 table。第一列必須是欄位名稱,每一列資料欄位數都必須相同。內部會建立 parameterized `INSERT` statement,並透過與 `mysql_batch_execute` 相同的 batch execution path 執行。
209
+
210
+ 匯入範例:
211
+
212
+ ```json
213
+ {
214
+ "tableName": "users",
215
+ "filePath": "./data/users.csv",
216
+ "transaction": "all"
217
+ }
218
+ ```
219
+
220
+ `mysql_export_csv` 會將 table 的所有 rows 匯出為 UTF-8 CSV file。即使 table 沒有 rows,也會使用 table 欄位名稱寫出 header row。
221
+
222
+ 匯出範例:
223
+
224
+ ```json
225
+ {
226
+ "tableName": "users",
227
+ "filePath": "./exports/users.csv"
228
+ }
229
+ ```
230
+
231
+ CSV 匯入/匯出使用標準逗號分隔 CSV 與雙引號 escaping。空白 CSV field 會以空字串匯入。
232
+
233
+ ## SQL Policy
234
+
235
+ server 在執行使用者提供的 SQL 前,會套用輕量 SQL policy:
236
+
237
+ - `mysql_query` 只允許單一 statement 的 `SELECT`、`SHOW`、`DESCRIBE`、`EXPLAIN` queries。
238
+ - `explain_query` 只接受單一 `SELECT` statement,並對它執行 `EXPLAIN`。
239
+ - `mysql_execute` 在 write mode 啟用時,只允許單一 statement 的 `INSERT`、`UPDATE`、`DELETE`、`REPLACE`。
240
+ - `mysql_batch_execute` 使用與 `mysql_execute` 相同的 SQL policy,並用多組參數重複執行。
241
+ - `mysql_import_csv` 使用 table policy,並走與 `mysql_batch_execute` 相同的 batch execution path。
242
+ - `mysql_export_csv` 會在匯出 table data 前套用 table policy。
243
+ - multi-statement SQL 會被拒絕。
244
+ - read-query tools 會拒絕 `SELECT ... INTO` 與 locking reads。
245
+ - `MYSQL_MCP_DENY_TABLES` 優先於 `MYSQL_MCP_ALLOW_TABLES`。
246
+ - 如果設定 `MYSQL_MCP_ALLOW_TABLES`,每個偵測到的 table 都必須包含在 allowlist 中。
247
+
248
+ Table policy matching 是基於 SQL parsing 的 best-effort guardrail。你可以使用 `table` 或 `database.table` entries。MySQL grants 仍然是最終安全邊界。
249
+
250
+ ### Policy 優先順序
251
+
252
+ Policy checks 會依照以下順序執行:
253
+
254
+ 1. 先執行內建 SQL safety checks,例如 single-statement enforcement,以及每個 tool 允許的 statement types。
255
+ 2. 接著檢查 `MYSQL_MCP_DENY_TABLES`。如果偵測到的 table 命中 denylist,command 會立即被拒絕。
256
+ 3. 再檢查 `MYSQL_MCP_ALLOW_TABLES`。如果有設定 allowlist,每個偵測到的 table 都必須包含在 allowlist 中。
257
+ 4. `MYSQL_POLICY_HOOK` 只會在內建 SQL policy 與 table allow/deny policy 都通過後才執行。
258
+
259
+ 如果同時設定 `MYSQL_MCP_ALLOW_TABLES` 與 `MYSQL_MCP_DENY_TABLES`,denylist 優先。例如:
260
+
261
+ ```env
262
+ MYSQL_MCP_ALLOW_TABLES=users,orders,payments
263
+ MYSQL_MCP_DENY_TABLES=payments
264
+ ```
265
+
266
+ 在這個設定下,`users` 與 `orders` 允許,`payments` 會被拒絕,其他 tables 也會被拒絕,因為它們不在 allowlist 中。
267
+
268
+ `MYSQL_POLICY_HOOK` 不能覆蓋內建 policy 的拒絕結果。它只能在 command 已經通過本機 policy 後,決定接下來是 `accept`、`reject`,或 `approval_required`。
269
+
270
+ ## Policy Hook 與 Approval
271
+
272
+ 設定 `MYSQL_POLICY_HOOK` 後,server 會在內建 policy 通過後、command 真正執行前,將每個 tool action POST 到 hook。
273
+
274
+ Hook request 範例:
275
+
276
+ ```json
277
+ {
278
+ "functionName": "mysql_execute",
279
+ "sql": "UPDATE users SET email = ? WHERE id = ?",
280
+ "statementType": "update",
281
+ "tableNames": ["users"],
282
+ "paramsPreview": ["new@example.com", 123],
283
+ "metadata": {
284
+ "database": "app_db",
285
+ "mode": "readwrite",
286
+ "timestamp": "2026-05-20T12:00:00.000Z"
287
+ }
288
+ }
289
+ ```
290
+
291
+ hook 必須回傳以下其中一種:
292
+
293
+ ```json
294
+ { "status": "accept" }
295
+ ```
296
+
297
+ ```json
298
+ { "status": "reject", "message": "Writes are blocked outside maintenance windows." }
299
+ ```
300
+
301
+ ```json
302
+ {
303
+ "status": "approval_required",
304
+ "message": "User approval is required before updating users."
305
+ }
306
+ ```
307
+
308
+ 如果回傳 `approval_required`,server 不會執行 command。它會將原始 pending command 存在記憶體中,並回傳包含 server-generated `approvalId` 的 `approval_required` response。hook 不提供 approval id。MCP host 取得使用者同意後,可以使用 `mysql_run_approved_command` 並傳入:
309
+
310
+ ```json
311
+ {
312
+ "approvalId": "apv_..."
313
+ }
314
+ ```
315
+
316
+ Pending approvals 是 one-time use,並會在 `MYSQL_APPROVAL_TTL_SECONDS` 後過期。設定 `MYSQL_POLICY_HOOK` 時,也會提供 `mysql_list_pending_approvals` 與 `mysql_cancel_approval`。
317
+
318
+ 這是一個 approval-friendly protocol。server 無法驗證是否真的有人類批准;MCP host 或外部平台負責將 approval request 呈現給使用者。
319
+
320
+ ## 安全注意事項
321
+
322
+ - 使用 dedicated MySQL user,並只給 assistant 所需的最小權限。
323
+ - 如果只需要 inspection/reporting,建議使用 read-only database credentials。
324
+ - 使用 `MYSQL_READ_ONLY=true` 或 `MYSQL_MCP_MODE=readonly`,可以避免 write execution tools 暴露給 MCP clients。
325
+ - `MYSQL_MCP_ALLOW_TABLES` 與 `MYSQL_MCP_DENY_TABLES` 是 MCP 層 guardrails,不能取代 MySQL grants。
326
+ - 需要外部 policy 或 approval workflow 時,可使用 `MYSQL_POLICY_HOOK`。
327
+ - 請小心使用 `mysql_execute`,它可以修改資料。
328
+ - 請小心使用 `mysql_import_csv`,它可以插入大量資料。
329
+ - batch execution 與 CSV import logs 會包含參數值與逐筆結果。請將 `MYSQL_LOG_PATH` 下的檔案視為敏感資料。
330
+ - CSV export 會將 table data 寫到本機 filesystem。請將匯出檔視為敏感資料。
331
+ - MySQL client configuration 已停用 multi-statement SQL。
332
+ - 不要將 `.env` 或真實 database credentials commit 到 GitHub。
333
+ - 對 production data 執行前,請審查 AI 產生的 SQL。
334
+
335
+ ## 開發
336
+
337
+ ```bash
338
+ npm run dev
339
+ ```
340
+
341
+ 這會以 watch mode 執行 TypeScript。
342
+
343
+ 建立 production build:
344
+
345
+ ```bash
346
+ npm run build
347
+ ```
348
+
349
+ 執行 integration test suite 前,請在 `.env` 設定 test database:
350
+
351
+ ```env
352
+ TEST_HOST=localhost
353
+ TEST_PORT=3306
354
+ TEST_USERNAME=test_user
355
+ TEST_PASSWORD=test_password
356
+ TEST_DB=test_database
357
+ ```
358
+
359
+ 然後執行:
360
+
361
+ ```bash
362
+ npm run test
363
+ ```
364
+
365
+ 測試會在 `TEST_DB` 建立並刪除暫時 tables、view、trigger。如果缺少 `TEST_*` 變數,integration test 會被 skip。
366
+
367
+ ## 專案結構
368
+
369
+ ```text
370
+ src/
371
+ config.ts 由環境變數驅動的 MCP policy configuration
372
+ csv.ts CSV parsing 與 writing helpers
373
+ csvTools.ts CSV import/export tool implementations
374
+ db.ts MySQL pool 與 query helpers
375
+ index.ts MCP server 與 tool registration
376
+ logs.ts Batch execution log helpers
377
+ policyHook.ts External policy hook client 與 approval response helpers
378
+ sqlPolicy.ts SQL parsing 與 policy enforcement
379
+ toolHandlers.ts Shared tool handler implementations
380
+ approvalStore.ts In-memory pending approval store
381
+ ```
382
+
383
+ ## 授權
384
+
385
+ 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
+ }