easy-mysql-mcp 1.0.4 → 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 +195 -3
- package/README.zh-TW.md +385 -0
- package/build/approvalStore.js +58 -0
- package/build/config.js +46 -0
- package/build/csv.js +109 -0
- package/build/csvTools.js +83 -0
- package/build/db.js +111 -0
- package/build/index.js +97 -31
- package/build/logs.js +28 -0
- package/build/policyHook.js +66 -0
- package/build/sqlPolicy.js +186 -0
- package/build/toolHandlers.js +189 -0
- package/package.json +5 -3
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
|
-
|
|
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
|
-
|
|
385
|
+
MIT. See [LICENSE.md](LICENSE.md).
|
package/README.zh-TW.md
ADDED
|
@@ -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
|
+
}
|