mcp-dbutils 0.16.0__py3-none-any.whl → 0.16.1__py3-none-any.whl

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.
mcp_dbutils/base.py CHANGED
@@ -299,6 +299,64 @@ class ConnectionServer:
299
299
  self.send_log(LOG_LEVEL_ERROR, f"Error in list_prompts: {str(e)}")
300
300
  raise
301
301
 
302
+ def _get_config_or_raise(self, connection: str) -> dict:
303
+ """读取配置文件并验证连接配置
304
+
305
+ Args:
306
+ connection: 连接名称
307
+
308
+ Returns:
309
+ dict: 连接配置
310
+
311
+ Raises:
312
+ ConfigurationError: 如果配置文件格式不正确或连接不存在
313
+ """
314
+ with open(self.config_path, 'r') as f:
315
+ config = yaml.safe_load(f)
316
+ if not config or 'connections' not in config:
317
+ raise ConfigurationError("Configuration file must contain 'connections' section")
318
+ if connection not in config['connections']:
319
+ available_connections = list(config['connections'].keys())
320
+ raise ConfigurationError(f"Connection not found: {connection}. Available connections: {available_connections}")
321
+
322
+ db_config = config['connections'][connection]
323
+
324
+ if 'type' not in db_config:
325
+ raise ConfigurationError("Database configuration must include 'type' field")
326
+
327
+ return db_config
328
+
329
+ def _create_handler_for_type(self, db_type: str, connection: str) -> ConnectionHandler:
330
+ """基于数据库类型创建相应的处理器
331
+
332
+ Args:
333
+ db_type: 数据库类型
334
+ connection: 连接名称
335
+
336
+ Returns:
337
+ ConnectionHandler: 数据库连接处理器
338
+
339
+ Raises:
340
+ ConfigurationError: 如果数据库类型不支持或导入失败
341
+ """
342
+ self.send_log(LOG_LEVEL_DEBUG, f"Creating handler for database type: {db_type}")
343
+
344
+ try:
345
+ if db_type == 'sqlite':
346
+ from .sqlite.handler import SQLiteHandler
347
+ return SQLiteHandler(self.config_path, connection, self.debug)
348
+ elif db_type == 'postgres':
349
+ from .postgres.handler import PostgreSQLHandler
350
+ return PostgreSQLHandler(self.config_path, connection, self.debug)
351
+ elif db_type == 'mysql':
352
+ from .mysql.handler import MySQLHandler
353
+ return MySQLHandler(self.config_path, connection, self.debug)
354
+ else:
355
+ raise ConfigurationError(f"Unsupported database type: {db_type}")
356
+ except ImportError as e:
357
+ # 捕获导入错误并转换为ConfigurationError,以保持与现有测试兼容
358
+ raise ConfigurationError(f"Failed to import handler for {db_type}: {str(e)}")
359
+
302
360
  @asynccontextmanager
303
361
  async def get_handler(self, connection: str) -> AsyncContextManager[ConnectionHandler]:
304
362
  """Get connection handler
@@ -311,74 +369,385 @@ class ConnectionServer:
311
369
  Returns:
312
370
  AsyncContextManager[ConnectionHandler]: Context manager for connection handler
313
371
  """
314
- # Read configuration file to determine database type
315
- with open(self.config_path, 'r') as f:
316
- config = yaml.safe_load(f)
317
- if not config or 'connections' not in config:
318
- raise ConfigurationError("Configuration file must contain 'connections' section")
319
- if connection not in config['connections']:
320
- available_connections = list(config['connections'].keys())
321
- raise ConfigurationError(f"Connection not found: {connection}. Available connections: {available_connections}")
372
+ # Read configuration file and validate connection
373
+ db_config = self._get_config_or_raise(connection)
374
+
375
+ # Create appropriate handler based on database type
376
+ handler = None
377
+ try:
378
+ db_type = db_config['type']
379
+ handler = self._create_handler_for_type(db_type, connection)
322
380
 
323
- db_config = config['connections'][connection]
381
+ # Set session for MCP logging
382
+ if hasattr(self.server, 'session'):
383
+ handler._session = self.server.session
324
384
 
