easy-mysql-mcp 1.1.0 → 1.1.2
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 +25 -1
- package/README.zh-TW.md +25 -1
- package/build/config.js +6 -0
- package/build/index.js +16 -3
- package/build/sqlPolicy.js +99 -1
- package/build/toolHandlers.js +12 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -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` |
|
|
@@ -157,6 +157,7 @@ MYSQL_DATABASE = "YOUR DB NAME"
|
|
|
157
157
|
| --- | --- |
|
|
158
158
|
| `mysql_query` | Execute a SQL query intended for data retrieval, such as `SELECT` |
|
|
159
159
|
| `mysql_execute` | Execute a data modification statement, such as `INSERT`, `UPDATE`, or `DELETE` |
|
|
160
|
+
| `mysql_schema_execute` | Execute schema modification statements in advanced mode, such as `CREATE TABLE`, `ALTER TABLE`, `CREATE VIEW`, `CREATE TRIGGER`, and `CREATE INDEX` |
|
|
160
161
|
| `mysql_batch_execute` | Execute one data modification statement repeatedly with multiple parameter sets |
|
|
161
162
|
| `mysql_import_csv` | Import a UTF-8 CSV file into a table using the header row as column names |
|
|
162
163
|
| `mysql_export_csv` | Export all rows from a table to a UTF-8 CSV file |
|
|
@@ -173,6 +174,8 @@ MYSQL_DATABASE = "YOUR DB NAME"
|
|
|
173
174
|
|
|
174
175
|
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
|
|
|
177
|
+
When `MYSQL_MCP_MODE=advanced`, `mysql_schema_execute` is registered. Existing write tools remain available because advanced mode includes read/write behavior.
|
|
178
|
+
|
|
176
179
|
### Batch Execute
|
|
177
180
|
|
|
178
181
|
`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 +240,12 @@ The server applies a lightweight SQL policy before executing user-provided SQL:
|
|
|
237
240
|
- `mysql_query` allows only single-statement `SELECT`, `SHOW`, `DESCRIBE`, and `EXPLAIN` queries.
|
|
238
241
|
- `explain_query` accepts only a single `SELECT` statement and runs `EXPLAIN` for it.
|
|
239
242
|
- `mysql_execute` allows only single-statement `INSERT`, `UPDATE`, `DELETE`, and `REPLACE` statements when write mode is enabled.
|
|
243
|
+
- `mysql_schema_execute` is only registered when `MYSQL_MCP_MODE=advanced`, and allows single-statement schema changes for tables, views, triggers, and indexes.
|
|
240
244
|
- `mysql_batch_execute` uses the same SQL policy as `mysql_execute` and applies the statement repeatedly with parameter arrays.
|
|
241
245
|
- `mysql_import_csv` uses table policy and the same batch execution path as `mysql_batch_execute`.
|
|
242
246
|
- `mysql_export_csv` uses table policy before exporting table data.
|
|
243
247
|
- Multi-statement SQL is rejected.
|
|
248
|
+
- `CREATE TABLE ... AS SELECT` is rejected because it copies data while creating a table.
|
|
244
249
|
- `SELECT ... INTO` and locking reads are rejected for read-query tools.
|
|
245
250
|
- `MYSQL_MCP_DENY_TABLES` rejects matching tables before `MYSQL_MCP_ALLOW_TABLES` is evaluated.
|
|
246
251
|
- If `MYSQL_MCP_ALLOW_TABLES` is set, every detected table must be included in the allowlist.
|
|
@@ -267,6 +272,23 @@ In this configuration, `users` and `orders` are allowed, `payments` is rejected,
|
|
|
267
272
|
|
|
268
273
|
`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
274
|
|
|
275
|
+
### Advanced Schema Mode
|
|
276
|
+
|
|
277
|
+
Set `MYSQL_MCP_MODE=advanced` to enable `mysql_schema_execute` for schema changes. This is an explicit opt-in mode for database structure operations.
|
|
278
|
+
|
|
279
|
+
Allowed schema statements:
|
|
280
|
+
|
|
281
|
+
| Object | Statements |
|
|
282
|
+
| --- | --- |
|
|
283
|
+
| Tables | `CREATE TABLE`, `ALTER TABLE`, `DROP TABLE`, `RENAME TABLE` |
|
|
284
|
+
| Views | `CREATE VIEW`, `CREATE OR REPLACE VIEW`, `DROP VIEW` |
|
|
285
|
+
| Triggers | `CREATE TRIGGER`, `DROP TRIGGER` |
|
|
286
|
+
| Indexes | `CREATE INDEX`, `DROP INDEX`, plus index changes through `ALTER TABLE` |
|
|
287
|
+
|
|
288
|
+
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`.
|
|
289
|
+
|
|
290
|
+
Advanced mode still rejects multi-statement SQL, unsupported DDL object types, and `CREATE TABLE ... AS SELECT`.
|
|
291
|
+
|
|
270
292
|
## Policy Hook and Approvals
|
|
271
293
|
|
|
272
294
|
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 +344,11 @@ This is an approval-friendly protocol. The server cannot verify that a human app
|
|
|
322
344
|
- Use a dedicated MySQL user with the minimum permissions your assistant needs.
|
|
323
345
|
- Prefer read-only database credentials if you only need inspection and reporting.
|
|
324
346
|
- Use `MYSQL_READ_ONLY=true` or `MYSQL_MCP_MODE=readonly` to hide write execution from MCP clients.
|
|
347
|
+
- Use `MYSQL_MCP_MODE=advanced` only when the assistant should be able to modify schema objects such as tables, views, triggers, and indexes.
|
|
325
348
|
- Use `MYSQL_MCP_ALLOW_TABLES` and `MYSQL_MCP_DENY_TABLES` as MCP-level guardrails, not as a replacement for MySQL grants.
|
|
326
349
|
- Use `MYSQL_POLICY_HOOK` when you need an external policy or approval workflow.
|
|
327
350
|
- Be careful with `mysql_execute`, because it can modify data.
|
|
351
|
+
- Be careful with `mysql_schema_execute`, because it can create, alter, or drop database structure.
|
|
328
352
|
- Be careful with `mysql_import_csv`, because it can insert many rows.
|
|
329
353
|
- Batch execution and CSV import logs include parameter values and per-row results. Treat files under `MYSQL_LOG_PATH` as sensitive.
|
|
330
354
|
- CSV export writes table data to the local filesystem. Treat exported files as sensitive.
|
package/README.zh-TW.md
CHANGED
|
@@ -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` 每個內部分批最多處理的參數組數 |
|
|
@@ -157,6 +157,7 @@ MYSQL_DATABASE = "YOUR DB NAME"
|
|
|
157
157
|
| --- | --- |
|
|
158
158
|
| `mysql_query` | 執行用於資料讀取的 SQL query,例如 `SELECT` |
|
|
159
159
|
| `mysql_execute` | 執行資料修改 statement,例如 `INSERT`、`UPDATE`、`DELETE` |
|
|
160
|
+
| `mysql_schema_execute` | 在 advanced mode 執行 schema 修改 statement,例如 `CREATE TABLE`、`ALTER TABLE`、`CREATE VIEW`、`CREATE TRIGGER`、`CREATE INDEX` |
|
|
160
161
|
| `mysql_batch_execute` | 使用多組參數重複執行同一個資料修改 statement |
|
|
161
162
|
| `mysql_import_csv` | 使用 CSV header row 作為欄位名稱,將 UTF-8 CSV 匯入 table |
|
|
162
163
|
| `mysql_export_csv` | 將 table 的所有 rows 匯出為 UTF-8 CSV |
|
|
@@ -173,6 +174,8 @@ MYSQL_DATABASE = "YOUR DB NAME"
|
|
|
173
174
|
|
|
174
175
|
當 `MYSQL_READ_ONLY=true` 或 `MYSQL_MCP_MODE=readonly` 時,`mysql_execute`、`mysql_batch_execute`、`mysql_import_csv` 不會被註冊。
|
|
175
176
|
|
|
177
|
+
當 `MYSQL_MCP_MODE=advanced` 時,會註冊 `mysql_schema_execute`。advanced mode 包含 read/write 行為,因此既有寫入工具仍會提供。
|
|
178
|
+
|
|
176
179
|
### Batch Execute
|
|
177
180
|
|
|
178
181
|
`mysql_batch_execute` 會使用多組參數重複執行同一個 parameterized write statement。它適合 bulk insert 或 repeated update,而且不需要啟用 multi-statement SQL。
|
|
@@ -237,10 +240,12 @@ server 在執行使用者提供的 SQL 前,會套用輕量 SQL policy:
|
|
|
237
240
|
- `mysql_query` 只允許單一 statement 的 `SELECT`、`SHOW`、`DESCRIBE`、`EXPLAIN` queries。
|
|
238
241
|
- `explain_query` 只接受單一 `SELECT` statement,並對它執行 `EXPLAIN`。
|
|
239
242
|
- `mysql_execute` 在 write mode 啟用時,只允許單一 statement 的 `INSERT`、`UPDATE`、`DELETE`、`REPLACE`。
|
|
243
|
+
- `mysql_schema_execute` 只有在 `MYSQL_MCP_MODE=advanced` 時註冊,允許 tables、views、triggers、indexes 的單一 schema 修改 statement。
|
|
240
244
|
- `mysql_batch_execute` 使用與 `mysql_execute` 相同的 SQL policy,並用多組參數重複執行。
|
|
241
245
|
- `mysql_import_csv` 使用 table policy,並走與 `mysql_batch_execute` 相同的 batch execution path。
|
|
242
246
|
- `mysql_export_csv` 會在匯出 table data 前套用 table policy。
|
|
243
247
|
- multi-statement SQL 會被拒絕。
|
|
248
|
+
- `CREATE TABLE ... AS SELECT` 會被拒絕,因為它會在建立 table 時複製資料。
|
|
244
249
|
- read-query tools 會拒絕 `SELECT ... INTO` 與 locking reads。
|
|
245
250
|
- `MYSQL_MCP_DENY_TABLES` 優先於 `MYSQL_MCP_ALLOW_TABLES`。
|
|
246
251
|
- 如果設定 `MYSQL_MCP_ALLOW_TABLES`,每個偵測到的 table 都必須包含在 allowlist 中。
|
|
@@ -267,6 +272,23 @@ MYSQL_MCP_DENY_TABLES=payments
|
|
|
267
272
|
|
|
268
273
|
`MYSQL_POLICY_HOOK` 不能覆蓋內建 policy 的拒絕結果。它只能在 command 已經通過本機 policy 後,決定接下來是 `accept`、`reject`,或 `approval_required`。
|
|
269
274
|
|
|
275
|
+
### Advanced Schema Mode
|
|
276
|
+
|
|
277
|
+
設定 `MYSQL_MCP_MODE=advanced` 可啟用 `mysql_schema_execute`,用於 schema changes。這是明確 opt-in 的資料庫結構操作模式。
|
|
278
|
+
|
|
279
|
+
允許的 schema statements:
|
|
280
|
+
|
|
281
|
+
| 物件 | Statements |
|
|
282
|
+
| --- | --- |
|
|
283
|
+
| Tables | `CREATE TABLE`、`ALTER TABLE`、`DROP TABLE`、`RENAME TABLE` |
|
|
284
|
+
| Views | `CREATE VIEW`、`CREATE OR REPLACE VIEW`、`DROP VIEW` |
|
|
285
|
+
| Triggers | `CREATE TRIGGER`、`DROP TRIGGER` |
|
|
286
|
+
| Indexes | `CREATE INDEX`、`DROP INDEX`,以及透過 `ALTER TABLE` 修改 index |
|
|
287
|
+
|
|
288
|
+
相同的 table allow/deny policy 會套用到偵測到的 schema objects 與 referenced tables。`MYSQL_MCP_DENY_TABLES` 仍然優先於 `MYSQL_MCP_ALLOW_TABLES`。
|
|
289
|
+
|
|
290
|
+
Advanced mode 仍會拒絕 multi-statement SQL、不支援的 DDL object types,以及 `CREATE TABLE ... AS SELECT`。
|
|
291
|
+
|
|
270
292
|
## Policy Hook 與 Approval
|
|
271
293
|
|
|
272
294
|
設定 `MYSQL_POLICY_HOOK` 後,server 會在內建 policy 通過後、command 真正執行前,將每個 tool action POST 到 hook。
|
|
@@ -322,9 +344,11 @@ Pending approvals 是 one-time use,並會在 `MYSQL_APPROVAL_TTL_SECONDS` 後
|
|
|
322
344
|
- 使用 dedicated MySQL user,並只給 assistant 所需的最小權限。
|
|
323
345
|
- 如果只需要 inspection/reporting,建議使用 read-only database credentials。
|
|
324
346
|
- 使用 `MYSQL_READ_ONLY=true` 或 `MYSQL_MCP_MODE=readonly`,可以避免 write execution tools 暴露給 MCP clients。
|
|
347
|
+
- 只有在 assistant 需要修改 tables、views、triggers、indexes 等 schema objects 時,才使用 `MYSQL_MCP_MODE=advanced`。
|
|
325
348
|
- `MYSQL_MCP_ALLOW_TABLES` 與 `MYSQL_MCP_DENY_TABLES` 是 MCP 層 guardrails,不能取代 MySQL grants。
|
|
326
349
|
- 需要外部 policy 或 approval workflow 時,可使用 `MYSQL_POLICY_HOOK`。
|
|
327
350
|
- 請小心使用 `mysql_execute`,它可以修改資料。
|
|
351
|
+
- 請小心使用 `mysql_schema_execute`,它可以建立、修改或刪除資料庫結構。
|
|
328
352
|
- 請小心使用 `mysql_import_csv`,它可以插入大量資料。
|
|
329
353
|
- batch execution 與 CSV import logs 會包含參數值與逐筆結果。請將 `MYSQL_LOG_PATH` 下的檔案視為敏感資料。
|
|
330
354
|
- 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
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import { isPolicyHookEnabled, isReadOnlyMode } from './config.js';
|
|
5
|
+
import { isAdvancedMode, isPolicyHookEnabled, isReadOnlyMode } from './config.js';
|
|
6
6
|
import { cleanupOldLogs } from './logs.js';
|
|
7
7
|
import { cancelApproval, listPendingApprovals, runApprovedCommand } from './approvalStore.js';
|
|
8
|
-
import { describeIndex, describeTable, explainQuery, getCurrentPrivileges, listTables, listTriggers, listViews, mysqlBatchExecute, mysqlExecute, mysqlExportCsv, mysqlImportCsv, mysqlQuery, } from './toolHandlers.js';
|
|
8
|
+
import { describeIndex, describeTable, explainQuery, getCurrentPrivileges, listTables, listTriggers, listViews, mysqlBatchExecute, mysqlExecute, mysqlExportCsv, mysqlImportCsv, mysqlSchemaExecute, mysqlQuery, } from './toolHandlers.js';
|
|
9
9
|
const { MYSQL_HOST, MYSQL_PORT, MYSQL_DATABASE, } = process.env;
|
|
10
10
|
// Initialize MCP Server/mcp
|
|
11
11
|
const server = new McpServer({
|
|
12
12
|
name: 'easy-mysql-mcp',
|
|
13
|
-
version: '1.
|
|
13
|
+
version: '1.1.2',
|
|
14
14
|
description: `MySQL Database: ${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_DATABASE}`,
|
|
15
15
|
});
|
|
16
16
|
// --- Register Tools ---
|
|
@@ -66,6 +66,19 @@ if (!isReadOnlyMode()) {
|
|
|
66
66
|
};
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
|
+
if (isAdvancedMode()) {
|
|
70
|
+
server.registerTool('mysql_schema_execute', {
|
|
71
|
+
description: 'Execute an advanced schema modification SQL statement, such as CREATE TABLE, ALTER TABLE, DROP VIEW, CREATE TRIGGER, or CREATE INDEX.',
|
|
72
|
+
inputSchema: z.object({
|
|
73
|
+
sql: z.string().describe('The single schema modification SQL statement to execute.'),
|
|
74
|
+
}),
|
|
75
|
+
}, async ({ sql }) => {
|
|
76
|
+
const result = await mysqlSchemaExecute(sql);
|
|
77
|
+
return {
|
|
78
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
69
82
|
if (isPolicyHookEnabled()) {
|
|
70
83
|
server.registerTool('mysql_run_approved_command', {
|
|
71
84
|
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.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "High performance MySQL MCP Server using mysql2",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37
|
-
"mysql2": "^3.22.
|
|
37
|
+
"mysql2": "^3.22.4",
|
|
38
38
|
"node-sql-parser": "^5.4.0",
|
|
39
39
|
"zod": "^4.4.3"
|
|
40
40
|
},
|