@cano721/mysql-mcp-server 0.5.0 → 0.7.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 +89 -11
- package/build/index.js +147 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
- [문제 해결](#문제-해결)
|
|
19
19
|
- [Node.js 버전 확인 및 업데이트](#0-nodejs-버전-확인-가장-중요)
|
|
20
20
|
- [npx 관련 문제](#npx-관련-문제)
|
|
21
|
+
- [변경 이력](#변경-이력)
|
|
21
22
|
- [라이선스](#라이선스)
|
|
22
23
|
|
|
23
24
|
## 보안 기능
|
|
@@ -54,6 +55,19 @@
|
|
|
54
55
|
npx @cano721/mysql-mcp-server
|
|
55
56
|
```
|
|
56
57
|
|
|
58
|
+
**💡 항상 최신 버전 사용하기:**
|
|
59
|
+
```bash
|
|
60
|
+
# @latest 태그를 붙이면 캐시를 무시하고 항상 최신 버전을 가져옵니다
|
|
61
|
+
npx @cano721/mysql-mcp-server@latest
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
MCP 설정에서도 `@latest`를 사용하면 항상 최신 버전을 사용할 수 있습니다:
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"args": ["@cano721/mysql-mcp-server@latest"]
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
57
71
|
**주의**: 일부 MCP 클라이언트에서 npx가 제대로 작동하지 않을 수 있습니다. 그런 경우 방법 2를 사용하세요.
|
|
58
72
|
|
|
59
73
|
### 방법 2: 전역 설치 (npx가 안 될 때 권장)
|
|
@@ -117,7 +131,7 @@ MCP 설정 파일에 다음 구성을 추가하세요:
|
|
|
117
131
|
"mcpServers": {
|
|
118
132
|
"mysql": {
|
|
119
133
|
"command": "npx",
|
|
120
|
-
"args": ["@cano721/mysql-mcp-server"],
|
|
134
|
+
"args": ["@cano721/mysql-mcp-server@latest"],
|
|
121
135
|
"env": {
|
|
122
136
|
"MYSQL_HOST": "your-mysql-host",
|
|
123
137
|
"MYSQL_PORT": "3306",
|
|
@@ -301,26 +315,74 @@ MySQL 서버에서 접근 가능한 모든 데이터베이스를 나열합니다
|
|
|
301
315
|
}
|
|
302
316
|
```
|
|
303
317
|
|
|
304
|
-
###
|
|
318
|
+
### analyze_query
|
|
319
|
+
|
|
320
|
+
쿼리 성능 및 통계를 분석합니다 (MYSQL_ALLOW_ANALYZE=true 필요).
|
|
321
|
+
|
|
322
|
+
**매개변수**:
|
|
323
|
+
- `query` (필수): 분석할 SQL 쿼리
|
|
324
|
+
- `database` (선택사항): 데이터베이스명
|
|
325
|
+
|
|
326
|
+
**예제**:
|
|
327
|
+
```json
|
|
328
|
+
{
|
|
329
|
+
"server_name": "mysql",
|
|
330
|
+
"tool_name": "analyze_query",
|
|
331
|
+
"arguments": {
|
|
332
|
+
"database": "my_database",
|
|
333
|
+
"query": "SELECT * FROM users WHERE id = 1"
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### get_related_tables
|
|
305
339
|
|
|
306
|
-
|
|
340
|
+
특정 테이블과 FK로 연결된 모든 연관 테이블을 depth별로 조회합니다.
|
|
307
341
|
|
|
308
342
|
**매개변수**:
|
|
309
|
-
- `table` (필수):
|
|
343
|
+
- `table` (필수): 연관 테이블을 찾을 기준 테이블명
|
|
310
344
|
- `database` (선택사항): 데이터베이스명
|
|
345
|
+
- `depth` (선택사항): 탐색할 최대 깊이 (기본값: 3, 10 초과 시 경고)
|
|
311
346
|
|
|
312
347
|
**예제**:
|
|
313
348
|
```json
|
|
314
349
|
{
|
|
315
350
|
"server_name": "mysql",
|
|
316
|
-
"tool_name": "
|
|
351
|
+
"tool_name": "get_related_tables",
|
|
317
352
|
"arguments": {
|
|
318
353
|
"database": "my_database",
|
|
319
|
-
"table": "
|
|
354
|
+
"table": "user",
|
|
355
|
+
"depth": 2
|
|
320
356
|
}
|
|
321
357
|
}
|
|
322
358
|
```
|
|
323
359
|
|
|
360
|
+
**응답 예시**:
|
|
361
|
+
```json
|
|
362
|
+
{
|
|
363
|
+
"root_table": "user",
|
|
364
|
+
"database": "my_database",
|
|
365
|
+
"requested_depth": 2,
|
|
366
|
+
"total_relations": 55,
|
|
367
|
+
"relations": [
|
|
368
|
+
{
|
|
369
|
+
"depth": 1,
|
|
370
|
+
"child_table": "user_matching_information",
|
|
371
|
+
"fk_column": "user_sn",
|
|
372
|
+
"parent_table": "user",
|
|
373
|
+
"constraint_name": "FK_USER_MATCHING_INFORMATION_USER_SN"
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
"depth": 2,
|
|
377
|
+
"child_table": "user_matching_profile",
|
|
378
|
+
"fk_column": "user_matching_information_sn",
|
|
379
|
+
"parent_table": "user_matching_information",
|
|
380
|
+
"constraint_name": "FK_USER_MATCHING_PROFILE_USER_INFORMATION_SN"
|
|
381
|
+
}
|
|
382
|
+
]
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
324
386
|
**SHOW 명령어 예제**:
|
|
325
387
|
```json
|
|
326
388
|
{
|
|
@@ -342,7 +404,7 @@ MySQL 연결 풀 동작을 더 세밀하게 제어하려면 추가 매개변수
|
|
|
342
404
|
"mcpServers": {
|
|
343
405
|
"mysql": {
|
|
344
406
|
"command": "npx",
|
|
345
|
-
"args": ["@cano721/mysql-mcp-server"],
|
|
407
|
+
"args": ["@cano721/mysql-mcp-server@latest"],
|
|
346
408
|
"env": {
|
|
347
409
|
"MYSQL_HOST": "your-mysql-host",
|
|
348
410
|
"MYSQL_PORT": "3306",
|
|
@@ -436,7 +498,7 @@ Kiro IDE에서 이 MCP 서버를 사용하는 예제:
|
|
|
436
498
|
"mcpServers": {
|
|
437
499
|
"mysql": {
|
|
438
500
|
"command": "npx",
|
|
439
|
-
"args": ["@cano721/mysql-mcp-server"],
|
|
501
|
+
"args": ["@cano721/mysql-mcp-server@latest"],
|
|
440
502
|
"env": {
|
|
441
503
|
"MYSQL_HOST": "localhost",
|
|
442
504
|
"MYSQL_PORT": "4307",
|
|
@@ -459,7 +521,7 @@ IntelliJ IDEA의 GitHub Copilot에서 MCP 서버를 사용하려면:
|
|
|
459
521
|
"servers": {
|
|
460
522
|
"mysql": {
|
|
461
523
|
"command": "npx",
|
|
462
|
-
"args": ["@cano721/mysql-mcp-server"],
|
|
524
|
+
"args": ["@cano721/mysql-mcp-server@latest"],
|
|
463
525
|
"env": {
|
|
464
526
|
"MYSQL_HOST": "localhost",
|
|
465
527
|
"MYSQL_PORT": "4307",
|
|
@@ -502,7 +564,7 @@ Cursor IDE에서 MCP 서버를 사용하려면 `.cursor/mcp.json` 파일을 생
|
|
|
502
564
|
"name": "mysql",
|
|
503
565
|
"type": "command",
|
|
504
566
|
"command": "npx",
|
|
505
|
-
"arguments": ["@cano721/mysql-mcp-server"],
|
|
567
|
+
"arguments": ["@cano721/mysql-mcp-server@latest"],
|
|
506
568
|
"environment": {
|
|
507
569
|
"MYSQL_HOST": "localhost",
|
|
508
570
|
"MYSQL_PORT": "4307",
|
|
@@ -649,7 +711,7 @@ MCP 설정에서 환경 변수가 제대로 설정되었는지 확인:
|
|
|
649
711
|
"mcpServers": {
|
|
650
712
|
"mysql": {
|
|
651
713
|
"command": "npx",
|
|
652
|
-
"args": ["@cano721/mysql-mcp-server"],
|
|
714
|
+
"args": ["@cano721/mysql-mcp-server@latest"],
|
|
653
715
|
"env": {
|
|
654
716
|
"MYSQL_HOST": "localhost",
|
|
655
717
|
"MYSQL_PORT": "4307",
|
|
@@ -705,6 +767,22 @@ export MYSQL_USER=developer
|
|
|
705
767
|
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | npx @cano721/mysql-mcp-server
|
|
706
768
|
```
|
|
707
769
|
|
|
770
|
+
## 변경 이력
|
|
771
|
+
|
|
772
|
+
전체 변경 이력은 [CHANGELOG.md](CHANGELOG.md)를 참조하세요.
|
|
773
|
+
|
|
774
|
+
### 최근 버전
|
|
775
|
+
|
|
776
|
+
| 버전 | 날짜 | 주요 변경 사항 |
|
|
777
|
+
|------|------|---------------|
|
|
778
|
+
| 0.7.0 | 2025-01-09 | `get_related_tables` 도구 추가 (FK 기반 연관 테이블 depth별 조회) |
|
|
779
|
+
| 0.6.0 | 2025-01-09 | `analyze_table` → `analyze_query`로 변경, `@latest` 태그 문서 추가 |
|
|
780
|
+
| 0.5.0 | 2025-01-09 | `explain_query`, `analyze_table` 전용 도구 추가 |
|
|
781
|
+
| 0.4.0 | 2025-01-09 | EXPLAIN/ANALYZE 지원, 연결 풀 기본값 1로 변경 |
|
|
782
|
+
| 0.3.0 | 2025-01-09 | Node.js 18+ 요구사항, 문제 해결 가이드 추가 |
|
|
783
|
+
| 0.2.0 | 2025-01-09 | IntelliJ, Cursor, Kiro IDE 설정 예제 추가 |
|
|
784
|
+
| 0.1.0 | 2025-01-09 | 초기 릴리스 |
|
|
785
|
+
|
|
708
786
|
## 라이선스
|
|
709
787
|
|
|
710
788
|
이 프로젝트는 MIT 라이선스에 따라 라이선스가 부여됩니다 - 자세한 내용은 [LICENSE](LICENSE) 파일을 참조하세요.
|
package/build/index.js
CHANGED
|
@@ -49,6 +49,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
49
49
|
// Check if optional commands are enabled
|
|
50
50
|
const allowExplain = process.env.MYSQL_ALLOW_EXPLAIN !== 'false';
|
|
51
51
|
const allowAnalyze = process.env.MYSQL_ALLOW_ANALYZE !== 'false';
|
|
52
|
+
console.error('[Setup] Security settings:', { allowExplain, allowAnalyze });
|
|
52
53
|
// Build allowed commands description for execute_query
|
|
53
54
|
const allowedCommands = ['SELECT', 'SHOW', 'DESCRIBE'];
|
|
54
55
|
if (allowExplain) {
|
|
@@ -118,6 +119,28 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
118
119
|
},
|
|
119
120
|
required: ["query"]
|
|
120
121
|
}
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "get_related_tables",
|
|
125
|
+
description: "Get all tables related to a specific table through foreign keys (parent and child relationships with depth)",
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: {
|
|
129
|
+
table: {
|
|
130
|
+
type: "string",
|
|
131
|
+
description: "Table name to find related tables for"
|
|
132
|
+
},
|
|
133
|
+
database: {
|
|
134
|
+
type: "string",
|
|
135
|
+
description: "Database name (optional, uses default if not specified)"
|
|
136
|
+
},
|
|
137
|
+
depth: {
|
|
138
|
+
type: "number",
|
|
139
|
+
description: "Maximum depth to traverse relationships (default: 3, warning if > 10)"
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
required: ["table"]
|
|
143
|
+
}
|
|
121
144
|
}
|
|
122
145
|
];
|
|
123
146
|
// Add explain_query tool if enabled
|
|
@@ -149,21 +172,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
149
172
|
// Add analyze_query tool if enabled
|
|
150
173
|
if (allowAnalyze) {
|
|
151
174
|
tools.push({
|
|
152
|
-
name: "
|
|
153
|
-
description: "Analyze
|
|
175
|
+
name: "analyze_query",
|
|
176
|
+
description: "Analyze query performance and statistics using ANALYZE",
|
|
154
177
|
inputSchema: {
|
|
155
178
|
type: "object",
|
|
156
179
|
properties: {
|
|
157
|
-
|
|
180
|
+
query: {
|
|
158
181
|
type: "string",
|
|
159
|
-
description: "
|
|
182
|
+
description: "SQL query to analyze (SELECT, UPDATE, DELETE, INSERT, REPLACE statements)"
|
|
160
183
|
},
|
|
161
184
|
database: {
|
|
162
185
|
type: "string",
|
|
163
186
|
description: "Database name (optional, uses default if not specified)"
|
|
164
187
|
}
|
|
165
188
|
},
|
|
166
|
-
required: ["
|
|
189
|
+
required: ["query"]
|
|
167
190
|
}
|
|
168
191
|
});
|
|
169
192
|
}
|
|
@@ -250,18 +273,133 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
250
273
|
}]
|
|
251
274
|
};
|
|
252
275
|
}
|
|
253
|
-
case "
|
|
254
|
-
console.error('[Tool] Executing
|
|
276
|
+
case "analyze_query": {
|
|
277
|
+
console.error('[Tool] Executing analyze_query');
|
|
278
|
+
const query = request.params.arguments?.query;
|
|
279
|
+
const database = request.params.arguments?.database;
|
|
280
|
+
if (!query) {
|
|
281
|
+
throw new McpError(ErrorCode.InvalidParams, "Query is required");
|
|
282
|
+
}
|
|
283
|
+
// Build ANALYZE query
|
|
284
|
+
const analyzeQuery = `ANALYZE ${query}`;
|
|
285
|
+
const { rows } = await executeQuery(pool, analyzeQuery, [], database);
|
|
286
|
+
return {
|
|
287
|
+
content: [{
|
|
288
|
+
type: "text",
|
|
289
|
+
text: JSON.stringify(rows, null, 2)
|
|
290
|
+
}]
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
case "get_related_tables": {
|
|
294
|
+
console.error('[Tool] Executing get_related_tables');
|
|
255
295
|
const table = request.params.arguments?.table;
|
|
256
296
|
const database = request.params.arguments?.database;
|
|
297
|
+
const requestedDepth = request.params.arguments?.depth || 3;
|
|
257
298
|
if (!table) {
|
|
258
299
|
throw new McpError(ErrorCode.InvalidParams, "Table name is required");
|
|
259
300
|
}
|
|
260
|
-
|
|
301
|
+
// Warning for deep queries
|
|
302
|
+
let warning;
|
|
303
|
+
if (requestedDepth > 10) {
|
|
304
|
+
warning = `⚠️ Warning: depth ${requestedDepth} may take a long time and return a large amount of data.`;
|
|
305
|
+
console.error(`[Warning] Deep query requested: depth=${requestedDepth}`);
|
|
306
|
+
}
|
|
307
|
+
// Get the actual database name
|
|
308
|
+
let dbName = database;
|
|
309
|
+
if (!dbName) {
|
|
310
|
+
const { rows: dbRows } = await executeQuery(pool, 'SELECT DATABASE() as db');
|
|
311
|
+
dbName = dbRows[0]?.db;
|
|
312
|
+
if (!dbName) {
|
|
313
|
+
throw new McpError(ErrorCode.InvalidParams, "Database name is required (no default database set)");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const results = [];
|
|
317
|
+
const visited = new Set();
|
|
318
|
+
const queue = [{ tableName: table, depth: 0 }];
|
|
319
|
+
visited.add(table);
|
|
320
|
+
while (queue.length > 0) {
|
|
321
|
+
const current = queue.shift();
|
|
322
|
+
if (current.depth >= requestedDepth)
|
|
323
|
+
continue;
|
|
324
|
+
// Find child tables (tables that reference the current table)
|
|
325
|
+
const childQuery = `
|
|
326
|
+
SELECT
|
|
327
|
+
TABLE_NAME as child_table,
|
|
328
|
+
COLUMN_NAME as fk_column,
|
|
329
|
+
REFERENCED_TABLE_NAME as parent_table,
|
|
330
|
+
CONSTRAINT_NAME as constraint_name
|
|
331
|
+
FROM information_schema.KEY_COLUMN_USAGE
|
|
332
|
+
WHERE REFERENCED_TABLE_SCHEMA = ?
|
|
333
|
+
AND REFERENCED_TABLE_NAME = ?
|
|
334
|
+
AND REFERENCED_TABLE_NAME IS NOT NULL
|
|
335
|
+
`;
|
|
336
|
+
const { rows: childRows } = await executeQuery(pool, childQuery, [dbName, current.tableName]);
|
|
337
|
+
for (const row of childRows) {
|
|
338
|
+
results.push({
|
|
339
|
+
depth: current.depth + 1,
|
|
340
|
+
child_table: row.child_table,
|
|
341
|
+
fk_column: row.fk_column,
|
|
342
|
+
parent_table: row.parent_table,
|
|
343
|
+
constraint_name: row.constraint_name
|
|
344
|
+
});
|
|
345
|
+
if (!visited.has(row.child_table)) {
|
|
346
|
+
visited.add(row.child_table);
|
|
347
|
+
queue.push({ tableName: row.child_table, depth: current.depth + 1 });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Find parent tables (tables that the current table references)
|
|
351
|
+
const parentQuery = `
|
|
352
|
+
SELECT
|
|
353
|
+
TABLE_NAME as child_table,
|
|
354
|
+
COLUMN_NAME as fk_column,
|
|
355
|
+
REFERENCED_TABLE_NAME as parent_table,
|
|
356
|
+
CONSTRAINT_NAME as constraint_name
|
|
357
|
+
FROM information_schema.KEY_COLUMN_USAGE
|
|
358
|
+
WHERE TABLE_SCHEMA = ?
|
|
359
|
+
AND TABLE_NAME = ?
|
|
360
|
+
AND REFERENCED_TABLE_NAME IS NOT NULL
|
|
361
|
+
`;
|
|
362
|
+
const { rows: parentRows } = await executeQuery(pool, parentQuery, [dbName, current.tableName]);
|
|
363
|
+
for (const row of parentRows) {
|
|
364
|
+
// Only add if not already in results (avoid duplicates)
|
|
365
|
+
const exists = results.some(r => r.child_table === row.child_table &&
|
|
366
|
+
r.parent_table === row.parent_table &&
|
|
367
|
+
r.fk_column === row.fk_column);
|
|
368
|
+
if (!exists) {
|
|
369
|
+
results.push({
|
|
370
|
+
depth: current.depth + 1,
|
|
371
|
+
child_table: row.child_table,
|
|
372
|
+
fk_column: row.fk_column,
|
|
373
|
+
parent_table: row.parent_table,
|
|
374
|
+
constraint_name: row.constraint_name
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
if (!visited.has(row.parent_table)) {
|
|
378
|
+
visited.add(row.parent_table);
|
|
379
|
+
queue.push({ tableName: row.parent_table, depth: current.depth + 1 });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// Sort by depth, then by child_table
|
|
384
|
+
results.sort((a, b) => {
|
|
385
|
+
if (a.depth !== b.depth)
|
|
386
|
+
return a.depth - b.depth;
|
|
387
|
+
return a.child_table.localeCompare(b.child_table);
|
|
388
|
+
});
|
|
389
|
+
const response = {
|
|
390
|
+
root_table: table,
|
|
391
|
+
database: dbName,
|
|
392
|
+
requested_depth: requestedDepth,
|
|
393
|
+
total_relations: results.length,
|
|
394
|
+
relations: results
|
|
395
|
+
};
|
|
396
|
+
if (warning) {
|
|
397
|
+
response.warning = warning;
|
|
398
|
+
}
|
|
261
399
|
return {
|
|
262
400
|
content: [{
|
|
263
401
|
type: "text",
|
|
264
|
-
text: JSON.stringify(
|
|
402
|
+
text: JSON.stringify(response, null, 2)
|
|
265
403
|
}]
|
|
266
404
|
};
|
|
267
405
|
}
|