325
- handler = None
326
- try:
327
- if 'type' not in db_config:
328
- raise ConfigurationError("Database configuration must include 'type' field")
329
-
330
- db_type = db_config['type']
331
- self.send_log(LOG_LEVEL_DEBUG, f"Creating handler for database type: {db_type}")
332
- if db_type == 'sqlite':
333
- from .sqlite.handler import SQLiteHandler
334
- handler = SQLiteHandler(self.config_path, connection, self.debug)
335
- elif db_type == 'postgres':
336
- from .postgres.handler import PostgreSQLHandler
337
- handler = PostgreSQLHandler(self.config_path, connection, self.debug)
338
- elif db_type == 'mysql':
339
- from .mysql.handler import MySQLHandler
340
- handler = MySQLHandler(self.config_path, connection, self.debug)
341
- else:
342
- raise ConfigurationError(f"Unsupported database type: {db_type}")
343
-
344
- # Set session for MCP logging
345
- if hasattr(self.server, 'session'):
346
- handler._session = self.server.session
347
-
348
- handler.stats.record_connection_start()
349
- self.send_log(LOG_LEVEL_DEBUG, f"Handler created successfully for {connection}")
350
- # 使用通用的方式处理统计信息序列化
385
+ handler.stats.record_connection_start()
386
+ self.send_log(LOG_LEVEL_DEBUG, f"Handler created successfully for {connection}")
387
+
388
+ yield handler
389
+ finally:
390
+ if handler:
391
+ self.send_log(LOG_LEVEL_DEBUG, f"Cleaning up handler for {connection}")
392
+ handler.stats.record_connection_end()
393
+
394
+ if hasattr(handler, 'cleanup') and callable(handler.cleanup):
395
+ await handler.cleanup()
396
+
397
+ def _get_available_tools(self) -> list[types.Tool]:
398
+ """返回所有可用的数据库工具列表
399
+
400
+ Returns:
401
+ list[types.Tool]: 工具列表
402
+ """
403
+ return [
404
+ types.Tool(
405
+ name="dbutils-run-query",
406
+ description="Execute read-only SQL query on database connection",
407
+ inputSchema={
408
+ "type": "object",
409
+ "properties": {
410
+ "connection": {
411
+ "type": "string",
412
+ "description": DATABASE_CONNECTION_NAME
413
+ },
414
+ "sql": {
415
+ "type": "string",
416
+ "description": "SQL query (SELECT only)"
417
+ }
418
+ },
419
+ "required": ["connection", "sql"]
420
+ }
421
+ ),
422
+ types.Tool(
423
+ name="dbutils-list-tables",
424
+ description="List all available tables in the specified database connection",
425
+ inputSchema={
426
+ "type": "object",
427
+ "properties": {
428
+ "connection": {
429
+ "type": "string",
430
+ "description": DATABASE_CONNECTION_NAME
431
+ }
432
+ },
433
+ "required": ["connection"]
434
+ }
435
+ ),
436
+ types.Tool(
437
+ name="dbutils-describe-table",
438
+ description="Get detailed information about a table's structure",
439
+ inputSchema={
440
+ "type": "object",
441
+ "properties": {
442
+ "connection": {
443
+ "type": "string",
444
+ "description": DATABASE_CONNECTION_NAME
445
+ },
446
+ "table": {
447
+ "type": "string",
448
+ "description": "Table name to describe"
449
+ }
450
+ },
451
+ "required": ["connection", "table"]
452
+ }
453
+ ),
454
+ types.Tool(
455
+ name="dbutils-get-ddl",
456
+ description="Get DDL statement for creating the table",
457
+ inputSchema={
458
+ "type": "object",
459
+ "properties": {
460
+ "connection": {
461
+ "type": "string",
462
+ "description": DATABASE_CONNECTION_NAME
463
+ },
464
+ "table": {
465
+ "type": "string",
466
+ "description": "Table name to get DDL for"
467
+ }
468
+ },
469
+ "required": ["connection", "table"]
470
+ }
471
+ ),
472
+ types.Tool(
473
+ name="dbutils-list-indexes",
474
+ description="List all indexes on the specified table",
475
+ inputSchema={
476
+ "type": "object",
477
+ "properties": {
478
+ "connection": {
479
+ "type": "string",
480
+ "description": DATABASE_CONNECTION_NAME
481
+ },
482
+ "table": {
483
+ "type": "string",
484
+ "description": "Table name to list indexes for"
485
+ }
486
+ },
487
+ "required": ["connection", "table"]
488
+ }
489
+ ),
490
+ types.Tool(
491
+ name="dbutils-get-stats",
492
+ description="Get table statistics like row count and size",
493
+ inputSchema={
494
+ "type": "object",
495
+ "properties": {
496
+ "connection": {
497
+ "type": "string",
498
+ "description": DATABASE_CONNECTION_NAME
499
+ },
500
+ "table": {
501
+ "type": "string",
502
+ "description": "Table name to get statistics for"
503
+ }
504
+ },
505
+ "required": ["connection", "table"]
506
+ }
507
+ ),
508
+ types.Tool(
509
+ name="dbutils-list-constraints",
510
+ description="List all constraints (primary key, foreign keys, etc) on the table",
511
+ inputSchema={
512
+ "type": "object",
513
+ "properties": {
514
+ "connection": {
515
+ "type": "string",
516
+ "description": DATABASE_CONNECTION_NAME
517
+ },
518
+ "table": {
519
+ "type": "string",
520
+ "description": "Table name to list constraints for"
521
+ }
522
+ },
523
+ "required": ["connection", "table"]
524
+ }
525
+ ),
526
+ types.Tool(
527
+ name="dbutils-explain-query",
528
+ description="Get execution plan for a SQL query",
529
+ inputSchema={
530
+ "type": "object",
531
+ "properties": {
532
+ "connection": {
533
+ "type": "string",
534
+ "description": DATABASE_CONNECTION_NAME
535
+ },
536
+ "sql": {
537
+ "type": "string",
538
+ "description": "SQL query to explain"
539
+ }
540
+ },
541
+ "required": ["connection", "sql"]
542
+ }
543
+ ),
544
+ types.Tool(
545
+ name="dbutils-get-performance",
546
+ description="Get database performance statistics",
547
+ inputSchema={
548
+ "type": "object",
549
+ "properties": {
550
+ "connection": {
551
+ "type": "string",
552
+ "description": DATABASE_CONNECTION_NAME
553
+ }
554
+ },
555
+ "required": ["connection"]
556
+ }
557
+ ),
558
+ types.Tool(
559
+ name="dbutils-analyze-query",
560
+ description="Analyze a SQL query for performance",
561
+ inputSchema={
562
+ "type": "object",
563
+ "properties": {
564
+ "connection": {
565
+ "type": "string",
566
+ "description": DATABASE_CONNECTION_NAME
567
+ },
568
+ "sql": {
569
+ "type": "string",
570
+ "description": "SQL query to analyze"
571
+ }
572
+ },
573
+ "required": ["connection", "sql"]
574
+ }
575
+ )
576
+ ]
577
+
578
+ async def _handle_list_tables(self, connection: str) -> list[types.TextContent]:
579
+ """处理列表表格工具调用
580
+
581
+ Args:
582
+ connection: 数据库连接名称
583
+
584
+ Returns:
585
+ list[types.TextContent]: 表格列表
586
+ """
587
+ async with self.get_handler(connection) as handler:
588
+ tables = await handler.get_tables()
589
+ if not tables:
590
+ # 空表列表的情况也返回数据库类型
591
+ return [types.TextContent(type="text", text=f"[{handler.db_type}] No tables found")]
592
+
593
+ formatted_tables = "\n".join([
594
+ f"Table: {table.name}\n" +
595
+ f"URI: {table.uri}\n" +
596
+ (f"Description: {table.description}\n" if table.description else "") +
597
+ "---"
598
+ for table in tables
599
+ ])
600
+ # 添加数据库类型前缀
601
+ return [types.TextContent(type="text", text=f"[{handler.db_type}]\n{formatted_tables}")]
602
+
603
+ async def _handle_run_query(self, connection: str, sql: str) -> list[types.TextContent]:
604
+ """处理运行查询工具调用
605
+
606
+ Args:
607
+ connection: 数据库连接名称
608
+ sql: SQL查询语句
609
+
610
+ Returns:
611
+ list[types.TextContent]: 查询结果
612
+
613
+ Raises:
614
+ ConfigurationError: 如果SQL为空或非SELECT语句
615
+ """
616
+ if not sql:
617
+ raise ConfigurationError(EMPTY_QUERY_ERROR)
618
+
619
+ # Only allow SELECT statements
620
+ if not sql.lower().startswith("select"):
621
+ raise ConfigurationError(SELECT_ONLY_ERROR)
622
+
623
+ async with self.get_handler(connection) as handler:
624
+ result = await handler.execute_query(sql)
625
+ return [types.TextContent(type="text", text=result)]
626
+
627
+ async def _handle_table_tools(self, name: str, connection: str, table: str) -> list[types.TextContent]:
628
+ """处理表相关工具调用
629
+
630
+ Args:
631
+ name: 工具名称
632
+ connection: 数据库连接名称
633
+ table: 表名
634
+
635
+ Returns:
636
+ list[types.TextContent]: 工具执行结果
637
+
638
+ Raises:
639
+ ConfigurationError: 如果表名为空
640
+ """
641
+ if not table:
642
+ raise ConfigurationError(EMPTY_TABLE_NAME_ERROR)
643
+
644
+ async with self.get_handler(connection) as handler:
645
+ result = await handler.execute_tool_query(name, table_name=table)
646
+ return [types.TextContent(type="text", text=result)]
647
+
648
+ async def _handle_explain_query(self, connection: str, sql: str) -> list[types.TextContent]:
649
+ """处理解释查询工具调用
650
+
651
+ Args:
652
+ connection: 数据库连接名称
653
+ sql: SQL查询语句
654
+
655
+ Returns:
656
+ list[types.TextContent]: 查询解释
657
+
658
+ Raises:
659
+ ConfigurationError: 如果SQL为空
660
+ """
661
+ if not sql:
662
+ raise ConfigurationError(EMPTY_QUERY_ERROR)
663
+
664
+ async with self.get_handler(connection) as handler:
665
+ result = await handler.execute_tool_query("dbutils-explain-query", sql=sql)
666
+ return [types.TextContent(type="text", text=result)]
667
+
668
+ async def _handle_performance(self, connection: str) -> list[types.TextContent]:
669
+ """处理性能统计工具调用
670
+
671
+ Args:
672
+ connection: 数据库连接名称
673
+
674
+ Returns:
675
+ list[types.TextContent]: 性能统计
676
+ """
677
+ async with self.get_handler(connection) as handler:
678
+ performance_stats = handler.stats.get_performance_stats()
679
+ return [types.TextContent(type="text", text=f"[{handler.db_type}]\n{performance_stats}")]
680
+
681
+ async def _handle_analyze_query(self, connection: str, sql: str) -> list[types.TextContent]:
682
+ """处理查询分析工具调用
683
+
684
+ Args:
685
+ connection: 数据库连接名称
686
+ sql: SQL查询语句
687
+
688
+ Returns:
689
+ list[types.TextContent]: 查询分析结果
690
+
691
+ Raises:
692
+ ConfigurationError: 如果SQL为空
693
+ """
694
+ if not sql:
695
+ raise ConfigurationError(EMPTY_QUERY_ERROR)
696
+
697
+ async with self.get_handler(connection) as handler:
698
+ # First get the execution plan
699
+ explain_result = await handler.explain_query(sql)
700
+
701
+ # Then execute the actual query to measure performance
702
+ start_time = datetime.now()
703
+ if sql.lower().startswith("select"):
351
704
  try:
