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 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
+ ```
@@ -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;
@@ -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,5 @@
1
+ export type DatabaseInfo = {
2
+ name: string;
3
+ defaultCharacterSetName?: string;
4
+ defaultCollationName?: string;
5
+ };
@@ -0,0 +1 @@
1
+ export {};
package/build/db.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { type Pool } from 'mysql2/promise';
2
+ export declare function getPool(): Pool;
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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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,4 @@
1
+ export type UserIdentity = {
2
+ user: string;
3
+ host: string;
4
+ };
@@ -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
+ }