easy-mysql-admin-mcp 0.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 +126 -0
- package/README.zh-TW.md +96 -0
- package/build/config.d.ts +18 -0
- package/build/config.js +28 -0
- package/build/confirmationStore.d.ts +18 -0
- package/build/confirmationStore.js +31 -0
- package/build/databaseTools.d.ts +5 -0
- package/build/databaseTools.js +1 -0
- package/build/db.d.ts +2 -0
- package/build/db.js +28 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +131 -0
- package/build/toolHandlers.d.ts +107 -0
- package/build/toolHandlers.js +95 -0
- package/build/userTools.d.ts +4 -0
- package/build/userTools.js +1 -0
- package/build/validation.d.ts +10 -0
- package/build/validation.js +25 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# easy-mysql-admin-mcp
|
|
2
|
+
|
|
3
|
+
High-privilege MySQL admin MCP server for database and user/grant management.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- List, create, and inspect databases
|
|
8
|
+
- List, create, and update users
|
|
9
|
+
- Grant and revoke privileges at database scope
|
|
10
|
+
- Protect destructive actions with short-lived confirmation tokens
|
|
11
|
+
|
|
12
|
+
## Available Tools
|
|
13
|
+
|
|
14
|
+
| Tool | Description |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| `mysql_list_databases` | List databases in the current MySQL instance |
|
|
17
|
+
| `mysql_create_database` | Create a database |
|
|
18
|
+
| `mysql_describe_database` | Inspect database charset and collation settings |
|
|
19
|
+
| `mysql_drop_database` | Request database deletion and return a confirmation token |
|
|
20
|
+
| `mysql_list_users` | List MySQL users |
|
|
21
|
+
| `mysql_create_user` | Create a MySQL user |
|
|
22
|
+
| `mysql_alter_user_password` | Change a MySQL user password |
|
|
23
|
+
| `mysql_grant_privileges` | Grant database privileges to a user |
|
|
24
|
+
| `mysql_revoke_privileges` | Revoke database privileges from a user |
|
|
25
|
+
| `mysql_show_grants` | Show grants for a user |
|
|
26
|
+
| `mysql_drop_user` | Request user deletion and return a confirmation token |
|
|
27
|
+
| `mysql_confirm_task` | Confirm and execute a destructive action token |
|
|
28
|
+
|
|
29
|
+
## Safety
|
|
30
|
+
|
|
31
|
+
- No raw SQL passthrough
|
|
32
|
+
- No table, view, index, or trigger management
|
|
33
|
+
- `DROP DATABASE` and `DROP USER` require `mysql_confirm_task`
|
|
34
|
+
- Confirmation tokens are random, single-use, and expire quickly
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
Use environment variables, matching the rest of the `easy-*-mcp` family.
|
|
39
|
+
|
|
40
|
+
| Variable | Required | Default | Description |
|
|
41
|
+
| --- | --- | --- | --- |
|
|
42
|
+
| `MYSQL_HOST` | Yes | - | MySQL host name or IP address |
|
|
43
|
+
| `MYSQL_PORT` | No | `3306` | MySQL port |
|
|
44
|
+
| `MYSQL_USER` | Yes | - | MySQL user name |
|
|
45
|
+
| `MYSQL_PASSWORD` | Yes | - | MySQL password |
|
|
46
|
+
| `MYSQL_DATABASE` | Yes | - | Default database/schema used for the admin connection |
|
|
47
|
+
| `MYSQL_CONNECTION_LIMIT` | No | `10` | Maximum number of active pool connections |
|
|
48
|
+
| `MYSQL_MAX_IDLE` | No | `10` | Maximum number of idle pool connections |
|
|
49
|
+
| `MYSQL_IDLE_TIMEOUT` | No | `60000` | Idle connection timeout in milliseconds |
|
|
50
|
+
| `MYSQL_QUEUE_LIMIT` | No | `0` | Maximum queued connection requests |
|
|
51
|
+
| `MYSQL_WAIT_FOR_CONNECTIONS` | No | `true` | Whether the pool waits when all connections are busy |
|
|
52
|
+
| `MYSQL_ENABLE_KEEP_ALIVE` | No | `true` | Whether TCP keep-alive is enabled |
|
|
53
|
+
| `MYSQL_KEEP_ALIVE_INITIAL_DELAY` | No | `0` | Initial TCP keep-alive delay in milliseconds |
|
|
54
|
+
| `MYSQL_ADMIN_TOKEN_TTL_SECONDS` | No | `120` | Confirmation token lifetime in seconds |
|
|
55
|
+
|
|
56
|
+
## Example
|
|
57
|
+
|
|
58
|
+
```env
|
|
59
|
+
MYSQL_HOST=localhost
|
|
60
|
+
MYSQL_PORT=3306
|
|
61
|
+
MYSQL_USER=root
|
|
62
|
+
MYSQL_PASSWORD=your_password
|
|
63
|
+
MYSQL_DATABASE=mysql
|
|
64
|
+
MYSQL_ADMIN_TOKEN_TTL_SECONDS=120
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Claude Desktop Example
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"mcpServers": {
|
|
72
|
+
"easy-mysql-admin-mcp": {
|
|
73
|
+
"command": "npx",
|
|
74
|
+
"args": ["-y", "easy-mysql-admin-mcp"],
|
|
75
|
+
"env": {
|
|
76
|
+
"MYSQL_HOST": "localhost",
|
|
77
|
+
"MYSQL_PORT": "3306",
|
|
78
|
+
"MYSQL_USER": "root",
|
|
79
|
+
"MYSQL_PASSWORD": "your_password",
|
|
80
|
+
"MYSQL_DATABASE": "mysql",
|
|
81
|
+
"MYSQL_ADMIN_TOKEN_TTL_SECONDS": "120"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Codex config.toml Example
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
[mcp_servers.easy-mysql-admin-mcp]
|
|
92
|
+
args = ["-y", "easy-mysql-admin-mcp"]
|
|
93
|
+
command = "npx"
|
|
94
|
+
enabled = true
|
|
95
|
+
|
|
96
|
+
[mcp_servers.easy-mysql-admin-mcp.env]
|
|
97
|
+
MYSQL_HOST = "localhost"
|
|
98
|
+
MYSQL_PORT = "3306"
|
|
99
|
+
MYSQL_USER = "root"
|
|
100
|
+
MYSQL_PASSWORD = "your_password"
|
|
101
|
+
MYSQL_DATABASE = "mysql"
|
|
102
|
+
MYSQL_ADMIN_TOKEN_TTL_SECONDS = "120"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## OpenCode opencode.jsonc Example
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"$schema": "https://opencode.ai/config.json",
|
|
110
|
+
"mcp": {
|
|
111
|
+
"easy-mysql-admin-mcp": {
|
|
112
|
+
"type": "local",
|
|
113
|
+
"command": ["npx", "-y", "easy-mysql-admin-mcp"],
|
|
114
|
+
"enabled": true,
|
|
115
|
+
"environment": {
|
|
116
|
+
"MYSQL_HOST": "localhost",
|
|
117
|
+
"MYSQL_PORT": "3306",
|
|
118
|
+
"MYSQL_USER": "root",
|
|
119
|
+
"MYSQL_PASSWORD": "your_password",
|
|
120
|
+
"MYSQL_DATABASE": "mysql",
|
|
121
|
+
"MYSQL_ADMIN_TOKEN_TTL_SECONDS": "120",
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
```
|
package/README.zh-TW.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# easy-mysql-admin-mcp
|
|
2
|
+
|
|
3
|
+
一個高權限的 MySQL 管理型 MCP server,專門處理 database 與使用者/權限管理。
|
|
4
|
+
|
|
5
|
+
這個專案的定位是 DBA 類工具,不提供任意 SQL 執行,也不重複處理 table、view、index、trigger 這些已由 `easy-mysql-mcp` 提供的能力。
|
|
6
|
+
|
|
7
|
+
## 功能
|
|
8
|
+
|
|
9
|
+
- 列出、建立、檢視 database
|
|
10
|
+
- 列出、建立、修改 MySQL 使用者
|
|
11
|
+
- 授權與撤權
|
|
12
|
+
- 對危險刪除操作提供短效確認 token
|
|
13
|
+
|
|
14
|
+
## 可用工具
|
|
15
|
+
|
|
16
|
+
| 工具 | 說明 |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `mysql_list_databases` | 列出目前 MySQL instance 內的 databases |
|
|
19
|
+
| `mysql_create_database` | 建立 database |
|
|
20
|
+
| `mysql_describe_database` | 檢視 database 的 charset 與 collation 設定 |
|
|
21
|
+
| `mysql_drop_database` | 提出刪除 database 的請求並回傳確認 token |
|
|
22
|
+
| `mysql_list_users` | 列出 MySQL users |
|
|
23
|
+
| `mysql_create_user` | 建立 MySQL user |
|
|
24
|
+
| `mysql_alter_user_password` | 修改 MySQL user 密碼 |
|
|
25
|
+
| `mysql_grant_privileges` | 對 user 授權 database privileges |
|
|
26
|
+
| `mysql_revoke_privileges` | 取消 user 的 database privileges |
|
|
27
|
+
| `mysql_show_grants` | 顯示指定 user 的 grants |
|
|
28
|
+
| `mysql_drop_user` | 提出刪除 user 的請求並回傳確認 token |
|
|
29
|
+
| `mysql_confirm_task` | 確認並執行先前產生的危險操作 token |
|
|
30
|
+
|
|
31
|
+
## 安全性
|
|
32
|
+
|
|
33
|
+
- 不提供原始 SQL passthrough
|
|
34
|
+
- 不處理 table、view、index、trigger 管理
|
|
35
|
+
- `DROP DATABASE` 與 `DROP USER` 一律要經過 `mysql_confirm_task`
|
|
36
|
+
- confirmation token 是隨機產生、只能使用一次、且會在短時間後過期
|
|
37
|
+
|
|
38
|
+
## 設定
|
|
39
|
+
|
|
40
|
+
請使用環境變數設定,風格與其他 `easy-*-mcp` 專案一致。
|
|
41
|
+
|
|
42
|
+
| 變數 | 必填 | 預設值 | 說明 |
|
|
43
|
+
| --- | --- | --- | --- |
|
|
44
|
+
| `MYSQL_HOST` | 是 | - | MySQL host name 或 IP address |
|
|
45
|
+
| `MYSQL_PORT` | 否 | `3306` | MySQL port |
|
|
46
|
+
| `MYSQL_USER` | 是 | - | MySQL 使用者名稱 |
|
|
47
|
+
| `MYSQL_PASSWORD` | 是 | - | MySQL 密碼 |
|
|
48
|
+
| `MYSQL_DATABASE` | 是 | - | 管理連線所使用的預設 database/schema |
|
|
49
|
+
| `MYSQL_CONNECTION_LIMIT` | 否 | `10` | pool 最大 active connections |
|
|
50
|
+
| `MYSQL_MAX_IDLE` | 否 | `10` | pool 最大 idle connections |
|
|
51
|
+
| `MYSQL_IDLE_TIMEOUT` | 否 | `60000` | idle connection timeout,單位毫秒 |
|
|
52
|
+
| `MYSQL_QUEUE_LIMIT` | 否 | `0` | 最大 queued connection requests |
|
|
53
|
+
| `MYSQL_WAIT_FOR_CONNECTIONS` | 否 | `true` | connection 滿時是否等待 |
|
|
54
|
+
| `MYSQL_ENABLE_KEEP_ALIVE` | 否 | `true` | 是否啟用 TCP keep-alive |
|
|
55
|
+
| `MYSQL_KEEP_ALIVE_INITIAL_DELAY` | 否 | `0` | TCP keep-alive 初始延遲,單位毫秒 |
|
|
56
|
+
| `MYSQL_ADMIN_TOKEN_TTL_SECONDS` | 否 | `120` | confirmation token 的有效秒數 |
|
|
57
|
+
|
|
58
|
+
## 範例
|
|
59
|
+
|
|
60
|
+
```env
|
|
61
|
+
MYSQL_HOST=localhost
|
|
62
|
+
MYSQL_PORT=3306
|
|
63
|
+
MYSQL_USER=root
|
|
64
|
+
MYSQL_PASSWORD=your_password
|
|
65
|
+
MYSQL_DATABASE=mysql
|
|
66
|
+
MYSQL_ADMIN_TOKEN_TTL_SECONDS=120
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Claude Desktop 範例
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"mcpServers": {
|
|
74
|
+
"easy-mysql-admin-mcp": {
|
|
75
|
+
"command": "npx",
|
|
76
|
+
"args": ["-y", "easy-mysql-admin-mcp"],
|
|
77
|
+
"env": {
|
|
78
|
+
"MYSQL_HOST": "localhost",
|
|
79
|
+
"MYSQL_PORT": "3306",
|
|
80
|
+
"MYSQL_USER": "root",
|
|
81
|
+
"MYSQL_PASSWORD": "your_password",
|
|
82
|
+
"MYSQL_DATABASE": "mysql",
|
|
83
|
+
"MYSQL_ADMIN_TOKEN_TTL_SECONDS": "120"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
更新設定後,請重新啟動 Claude Desktop。
|
|
91
|
+
|
|
92
|
+
## 備註
|
|
93
|
+
|
|
94
|
+
- `mysql_drop_database` 與 `mysql_drop_user` 不會直接執行
|
|
95
|
+
- 這兩個動作會先產生 token,使用者確認後才會透過 `mysql_confirm_task` 真正執行
|
|
96
|
+
- token 是短效且單次使用,不會保留成長期 pending queue
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type AdminConfig = {
|
|
2
|
+
mysql: {
|
|
3
|
+
host: string;
|
|
4
|
+
port: number;
|
|
5
|
+
user: string;
|
|
6
|
+
password: string;
|
|
7
|
+
database: string;
|
|
8
|
+
connectionLimit: number;
|
|
9
|
+
maxIdle: number;
|
|
10
|
+
idleTimeout: number;
|
|
11
|
+
queueLimit: number;
|
|
12
|
+
waitForConnections: boolean;
|
|
13
|
+
enableKeepAlive: boolean;
|
|
14
|
+
keepAliveInitialDelay: number;
|
|
15
|
+
};
|
|
16
|
+
tokenTtlSeconds: number;
|
|
17
|
+
};
|
|
18
|
+
export declare function parseAdminConfig(env: NodeJS.ProcessEnv): AdminConfig;
|
package/build/config.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function parsePositiveInt(value, defaultValue) {
|
|
2
|
+
const parsed = value ? Number.parseInt(value, 10) : NaN;
|
|
3
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : defaultValue;
|
|
4
|
+
}
|
|
5
|
+
function parseBoolean(value, defaultValue = true) {
|
|
6
|
+
if (value === undefined)
|
|
7
|
+
return defaultValue;
|
|
8
|
+
return value.toLowerCase() !== 'false';
|
|
9
|
+
}
|
|
10
|
+
export function parseAdminConfig(env) {
|
|
11
|
+
return {
|
|
12
|
+
mysql: {
|
|
13
|
+
host: env.MYSQL_HOST ?? '',
|
|
14
|
+
port: parsePositiveInt(env.MYSQL_PORT, 3306),
|
|
15
|
+
user: env.MYSQL_USER ?? '',
|
|
16
|
+
password: env.MYSQL_PASSWORD ?? '',
|
|
17
|
+
database: env.MYSQL_DATABASE ?? '',
|
|
18
|
+
connectionLimit: parsePositiveInt(env.MYSQL_CONNECTION_LIMIT, 10),
|
|
19
|
+
maxIdle: parsePositiveInt(env.MYSQL_MAX_IDLE, 10),
|
|
20
|
+
idleTimeout: parsePositiveInt(env.MYSQL_IDLE_TIMEOUT, 60000),
|
|
21
|
+
queueLimit: env.MYSQL_QUEUE_LIMIT ? Number.parseInt(env.MYSQL_QUEUE_LIMIT, 10) : 0,
|
|
22
|
+
waitForConnections: parseBoolean(env.MYSQL_WAIT_FOR_CONNECTIONS, true),
|
|
23
|
+
enableKeepAlive: parseBoolean(env.MYSQL_ENABLE_KEEP_ALIVE, true),
|
|
24
|
+
keepAliveInitialDelay: env.MYSQL_KEEP_ALIVE_INITIAL_DELAY ? Number.parseInt(env.MYSQL_KEEP_ALIVE_INITIAL_DELAY, 10) : 0,
|
|
25
|
+
},
|
|
26
|
+
tokenTtlSeconds: parsePositiveInt(env.MYSQL_ADMIN_TOKEN_TTL_SECONDS, 120),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type ConfirmationAction = 'drop_database' | 'drop_user';
|
|
2
|
+
export type ConfirmationTask = {
|
|
3
|
+
action: ConfirmationAction;
|
|
4
|
+
target: string;
|
|
5
|
+
createdAt: number;
|
|
6
|
+
expiresAt: number;
|
|
7
|
+
used: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type ConfirmationStore = {
|
|
10
|
+
create(task: {
|
|
11
|
+
action: ConfirmationAction;
|
|
12
|
+
target: string;
|
|
13
|
+
}): string;
|
|
14
|
+
consume(token: string): ConfirmationTask | undefined;
|
|
15
|
+
};
|
|
16
|
+
export declare function createConfirmationStore(options: {
|
|
17
|
+
defaultTtlMs: number;
|
|
18
|
+
}): ConfirmationStore;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
export function createConfirmationStore(options) {
|
|
3
|
+
const tasks = new Map();
|
|
4
|
+
return {
|
|
5
|
+
create(task) {
|
|
6
|
+
const token = randomUUID();
|
|
7
|
+
const now = Date.now();
|
|
8
|
+
tasks.set(token, {
|
|
9
|
+
action: task.action,
|
|
10
|
+
target: task.target,
|
|
11
|
+
createdAt: now,
|
|
12
|
+
expiresAt: now + options.defaultTtlMs,
|
|
13
|
+
used: false,
|
|
14
|
+
});
|
|
15
|
+
return token;
|
|
16
|
+
},
|
|
17
|
+
consume(token) {
|
|
18
|
+
const task = tasks.get(token);
|
|
19
|
+
if (!task) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
if (task.used || task.expiresAt <= Date.now()) {
|
|
23
|
+
tasks.delete(token);
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
task.used = true;
|
|
27
|
+
tasks.delete(token);
|
|
28
|
+
return task;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/build/db.d.ts
ADDED
package/build/db.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import mysql from 'mysql2/promise';
|
|
2
|
+
import { parseAdminConfig } from './config.js';
|
|
3
|
+
let pool;
|
|
4
|
+
export function getPool() {
|
|
5
|
+
if (pool) {
|
|
6
|
+
return pool;
|
|
7
|
+
}
|
|
8
|
+
const config = parseAdminConfig(process.env);
|
|
9
|
+
if (!config.mysql.host || !config.mysql.user || !config.mysql.password || !config.mysql.database) {
|
|
10
|
+
throw new Error('Missing required environment variables for MySQL admin connection.');
|
|
11
|
+
}
|
|
12
|
+
pool = mysql.createPool({
|
|
13
|
+
host: config.mysql.host,
|
|
14
|
+
port: config.mysql.port,
|
|
15
|
+
user: config.mysql.user,
|
|
16
|
+
password: config.mysql.password,
|
|
17
|
+
database: config.mysql.database,
|
|
18
|
+
multipleStatements: false,
|
|
19
|
+
waitForConnections: config.mysql.waitForConnections,
|
|
20
|
+
connectionLimit: config.mysql.connectionLimit,
|
|
21
|
+
maxIdle: config.mysql.maxIdle,
|
|
22
|
+
idleTimeout: config.mysql.idleTimeout,
|
|
23
|
+
queueLimit: config.mysql.queueLimit,
|
|
24
|
+
enableKeepAlive: config.mysql.enableKeepAlive,
|
|
25
|
+
keepAliveInitialDelay: config.mysql.keepAliveInitialDelay,
|
|
26
|
+
});
|
|
27
|
+
return pool;
|
|
28
|
+
}
|
package/build/index.d.ts
ADDED
package/build/index.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { parseAdminConfig } from './config.js';
|
|
6
|
+
import { createConfirmationStore } from './confirmationStore.js';
|
|
7
|
+
import { getPool } from './db.js';
|
|
8
|
+
import { createAdminHandlers } from './toolHandlers.js';
|
|
9
|
+
const config = parseAdminConfig(process.env);
|
|
10
|
+
const store = createConfirmationStore({ defaultTtlMs: config.tokenTtlSeconds * 1000 });
|
|
11
|
+
getPool();
|
|
12
|
+
const handlers = createAdminHandlers(store, config.tokenTtlSeconds);
|
|
13
|
+
const server = new McpServer({
|
|
14
|
+
name: 'easy-mysql-admin-mcp',
|
|
15
|
+
version: '0.1.0',
|
|
16
|
+
description: `MySQL Admin: ${config.mysql.host}:${config.mysql.port}/${config.mysql.database}`,
|
|
17
|
+
});
|
|
18
|
+
server.registerTool('mysql_list_databases', {
|
|
19
|
+
description: 'List databases in the current MySQL instance.',
|
|
20
|
+
inputSchema: z.object({}),
|
|
21
|
+
}, async () => ({
|
|
22
|
+
content: [{ type: 'text', text: JSON.stringify(await handlers.listDatabases(), null, 2) }],
|
|
23
|
+
}));
|
|
24
|
+
server.registerTool('mysql_create_database', {
|
|
25
|
+
description: 'Create a database.',
|
|
26
|
+
inputSchema: z.object({
|
|
27
|
+
database: z.string().min(1),
|
|
28
|
+
}),
|
|
29
|
+
}, async ({ database }) => ({
|
|
30
|
+
content: [{ type: 'text', text: JSON.stringify(await handlers.createDatabase(database), null, 2) }],
|
|
31
|
+
}));
|
|
32
|
+
server.registerTool('mysql_describe_database', {
|
|
33
|
+
description: 'Inspect a database definition.',
|
|
34
|
+
inputSchema: z.object({
|
|
35
|
+
database: z.string().min(1),
|
|
36
|
+
}),
|
|
37
|
+
}, async ({ database }) => ({
|
|
38
|
+
content: [{ type: 'text', text: JSON.stringify(await handlers.describeDatabase(database), null, 2) }],
|
|
39
|
+
}));
|
|
40
|
+
server.registerTool('mysql_drop_database', {
|
|
41
|
+
description: 'Request database deletion and return a short-lived confirmation token.',
|
|
42
|
+
inputSchema: z.object({
|
|
43
|
+
database: z.string().min(1),
|
|
44
|
+
}),
|
|
45
|
+
}, async ({ database }) => ({
|
|
46
|
+
content: [{ type: 'text', text: JSON.stringify(await handlers.dropDatabase(database), null, 2) }],
|
|
47
|
+
}));
|
|
48
|
+
server.registerTool('mysql_list_users', {
|
|
49
|
+
description: 'List MySQL users.',
|
|
50
|
+
inputSchema: z.object({}),
|
|
51
|
+
}, async () => ({
|
|
52
|
+
content: [{ type: 'text', text: JSON.stringify(await handlers.listUsers(), null, 2) }],
|
|
53
|
+
}));
|
|
54
|
+
server.registerTool('mysql_create_user', {
|
|
55
|
+
description: 'Create a MySQL user.',
|
|
56
|
+
inputSchema: z.object({
|
|
57
|
+
user: z.string().min(1),
|
|
58
|
+
host: z.string().min(1),
|
|
59
|
+
password: z.string().min(1),
|
|
60
|
+
}),
|
|
61
|
+
}, async ({ user, host, password }) => ({
|
|
62
|
+
content: [{ type: 'text', text: JSON.stringify(await handlers.createUser({ user, host, password }), null, 2) }],
|
|
63
|
+
}));
|
|
64
|
+
server.registerTool('mysql_alter_user_password', {
|
|
65
|
+
description: 'Change a MySQL user password.',
|
|
66
|
+
inputSchema: z.object({
|
|
67
|
+
user: z.string().min(1),
|
|
68
|
+
host: z.string().min(1),
|
|
69
|
+
password: z.string().min(1),
|
|
70
|
+
}),
|
|
71
|
+
}, async ({ user, host, password }) => ({
|
|
72
|
+
content: [{ type: 'text', text: JSON.stringify(await handlers.alterUserPassword({ user, host, password }), null, 2) }],
|
|
73
|
+
}));
|
|
74
|
+
server.registerTool('mysql_grant_privileges', {
|
|
75
|
+
description: 'Grant privileges on a database to a user.',
|
|
76
|
+
inputSchema: z.object({
|
|
77
|
+
user: z.string().min(1),
|
|
78
|
+
host: z.string().min(1),
|
|
79
|
+
database: z.string().min(1),
|
|
80
|
+
privileges: z.array(z.string().min(1)).min(1),
|
|
81
|
+
withGrantOption: z.boolean().optional(),
|
|
82
|
+
}),
|
|
83
|
+
}, async ({ user, host, database, privileges, withGrantOption }) => ({
|
|
84
|
+
content: [{ type: 'text', text: JSON.stringify(await handlers.grantPrivileges({ user, host, database, privileges, withGrantOption }), null, 2) }],
|
|
85
|
+
}));
|
|
86
|
+
server.registerTool('mysql_revoke_privileges', {
|
|
87
|
+
description: 'Revoke privileges on a database from a user.',
|
|
88
|
+
inputSchema: z.object({
|
|
89
|
+
user: z.string().min(1),
|
|
90
|
+
host: z.string().min(1),
|
|
91
|
+
database: z.string().min(1),
|
|
92
|
+
privileges: z.array(z.string().min(1)).min(1),
|
|
93
|
+
}),
|
|
94
|
+
}, async ({ user, host, database, privileges }) => ({
|
|
95
|
+
content: [{ type: 'text', text: JSON.stringify(await handlers.revokePrivileges({ user, host, database, privileges }), null, 2) }],
|
|
96
|
+
}));
|
|
97
|
+
server.registerTool('mysql_show_grants', {
|
|
98
|
+
description: 'Show grants for a user.',
|
|
99
|
+
inputSchema: z.object({
|
|
100
|
+
user: z.string().min(1),
|
|
101
|
+
host: z.string().min(1),
|
|
102
|
+
}),
|
|
103
|
+
}, async ({ user, host }) => ({
|
|
104
|
+
content: [{ type: 'text', text: JSON.stringify(await handlers.showGrants({ user, host }), null, 2) }],
|
|
105
|
+
}));
|
|
106
|
+
server.registerTool('mysql_drop_user', {
|
|
107
|
+
description: 'Request user deletion and return a short-lived confirmation token.',
|
|
108
|
+
inputSchema: z.object({
|
|
109
|
+
user: z.string().min(1),
|
|
110
|
+
host: z.string().min(1),
|
|
111
|
+
}),
|
|
112
|
+
}, async ({ user, host }) => ({
|
|
113
|
+
content: [{ type: 'text', text: JSON.stringify(await handlers.dropUser({ user, host }), null, 2) }],
|
|
114
|
+
}));
|
|
115
|
+
server.registerTool('mysql_confirm_task', {
|
|
116
|
+
description: 'Confirm and execute a previously issued destructive action token.',
|
|
117
|
+
inputSchema: z.object({
|
|
118
|
+
token: z.string().min(1),
|
|
119
|
+
}),
|
|
120
|
+
}, async ({ token }) => ({
|
|
121
|
+
content: [{ type: 'text', text: JSON.stringify(await handlers.confirmTask(token), null, 2) }],
|
|
122
|
+
}));
|
|
123
|
+
async function main() {
|
|
124
|
+
const transport = new StdioServerTransport();
|
|
125
|
+
await server.connect(transport);
|
|
126
|
+
console.error('easy-mysql-admin-mcp running on stdio');
|
|
127
|
+
}
|
|
128
|
+
main().catch((error) => {
|
|
129
|
+
console.error('Fatal error in main():', error);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { ConfirmationStore } from './confirmationStore.js';
|
|
2
|
+
export type ErrorPayload = {
|
|
3
|
+
error: string;
|
|
4
|
+
code?: string;
|
|
5
|
+
};
|
|
6
|
+
export type DatabaseRecord = {
|
|
7
|
+
name: string;
|
|
8
|
+
defaultCharacterSetName: string | null;
|
|
9
|
+
defaultCollationName: string | null;
|
|
10
|
+
};
|
|
11
|
+
export type UserRecord = {
|
|
12
|
+
user: string;
|
|
13
|
+
host: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function formatError(error: unknown, code?: string): ErrorPayload;
|
|
16
|
+
export declare function createAdminHandlers(store: ConfirmationStore, tokenTtlSeconds: number): {
|
|
17
|
+
listDatabases: () => Promise<DatabaseRecord[]>;
|
|
18
|
+
createDatabase: (database: string) => Promise<{
|
|
19
|
+
ok: boolean;
|
|
20
|
+
database: string;
|
|
21
|
+
}>;
|
|
22
|
+
describeDatabase: (database: string) => Promise<DatabaseRecord[]>;
|
|
23
|
+
dropDatabase: (database: string) => Promise<{
|
|
24
|
+
status: string;
|
|
25
|
+
token: string;
|
|
26
|
+
expiresInSeconds: number;
|
|
27
|
+
}>;
|
|
28
|
+
listUsers: () => Promise<UserRecord[]>;
|
|
29
|
+
createUser: (input: {
|
|
30
|
+
user: string;
|
|
31
|
+
host: string;
|
|
32
|
+
password: string;
|
|
33
|
+
}) => Promise<{
|
|
34
|
+
ok: boolean;
|
|
35
|
+
user: string;
|
|
36
|
+
host: string;
|
|
37
|
+
}>;
|
|
38
|
+
alterUserPassword: (input: {
|
|
39
|
+
user: string;
|
|
40
|
+
host: string;
|
|
41
|
+
password: string;
|
|
42
|
+
}) => Promise<{
|
|
43
|
+
ok: boolean;
|
|
44
|
+
user: string;
|
|
45
|
+
host: string;
|
|
46
|
+
}>;
|
|
47
|
+
grantPrivileges: (input: {
|
|
48
|
+
user: string;
|
|
49
|
+
host: string;
|
|
50
|
+
database: string;
|
|
51
|
+
privileges: string[];
|
|
52
|
+
withGrantOption?: boolean;
|
|
53
|
+
}) => Promise<{
|
|
54
|
+
database: string;
|
|
55
|
+
privileges: string[];
|
|
56
|
+
withGrantOption: boolean;
|
|
57
|
+
user: string;
|
|
58
|
+
host: string;
|
|
59
|
+
ok: boolean;
|
|
60
|
+
}>;
|
|
61
|
+
revokePrivileges: (input: {
|
|
62
|
+
user: string;
|
|
63
|
+
host: string;
|
|
64
|
+
database: string;
|
|
65
|
+
privileges: string[];
|
|
66
|
+
}) => Promise<{
|
|
67
|
+
database: string;
|
|
68
|
+
privileges: string[];
|
|
69
|
+
user: string;
|
|
70
|
+
host: string;
|
|
71
|
+
ok: boolean;
|
|
72
|
+
}>;
|
|
73
|
+
showGrants: (input: {
|
|
74
|
+
user: string;
|
|
75
|
+
host: string;
|
|
76
|
+
}) => Promise<Record<string, string>[]>;
|
|
77
|
+
dropUser: (input: {
|
|
78
|
+
user: string;
|
|
79
|
+
host: string;
|
|
80
|
+
}) => Promise<{
|
|
81
|
+
status: string;
|
|
82
|
+
token: string;
|
|
83
|
+
expiresInSeconds: number;
|
|
84
|
+
}>;
|
|
85
|
+
confirmTask: (token: string) => Promise<{
|
|
86
|
+
status: string;
|
|
87
|
+
error: string;
|
|
88
|
+
code: string;
|
|
89
|
+
ok?: undefined;
|
|
90
|
+
action?: undefined;
|
|
91
|
+
target?: undefined;
|
|
92
|
+
} | {
|
|
93
|
+
status: string;
|
|
94
|
+
ok: boolean;
|
|
95
|
+
action: "drop_database";
|
|
96
|
+
target: string;
|
|
97
|
+
error?: undefined;
|
|
98
|
+
code?: undefined;
|
|
99
|
+
} | {
|
|
100
|
+
status: string;
|
|
101
|
+
ok: boolean;
|
|
102
|
+
action: "drop_user";
|
|
103
|
+
target: string;
|
|
104
|
+
error?: undefined;
|
|
105
|
+
code?: undefined;
|
|
106
|
+
}>;
|
|
107
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import mysql from 'mysql2';
|
|
2
|
+
import { getPool } from './db.js';
|
|
3
|
+
import { formatUserHost, parsePrivilegeList, parseUserIdentity, validateDatabaseName } from './validation.js';
|
|
4
|
+
export function formatError(error, code = 'ADMIN_OPERATION_FAILED') {
|
|
5
|
+
return {
|
|
6
|
+
error: error instanceof Error ? error.message : String(error),
|
|
7
|
+
code,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function createAdminHandlers(store, tokenTtlSeconds) {
|
|
11
|
+
const pool = getPool();
|
|
12
|
+
return {
|
|
13
|
+
listDatabases: async () => {
|
|
14
|
+
const [rows] = await pool.execute(`
|
|
15
|
+
SELECT schema_name AS name, default_character_set_name AS defaultCharacterSetName, default_collation_name AS defaultCollationName
|
|
16
|
+
FROM information_schema.schemata
|
|
17
|
+
ORDER BY schema_name
|
|
18
|
+
`);
|
|
19
|
+
return rows;
|
|
20
|
+
},
|
|
21
|
+
createDatabase: async (database) => {
|
|
22
|
+
const safeName = validateDatabaseName(database);
|
|
23
|
+
await pool.execute(`CREATE DATABASE ${pool.escapeId(safeName)}`);
|
|
24
|
+
return { ok: true, database: safeName };
|
|
25
|
+
},
|
|
26
|
+
describeDatabase: async (database) => {
|
|
27
|
+
const safeName = validateDatabaseName(database);
|
|
28
|
+
const [rows] = await pool.execute(`SELECT schema_name AS name, default_character_set_name AS defaultCharacterSetName, default_collation_name AS defaultCollationName
|
|
29
|
+
FROM information_schema.schemata
|
|
30
|
+
WHERE schema_name = ?`, [safeName]);
|
|
31
|
+
return rows;
|
|
32
|
+
},
|
|
33
|
+
dropDatabase: async (database) => {
|
|
34
|
+
const safeName = validateDatabaseName(database);
|
|
35
|
+
const token = store.create({ action: 'drop_database', target: safeName });
|
|
36
|
+
return { status: 'confirmation_required', token, expiresInSeconds: tokenTtlSeconds };
|
|
37
|
+
},
|
|
38
|
+
listUsers: async () => {
|
|
39
|
+
const [rows] = await pool.execute(`
|
|
40
|
+
SELECT user, host
|
|
41
|
+
FROM mysql.user
|
|
42
|
+
ORDER BY user, host
|
|
43
|
+
`);
|
|
44
|
+
return rows;
|
|
45
|
+
},
|
|
46
|
+
createUser: async (input) => {
|
|
47
|
+
const { user, host } = parseUserIdentity(input);
|
|
48
|
+
await pool.execute(`CREATE USER ${formatUserHost(user, host)} IDENTIFIED BY ${mysql.escape(input.password)}`);
|
|
49
|
+
return { ok: true, user, host };
|
|
50
|
+
},
|
|
51
|
+
alterUserPassword: async (input) => {
|
|
52
|
+
const { user, host } = parseUserIdentity(input);
|
|
53
|
+
await pool.execute(`ALTER USER ${formatUserHost(user, host)} IDENTIFIED BY ${mysql.escape(input.password)}`);
|
|
54
|
+
return { ok: true, user, host };
|
|
55
|
+
},
|
|
56
|
+
grantPrivileges: async (input) => {
|
|
57
|
+
const identity = parseUserIdentity(input);
|
|
58
|
+
const safeDatabase = validateDatabaseName(input.database);
|
|
59
|
+
const privileges = parsePrivilegeList(input.privileges).join(', ');
|
|
60
|
+
const grantOption = input.withGrantOption ? ' WITH GRANT OPTION' : '';
|
|
61
|
+
await pool.execute(`GRANT ${privileges} ON ${pool.escapeId(safeDatabase)}.* TO ${formatUserHost(identity.user, identity.host)}${grantOption}`);
|
|
62
|
+
return { ok: true, ...identity, database: safeDatabase, privileges: input.privileges, withGrantOption: Boolean(input.withGrantOption) };
|
|
63
|
+
},
|
|
64
|
+
revokePrivileges: async (input) => {
|
|
65
|
+
const identity = parseUserIdentity(input);
|
|
66
|
+
const safeDatabase = validateDatabaseName(input.database);
|
|
67
|
+
const privileges = parsePrivilegeList(input.privileges).join(', ');
|
|
68
|
+
await pool.execute(`REVOKE ${privileges} ON ${pool.escapeId(safeDatabase)}.* FROM ${formatUserHost(identity.user, identity.host)}`);
|
|
69
|
+
return { ok: true, ...identity, database: safeDatabase, privileges: input.privileges };
|
|
70
|
+
},
|
|
71
|
+
showGrants: async (input) => {
|
|
72
|
+
const { user, host } = parseUserIdentity(input);
|
|
73
|
+
const [rows] = await pool.execute(`SHOW GRANTS FOR ${formatUserHost(user, host)}`);
|
|
74
|
+
return rows;
|
|
75
|
+
},
|
|
76
|
+
dropUser: async (input) => {
|
|
77
|
+
const { user, host } = parseUserIdentity(input);
|
|
78
|
+
const token = store.create({ action: 'drop_user', target: `${user}@${host}` });
|
|
79
|
+
return { status: 'confirmation_required', token, expiresInSeconds: tokenTtlSeconds };
|
|
80
|
+
},
|
|
81
|
+
confirmTask: async (token) => {
|
|
82
|
+
const task = store.consume(token);
|
|
83
|
+
if (!task) {
|
|
84
|
+
return { status: 'error', error: 'token not found or expired', code: 'TOKEN_INVALID' };
|
|
85
|
+
}
|
|
86
|
+
if (task.action === 'drop_database') {
|
|
87
|
+
await pool.execute(`DROP DATABASE ${pool.escapeId(task.target)}`);
|
|
88
|
+
return { status: 'confirmed', ok: true, action: task.action, target: task.target };
|
|
89
|
+
}
|
|
90
|
+
const [user, host] = task.target.split('@');
|
|
91
|
+
await pool.execute(`DROP USER ${formatUserHost(user, host)}`);
|
|
92
|
+
return { status: 'confirmed', ok: true, action: task.action, target: task.target };
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function validateDatabaseName(name: string): string;
|
|
2
|
+
export declare function parseUserIdentity(input: {
|
|
3
|
+
user: string;
|
|
4
|
+
host: string;
|
|
5
|
+
}): {
|
|
6
|
+
user: string;
|
|
7
|
+
host: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function parsePrivilegeList(input: string[]): string[];
|
|
10
|
+
export declare function formatUserHost(user: string, host: string): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import mysql from 'mysql2';
|
|
2
|
+
const VALID_PRIVILEGES = new Set(['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'CREATE', 'ALTER', 'DROP', 'INDEX', 'EXECUTE', 'REFERENCES']);
|
|
3
|
+
export function validateDatabaseName(name) {
|
|
4
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
5
|
+
throw new Error('invalid database name');
|
|
6
|
+
}
|
|
7
|
+
return name;
|
|
8
|
+
}
|
|
9
|
+
export function parseUserIdentity(input) {
|
|
10
|
+
if (!input.user || !input.host) {
|
|
11
|
+
throw new Error('invalid user identity');
|
|
12
|
+
}
|
|
13
|
+
return { user: input.user, host: input.host };
|
|
14
|
+
}
|
|
15
|
+
export function parsePrivilegeList(input) {
|
|
16
|
+
for (const privilege of input) {
|
|
17
|
+
if (!VALID_PRIVILEGES.has(privilege)) {
|
|
18
|
+
throw new Error(`invalid privilege: ${privilege}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return input;
|
|
22
|
+
}
|
|
23
|
+
export function formatUserHost(user, host) {
|
|
24
|
+
return `${mysql.escape(user)}@${mysql.escape(host)}`;
|
|
25
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "easy-mysql-admin-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "High privilege MySQL admin MCP server for database and user management",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"mysql",
|
|
9
|
+
"database",
|
|
10
|
+
"admin",
|
|
11
|
+
"dba",
|
|
12
|
+
"claude",
|
|
13
|
+
"codex",
|
|
14
|
+
"opencode"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/chenkumi/easy-mysql-admin-mcp#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/chenkumi/easy-mysql-admin-mcp/issues"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/chenkumi/easy-mysql-admin-mcp.git"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"author": "Chenkumi",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"main": "build/index.js",
|
|
28
|
+
"types": "./build/index.d.ts",
|
|
29
|
+
"bin": {
|
|
30
|
+
"easy-mysql-admin-mcp": "build/index.js"
|
|
31
|
+
},
|
|
32
|
+
"directories": {
|
|
33
|
+
"test": "test"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"build",
|
|
37
|
+
"README.md",
|
|
38
|
+
"README.zh-TW.md"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc",
|
|
42
|
+
"dev": "tsc --watch",
|
|
43
|
+
"start": "node build/index.js",
|
|
44
|
+
"test": "npm run build && node --test test/*.test.mjs",
|
|
45
|
+
"prepack": "npm run build"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
|
+
"mysql2": "^3.22.5",
|
|
50
|
+
"zod": "^4.4.3"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^25.9.3",
|
|
54
|
+
"typescript": "^6.0.3"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=20"
|
|
58
|
+
},
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public"
|
|
61
|
+
}
|
|
62
|
+
}
|