352
- if hasattr(handler.stats, 'to_dict') and callable(handler.stats.to_dict):
353
- stats_dict = handler.stats.to_dict()
354
- stats_json = json.dumps(stats_dict)
355
- self.send_log(LOG_LEVEL_INFO, f"Resource stats: {stats_json}")
356
- else:
357
- self.send_log(LOG_LEVEL_INFO, "Resource stats not available")
358
- except TypeError:
359
- self.send_log(LOG_LEVEL_INFO, "Resource stats: [Could not serialize stats object]")
360
- yield handler
361
- except yaml.YAMLError as e:
362
- raise ConfigurationError(f"Invalid YAML configuration: {str(e)}")
363
- except ImportError as e:
364
- raise ConfigurationError(f"Failed to import handler for {db_type}: {str(e)}")
365
- finally:
366
- if handler:
367
- self.send_log(LOG_LEVEL_DEBUG, f"Cleaning up handler for {connection}")
368
- handler.stats.record_connection_end()
369
- # 使用通用的方式处理统计信息序列化
370
- try:
371
- if hasattr(handler.stats, 'to_dict') and callable(handler.stats.to_dict):
372
- stats_dict = handler.stats.to_dict()
373
- stats_json = json.dumps(stats_dict)
374
- self.send_log(LOG_LEVEL_INFO, f"Final resource stats: {stats_json}")
375
- else:
376
- self.send_log(LOG_LEVEL_INFO, "Final resource stats not available")
377
- except TypeError:
378
- self.send_log(LOG_LEVEL_INFO, "Final resource stats: [Could not serialize stats object]")
379
- # 清理资源
380
- if hasattr(handler, 'cleanup') and callable(handler.cleanup):
381
- await handler.cleanup()
705
+ await handler.execute_query(sql)
706
+ except Exception as e:
707
+ # If query fails, we still provide the execution plan
708
+ self.send_log(LOG_LEVEL_ERROR, f"Query execution failed during analysis: {str(e)}")
709
+ duration = (datetime.now() - start_time).total_seconds()
710
+
711
+ # Combine analysis results
712
+ analysis = [
713
+ f"[{handler.db_type}] Query Analysis",
714
+ f"SQL: {sql}",
715
+ "",
716
+ f"Execution Time: {duration*1000:.2f}ms",
717
+ "",
718
+ "Execution Plan:",
719
+ explain_result
720
+ ]
721
+
722
+ # Add optimization suggestions
723
+ suggestions = self._get_optimization_suggestions(explain_result, duration)
724
+ if suggestions:
725
+ analysis.append("\nOptimization Suggestions:")
726
+ analysis.extend(suggestions)
727
+
728
+ return [types.TextContent(type="text", text="\n".join(analysis))]
729
+
730
+ def _get_optimization_suggestions(self, explain_result: str, duration: float) -> list[str]:
731
+ """根据执行计划和耗时获取优化建议
732
+
733
+ Args:
734
+ explain_result: 执行计划
735
+ duration: 查询耗时(秒)
736
+
737
+ Returns:
738
+ list[str]: 优化建议列表
739
+ """
740
+ suggestions = []
741
+ if "seq scan" in explain_result.lower() and duration > 0.1:
742
+ suggestions.append("- Consider adding an index to avoid sequential scan")
743
+ if "hash join" in explain_result.lower() and duration > 0.5:
744
+ suggestions.append("- Consider optimizing join conditions")
745
+ if duration > 0.5: # 500ms
746
+ suggestions.append("- Query is slow, consider optimizing or adding caching")
747
+ if "temporary" in explain_result.lower():
748
+ suggestions.append("- Query creates temporary tables, consider restructuring")
749
+
750
+ return suggestions
382
751
 
