@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.
Files changed (3) hide show
  1. package/README.md +89 -11
  2. package/build/index.js +147 -9
  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,74 @@ 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 초과 시 경고)
311
346
 
312
347
  **예제**:
313
348
  ```json
314
349
  {
315
350
  "server_name": "mysql",
316
- "tool_name": "analyze_table",
351
+ "tool_name": "get_related_tables",
317
352
  "arguments": {
318
353
  "database": "my_database",
319
- "table": "users"
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: "analyze_table",
153
- description: "Analyze table statistics for query optimization",
175
+ name: "analyze_query",
176
+ description: "Analyze query performance and statistics using ANALYZE",
154
177
  inputSchema: {
155
178
  type: "object",
156
179
  properties: {
157
- table: {
180
+ query: {
158
181
  type: "string",
159
- description: "Table name to analyze"
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: ["table"]
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 "analyze_table": {
254
- console.error('[Tool] Executing analyze_table');
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
- const { rows } = await executeQuery(pool, `ANALYZE TABLE \`${table}\``, [], database);
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(rows, null, 2)
402
+ text: JSON.stringify(response, null, 2)
265
403
  }]
266
404
  };
267
405
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cano721/mysql-mcp-server",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "An MCP server that provides read-only access to MySQL databases.",
5
5
  "type": "module",
6
6
  "bin": {