@ahngbeom/mysql-mcp-server 1.0.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/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +314 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ahngbeom
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# MySQL MCP Server
|
|
2
|
+
|
|
3
|
+
Claude Code에서 MySQL 데이터베이스에 접근하기 위한 **읽기 전용** MCP(Model Context Protocol) 서버입니다.
|
|
4
|
+
|
|
5
|
+
## 특징
|
|
6
|
+
|
|
7
|
+
- **읽기 전용**: SELECT, SHOW, DESCRIBE, EXPLAIN 쿼리만 허용
|
|
8
|
+
- **안전한 연결**: Connection Pool 사용
|
|
9
|
+
- **다양한 도구 제공**: 테이블 목록, 스키마 조회, 인덱스 확인, 실행 계획 분석
|
|
10
|
+
|
|
11
|
+
## 설치
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @ahngbeom/mysql-mcp-server
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
또는 직접 빌드:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
git clone https://github.com/ahngbeom/mysql-mcp-server.git
|
|
21
|
+
cd mysql-mcp-server
|
|
22
|
+
npm install
|
|
23
|
+
npm run build
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 사용법
|
|
27
|
+
|
|
28
|
+
### Claude Code 설정
|
|
29
|
+
|
|
30
|
+
`~/.claude.json` 또는 프로젝트의 `.mcp.json`에 추가:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"mcpServers": {
|
|
35
|
+
"mysql": {
|
|
36
|
+
"command": "npx",
|
|
37
|
+
"args": ["@ahngbeom/mysql-mcp-server"],
|
|
38
|
+
"env": {
|
|
39
|
+
"MYSQL_HOST": "localhost",
|
|
40
|
+
"MYSQL_PORT": "3306",
|
|
41
|
+
"MYSQL_USER": "your_user",
|
|
42
|
+
"MYSQL_PASS": "your_password",
|
|
43
|
+
"MYSQL_DB": "your_database"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 환경 변수
|
|
51
|
+
|
|
52
|
+
| 변수 | 설명 | 기본값 |
|
|
53
|
+
|------|------|--------|
|
|
54
|
+
| `MYSQL_HOST` | MySQL 호스트 | `localhost` |
|
|
55
|
+
| `MYSQL_PORT` | MySQL 포트 | `3306` |
|
|
56
|
+
| `MYSQL_USER` | 사용자 이름 | `root` |
|
|
57
|
+
| `MYSQL_PASS` | 비밀번호 | (없음) |
|
|
58
|
+
| `MYSQL_DB` | 데이터베이스 이름 | `mysql` |
|
|
59
|
+
|
|
60
|
+
## 제공 도구
|
|
61
|
+
|
|
62
|
+
### `query`
|
|
63
|
+
읽기 전용 SQL 쿼리를 실행합니다.
|
|
64
|
+
```
|
|
65
|
+
SELECT * FROM users LIMIT 10
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### `list_tables`
|
|
69
|
+
현재 데이터베이스의 모든 테이블을 조회합니다.
|
|
70
|
+
|
|
71
|
+
### `describe_table`
|
|
72
|
+
테이블의 구조(컬럼, 타입, 키)를 확인합니다.
|
|
73
|
+
|
|
74
|
+
### `show_indexes`
|
|
75
|
+
테이블의 인덱스 정보를 조회합니다.
|
|
76
|
+
|
|
77
|
+
### `show_create_table`
|
|
78
|
+
테이블의 CREATE TABLE 문을 확인합니다.
|
|
79
|
+
|
|
80
|
+
### `explain`
|
|
81
|
+
쿼리의 실행 계획을 분석합니다.
|
|
82
|
+
|
|
83
|
+
## 보안 참고사항
|
|
84
|
+
|
|
85
|
+
- 이 서버는 **읽기 전용**으로 설계되었습니다
|
|
86
|
+
- INSERT, UPDATE, DELETE, DROP 등의 쿼리는 차단됩니다
|
|
87
|
+
- 프로덕션 환경에서는 읽기 전용 DB 사용자 계정 사용을 권장합니다
|
|
88
|
+
|
|
89
|
+
## 라이선스
|
|
90
|
+
|
|
91
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Custom MySQL MCP Server
|
|
4
|
+
*
|
|
5
|
+
* MCP(Model Context Protocol)를 통해 Claude Code에서 MySQL 데이터베이스에 접근
|
|
6
|
+
*
|
|
7
|
+
* 환경변수:
|
|
8
|
+
* MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASS, MYSQL_DB
|
|
9
|
+
*/
|
|
10
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
11
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
13
|
+
import mysql from "mysql2/promise";
|
|
14
|
+
// 환경변수에서 연결 정보 읽기
|
|
15
|
+
const config = {
|
|
16
|
+
host: process.env.MYSQL_HOST || "localhost",
|
|
17
|
+
port: parseInt(process.env.MYSQL_PORT || "3306"),
|
|
18
|
+
user: process.env.MYSQL_USER || "root",
|
|
19
|
+
password: process.env.MYSQL_PASS || "",
|
|
20
|
+
database: process.env.MYSQL_DB || "mysql",
|
|
21
|
+
};
|
|
22
|
+
let pool = null;
|
|
23
|
+
// 연결 풀 생성
|
|
24
|
+
function getPool() {
|
|
25
|
+
if (!pool) {
|
|
26
|
+
pool = mysql.createPool({
|
|
27
|
+
...config,
|
|
28
|
+
waitForConnections: true,
|
|
29
|
+
connectionLimit: 5,
|
|
30
|
+
queueLimit: 0,
|
|
31
|
+
connectTimeout: 10000,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return pool;
|
|
35
|
+
}
|
|
36
|
+
// 쿼리 실행 (읽기 전용)
|
|
37
|
+
async function executeQuery(sql) {
|
|
38
|
+
const conn = getPool();
|
|
39
|
+
const [rows] = await conn.query(sql);
|
|
40
|
+
return rows;
|
|
41
|
+
}
|
|
42
|
+
// MCP 서버 생성
|
|
43
|
+
const server = new Server({
|
|
44
|
+
name: "mysql-mcp-server",
|
|
45
|
+
version: "1.0.0",
|
|
46
|
+
}, {
|
|
47
|
+
capabilities: {
|
|
48
|
+
tools: {},
|
|
49
|
+
resources: {},
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
// 도구 목록 정의
|
|
53
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
54
|
+
return {
|
|
55
|
+
tools: [
|
|
56
|
+
{
|
|
57
|
+
name: "query",
|
|
58
|
+
description: "Execute a read-only SELECT query and return results.",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
sql: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "The SELECT SQL query to execute (read-only)",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
required: ["sql"],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "list_tables",
|
|
72
|
+
description: "List all tables in the current database",
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: {},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "describe_table",
|
|
80
|
+
description: "Show the structure of a table (columns, types, keys)",
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: "object",
|
|
83
|
+
properties: {
|
|
84
|
+
table: {
|
|
85
|
+
type: "string",
|
|
86
|
+
description: "Table name to describe",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
required: ["table"],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "show_indexes",
|
|
94
|
+
description: "Show indexes on a table",
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: {
|
|
98
|
+
table: {
|
|
99
|
+
type: "string",
|
|
100
|
+
description: "Table name",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
required: ["table"],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "show_create_table",
|
|
108
|
+
description: "Show the CREATE TABLE statement for a table",
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: "object",
|
|
111
|
+
properties: {
|
|
112
|
+
table: {
|
|
113
|
+
type: "string",
|
|
114
|
+
description: "Table name",
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
required: ["table"],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "explain",
|
|
122
|
+
description: "Show query execution plan",
|
|
123
|
+
inputSchema: {
|
|
124
|
+
type: "object",
|
|
125
|
+
properties: {
|
|
126
|
+
sql: {
|
|
127
|
+
type: "string",
|
|
128
|
+
description: "The SQL query to explain",
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
required: ["sql"],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
// 도구 실행 핸들러
|
|
138
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
139
|
+
const { name, arguments: args } = request.params;
|
|
140
|
+
try {
|
|
141
|
+
switch (name) {
|
|
142
|
+
case "query": {
|
|
143
|
+
const sql = args.sql;
|
|
144
|
+
const trimmedSql = sql.trim().toLowerCase();
|
|
145
|
+
// 읽기 전용: SELECT, SHOW, DESCRIBE, EXPLAIN만 허용
|
|
146
|
+
const allowedPrefixes = ["select", "show", "describe", "explain"];
|
|
147
|
+
const isReadOnly = allowedPrefixes.some(prefix => trimmedSql.startsWith(prefix));
|
|
148
|
+
if (!isReadOnly) {
|
|
149
|
+
return {
|
|
150
|
+
content: [
|
|
151
|
+
{
|
|
152
|
+
type: "text",
|
|
153
|
+
text: "Error: This server is read-only. Only SELECT, SHOW, DESCRIBE, EXPLAIN queries are allowed.",
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
isError: true,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const rows = await executeQuery(sql);
|
|
160
|
+
return {
|
|
161
|
+
content: [
|
|
162
|
+
{
|
|
163
|
+
type: "text",
|
|
164
|
+
text: JSON.stringify(rows, null, 2),
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
case "list_tables": {
|
|
170
|
+
const rows = await executeQuery("SHOW TABLES");
|
|
171
|
+
const tables = rows.map((row) => Object.values(row)[0]);
|
|
172
|
+
return {
|
|
173
|
+
content: [
|
|
174
|
+
{
|
|
175
|
+
type: "text",
|
|
176
|
+
text: JSON.stringify(tables, null, 2),
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
case "describe_table": {
|
|
182
|
+
const table = args.table;
|
|
183
|
+
const rows = await executeQuery(`DESCRIBE \`${table}\``);
|
|
184
|
+
return {
|
|
185
|
+
content: [
|
|
186
|
+
{
|
|
187
|
+
type: "text",
|
|
188
|
+
text: JSON.stringify(rows, null, 2),
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
case "show_indexes": {
|
|
194
|
+
const table = args.table;
|
|
195
|
+
const rows = await executeQuery(`SHOW INDEX FROM \`${table}\``);
|
|
196
|
+
return {
|
|
197
|
+
content: [
|
|
198
|
+
{
|
|
199
|
+
type: "text",
|
|
200
|
+
text: JSON.stringify(rows, null, 2),
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
case "show_create_table": {
|
|
206
|
+
const table = args.table;
|
|
207
|
+
const rows = await executeQuery(`SHOW CREATE TABLE \`${table}\``);
|
|
208
|
+
return {
|
|
209
|
+
content: [
|
|
210
|
+
{
|
|
211
|
+
type: "text",
|
|
212
|
+
text: rows[0]?.["Create Table"] || "Table not found",
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
case "explain": {
|
|
218
|
+
const sql = args.sql;
|
|
219
|
+
const rows = await executeQuery(`EXPLAIN ${sql}`);
|
|
220
|
+
return {
|
|
221
|
+
content: [
|
|
222
|
+
{
|
|
223
|
+
type: "text",
|
|
224
|
+
text: JSON.stringify(rows, null, 2),
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
default:
|
|
230
|
+
return {
|
|
231
|
+
content: [
|
|
232
|
+
{
|
|
233
|
+
type: "text",
|
|
234
|
+
text: `Unknown tool: ${name}`,
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
242
|
+
return {
|
|
243
|
+
content: [
|
|
244
|
+
{
|
|
245
|
+
type: "text",
|
|
246
|
+
text: `MySQL Error: ${message}`,
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
isError: true,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
// 리소스 목록 (스키마 정보 제공)
|
|
254
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
255
|
+
try {
|
|
256
|
+
const rows = await executeQuery("SHOW TABLES");
|
|
257
|
+
const tables = rows.map((row) => Object.values(row)[0]);
|
|
258
|
+
return {
|
|
259
|
+
resources: tables.map((table) => ({
|
|
260
|
+
uri: `mysql:///${config.database}/${table}`,
|
|
261
|
+
mimeType: "application/json",
|
|
262
|
+
name: `Table: ${table}`,
|
|
263
|
+
description: `Schema and sample data from ${table}`,
|
|
264
|
+
})),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return { resources: [] };
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
// 리소스 읽기 (테이블 스키마 + 샘플 데이터)
|
|
272
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
273
|
+
const uri = request.params.uri;
|
|
274
|
+
const match = uri.match(/mysql:\/\/\/([^/]+)\/(.+)/);
|
|
275
|
+
if (!match) {
|
|
276
|
+
throw new Error(`Invalid resource URI: ${uri}`);
|
|
277
|
+
}
|
|
278
|
+
const [, , table] = match;
|
|
279
|
+
const structure = await executeQuery(`DESCRIBE \`${table}\``);
|
|
280
|
+
const sample = await executeQuery(`SELECT * FROM \`${table}\` LIMIT 5`);
|
|
281
|
+
const countResult = await executeQuery(`SELECT COUNT(*) as count FROM \`${table}\``);
|
|
282
|
+
return {
|
|
283
|
+
contents: [
|
|
284
|
+
{
|
|
285
|
+
uri,
|
|
286
|
+
mimeType: "application/json",
|
|
287
|
+
text: JSON.stringify({
|
|
288
|
+
table,
|
|
289
|
+
rowCount: countResult[0]?.count,
|
|
290
|
+
columns: structure,
|
|
291
|
+
sampleData: sample,
|
|
292
|
+
}, null, 2),
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
// 서버 시작
|
|
298
|
+
async function main() {
|
|
299
|
+
const transport = new StdioServerTransport();
|
|
300
|
+
await server.connect(transport);
|
|
301
|
+
// 연결 테스트
|
|
302
|
+
try {
|
|
303
|
+
await executeQuery("SELECT 1");
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
307
|
+
console.error(`MySQL connection failed: ${message}`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
main().catch((error) => {
|
|
312
|
+
console.error("Fatal error:", error);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ahngbeom/mysql-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Read-only MySQL MCP Server for Claude Code",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mysql-mcp-server": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"dev": "tsx src/index.ts",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mcp",
|
|
18
|
+
"model-context-protocol",
|
|
19
|
+
"mysql",
|
|
20
|
+
"claude",
|
|
21
|
+
"claude-code",
|
|
22
|
+
"ai",
|
|
23
|
+
"database"
|
|
24
|
+
],
|
|
25
|
+
"author": "ahngbeom",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/ahngbeom/mysql-mcp-server.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/ahngbeom/mysql-mcp-server/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/ahngbeom/mysql-mcp-server#readme",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18.0.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
40
|
+
"mysql2": "^3.11.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^20.0.0",
|
|
44
|
+
"tsx": "^4.19.0",
|
|
45
|
+
"typescript": "^5.5.0"
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"dist",
|
|
49
|
+
"README.md",
|
|
50
|
+
"LICENSE"
|
|
51
|
+
]
|
|
52
|
+
}
|