383
752
  def _setup_handlers(self):
384
753
  """Setup MCP handlers"""
@@ -409,180 +778,7 @@ class ConnectionServer:
409
778
 
410
779
  @self.server.list_tools()
411
780
  async def handle_list_tools() -> list[types.Tool]:
412
- return [
413
- types.Tool(
414
- name="dbutils-run-query",
415
- description="Execute read-only SQL query on database connection",
416
- inputSchema={
417
- "type": "object",
418
- "properties": {
419
- "connection": {
420
- "type": "string",
421
- "description": DATABASE_CONNECTION_NAME
422
- },
423
- "sql": {
424
- "type": "string",
425
- "description": "SQL query (SELECT only)"
426
- }
427
- },
428
- "required": ["connection", "sql"]
429
- }
430
- ),
431
- types.Tool(
432
- name="dbutils-list-tables",
433
- description="List all available tables in the specified database connection",
434
- inputSchema={
435
- "type": "object",
436
- "properties": {
437
- "connection": {
438
- "type": "string",
439
- "description": DATABASE_CONNECTION_NAME
440
- }
441
- },
442
- "required": ["connection"]
443
- }
444
- ),
445
- types.Tool(
446
- name="dbutils-describe-table",
447
- description="Get detailed information about a table's structure",
448
- inputSchema={
449
- "type": "object",
450
- "properties": {
451
- "connection": {
452
- "type": "string",
453
- "description": DATABASE_CONNECTION_NAME
454
- },
455
- "table": {
456
- "type": "string",
457
- "description": "Table name to describe"
458
- }
459
- },
460
- "required": ["connection", "table"]
461
- }
462
- ),
463
- types.Tool(
464
- name="dbutils-get-ddl",
465
- description="Get DDL statement for creating the table",
466
- inputSchema={
467
- "type": "object",
468
- "properties": {
469
- "connection": {
470
- "type": "string",
471
- "description": DATABASE_CONNECTION_NAME
472
- },
473
- "table": {
474
- "type": "string",
475
- "description": "Table name to get DDL for"
476
- }
477
- },
478
- "required": ["connection", "table"]
479
- }
480
- ),
481
- types.Tool(
482
- name="dbutils-list-indexes",
483
- description="List all indexes on the specified table",
484
- inputSchema={
485
- "type": "object",
486
- "properties": {
487
- "connection": {
488
- "type": "string",
489
- "description": DATABASE_CONNECTION_NAME
490
- },
491
- "table": {
492
- "type": "string",
493
- "description": "Table name to list indexes for"
494
- }
495
- },
496
- "required": ["connection", "table"]
497
- }
498
- ),
499
- types.Tool(
500
- name="dbutils-get-stats",
501
- description="Get table statistics like row count and size",
502
- inputSchema={
503
- "type": "object",
504
- "properties": {
505
- "connection": {
506
- "type": "string",
507
- "description": DATABASE_CONNECTION_NAME
508
- },
509
- "table": {
510
- "type": "string",
511
- "description": "Table name to get statistics for"
512
- }
513
- },
514
- "required": ["connection", "table"]
515
- }
516
- ),
517
- types.Tool(
518
- name="dbutils-list-constraints",
519
- description="List all constraints (primary key, foreign keys, etc) on the table",
520
- inputSchema={
521
- "type": "object",
522
- "properties": {
523
- "connection": {
524
- "type": "string",
525
- "description": DATABASE_CONNECTION_NAME
526
- },
527
- "table": {
528
- "type": "string",
529
- "description": "Table name to list constraints for"
530
- }
531
- },
532
- "required": ["connection", "table"]
533
- }
534
- ),
535
- types.Tool(
536
- name="dbutils-explain-query",
537
- description="Get execution plan for a SQL query",
538
- inputSchema={
539
- "type": "object",
540
- "properties": {
541
- "connection": {
542
- "type": "string",
543
- "description": DATABASE_CONNECTION_NAME
544
- },
545
- "sql": {
546
- "type": "string",
547
- "description": "SQL query to explain"
548
- }
549
- },
550
- "required": ["connection", "sql"]
551
- }
552
- ),
553
- types.Tool(
554
- name="dbutils-get-performance",
555
- description="Get database performance statistics",
556
- inputSchema={
557
- "type": "object",
558
- "properties": {
559
- "connection": {
560
- "type": "string",
561
- "description": DATABASE_CONNECTION_NAME
562
- }
563
- },
564
- "required": ["connection"]
565
- }
566
- ),
567
- types.Tool(
568
- name="dbutils-analyze-query",
569
- description="Analyze a SQL query for performance",
570
- inputSchema={
571
- "type": "object",
572
- "properties": {
573
- "connection": {
574
- "type": "string",
575
- "description": DATABASE_CONNECTION_NAME
576
- },
577
- "sql": {
578
- "type": "string",
579
- "description": "SQL query to analyze"
580
- }
581
- },
582
- "required": ["connection", "sql"]
583
- }
584
- )
585
- ]
781
+ return self._get_available_tools()
586
782
 
587
783
  @self.server.call_tool()
588
784
  async def handle_call_tool(name: str, arguments: dict) -> list[types.TextContent]:
@@ -592,100 +788,22 @@ class ConnectionServer:
592
788
  connection = arguments["connection"]
593
789
 
594
790
  if name == "dbutils-list-tables":
595
- async with self.get_handler(connection) as handler:
596
- tables = await handler.get_tables()
597
- if not tables:
598
- # 空表列表的情况也返回数据库类型
599
- return [types.TextContent(type="text", text=f"[{handler.db_type}] No tables found")]
600
-
601
- formatted_tables = "\n".join([
602
- f"Table: {table.name}\n" +
603
- f"URI: {table.uri}\n" +
604
- (f"Description: {table.description}\n" if table.description else "") +
605
- "---"
606
- for table in tables
607
- ])
608
- # 添加数据库类型前缀
609
- return [types.TextContent(type="text", text=f"[{handler.db_type}]\n{formatted_tables}")]
791
+ return await self._handle_list_tables(connection)
610
792
  elif name == "dbutils-run-query":
