@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.
Files changed (3) hide show
  1. package/README.md +107 -11
  2. package/build/index.js +185 -0
  3. 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
- ### analyze_table
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
- 테이블 통계를 분석합니다 (MYSQL_ALLOW_ANALYZE=true 필요).
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": "analyze_table",
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": "users"
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cano721/mysql-mcp-server",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "An MCP server that provides read-only access to MySQL databases.",
5
5
  "type": "module",
6
6
  "bin": {