easy-mysql-mcp 1.1.1 → 1.2.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 +29 -2
- package/README.zh-TW.md +29 -2
- package/build/config.js +6 -0
- package/build/index.js +25 -3
- package/build/sqlPolicy.js +99 -1
- package/build/toolHandlers.js +12 -1
- package/package.json +7 -5
package/MANUAL.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# easy-mysql-mcp Manual
|
|
2
|
+
|
|
3
|
+
`easy-mysql-mcp` 是一個用來操作 MySQL 的 MCP server,提供查詢、寫入、批次執行、CSV 匯入,以及 schema / 權限檢查工具。
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
適合用在:
|
|
8
|
+
- 讀取資料
|
|
9
|
+
- 新增、更新、刪除資料
|
|
10
|
+
- 批次執行 SQL
|
|
11
|
+
- CSV 匯入與匯出
|
|
12
|
+
- 需要查看 schema、index、trigger、權限與 query plan 的場景
|
|
13
|
+
|
|
14
|
+
不適合用在:
|
|
15
|
+
- 多語句 SQL
|
|
16
|
+
- 未驗證字串直接拼接 SQL
|
|
17
|
+
- 繞過 denylist / allowlist / policy hook
|
|
18
|
+
|
|
19
|
+
## When to Consult This Manual
|
|
20
|
+
|
|
21
|
+
如果你遇到以下情況,先看這份手冊:
|
|
22
|
+
- 不確定 `mysql_query`、`mysql_execute`、`mysql_batch_execute` 要怎麼用
|
|
23
|
+
- SQL 被拒絕或回傳錯誤
|
|
24
|
+
- 不確定 `?` 參數要怎麼綁定
|
|
25
|
+
- 不確定哪些 SQL 寫法是安全且允許的
|
|
26
|
+
- 不確定應該先查 schema 還是直接執行
|
|
27
|
+
|
|
28
|
+
## Modes
|
|
29
|
+
|
|
30
|
+
- `readonly`: 只提供讀取相關工具
|
|
31
|
+
- `readwrite`: 預設模式,允許一般讀寫,但不包含 DDL
|
|
32
|
+
- `advanced`: 允許 schema / DDL 類工具
|
|
33
|
+
|
|
34
|
+
## Tools
|
|
35
|
+
|
|
36
|
+
- `mysql_query`: 讀取資料
|
|
37
|
+
- `mysql_execute`: 執行單一寫入或變更 SQL
|
|
38
|
+
- `mysql_batch_execute`: 以多組參數批次執行同一段 SQL
|
|
39
|
+
- `mysql_import_csv`: 匯入 UTF-8 CSV
|
|
40
|
+
- `mysql_export_csv`: 匯出表格為 UTF-8 CSV
|
|
41
|
+
- `mysql_schema_execute`: 執行 schema / DDL 變更,僅 advanced 模式可用
|
|
42
|
+
- `mysql_list_pending_approvals`: 列出待審核命令
|
|
43
|
+
- `mysql_run_approved_command`: 執行已核准命令
|
|
44
|
+
- `mysql_cancel_approval`: 取消審核
|
|
45
|
+
- `explain_query`: 檢查 query plan
|
|
46
|
+
- `list_tables`, `list_views`, `describe_table`, `describe_index`, `list_triggers`
|
|
47
|
+
- `get_current_privileges`
|
|
48
|
+
|
|
49
|
+
## Execute Usage
|
|
50
|
+
|
|
51
|
+
`mysql_execute` 只應使用單一 SQL 指令。適合:
|
|
52
|
+
- `INSERT`
|
|
53
|
+
- `UPDATE`
|
|
54
|
+
- `DELETE`
|
|
55
|
+
|
|
56
|
+
`mysql_batch_execute` 適合:
|
|
57
|
+
- 同一個 SQL 搭配多組參數重複執行
|
|
58
|
+
- 大量資料寫入
|
|
59
|
+
|
|
60
|
+
不要這樣用:
|
|
61
|
+
- 多語句連寫
|
|
62
|
+
- 把使用者輸入直接拼到 SQL 字串裡
|
|
63
|
+
|
|
64
|
+
參數寫法以 `mysql2` 的 binding 方式為準:
|
|
65
|
+
- 位置型參數使用 `?`
|
|
66
|
+
- 批次執行時,`paramsList` 裡每一組參數都會依序對應 SQL 中的 `?`
|
|
67
|
+
- 建議不要手動拼接字串值,改用參數綁定
|
|
68
|
+
|
|
69
|
+
## SQL Algebra / Composition Rules
|
|
70
|
+
|
|
71
|
+
MySQL 查詢可視為一個由上而下組裝的代數式:
|
|
72
|
+
|
|
73
|
+
1. 先決定資料來源:`FROM`
|
|
74
|
+
2. 再加條件:`WHERE`
|
|
75
|
+
3. 需要關聯時使用 `JOIN ... ON`
|
|
76
|
+
4. 有聚合時使用 `GROUP BY`
|
|
77
|
+
5. 聚合後條件放在 `HAVING`
|
|
78
|
+
6. 排序使用 `ORDER BY`
|
|
79
|
+
7. 最後限制筆數:`LIMIT` / `OFFSET`
|
|
80
|
+
|
|
81
|
+
常見正確組合:
|
|
82
|
+
- 單表查詢:`SELECT ... FROM ... WHERE ...`
|
|
83
|
+
- 聚合查詢:`SELECT ... COUNT(*) ... GROUP BY ... HAVING ...`
|
|
84
|
+
- 多表查詢:`SELECT ... FROM a JOIN b ON ... WHERE ...`
|
|
85
|
+
|
|
86
|
+
值與欄位的規則:
|
|
87
|
+
- 欄位名稱必要時用反引號包住
|
|
88
|
+
- 字串值使用單引號
|
|
89
|
+
- 數值不要加引號
|
|
90
|
+
- 日期與時間要用正確的 SQL 字面值
|
|
91
|
+
- 不要把未驗證內容直接串進條件式
|
|
92
|
+
|
|
93
|
+
## Safety Rules
|
|
94
|
+
|
|
95
|
+
- 不支援 multi-statement
|
|
96
|
+
- 不支援 `SELECT ... INTO`
|
|
97
|
+
- 不支援 locking reads
|
|
98
|
+
- 不支援 `CREATE TABLE ... AS SELECT`
|
|
99
|
+
- 寫入前先確認 schema
|
|
100
|
+
- deny tables 的優先級高於 allow tables
|
|
101
|
+
|
|
102
|
+
## Examples
|
|
103
|
+
|
|
104
|
+
- 查詢單一使用者:
|
|
105
|
+
- `SELECT id, name FROM users WHERE id = ?`
|
|
106
|
+
- 聚合統計:
|
|
107
|
+
- `SELECT status, COUNT(*) FROM orders GROUP BY status`
|
|
108
|
+
- join 查詢:
|
|
109
|
+
- `SELECT o.id, u.name FROM orders o JOIN users u ON o.user_id = u.id`
|
|
110
|
+
- 更新資料:
|
|
111
|
+
- `UPDATE users SET name = ? WHERE id = ?`
|
|
112
|
+
- 分頁查詢:
|
|
113
|
+
- `SELECT * FROM posts ORDER BY created_at DESC LIMIT 20 OFFSET 0`
|
|
114
|
+
|
|
115
|
+
## Troubleshooting
|
|
116
|
+
|
|
117
|
+
- 查不到欄位時,先用 `describe_table`
|
|
118
|
+
- SQL 被拒絕時,先檢查是否是多語句或受限語法
|
|
119
|
+
- 結果不如預期時,先確認 `WHERE` 與 `JOIN ON`
|
|
120
|
+
- 效能不好時,先看 `explain_query`
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ This project uses Node.js, TypeScript, the official MCP SDK, and `mysql2/promise
|
|
|
16
16
|
|
|
17
17
|
## Requirements
|
|
18
18
|
|
|
19
|
-
- Node.js
|
|
19
|
+
- Node.js 20 or newer
|
|
20
20
|
- npm
|
|
21
21
|
- A reachable MySQL-compatible database
|
|
22
22
|
|
|
@@ -55,7 +55,7 @@ Configure the server with environment variables. You can provide them through yo
|
|
|
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
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 |
|
|
58
|
+
| `MYSQL_MCP_MODE` | No | `readwrite` | MCP policy mode. Use `readonly` to disable write execution or `advanced` to enable schema modification tools |
|
|
59
59
|
| `MYSQL_MCP_ALLOW_TABLES` | No | - | Comma-separated table allowlist, such as `users,orders` |
|
|
60
60
|
| `MYSQL_MCP_DENY_TABLES` | No | - | Comma-separated table denylist, such as `payments,secrets` |
|
|
61
61
|
| `MYSQL_BATCH_MAX_SIZE` | No | `100` | Maximum number of parameter sets per internal batch for `mysql_batch_execute` |
|
|
@@ -91,6 +91,8 @@ npm start
|
|
|
91
91
|
|
|
92
92
|
The server communicates over stdio and is normally launched by an MCP client rather than run manually.
|
|
93
93
|
|
|
94
|
+
If you are unsure how a tool should be used, or an operation fails, call `mysql_manual` first. It returns the built-in manual with safe usage rules, parameter binding guidance, and SQL composition notes.
|
|
95
|
+
|
|
94
96
|
## Claude Desktop Example
|
|
95
97
|
|
|
96
98
|
```json
|
|
@@ -155,8 +157,10 @@ MYSQL_DATABASE = "YOUR DB NAME"
|
|
|
155
157
|
|
|
156
158
|
| Tool | Description |
|
|
157
159
|
| --- | --- |
|
|
160
|
+
| `mysql_manual` | Return the built-in manual. Use this first when you are unsure how to use MySQL tools or need help diagnosing an operation error |
|
|
158
161
|
| `mysql_query` | Execute a SQL query intended for data retrieval, such as `SELECT` |
|
|
159
162
|
| `mysql_execute` | Execute a data modification statement, such as `INSERT`, `UPDATE`, or `DELETE` |
|
|
163
|
+
| `mysql_schema_execute` | Execute schema modification statements in advanced mode, such as `CREATE TABLE`, `ALTER TABLE`, `CREATE VIEW`, `CREATE TRIGGER`, and `CREATE INDEX` |
|
|
160
164
|
| `mysql_batch_execute` | Execute one data modification statement repeatedly with multiple parameter sets |
|
|
161
165
|
| `mysql_import_csv` | Import a UTF-8 CSV file into a table using the header row as column names |
|
|
162
166
|
| `mysql_export_csv` | Export all rows from a table to a UTF-8 CSV file |
|
|
@@ -173,6 +177,8 @@ MYSQL_DATABASE = "YOUR DB NAME"
|
|
|
173
177
|
|
|
174
178
|
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
179
|
|
|
180
|
+
When `MYSQL_MCP_MODE=advanced`, `mysql_schema_execute` is registered. Existing write tools remain available because advanced mode includes read/write behavior.
|
|
181
|
+
|
|
176
182
|
### Batch Execute
|
|
177
183
|
|
|
178
184
|
`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.
|
|
@@ -237,10 +243,12 @@ The server applies a lightweight SQL policy before executing user-provided SQL:
|
|
|
237
243
|
- `mysql_query` allows only single-statement `SELECT`, `SHOW`, `DESCRIBE`, and `EXPLAIN` queries.
|
|
238
244
|
- `explain_query` accepts only a single `SELECT` statement and runs `EXPLAIN` for it.
|
|
239
245
|
- `mysql_execute` allows only single-statement `INSERT`, `UPDATE`, `DELETE`, and `REPLACE` statements when write mode is enabled.
|
|
246
|
+
- `mysql_schema_execute` is only registered when `MYSQL_MCP_MODE=advanced`, and allows single-statement schema changes for tables, views, triggers, and indexes.
|
|
240
247
|
- `mysql_batch_execute` uses the same SQL policy as `mysql_execute` and applies the statement repeatedly with parameter arrays.
|
|
241
248
|
- `mysql_import_csv` uses table policy and the same batch execution path as `mysql_batch_execute`.
|
|
242
249
|
- `mysql_export_csv` uses table policy before exporting table data.
|
|
243
250
|
- Multi-statement SQL is rejected.
|
|
251
|
+
- `CREATE TABLE ... AS SELECT` is rejected because it copies data while creating a table.
|
|
244
252
|
- `SELECT ... INTO` and locking reads are rejected for read-query tools.
|
|
245
253
|
- `MYSQL_MCP_DENY_TABLES` rejects matching tables before `MYSQL_MCP_ALLOW_TABLES` is evaluated.
|
|
246
254
|
- If `MYSQL_MCP_ALLOW_TABLES` is set, every detected table must be included in the allowlist.
|
|
@@ -267,6 +275,23 @@ In this configuration, `users` and `orders` are allowed, `payments` is rejected,
|
|
|
267
275
|
|
|
268
276
|
`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
277
|
|
|
278
|
+
### Advanced Schema Mode
|
|
279
|
+
|
|
280
|
+
Set `MYSQL_MCP_MODE=advanced` to enable `mysql_schema_execute` for schema changes. This is an explicit opt-in mode for database structure operations.
|
|
281
|
+
|
|
282
|
+
Allowed schema statements:
|
|
283
|
+
|
|
284
|
+
| Object | Statements |
|
|
285
|
+
| --- | --- |
|
|
286
|
+
| Tables | `CREATE TABLE`, `ALTER TABLE`, `DROP TABLE`, `RENAME TABLE` |
|
|
287
|
+
| Views | `CREATE VIEW`, `CREATE OR REPLACE VIEW`, `DROP VIEW` |
|
|
288
|
+
| Triggers | `CREATE TRIGGER`, `DROP TRIGGER` |
|
|
289
|
+
| Indexes | `CREATE INDEX`, `DROP INDEX`, plus index changes through `ALTER TABLE` |
|
|
290
|
+
|
|
291
|
+
The same table allow/deny policy applies to detected schema objects and referenced tables. `MYSQL_MCP_DENY_TABLES` still takes precedence over `MYSQL_MCP_ALLOW_TABLES`.
|
|
292
|
+
|
|
293
|
+
Advanced mode still rejects multi-statement SQL, unsupported DDL object types, and `CREATE TABLE ... AS SELECT`.
|
|
294
|
+
|
|
270
295
|
## Policy Hook and Approvals
|
|
271
296
|
|
|
272
297
|
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.
|
|
@@ -322,9 +347,11 @@ This is an approval-friendly protocol. The server cannot verify that a human app
|
|
|
322
347
|
- Use a dedicated MySQL user with the minimum permissions your assistant needs.
|
|
323
348
|
- Prefer read-only database credentials if you only need inspection and reporting.
|
|
324
349
|
- Use `MYSQL_READ_ONLY=true` or `MYSQL_MCP_MODE=readonly` to hide write execution from MCP clients.
|
|
350
|
+
- Use `MYSQL_MCP_MODE=advanced` only when the assistant should be able to modify schema objects such as tables, views, triggers, and indexes.
|
|
325
351
|
- Use `MYSQL_MCP_ALLOW_TABLES` and `MYSQL_MCP_DENY_TABLES` as MCP-level guardrails, not as a replacement for MySQL grants.
|
|
326
352
|
- Use `MYSQL_POLICY_HOOK` when you need an external policy or approval workflow.
|
|
327
353
|
- Be careful with `mysql_execute`, because it can modify data.
|
|
354
|
+
- Be careful with `mysql_schema_execute`, because it can create, alter, or drop database structure.
|
|
328
355
|
- Be careful with `mysql_import_csv`, because it can insert many rows.
|
|
329
356
|
- Batch execution and CSV import logs include parameter values and per-row results. Treat files under `MYSQL_LOG_PATH` as sensitive.
|
|
330
357
|
- CSV export writes table data to the local filesystem. Treat exported files as sensitive.
|
package/README.zh-TW.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
## 需求
|
|
18
18
|
|
|
19
|
-
- Node.js
|
|
19
|
+
- Node.js 20 或更新版本
|
|
20
20
|
- npm
|
|
21
21
|
- 可連線的 MySQL-compatible database
|
|
22
22
|
|
|
@@ -55,7 +55,7 @@ npm run build
|
|
|
55
55
|
| `MYSQL_ENABLE_KEEP_ALIVE` | 否 | `true` | 是否啟用 TCP keep-alive |
|
|
56
56
|
| `MYSQL_KEEP_ALIVE_INITIAL_DELAY` | 否 | `0` | TCP keep-alive 初始延遲,單位毫秒 |
|
|
57
57
|
| `MYSQL_READ_ONLY` | 否 | `false` | 設為 `true` 時啟用唯讀模式,且不註冊 `mysql_execute` |
|
|
58
|
-
| `MYSQL_MCP_MODE` | 否 | `readwrite` | MCP policy mode。使用 `readonly`
|
|
58
|
+
| `MYSQL_MCP_MODE` | 否 | `readwrite` | MCP policy mode。使用 `readonly` 可停用寫入執行,或使用 `advanced` 啟用結構修改工具 |
|
|
59
59
|
| `MYSQL_MCP_ALLOW_TABLES` | 否 | - | table allowlist,逗號分隔,例如 `users,orders` |
|
|
60
60
|
| `MYSQL_MCP_DENY_TABLES` | 否 | - | table denylist,逗號分隔,例如 `payments,secrets` |
|
|
61
61
|
| `MYSQL_BATCH_MAX_SIZE` | 否 | `100` | `mysql_batch_execute` 每個內部分批最多處理的參數組數 |
|
|
@@ -153,10 +153,14 @@ MYSQL_DATABASE = "YOUR DB NAME"
|
|
|
153
153
|
|
|
154
154
|
## 可用工具
|
|
155
155
|
|
|
156
|
+
如果你不確定某個工具該怎麼用,或操作失敗,請先呼叫 `mysql_manual`。它會回傳內建手冊,包含安全使用規則、參數綁定指引,以及 SQL 組合方式。
|
|
157
|
+
|
|
156
158
|
| 工具 | 說明 |
|
|
157
159
|
| --- | --- |
|
|
160
|
+
| `mysql_manual` | 回傳內建手冊。當你不確定如何使用 MySQL 工具,或需要排查操作錯誤時,請先查這個工具 |
|
|
158
161
|
| `mysql_query` | 執行用於資料讀取的 SQL query,例如 `SELECT` |
|
|
159
162
|
| `mysql_execute` | 執行資料修改 statement,例如 `INSERT`、`UPDATE`、`DELETE` |
|
|
163
|
+
| `mysql_schema_execute` | 在 advanced mode 執行 schema 修改 statement,例如 `CREATE TABLE`、`ALTER TABLE`、`CREATE VIEW`、`CREATE TRIGGER`、`CREATE INDEX` |
|
|
160
164
|
| `mysql_batch_execute` | 使用多組參數重複執行同一個資料修改 statement |
|
|
161
165
|
| `mysql_import_csv` | 使用 CSV header row 作為欄位名稱,將 UTF-8 CSV 匯入 table |
|
|
162
166
|
| `mysql_export_csv` | 將 table 的所有 rows 匯出為 UTF-8 CSV |
|
|
@@ -173,6 +177,8 @@ MYSQL_DATABASE = "YOUR DB NAME"
|
|
|
173
177
|
|
|
174
178
|
當 `MYSQL_READ_ONLY=true` 或 `MYSQL_MCP_MODE=readonly` 時,`mysql_execute`、`mysql_batch_execute`、`mysql_import_csv` 不會被註冊。
|
|
175
179
|
|
|
180
|
+
當 `MYSQL_MCP_MODE=advanced` 時,會註冊 `mysql_schema_execute`。advanced mode 包含 read/write 行為,因此既有寫入工具仍會提供。
|
|
181
|
+
|
|
176
182
|
### Batch Execute
|
|
177
183
|
|
|
178
184
|
`mysql_batch_execute` 會使用多組參數重複執行同一個 parameterized write statement。它適合 bulk insert 或 repeated update,而且不需要啟用 multi-statement SQL。
|
|
@@ -237,10 +243,12 @@ server 在執行使用者提供的 SQL 前,會套用輕量 SQL policy:
|
|
|
237
243
|
- `mysql_query` 只允許單一 statement 的 `SELECT`、`SHOW`、`DESCRIBE`、`EXPLAIN` queries。
|
|
238
244
|
- `explain_query` 只接受單一 `SELECT` statement,並對它執行 `EXPLAIN`。
|
|
239
245
|
- `mysql_execute` 在 write mode 啟用時,只允許單一 statement 的 `INSERT`、`UPDATE`、`DELETE`、`REPLACE`。
|
|
246
|
+
- `mysql_schema_execute` 只有在 `MYSQL_MCP_MODE=advanced` 時註冊,允許 tables、views、triggers、indexes 的單一 schema 修改 statement。
|
|
240
247
|
- `mysql_batch_execute` 使用與 `mysql_execute` 相同的 SQL policy,並用多組參數重複執行。
|
|
241
248
|
- `mysql_import_csv` 使用 table policy,並走與 `mysql_batch_execute` 相同的 batch execution path。
|
|
242
249
|
- `mysql_export_csv` 會在匯出 table data 前套用 table policy。
|
|
243
250
|
- multi-statement SQL 會被拒絕。
|
|
251
|
+
- `CREATE TABLE ... AS SELECT` 會被拒絕,因為它會在建立 table 時複製資料。
|
|
244
252
|
- read-query tools 會拒絕 `SELECT ... INTO` 與 locking reads。
|
|
245
253
|
- `MYSQL_MCP_DENY_TABLES` 優先於 `MYSQL_MCP_ALLOW_TABLES`。
|
|
246
254
|
- 如果設定 `MYSQL_MCP_ALLOW_TABLES`,每個偵測到的 table 都必須包含在 allowlist 中。
|
|
@@ -267,6 +275,23 @@ MYSQL_MCP_DENY_TABLES=payments
|
|
|
267
275
|
|
|
268
276
|
`MYSQL_POLICY_HOOK` 不能覆蓋內建 policy 的拒絕結果。它只能在 command 已經通過本機 policy 後,決定接下來是 `accept`、`reject`,或 `approval_required`。
|
|
269
277
|
|
|
278
|
+
### Advanced Schema Mode
|
|
279
|
+
|
|
280
|
+
設定 `MYSQL_MCP_MODE=advanced` 可啟用 `mysql_schema_execute`,用於 schema changes。這是明確 opt-in 的資料庫結構操作模式。
|
|
281
|
+
|
|
282
|
+
允許的 schema statements:
|
|
283
|
+
|
|
284
|
+
| 物件 | Statements |
|
|
285
|
+
| --- | --- |
|
|
286
|
+
| Tables | `CREATE TABLE`、`ALTER TABLE`、`DROP TABLE`、`RENAME TABLE` |
|
|
287
|
+
| Views | `CREATE VIEW`、`CREATE OR REPLACE VIEW`、`DROP VIEW` |
|
|
288
|
+
| Triggers | `CREATE TRIGGER`、`DROP TRIGGER` |
|
|
289
|
+
| Indexes | `CREATE INDEX`、`DROP INDEX`,以及透過 `ALTER TABLE` 修改 index |
|
|
290
|
+
|
|
291
|
+
相同的 table allow/deny policy 會套用到偵測到的 schema objects 與 referenced tables。`MYSQL_MCP_DENY_TABLES` 仍然優先於 `MYSQL_MCP_ALLOW_TABLES`。
|
|
292
|
+
|
|
293
|
+
Advanced mode 仍會拒絕 multi-statement SQL、不支援的 DDL object types,以及 `CREATE TABLE ... AS SELECT`。
|
|
294
|
+
|
|
270
295
|
## Policy Hook 與 Approval
|
|
271
296
|
|
|
272
297
|
設定 `MYSQL_POLICY_HOOK` 後,server 會在內建 policy 通過後、command 真正執行前,將每個 tool action POST 到 hook。
|
|
@@ -322,9 +347,11 @@ Pending approvals 是 one-time use,並會在 `MYSQL_APPROVAL_TTL_SECONDS` 後
|
|
|
322
347
|
- 使用 dedicated MySQL user,並只給 assistant 所需的最小權限。
|
|
323
348
|
- 如果只需要 inspection/reporting,建議使用 read-only database credentials。
|
|
324
349
|
- 使用 `MYSQL_READ_ONLY=true` 或 `MYSQL_MCP_MODE=readonly`,可以避免 write execution tools 暴露給 MCP clients。
|
|
350
|
+
- 只有在 assistant 需要修改 tables、views、triggers、indexes 等 schema objects 時,才使用 `MYSQL_MCP_MODE=advanced`。
|
|
325
351
|
- `MYSQL_MCP_ALLOW_TABLES` 與 `MYSQL_MCP_DENY_TABLES` 是 MCP 層 guardrails,不能取代 MySQL grants。
|
|
326
352
|
- 需要外部 policy 或 approval workflow 時,可使用 `MYSQL_POLICY_HOOK`。
|
|
327
353
|
- 請小心使用 `mysql_execute`,它可以修改資料。
|
|
354
|
+
- 請小心使用 `mysql_schema_execute`,它可以建立、修改或刪除資料庫結構。
|
|
328
355
|
- 請小心使用 `mysql_import_csv`,它可以插入大量資料。
|
|
329
356
|
- batch execution 與 CSV import logs 會包含參數值與逐筆結果。請將 `MYSQL_LOG_PATH` 下的檔案視為敏感資料。
|
|
330
357
|
- CSV export 會將 table data 寫到本機 filesystem。請將匯出檔視為敏感資料。
|
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, mysqlBatchExecute, mysqlExecute, mysqlExportCsv, mysqlImportCsv, mysqlQuery, } from './toolHandlers.js';
|
|
10
|
+
import { describeIndex, describeTable, explainQuery, getCurrentPrivileges, listTables, listTriggers, listViews, mysqlBatchExecute, mysqlExecute, mysqlExportCsv, mysqlImportCsv, mysqlSchemaExecute, mysqlQuery, } from './toolHandlers.js';
|
|
9
11
|
const { MYSQL_HOST, MYSQL_PORT, MYSQL_DATABASE, } = process.env;
|
|
10
12
|
// Initialize MCP Server/mcp
|
|
11
13
|
const server = new McpServer({
|
|
12
14
|
name: 'easy-mysql-mcp',
|
|
13
|
-
version: '1.1.
|
|
15
|
+
version: '1.1.2',
|
|
14
16
|
description: `MySQL Database: ${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_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('mysql_manual', {
|
|
22
|
+
description: 'Return the MySQL MCP manual. Use this first when you are unsure how to use mysql_query, mysql_execute, mysql_batch_execute, or when an operation fails and you need the safe usage rules, parameter rules, or SQL composition guidance.',
|
|
23
|
+
inputSchema: z.object({}),
|
|
24
|
+
}, async () => ({
|
|
25
|
+
content: [{ type: 'text', text: manualText }],
|
|
26
|
+
}));
|
|
18
27
|
server.registerTool('mysql_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('mysql_schema_execute', {
|
|
80
|
+
description: 'Execute an advanced schema modification SQL statement, such as CREATE TABLE, ALTER TABLE, DROP VIEW, CREATE TRIGGER, or CREATE INDEX.',
|
|
81
|
+
inputSchema: z.object({
|
|
82
|
+
sql: z.string().describe('The single schema modification SQL statement to execute.'),
|
|
83
|
+
}),
|
|
84
|
+
}, async ({ sql }) => {
|
|
85
|
+
const result = await mysqlSchemaExecute(sql);
|
|
86
|
+
return {
|
|
87
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
}
|
|
69
91
|
if (isPolicyHookEnabled()) {
|
|
70
92
|
server.registerTool('mysql_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,14 @@
|
|
|
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/mysql.js');
|
|
5
5
|
const parser = new Parser();
|
|
6
6
|
const parserOptions = { database: 'MySQL' };
|
|
7
7
|
const readQueryTypes = new Set(['select', 'show', 'desc', 'describe', 'explain']);
|
|
8
8
|
const executeTypes = new Set(['insert', 'update', 'delete', 'replace']);
|
|
9
|
+
const schemaExecuteTypes = new Set(['create', 'alter', 'drop', 'rename']);
|
|
10
|
+
const createSchemaKeywords = new Set(['table', 'view', 'trigger', 'index']);
|
|
11
|
+
const dropSchemaKeywords = new Set(['table', 'view', 'trigger', 'index']);
|
|
9
12
|
export class SqlPolicyError extends Error {
|
|
10
13
|
constructor(message) {
|
|
11
14
|
super(message);
|
|
@@ -45,6 +48,18 @@ export function assertExecuteAllowed(sql) {
|
|
|
45
48
|
}
|
|
46
49
|
assertTablePolicy(parsed.tables);
|
|
47
50
|
}
|
|
51
|
+
export function assertSchemaExecuteAllowed(sql) {
|
|
52
|
+
if (!isAdvancedMode()) {
|
|
53
|
+
throw new SqlPolicyError('SQL rejected: schema execution requires MYSQL_MCP_MODE=advanced.');
|
|
54
|
+
}
|
|
55
|
+
const parsed = parseSingleStatement(sql);
|
|
56
|
+
if (!schemaExecuteTypes.has(parsed.type)) {
|
|
57
|
+
throw new SqlPolicyError(`SQL rejected: mysql_schema_execute only allows CREATE, ALTER, DROP, and RENAME statements. Received ${parsed.type.toUpperCase()}.`);
|
|
58
|
+
}
|
|
59
|
+
assertSchemaAstAllowed(parsed.ast);
|
|
60
|
+
assertNoUnsafeReadOptions(parsed.ast);
|
|
61
|
+
assertTablePolicy(parsed.tables);
|
|
62
|
+
}
|
|
48
63
|
export function assertTablesAllowed(tables) {
|
|
49
64
|
assertTablePolicy(tables.map((table) => normalizeTableName(table)));
|
|
50
65
|
}
|
|
@@ -111,6 +126,39 @@ function assertNoUnsafeReadOptions(ast) {
|
|
|
111
126
|
if (ast?.type === 'explain') {
|
|
112
127
|
assertNoUnsafeReadOptions(ast.expr);
|
|
113
128
|
}
|
|
129
|
+
if (ast?.type === 'create' && ast.keyword === 'view') {
|
|
130
|
+
assertNoUnsafeReadOptions(ast.select);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function assertSchemaAstAllowed(ast) {
|
|
134
|
+
switch (ast?.type) {
|
|
135
|
+
case 'create': {
|
|
136
|
+
const keyword = normalizeKeyword(ast.keyword);
|
|
137
|
+
if (!createSchemaKeywords.has(keyword)) {
|
|
138
|
+
throw new SqlPolicyError(`SQL rejected: mysql_schema_execute does not allow CREATE ${keyword.toUpperCase()}.`);
|
|
139
|
+
}
|
|
140
|
+
if (keyword === 'table' && ast.query_expr) {
|
|
141
|
+
throw new SqlPolicyError('SQL rejected: CREATE TABLE ... AS SELECT is not allowed by mysql_schema_execute.');
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
case 'alter':
|
|
146
|
+
if (!ast.table) {
|
|
147
|
+
throw new SqlPolicyError('SQL rejected: mysql_schema_execute only allows ALTER TABLE statements.');
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
case 'drop': {
|
|
151
|
+
const keyword = normalizeKeyword(ast.keyword);
|
|
152
|
+
if (!dropSchemaKeywords.has(keyword)) {
|
|
153
|
+
throw new SqlPolicyError(`SQL rejected: mysql_schema_execute does not allow DROP ${keyword.toUpperCase()}.`);
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
case 'rename':
|
|
158
|
+
return;
|
|
159
|
+
default:
|
|
160
|
+
throw new SqlPolicyError(`SQL rejected: mysql_schema_execute does not allow ${String(ast?.type ?? 'unknown').toUpperCase()} statements.`);
|
|
161
|
+
}
|
|
114
162
|
}
|
|
115
163
|
function assertTablePolicy(tables) {
|
|
116
164
|
for (const table of tables) {
|
|
@@ -175,6 +223,12 @@ function collectTablesFromAst(ast) {
|
|
|
175
223
|
return [];
|
|
176
224
|
}
|
|
177
225
|
switch (ast.type) {
|
|
226
|
+
case 'create':
|
|
227
|
+
return collectCreateTablesFromAst(ast);
|
|
228
|
+
case 'drop':
|
|
229
|
+
return collectDropTablesFromAst(ast);
|
|
230
|
+
case 'rename':
|
|
231
|
+
return collectRenameTablesFromAst(ast);
|
|
178
232
|
case 'desc':
|
|
179
233
|
case 'describe':
|
|
180
234
|
return typeof ast.table === 'string' ? [ast.table] : [];
|
|
@@ -184,3 +238,47 @@ function collectTablesFromAst(ast) {
|
|
|
184
238
|
return [];
|
|
185
239
|
}
|
|
186
240
|
}
|
|
241
|
+
function collectCreateTablesFromAst(ast) {
|
|
242
|
+
const tables = [
|
|
243
|
+
...collectTableLikeNames(ast.table),
|
|
244
|
+
...collectTableLikeNames(ast.view),
|
|
245
|
+
...collectTableLikeNames(ast.trigger),
|
|
246
|
+
...collectTableLikeNames(ast.like?.table),
|
|
247
|
+
];
|
|
248
|
+
if (ast.keyword === 'view') {
|
|
249
|
+
tables.push(...collectTablesFromAst(ast.select));
|
|
250
|
+
}
|
|
251
|
+
return tables;
|
|
252
|
+
}
|
|
253
|
+
function collectDropTablesFromAst(ast) {
|
|
254
|
+
if (ast.keyword === 'index') {
|
|
255
|
+
return collectTableLikeNames(ast.table);
|
|
256
|
+
}
|
|
257
|
+
return collectTableLikeNames(ast.name);
|
|
258
|
+
}
|
|
259
|
+
function collectRenameTablesFromAst(ast) {
|
|
260
|
+
return collectTableLikeNames(ast.table);
|
|
261
|
+
}
|
|
262
|
+
function collectTableLikeNames(value) {
|
|
263
|
+
if (!value) {
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
if (Array.isArray(value)) {
|
|
267
|
+
return value.flatMap((item) => collectTableLikeNames(item));
|
|
268
|
+
}
|
|
269
|
+
if (typeof value === 'string') {
|
|
270
|
+
return [value];
|
|
271
|
+
}
|
|
272
|
+
if (typeof value === 'object') {
|
|
273
|
+
const objectName = value.table ?? value.view ?? value.trigger;
|
|
274
|
+
if (typeof objectName === 'string') {
|
|
275
|
+
return value.db || value.schema
|
|
276
|
+
? [`${value.db ?? value.schema}.${objectName}`]
|
|
277
|
+
: [objectName];
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
function normalizeKeyword(keyword) {
|
|
283
|
+
return typeof keyword === 'string' ? keyword.toLowerCase() : 'unknown';
|
|
284
|
+
}
|
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 mysqlQuery(sql) {
|
|
8
8
|
assertReadQueryAllowed(sql);
|
|
9
9
|
const analysis = analyzeSql(sql);
|
|
@@ -26,6 +26,17 @@ export async function mysqlExecute(sql, params) {
|
|
|
26
26
|
summary: { sql, paramsPreview: params ?? null },
|
|
27
27
|
}, () => db.execute(sql, params));
|
|
28
28
|
}
|
|
29
|
+
export async function mysqlSchemaExecute(sql) {
|
|
30
|
+
assertSchemaExecuteAllowed(sql);
|
|
31
|
+
const analysis = analyzeSql(sql);
|
|
32
|
+
return runWithPolicy({
|
|
33
|
+
functionName: 'mysql_schema_execute',
|
|
34
|
+
sql,
|
|
35
|
+
statementType: analysis.statementType,
|
|
36
|
+
tableNames: analysis.tableNames,
|
|
37
|
+
summary: { sql },
|
|
38
|
+
}, () => db.query(sql));
|
|
39
|
+
}
|
|
29
40
|
export async function mysqlBatchExecute(sql, paramsList, transaction) {
|
|
30
41
|
assertExecuteAllowed(sql);
|
|
31
42
|
const analysis = analyzeSql(sql);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "easy-mysql-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "High performance MySQL MCP Server using mysql2",
|
|
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",
|
|
@@ -34,12 +36,12 @@
|
|
|
34
36
|
},
|
|
35
37
|
"dependencies": {
|
|
36
38
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37
|
-
"mysql2": "^3.22.
|
|
39
|
+
"mysql2": "^3.22.5",
|
|
38
40
|
"node-sql-parser": "^5.4.0",
|
|
39
41
|
"zod": "^4.4.3"
|
|
40
42
|
},
|
|
41
43
|
"devDependencies": {
|
|
42
|
-
"@types/node": "^25.
|
|
44
|
+
"@types/node": "^25.9.3",
|
|
43
45
|
"typescript": "^6.0.3"
|
|
44
46
|
},
|
|
45
47
|
"repository": {
|