611
793
  sql = arguments.get("sql", "").strip()
612
- if not sql:
613
- raise ConfigurationError(EMPTY_QUERY_ERROR)
614
-
615
- # Only allow SELECT statements
616
- if not sql.lower().startswith("select"):
617
- raise ConfigurationError(SELECT_ONLY_ERROR)
618
-
619
- async with self.get_handler(connection) as handler:
620
- result = await handler.execute_query(sql)
621
- return [types.TextContent(type="text", text=result)]
794
+ return await self._handle_run_query(connection, sql)
622
795
  elif name in ["dbutils-describe-table", "dbutils-get-ddl", "dbutils-list-indexes",
623
796
  "dbutils-get-stats", "dbutils-list-constraints"]:
624
797
  table = arguments.get("table", "").strip()
625
- if not table:
626
- raise ConfigurationError(EMPTY_TABLE_NAME_ERROR)
627
-
628
- async with self.get_handler(connection) as handler:
629
- result = await handler.execute_tool_query(name, table_name=table)
630
- return [types.TextContent(type="text", text=result)]
798
+ return await self._handle_table_tools(name, connection, table)
631
799
  elif name == "dbutils-explain-query":
632
800
  sql = arguments.get("sql", "").strip()
633
- if not sql:
634
- raise ConfigurationError(EMPTY_QUERY_ERROR)
635
-
636
- async with self.get_handler(connection) as handler:
637
- result = await handler.execute_tool_query(name, sql=sql)
638
- return [types.TextContent(type="text", text=result)]
801
+ return await self._handle_explain_query(connection, sql)
639
802
  elif name == "dbutils-get-performance":
640
- async with self.get_handler(connection) as handler:
641
- performance_stats = handler.stats.get_performance_stats()
642
- return [types.TextContent(type="text", text=f"[{handler.db_type}]\n{performance_stats}")]
803
+ return await self._handle_performance(connection)
643
804
  elif name == "dbutils-analyze-query":
644
805
  sql = arguments.get("sql", "").strip()
645
- if not sql:
646
- raise ConfigurationError(EMPTY_QUERY_ERROR)
647
-
648
- async with self.get_handler(connection) as handler:
649
- # First get the execution plan
650
- explain_result = await handler.explain_query(sql)
651
-
652
- # Then execute the actual query to measure performance
653
- start_time = datetime.now()
654
- if sql.lower().startswith("select"):
655
- try:
656
- await handler.execute_query(sql)
657
- except Exception as e:
658
- # If query fails, we still provide the execution plan
659
- self.send_log(LOG_LEVEL_ERROR, f"Query execution failed during analysis: {str(e)}")
660
- duration = (datetime.now() - start_time).total_seconds()
661
-
662
- # Combine analysis results
663
- analysis = [
664
- f"[{handler.db_type}] Query Analysis",
665
- f"SQL: {sql}",
666
- "",
667
- f"Execution Time: {duration*1000:.2f}ms",
668
- "",
669
- "Execution Plan:",
670
- explain_result
671
- ]
672
-
673
- # Add optimization suggestions based on execution plan and timing
674
- suggestions = []
675
- if "seq scan" in explain_result.lower() and duration > 0.1:
676
- suggestions.append("- Consider adding an index to avoid sequential scan")
677
- if "hash join" in explain_result.lower() and duration > 0.5:
678
- suggestions.append("- Consider optimizing join conditions")
679
- if duration > 0.5: # 500ms
680
- suggestions.append("- Query is slow, consider optimizing or adding caching")
681
- if "temporary" in explain_result.lower():
682
- suggestions.append("- Query creates temporary tables, consider restructuring")
683
-
684
- if suggestions:
685
- analysis.append("\nOptimization Suggestions:")
686
- analysis.extend(suggestions)
687
-
688
- return [types.TextContent(type="text", text="\n".join(analysis))]
806
+ return await self._handle_analyze_query(connection, sql)
689
807
  else:
690
808
  raise ConfigurationError(f"Unknown tool: {name}")
691
809
 
@@ -77,15 +77,19 @@ class MySQLConfig(ConnectionConfig):
77
77
  ssl: Optional[SSLConfig] = None
78
78
 
79
79
  @classmethod
80
- def from_yaml(cls, yaml_path: str, db_name: str, local_host: Optional[str] = None) -> 'MySQLConfig':
81
- """Create configuration from YAML file
82
-
80
+ def _validate_connection_config(cls, configs: dict, db_name: str) -> dict:
81
+ """验证连接配置是否有效
82
+
83
83
  Args:
