@cano721/mysql-mcp-server 0.6.0 → 0.8.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 +107 -11
- package/build/index.js +185 -0
- 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,91 @@ 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 초과 시 경고)
|
|
346
|
+
- `include_pattern_match` (선택사항): 컬럼명 패턴 매칭 포함 여부 (기본값: false)
|
|
347
|
+
|
|
348
|
+
**검색 방법**:
|
|
349
|
+
1. **FK 제약조건 기반** (기본): 실제 Foreign Key가 설정된 테이블만 조회
|
|
350
|
+
2. **패턴 매칭 포함**: `user_sn`, `user_id` 같은 컬럼명 패턴으로 추가 테이블 탐색
|
|
311
351
|
|
|
312
352
|
**예제**:
|
|
313
353
|
```json
|
|
314
354
|
{
|
|
315
355
|
"server_name": "mysql",
|
|
316
|
-
"tool_name": "
|
|
356
|
+
"tool_name": "get_related_tables",
|
|
357
|
+
"arguments": {
|
|
358
|
+
"database": "my_database",
|
|
359
|
+
"table": "user",
|
|
360
|
+
"depth": 2
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
**패턴 매칭 포함 예제**:
|
|
366
|
+
```json
|
|
367
|
+
{
|
|
368
|
+
"server_name": "mysql",
|
|
369
|
+
"tool_name": "get_related_tables",
|
|
317
370
|
"arguments": {
|
|
318
371
|
"database": "my_database",
|
|
319
|
-
"table": "
|
|
372
|
+
"table": "user",
|
|
373
|
+
"depth": 2,
|
|
374
|
+
"include_pattern_match": true
|
|
320
375
|
}
|
|
321
376
|
}
|
|
322
377
|
```
|
|
323
378
|
|
|
379
|
+
**응답 예시**:
|
|
380
|
+
```json
|
|
381
|
+
{
|
|
382
|
+
"root_table": "user",
|
|
383
|
+
"database": "my_database",
|
|
384
|
+
"requested_depth": 2,
|
|
385
|
+
"search_method": "fk_constraint",
|
|
386
|
+
"fk_relations_count": 55,
|
|
387
|
+
"pattern_match_count": 0,
|
|
388
|
+
"total_relations": 55,
|
|
389
|
+
"fk_relations": [
|
|
390
|
+
{
|
|
391
|
+
"depth": 1,
|
|
392
|
+
"child_table": "user_matching_information",
|
|
393
|
+
"fk_column": "user_sn",
|
|
394
|
+
"parent_table": "user",
|
|
395
|
+
"constraint_name": "FK_USER_MATCHING_INFORMATION_USER_SN",
|
|
396
|
+
"match_type": "fk_constraint"
|
|
397
|
+
}
|
|
398
|
+
],
|
|
399
|
+
"note": "FK 제약조건 기반으로 조회되었습니다. 패턴 매칭도 포함하려면 '패턴 매칭도 포함해줘'라고 요청해보세요."
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
324
403
|
**SHOW 명령어 예제**:
|
|
325
404
|
```json
|
|
326
405
|
{
|
|
@@ -342,7 +421,7 @@ MySQL 연결 풀 동작을 더 세밀하게 제어하려면 추가 매개변수
|
|
|
342
421
|
"mcpServers": {
|
|
343
422
|
"mysql": {
|
|
344
423
|
"command": "npx",
|
|
345
|
-
"args": ["@cano721/mysql-mcp-server"],
|
|
424
|
+
"args": ["@cano721/mysql-mcp-server@latest"],
|
|
346
425
|
"env": {
|
|
347
426
|
"MYSQL_HOST": "your-mysql-host",
|
|
348
427
|
"MYSQL_PORT": "3306",
|
|
@@ -436,7 +515,7 @@ Kiro IDE에서 이 MCP 서버를 사용하는 예제:
|
|
|
436
515
|
"mcpServers": {
|
|
437
516
|
"mysql": {
|
|
438
517
|
"command": "npx",
|
|
439
|
-
"args": ["@cano721/mysql-mcp-server"],
|
|
518
|
+
"args": ["@cano721/mysql-mcp-server@latest"],
|
|
440
519
|
"env": {
|
|
441
520
|
"MYSQL_HOST": "localhost",
|
|
442
521
|
"MYSQL_PORT": "4307",
|
|
@@ -459,7 +538,7 @@ IntelliJ IDEA의 GitHub Copilot에서 MCP 서버를 사용하려면:
|
|
|
459
538
|
"servers": {
|
|
460
539
|
"mysql": {
|
|
461
540
|
"command": "npx",
|
|
462
|
-
"args": ["@cano721/mysql-mcp-server"],
|
|
541
|
+
"args": ["@cano721/mysql-mcp-server@latest"],
|
|
463
542
|
"env": {
|
|
464
543
|
"MYSQL_HOST": "localhost",
|
|
465
544
|
"MYSQL_PORT": "4307",
|
|
@@ -502,7 +581,7 @@ Cursor IDE에서 MCP 서버를 사용하려면 `.cursor/mcp.json` 파일을 생
|
|
|
502
581
|
"name": "mysql",
|
|
503
582
|
"type": "command",
|
|
504
583
|
"command": "npx",
|
|
505
|
-
"arguments": ["@cano721/mysql-mcp-server"],
|
|
584
|
+
"arguments": ["@cano721/mysql-mcp-server@latest"],
|
|
506
585
|
"environment": {
|
|
507
586
|
"MYSQL_HOST": "localhost",
|
|
508
587
|
"MYSQL_PORT": "4307",
|
|
@@ -649,7 +728,7 @@ MCP 설정에서 환경 변수가 제대로 설정되었는지 확인:
|
|
|
649
728
|
"mcpServers": {
|
|
650
729
|
"mysql": {
|
|
651
730
|
"command": "npx",
|
|
652
|
-
"args": ["@cano721/mysql-mcp-server"],
|
|
731
|
+
"args": ["@cano721/mysql-mcp-server@latest"],
|
|
653
732
|
"env": {
|
|
654
733
|
"MYSQL_HOST": "localhost",
|
|
655
734
|
"MYSQL_PORT": "4307",
|
|
@@ -705,6 +784,23 @@ export MYSQL_USER=developer
|
|
|
705
784
|
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | npx @cano721/mysql-mcp-server
|
|
706
785
|
```
|
|
707
786
|
|
|
787
|
+
## 변경 이력
|
|
788
|
+
|
|
789
|
+
전체 변경 이력은 [CHANGELOG.md](CHANGELOG.md)를 참조하세요.
|
|
790
|
+
|
|
791
|
+
### 최근 버전
|
|
792
|
+
|
|
793
|
+
| 버전 | 날짜 | 주요 변경 사항 |
|
|
794
|
+
|------|------|---------------|
|
|
795
|
+
| 0.8.0 | 2025-01-09 | `get_related_tables`에 패턴 매칭 옵션 추가, depth 제한 제거 |
|
|
796
|
+
| 0.7.0 | 2025-01-09 | `get_related_tables` 도구 추가 (FK 기반 연관 테이블 depth별 조회) |
|
|
797
|
+
| 0.6.0 | 2025-01-09 | `analyze_table` → `analyze_query`로 변경, `@latest` 태그 문서 추가 |
|
|
798
|
+
| 0.5.0 | 2025-01-09 | `explain_query`, `analyze_table` 전용 도구 추가 |
|
|
799
|
+
| 0.4.0 | 2025-01-09 | EXPLAIN/ANALYZE 지원, 연결 풀 기본값 1로 변경 |
|
|
800
|
+
| 0.3.0 | 2025-01-09 | Node.js 18+ 요구사항, 문제 해결 가이드 추가 |
|
|
801
|
+
| 0.2.0 | 2025-01-09 | IntelliJ, Cursor, Kiro IDE 설정 예제 추가 |
|
|
802
|
+
| 0.1.0 | 2025-01-09 | 초기 릴리스 |
|
|
803
|
+
|
|
708
804
|
## 라이선스
|
|
709
805
|
|
|
710
806
|
이 프로젝트는 MIT 라이선스에 따라 라이선스가 부여됩니다 - 자세한 내용은 [LICENSE](LICENSE) 파일을 참조하세요.
|
package/build/index.js
CHANGED
|
@@ -119,6 +119,32 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
119
119
|
},
|
|
120
120
|
required: ["query"]
|
|
121
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). Results are based on FK constraints only. Set include_pattern_match=true to also find tables by column name patterns.",
|
|
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
|
+
include_pattern_match: {
|
|
142
|
+
type: "boolean",
|
|
143
|
+
description: "Include tables found by column name pattern matching (e.g., user_sn, user_id). Default: false"
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
required: ["table"]
|
|
147
|
+
}
|
|
122
148
|
}
|
|
123
149
|
];
|
|
124
150
|
// Add explain_query tool if enabled
|
|
@@ -268,6 +294,165 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
268
294
|
}]
|
|
269
295
|
};
|
|
270
296
|
}
|
|
297
|
+
case "get_related_tables": {
|
|
298
|
+
console.error('[Tool] Executing get_related_tables');
|
|
299
|
+
const table = request.params.arguments?.table;
|
|
300
|
+
const database = request.params.arguments?.database;
|
|
301
|
+
const requestedDepth = request.params.arguments?.depth || 3;
|
|
302
|
+
const includePatternMatch = request.params.arguments?.include_pattern_match || false;
|
|
303
|
+
if (!table) {
|
|
304
|
+
throw new McpError(ErrorCode.InvalidParams, "Table name is required");
|
|
305
|
+
}
|
|
306
|
+
// Warning for deep queries
|
|
307
|
+
let warning;
|
|
308
|
+
if (requestedDepth > 10) {
|
|
309
|
+
warning = `⚠️ Warning: depth ${requestedDepth} may take a long time and return a large amount of data.`;
|
|
310
|
+
console.error(`[Warning] Deep query requested: depth=${requestedDepth}`);
|
|
311
|
+
}
|
|
312
|
+
// Get the actual database name
|
|
313
|
+
let dbName = database;
|
|
314
|
+
if (!dbName) {
|
|
315
|
+
const { rows: dbRows } = await executeQuery(pool, 'SELECT DATABASE() as db');
|
|
316
|
+
dbName = dbRows[0]?.db;
|
|
317
|
+
if (!dbName) {
|
|
318
|
+
throw new McpError(ErrorCode.InvalidParams, "Database name is required (no default database set)");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const results = [];
|
|
322
|
+
const visited = new Set();
|
|
323
|
+
const queue = [{ tableName: table, depth: 0 }];
|
|
324
|
+
visited.add(table);
|
|
325
|
+
while (queue.length > 0) {
|
|
326
|
+
const current = queue.shift();
|
|
327
|
+
if (current.depth >= requestedDepth)
|
|
328
|
+
continue;
|
|
329
|
+
// Find child tables (tables that reference the current table)
|
|
330
|
+
const childQuery = `
|
|
331
|
+
SELECT
|
|
332
|
+
TABLE_NAME as child_table,
|
|
333
|
+
COLUMN_NAME as fk_column,
|
|
334
|
+
REFERENCED_TABLE_NAME as parent_table,
|
|
335
|
+
CONSTRAINT_NAME as constraint_name
|
|
336
|
+
FROM information_schema.KEY_COLUMN_USAGE
|
|
337
|
+
WHERE REFERENCED_TABLE_SCHEMA = ?
|
|
338
|
+
AND REFERENCED_TABLE_NAME = ?
|
|
339
|
+
AND REFERENCED_TABLE_NAME IS NOT NULL
|
|
340
|
+
`;
|
|
341
|
+
const { rows: childRows } = await executeQuery(pool, childQuery, [dbName, current.tableName]);
|
|
342
|
+
for (const row of childRows) {
|
|
343
|
+
results.push({
|
|
344
|
+
depth: current.depth + 1,
|
|
345
|
+
child_table: row.child_table,
|
|
346
|
+
fk_column: row.fk_column,
|
|
347
|
+
parent_table: row.parent_table,
|
|
348
|
+
constraint_name: row.constraint_name,
|
|
349
|
+
match_type: 'fk_constraint'
|
|
350
|
+
});
|
|
351
|
+
if (!visited.has(row.child_table)) {
|
|
352
|
+
visited.add(row.child_table);
|
|
353
|
+
queue.push({ tableName: row.child_table, depth: current.depth + 1 });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Find parent tables (tables that the current table references)
|
|
357
|
+
const parentQuery = `
|
|
358
|
+
SELECT
|
|
359
|
+
TABLE_NAME as child_table,
|
|
360
|
+
COLUMN_NAME as fk_column,
|
|
361
|
+
REFERENCED_TABLE_NAME as parent_table,
|
|
362
|
+
CONSTRAINT_NAME as constraint_name
|
|
363
|
+
FROM information_schema.KEY_COLUMN_USAGE
|
|
364
|
+
WHERE TABLE_SCHEMA = ?
|
|
365
|
+
AND TABLE_NAME = ?
|
|
366
|
+
AND REFERENCED_TABLE_NAME IS NOT NULL
|
|
367
|
+
`;
|
|
368
|
+
const { rows: parentRows } = await executeQuery(pool, parentQuery, [dbName, current.tableName]);
|
|
369
|
+
for (const row of parentRows) {
|
|
370
|
+
// Only add if not already in results (avoid duplicates)
|
|
371
|
+
const exists = results.some(r => r.child_table === row.child_table &&
|
|
372
|
+
r.parent_table === row.parent_table &&
|
|
373
|
+
r.fk_column === row.fk_column);
|
|
374
|
+
if (!exists) {
|
|
375
|
+
results.push({
|
|
376
|
+
depth: current.depth + 1,
|
|
377
|
+
child_table: row.child_table,
|
|
378
|
+
fk_column: row.fk_column,
|
|
379
|
+
parent_table: row.parent_table,
|
|
380
|
+
constraint_name: row.constraint_name,
|
|
381
|
+
match_type: 'fk_constraint'
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
if (!visited.has(row.parent_table)) {
|
|
385
|
+
visited.add(row.parent_table);
|
|
386
|
+
queue.push({ tableName: row.parent_table, depth: current.depth + 1 });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// Sort by depth, then by child_table
|
|
391
|
+
results.sort((a, b) => {
|
|
392
|
+
if (a.depth !== b.depth)
|
|
393
|
+
return a.depth - b.depth;
|
|
394
|
+
return a.child_table.localeCompare(b.child_table);
|
|
395
|
+
});
|
|
396
|
+
// Pattern matching for tables without FK constraints
|
|
397
|
+
let patternMatchResults = [];
|
|
398
|
+
if (includePatternMatch) {
|
|
399
|
+
console.error('[Tool] Including pattern match results');
|
|
400
|
+
// Common patterns: table_sn, table_id, table_code, sn_table, id_table
|
|
401
|
+
const patterns = [
|
|
402
|
+
`${table}_sn`, `${table}_id`, `${table}_code`,
|
|
403
|
+
`sn_${table}`, `id_${table}`,
|
|
404
|
+
`${table}sn`, `${table}id`
|
|
405
|
+
];
|
|
406
|
+
const patternQuery = `
|
|
407
|
+
SELECT DISTINCT
|
|
408
|
+
c.TABLE_NAME as related_table,
|
|
409
|
+
c.COLUMN_NAME as matching_column
|
|
410
|
+
FROM information_schema.COLUMNS c
|
|
411
|
+
WHERE c.TABLE_SCHEMA = ?
|
|
412
|
+
AND c.TABLE_NAME != ?
|
|
413
|
+
AND (${patterns.map(() => 'LOWER(c.COLUMN_NAME) = LOWER(?)').join(' OR ')})
|
|
414
|
+
`;
|
|
415
|
+
const { rows: patternRows } = await executeQuery(pool, patternQuery, [dbName, table, ...patterns]);
|
|
416
|
+
// Filter out tables already found via FK
|
|
417
|
+
const fkTables = new Set(results.map(r => r.child_table));
|
|
418
|
+
fkTables.add(table);
|
|
419
|
+
for (const row of patternRows) {
|
|
420
|
+
if (!fkTables.has(row.related_table)) {
|
|
421
|
+
patternMatchResults.push({
|
|
422
|
+
depth: 1,
|
|
423
|
+
child_table: row.related_table,
|
|
424
|
+
fk_column: row.matching_column,
|
|
425
|
+
parent_table: table,
|
|
426
|
+
constraint_name: '(pattern match)',
|
|
427
|
+
match_type: 'pattern_match'
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const response = {
|
|
433
|
+
root_table: table,
|
|
434
|
+
database: dbName,
|
|
435
|
+
requested_depth: requestedDepth,
|
|
436
|
+
search_method: includePatternMatch ? 'fk_constraint + pattern_match' : 'fk_constraint',
|
|
437
|
+
fk_relations_count: results.length,
|
|
438
|
+
pattern_match_count: patternMatchResults.length,
|
|
439
|
+
total_relations: results.length + patternMatchResults.length,
|
|
440
|
+
fk_relations: results,
|
|
441
|
+
pattern_match_relations: includePatternMatch ? patternMatchResults : undefined,
|
|
442
|
+
note: includePatternMatch
|
|
443
|
+
? "FK 제약조건 + 컬럼명 패턴 매칭 결과입니다."
|
|
444
|
+
: "FK 제약조건 기반으로 조회되었습니다. 패턴 매칭도 포함하려면 '패턴 매칭도 포함해줘'라고 요청해보세요."
|
|
445
|
+
};
|
|
446
|
+
if (warning) {
|
|
447
|
+
response.warning = warning;
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
content: [{
|
|
451
|
+
type: "text",
|
|
452
|
+
text: JSON.stringify(response, null, 2)
|
|
453
|
+
}]
|
|
454
|
+
};
|
|
455
|
+
}
|
|
271
456
|
default:
|
|
272
457
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
273
458
|
}
|