84
- yaml_path: Path to YAML configuration file
85
- db_name: Connection configuration name to use
86
- local_host: Optional local host address
84
+ configs: 配置字典
85
+ db_name: 连接名称
86
+
87
+ Returns:
88
+ dict: 数据库配置
89
+
90
+ Raises:
91
+ ValueError: 如果配置无效
87
92
  """
88
- configs = cls.load_yaml_config(yaml_path)
89
93
  if not db_name:
90
94
  raise ValueError("Connection name must be specified")
91
95
  if db_name not in configs:
@@ -103,57 +107,121 @@ class MySQLConfig(ConnectionConfig):
103
107
  raise ValueError("User must be specified in connection configuration")
104
108
  if not db_config.get('password'):
105
109
  raise ValueError("Password must be specified in connection configuration")
110
+
111
+ return db_config
112
+
113
+ @classmethod
114
+ def _create_config_from_url(cls, db_config: dict, local_host: Optional[str] = None) -> 'MySQLConfig':
115
+ """从URL创建配置
116
+
117
+ Args:
118
+ db_config: 数据库配置
119
+ local_host: 可选的本地主机地址
120
+
121
+ Returns:
122
+ MySQLConfig: 配置对象
123
+ """
124
+ # Parse URL for connection parameters
125
+ params = parse_url(db_config['url'])
126
+ config = cls(
127
+ database=params['database'],
128
+ user=db_config['user'],
129
+ password=db_config['password'],
130
+ host=params['host'],
131
+ port=params['port'],
132
+ charset=params['charset'],
133
+ local_host=local_host,
134
+ url=db_config['url'],
135
+ ssl=params.get('ssl')
136
+ )
137
+ return config
138
+
139
+ @classmethod
140
+ def _create_config_from_params(cls, db_config: dict, local_host: Optional[str] = None) -> 'MySQLConfig':
141
+ """从参数创建配置
142
+
143
+ Args:
144
+ db_config: 数据库配置
145
+ local_host: 可选的本地主机地址
146
+
147
+ Returns:
148
+ MySQLConfig: 配置对象
149
+
150
+ Raises:
151
+ ValueError: 如果缺少必需参数或SSL配置无效
152
+ """
153
+ if not db_config.get('database'):
154
+ raise ValueError("MySQL database name must be specified in configuration")
155
+ if not db_config.get('host'):
156
+ raise ValueError("Host must be specified in connection configuration")
157
+ if not db_config.get('port'):
158
+ raise ValueError("Port must be specified in connection configuration")
159
+
160
+ # Parse SSL configuration if present
161
+ ssl_config = cls._parse_ssl_config(db_config)
162
+
163
+ config = cls(
164
+ database=db_config['database'],
165
+ user=db_config['user'],
166
+ password=db_config['password'],
167
+ host=db_config['host'],
168
+ port=str(db_config['port']),
169
+ charset=db_config.get('charset', 'utf8mb4'),
170
+ local_host=local_host,
171
+ ssl=ssl_config
172
+ )
173
+ return config
174
+
175
+ @classmethod
176
+ def _parse_ssl_config(cls, db_config: dict) -> Optional[SSLConfig]:
177
+ """解析SSL配置
178
+
179
+ Args:
180
+ db_config: 数据库配置
181
+
182
+ Returns:
183
+ Optional[SSLConfig]: SSL配置或None
184
+
185
+ Raises:
186
+ ValueError: 如果SSL配置无效
187
+ """
188
+ if 'ssl' not in db_config:
189
+ return None
190
+
191
+ ssl_params = db_config['ssl']
192
+ if not isinstance(ssl_params, dict):
193
+ raise ValueError("SSL configuration must be a dictionary")
194
+
195
+ if ssl_params.get('mode') not in [None, 'disabled', 'required', 'verify_ca', 'verify_identity']:
196
+ raise ValueError(f"Invalid ssl-mode: {ssl_params.get('mode')}")
197
+
198
+ return SSLConfig(
199
+ mode=ssl_params.get('mode', 'disabled'),
200
+ ca=ssl_params.get('ca'),
201
+ cert=ssl_params.get('cert'),
202
+ key=ssl_params.get('key')
203
+ )
106
204
 
107
- # Get connection parameters
205
+ @classmethod
206
+ def from_yaml(cls, yaml_path: str, db_name: str, local_host: Optional[str] = None) -> 'MySQLConfig':
207
+ """Create configuration from YAML file
208
+
209
+ Args:
210
+ yaml_path: Path to YAML configuration file
211
+ db_name: Connection configuration name to use
212
+ local_host: Optional local host address
213
+ """
214
+ configs = cls.load_yaml_config(yaml_path)
215
+
216
+ # Validate connection config
217
+ db_config = cls._validate_connection_config(configs, db_name)
218
+
219
+ # Create configuration based on URL or parameters
108
220
  if 'url' in db_config:
109
- # Parse URL for connection parameters
110
- params = parse_url(db_config['url'])
111
- config = cls(
112
- database=params['database'],
113
- user=db_config['user'],
114
- password=db_config['password'],
115
- host=params['host'],
116
- port=params['port'],
117
- charset=params['charset'],
118
- local_host=local_host,
119
- url=db_config['url'],
120
- ssl=params.get('ssl')
121
- )
221
+ config = cls._create_config_from_url(db_config, local_host)
122
222
  else:
123
- if not db_config.get('database'):
124
- raise ValueError("MySQL database name must be specified in configuration")
125
- if not db_config.get('host'):
126
- raise ValueError("Host must be specified in connection configuration")
127
- if not db_config.get('port'):
128
- raise ValueError("Port must be specified in connection configuration")
129
-
130
- # Parse SSL configuration if present
131
- ssl_config = None
132
- if 'ssl' in db_config:
133
- ssl_params = db_config['ssl']
134
- if not isinstance(ssl_params, dict):
135
- raise ValueError("SSL configuration must be a dictionary")
136
-
137
- if ssl_params.get('mode') not in [None, 'disabled', 'required', 'verify_ca', 'verify_identity']:
138
- raise ValueError(f"Invalid ssl-mode: {ssl_params.get('mode')}")
139
-
140
- ssl_config = SSLConfig(
141
- mode=ssl_params.get('mode', 'disabled'),
142
- ca=ssl_params.get('ca'),
143
- cert=ssl_params.get('cert'),
144
- key=ssl_params.get('key')
145
- )
223
+ config = cls._create_config_from_params(db_config, local_host)
146
224
 
147
- config = cls(
148
- database=db_config['database'],
149
- user=db_config['user'],
150
- password=db_config['password'],
151
- host=db_config['host'],
152
- port=str(db_config['port']),
153
- charset=db_config.get('charset', 'utf8mb4'),
154
- local_host=local_host,
155
- ssl=ssl_config
156
- )
157
225
  config.debug = cls.get_debug_mode()
158
226
  return config
159
227
 
@@ -36,7 +36,7 @@ class MySQLHandler(ConnectionHandler):
36
36
  try:
37
37
  conn_params = self.config.get_connection_params()
38
38
  conn = mysql.connector.connect(**conn_params)
39
- with conn.cursor(dictionary=True) as cur:
39
+ with conn.cursor(dictionary=True) as cur: # NOSONAR
40
40
  cur.execute("""
41
41
  SELECT
42
42
  TABLE_NAME as table_name,
@@ -66,7 +66,7 @@ class MySQLHandler(ConnectionHandler):
66
66
  try:
67
67
  conn_params = self.config.get_connection_params()
68
68
  conn = mysql.connector.connect(**conn_params)
69
- with conn.cursor(dictionary=True) as cur:
69
+ with conn.cursor(dictionary=True) as cur: # NOSONAR
70
70
  # Get column information
71
71
  cur.execute("""
72
72
  SELECT
@@ -118,7 +118,7 @@ class MySQLHandler(ConnectionHandler):
118
118
  conn = mysql.connector.connect(**conn_params)
119
119
  self.log("debug", f"Executing query: {sql}")
120
120
 
121
- with conn.cursor(dictionary=True) as cur:
121
+ with conn.cursor(dictionary=True) as cur: # NOSONAR
122
122
  # Start read-only transaction
123
123
  cur.execute("SET TRANSACTION READ ONLY")
124
124
  try:
@@ -150,7 +150,7 @@ class MySQLHandler(ConnectionHandler):
150
150
  try:
151
151
  conn_params = self.config.get_connection_params()
152
152
  conn = mysql.connector.connect(**conn_params)
153
- with conn.cursor(dictionary=True) as cur:
153
+ with conn.cursor(dictionary=True) as cur: # NOSONAR
154
154
  # Get table information and comment
155
155
  cur.execute("""
156
156
  SELECT
@@ -220,7 +220,7 @@ class MySQLHandler(ConnectionHandler):
220
220
  try:
221
221
  conn_params = self.config.get_connection_params()
222
222
  conn = mysql.connector.connect(**conn_params)
223
- with conn.cursor(dictionary=True) as cur:
223
+ with conn.cursor(dictionary=True) as cur: # NOSONAR
224
224
  # MySQL provides a SHOW CREATE TABLE statement
225
225
  cur.execute(f"SHOW CREATE TABLE {table_name}")
226
226
  result = cur.fetchone()
@@ -242,7 +242,7 @@ class MySQLHandler(ConnectionHandler):
242
242
  try:
243
243
  conn_params = self.config.get_connection_params()
244
244
  conn = mysql.connector.connect(**conn_params)
245
- with conn.cursor(dictionary=True) as cur:
245
+ with conn.cursor(dictionary=True) as cur: # NOSONAR
246
246
  # Get index information
247
247
  cur.execute("""
248
248
  SELECT
@@ -301,7 +301,7 @@ class MySQLHandler(ConnectionHandler):
301
301
  try:
302
302
  conn_params = self.config.get_connection_params()
303
303
  conn = mysql.connector.connect(**conn_params)
304
- with conn.cursor(dictionary=True) as cur:
304
+ with conn.cursor(dictionary=True) as cur: # NOSONAR
305
305
  # Get table statistics
306
306
  cur.execute("""
307
307
  SELECT
@@ -366,7 +366,7 @@ class MySQLHandler(ConnectionHandler):
366
366
  try:
367
367
  conn_params = self.config.get_connection_params()
368
368
  conn = mysql.connector.connect(**conn_params)
369
- with conn.cursor(dictionary=True) as cur:
369
+ with conn.cursor(dictionary=True) as cur: # NOSONAR
370
370
  # Get constraint information
371
371
  cur.execute("""
372
372
  SELECT
@@ -429,7 +429,7 @@ class MySQLHandler(ConnectionHandler):
429
429
  try:
430
430
  conn_params = self.config.get_connection_params()
431
431
  conn = mysql.connector.connect(**conn_params)
432
- with conn.cursor(dictionary=True) as cur:
432
+ with conn.cursor(dictionary=True) as cur: # NOSONAR
433
433
  # Get EXPLAIN output
434
434
  cur.execute(f"EXPLAIN FORMAT=TREE {sql}")
435
435
  explain_result = cur.fetchall()
@@ -49,7 +49,7 @@ class MySQLServer(ConnectionServer):
49
49
  """列出所有表资源"""
50
50
  try:
51
51
  conn = self.pool.get_connection()
52
- with conn.cursor(dictionary=True) as cur:
52
+ with conn.cursor(dictionary=True) as cur: # NOSONAR - dictionary参数是正确的,用于返回字典格式的结果
53
53
  cur.execute("""
54
54
  SELECT
55
55
  table_name,
@@ -78,7 +78,7 @@ class MySQLServer(ConnectionServer):
78
78
  try:
79
79
  table_name = uri.split('/')[-2]
80
80
  conn = self.pool.get_connection()
81
- with conn.cursor(dictionary=True) as cur:
81
+ with conn.cursor(dictionary=True) as cur: # NOSONAR - dictionary参数是正确的,用于返回字典格式的结果
82
82
  # 获取列信息
83
83
  cur.execute("""
84
84
  SELECT
@@ -170,7 +170,7 @@ class MySQLServer(ConnectionServer):
170
170
  conn = self.pool.get_connection()
171
171
 
172
172
  self.log("info", f"执行查询: {sql}")
173
- with conn.cursor(dictionary=True) as cur:
173
+ with conn.cursor(dictionary=True) as cur: # NOSONAR - dictionary参数是正确的,用于返回字典格式的结果
174
174
  # 设置只读事务
175
175
  cur.execute("SET TRANSACTION READ ONLY")
176
176
  try:
@@ -142,7 +142,7 @@ class SQLiteHandler(ConnectionHandler):
142
142
  with sqlite3.connect(self.config.path) as conn:
143
143
  cur = conn.cursor()
144
144
  # SQLite provides the complete CREATE statement
145
- cur.execute(f"SELECT sql FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
145
+ cur.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
146
146
  result = cur.fetchone()
147
147
 
148
148
  if not result:
@@ -151,7 +151,7 @@ class SQLiteHandler(ConnectionHandler):
151
151
  ddl = result[0]
152
152
 
153
153
  # Get indexes
154
- cur.execute(f"SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name=?", (table_name,))
154
+ cur.execute("SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name=?", (table_name,))
155
155
  indexes = cur.fetchall()
156
156
 
157
157
  # Add index definitions
@@ -234,9 +234,9 @@ class SQLiteHandler(ConnectionHandler):
234
234
  indexes = cur.fetchall()
235
235
 
236
236
  # Get page count and size
237
- cur.execute(f"PRAGMA page_count")
237
+ cur.execute("PRAGMA page_count")
238
238
  page_count = cur.fetchone()[0]
239
- cur.execute(f"PRAGMA page_size")
239
+ cur.execute("PRAGMA page_size")
240
240
  page_size = cur.fetchone()[0]
241
241
 
242
242
  # Calculate total size
@@ -54,7 +54,7 @@ class SQLiteServer(ConnectionServer):
54
54
  # 使用默认连接
55
55
  conn = self._get_connection()
56
56
 
57
- with closing(conn) as connection:
57
+ with closing(conn) as _:
58
58
  cursor = conn.execute(
59
59
  "SELECT name FROM sqlite_master WHERE type='table'"
60
60
  )
@@ -155,7 +155,7 @@ class SQLiteServer(ConnectionServer):
155
155
  # 使用默认连接
156
156
  conn = self._get_connection()
157
157
 
158
- with closing(conn) as connection:
158
+ with closing(conn) as _:
159
159
  self.log("info", f"执行查询: {sql}")
160
160
  cursor = conn.execute(sql)
161
161
  results = cursor.fetchall()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-dbutils
3
- Version: 0.16.0
3
+ Version: 0.16.1
4
4
  Summary: MCP Database Utilities Service
5
5
  Author: Dong Hao
6
6
  License-Expression: MIT
@@ -1,22 +1,22 @@
1
1
  mcp_dbutils/__init__.py,sha256=6LLccQv7je2L4IpY_I3OzSJZcK32VUDJv2IY31y6eYg,1900
2
- mcp_dbutils/base.py,sha256=d4kt29NQeE7xZ0DlGpJcpBXS1pY1VnY98gX-xJ-Ehpg,29126
2
+ mcp_dbutils/base.py,sha256=9UDhPFpw6YdkUMhAqgGyy6vuXA2C78LIGrWiW0zeB8s,30839
3
3
  mcp_dbutils/config.py,sha256=bmXpOd1fyYfoyUS75I035ChT6t3wP5AyEnJ06e2ZS2o,1848
4
4
  mcp_dbutils/log.py,sha256=mqxi6I_IL-MF1F_pxBtnYZQKOHbGBJ74gsvZHVelr1w,823
5
5
  mcp_dbutils/stats.py,sha256=wMqWPfGnEOg9v5YBtTsARV-1YsFUMM_pKdzitzSU9x4,7137
6
6
  mcp_dbutils/mysql/__init__.py,sha256=gNhoHaxK1qhvMAH5AVl1vfV1rUpcbV9KZWUQb41aaQk,129
7
- mcp_dbutils/mysql/config.py,sha256=Yvdd02ZwPMM7RCjEvjNJphGiUfImI7Q2gcEH6Zi3Vjo,8071
8
- mcp_dbutils/mysql/handler.py,sha256=CZoce_Fs2YnGKvs6tIc083lRm4vo1cYT9VvsHfU1peI,19738
9
- mcp_dbutils/mysql/server.py,sha256=81DGLCg7iLR4TgydoQXCHxl-9RxZfv1vYGJmiYqj5sQ,8352
7
+ mcp_dbutils/mysql/config.py,sha256=BTPPFqlhoTp7EBFIeLJZh8x6bCn3q9NivHYz9yZHziw,9820
8
+ mcp_dbutils/mysql/handler.py,sha256=knBoFVYmdse5hsjr4GPi4fZhEaYOPRBPGR2d3w8qqzw,19837
9
+ mcp_dbutils/mysql/server.py,sha256=1bWAu7qHYXVeTZu4wdEpS6gSVB0RoXKI3Smy_ix-y8A,8586
10
10
  mcp_dbutils/postgres/__init__.py,sha256=-2zYuEJEQ2AMvmGhH5Z_umerSvt7S4xOa_XV4wgvGfI,154
11
11
  mcp_dbutils/postgres/config.py,sha256=NyQOVhkXJ1S-JD0w-ePNjTKI1Ja-aZQkDUdHi6U7Vl4,7752
12
12
  mcp_dbutils/postgres/handler.py,sha256=ppltSKtSk-BlPpp3iEVJlmoyl4AmqKcQHx_0zHlz03Y,24403
13
13
  mcp_dbutils/postgres/server.py,sha256=_CiJC9PitpI1NB99Q1Bcs5TYADNgDpYMwv88fRHQunE,8640
14
14
  mcp_dbutils/sqlite/__init__.py,sha256=fK_3-WylCBYpBAzwuopi8hlwoIGJm2TPAlwcPWG46I0,134
15
15
  mcp_dbutils/sqlite/config.py,sha256=j67TJ8mQJ2D886MthSa-zYMtvUUYyyxYLMlNxkYoqZE,4509
16
- mcp_dbutils/sqlite/handler.py,sha256=T5atpRn71IwlTyPKhdpQsGIJb16716SKLBplsl9Wv50,17750
17
- mcp_dbutils/sqlite/server.py,sha256=3W0kB1mrVzcfMjVG3dkmt1R4Mh4lbiXkQsXon7e4i1U,7199
18
- mcp_dbutils-0.16.0.dist-info/METADATA,sha256=UyKCCoqb-E1U58KCKMi6E_MddsW0Nc5k6-_0RZW-4xw,16714
19
- mcp_dbutils-0.16.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
- mcp_dbutils-0.16.0.dist-info/entry_points.txt,sha256=XTjt0QmYRgKOJQT6skR9bp1EMUfIrgpHeZJPZ3CJffs,49
21
- mcp_dbutils-0.16.0.dist-info/licenses/LICENSE,sha256=1A_CwpWVlbjrKdVEYO77vYfnXlW7oxcilZ8FpA_BzCI,1065
22
- mcp_dbutils-0.16.0.dist-info/RECORD,,
16
+ mcp_dbutils/sqlite/handler.py,sha256=25zqoQpMhRNKeO3MH2a0E5dO3-4A8sPb7q87cn7Cs0E,17746
17
+ mcp_dbutils/sqlite/server.py,sha256=jqpE8d9vJETMs5xYGB7P0tvNDPes6Yn5ZM_iCCF7Tv4,7181
18
+ mcp_dbutils-0.16.1.dist-info/METADATA,sha256=mjE8oqdIRiIdiRlyWufFfxWGoHT8aGIoSaVTEuFlkJs,16714
19
+ mcp_dbutils-0.16.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
+ mcp_dbutils-0.16.1.dist-info/entry_points.txt,sha256=XTjt0QmYRgKOJQT6skR9bp1EMUfIrgpHeZJPZ3CJffs,49
21
+ mcp_dbutils-0.16.1.dist-info/licenses/LICENSE,sha256=1A_CwpWVlbjrKdVEYO77vYfnXlW7oxcilZ8FpA_BzCI,1065
22
+ mcp_dbutils-0.16.1.dist-info/RECORD,,