mcp-dbutils 0.8.0__py3-none-any.whl → 0.10.0__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.
@@ -7,15 +7,15 @@ from typing import Optional, List
7
7
  import mcp.types as types
8
8
  from importlib.metadata import metadata
9
9
 
10
- from ..base import DatabaseServer
10
+ from ..base import ConnectionServer
11
11
  from ..log import create_logger
12
- from .config import SqliteConfig
12
+ from .config import SQLiteConfig
13
13
 
14
14
  # 获取包信息用于日志命名
15
15
  pkg_meta = metadata("mcp-dbutils")
16
16
 
17
- class SqliteServer(DatabaseServer):
18
- def __init__(self, config: SqliteConfig, config_path: Optional[str] = None):
17
+ class SQLiteServer(ConnectionServer):
18
+ def __init__(self, config: SQLiteConfig, config_path: Optional[str] = None):
19
19
  """初始化 SQLite 服务器
20
20
 
21
21
  Args:
@@ -32,13 +32,13 @@ class SqliteServer(DatabaseServer):
32
32
 
33
33
  # 测试连接
34
34
  try:
35
- self.log("debug", f"正在连接数据库: {self.config.get_masked_connection_info()}")
35
+ self.log("debug", f"正在连接: {self.config.get_masked_connection_info()}")
36
36
  connection_params = self.config.get_connection_params()
37
37
  with closing(sqlite3.connect(**connection_params)) as conn:
38
38
  conn.row_factory = sqlite3.Row
39
- self.log("info", "数据库连接测试成功")
39
+ self.log("info", "连接测试成功")
40
40
  except sqlite3.Error as e:
41
- self.log("error", f"数据库连接失败: {str(e)}")
41
+ self.log("error", f"连接失败: {str(e)}")
42
42
  raise
43
43
 
44
44
  def _get_connection(self):
@@ -53,13 +53,13 @@ class SqliteServer(DatabaseServer):
53
53
  use_default = True
54
54
  conn = None
55
55
  try:
56
- database = arguments.get("database")
57
- if database and self.config_path:
58
- # 使用指定的数据库配置
59
- config = SqliteConfig.from_yaml(self.config_path, database)
56
+ connection = arguments.get("connection")
57
+ if connection and self.config_path:
58
+ # 使用指定的数据库连接
59
+ config = SQLiteConfig.from_yaml(self.config_path, connection)
60
60
  connection_params = config.get_connection_params()
61
61
  masked_params = config.get_masked_connection_info()
62
- self.log("info", f"使用配置 {database} 连接数据库: {masked_params}")
62
+ self.log("info", f"使用配置 {connection} 连接: {masked_params}")
63
63
  conn = sqlite3.connect(**connection_params)
64
64
  conn.row_factory = sqlite3.Row
65
65
  use_default = False
@@ -126,9 +126,9 @@ class SqliteServer(DatabaseServer):
126
126
  inputSchema={
127
127
  "type": "object",
128
128
  "properties": {
129
- "database": {
129
+ "connection": {
130
130
  "type": "string",
131
- "description": "数据库配置名称(可选)"
131
+ "description": "数据库连接名称(可选)"
132
132
  },
133
133
  "sql": {
134
134
  "type": "string",
@@ -156,13 +156,13 @@ class SqliteServer(DatabaseServer):
156
156
  use_default = True
157
157
  conn = None
158
158
  try:
159
- database = arguments.get("database")
160
- if database and self.config_path:
161
- # 使用指定的数据库配置
162
- config = SqliteConfig.from_yaml(self.config_path, database)
159
+ connection = arguments.get("connection")
160
+ if connection and self.config_path:
161
+ # 使用指定的数据库连接
162
+ config = SQLiteConfig.from_yaml(self.config_path, connection)
163
163
  connection_params = config.get_connection_params()
164
164
  masked_params = config.get_masked_connection_info()
165
- self.log("info", f"使用配置 {database} 连接数据库: {masked_params}")
165
+ self.log("info", f"使用配置 {connection} 连接: {masked_params}")
166
166
  conn = sqlite3.connect(**connection_params)
167
167
  conn.row_factory = sqlite3.Row
168
168
  use_default = False
@@ -180,7 +180,7 @@ class SqliteServer(DatabaseServer):
180
180
 
181
181
  result_text = str({
182
182
  'type': 'sqlite',
183
- 'config_name': database or 'default',
183
+ 'config_name': connection or 'default',
184
184
  'query_result': {
185
185
  'columns': columns,
186
186
  'rows': formatted_results,
@@ -194,7 +194,7 @@ class SqliteServer(DatabaseServer):
194
194
  except sqlite3.Error as e:
195
195
  error_msg = str({
196
196
  'type': 'sqlite',
197
- 'config_name': database or 'default',
197
+ 'config_name': connection or 'default',
198
198
  'error': f"查询执行失败: {str(e)}"
199
199
  })
200
200
  self.log("error", error_msg)
mcp_dbutils/stats.py CHANGED
@@ -2,8 +2,9 @@
2
2
 
3
3
  from dataclasses import dataclass
4
4
  from datetime import datetime
5
- from typing import Optional
5
+ from typing import Optional, List, Tuple
6
6
  import sys
7
+ import statistics
7
8
 
8
9
  @dataclass
9
10
  class ResourceStats:
@@ -24,11 +25,23 @@ class ResourceStats:
24
25
 
25
26
  # Resource stats
26
27
  estimated_memory: int = 0
28
+
29
+ # Performance monitoring
30
+ query_durations: List[float] = None # 查询执行时间列表 (秒)
31
+ query_types: dict[str, int] = None # 查询类型统计 (SELECT, EXPLAIN等)
32
+ slow_queries: List[Tuple[str, float]] = None # 慢查询记录 (SQL, 时间)
33
+ peak_memory: int = 0 # 峰值内存使用
27
34
 
28
35
  def __post_init__(self):
29
36
  """Initialize mutable defaults"""
30
37
  if self.error_types is None:
31
38
  self.error_types = {}
39
+ if self.query_durations is None:
40
+ self.query_durations = []
41
+ if self.query_types is None:
42
+ self.query_types = {}
43
+ if self.slow_queries is None:
44
+ self.slow_queries = []
32
45
 
33
46
  def record_connection_start(self):
34
47
  """Record new connection start"""
@@ -45,6 +58,26 @@ class ResourceStats:
45
58
  self.query_count += 1
46
59
  self.last_query_time = datetime.now()
47
60
 
61
+ def record_query_duration(self, sql: str, duration: float):
62
+ """Record query execution time and type
63
+
64
+ Args:
65
+ sql: SQL query text
66
+ duration: Execution time in seconds
67
+ """
68
+ self.query_durations.append(duration)
69
+
70
+ # Record query type
71
+ query_type = sql.strip().split()[0].upper()
72
+ self.query_types[query_type] = self.query_types.get(query_type, 0) + 1
73
+
74
+ # Record slow queries (over 100ms)
75
+ if duration > 0.1: # 100ms
76
+ # Keep at most 10 slow queries
77
+ if len(self.slow_queries) >= 10:
78
+ self.slow_queries.pop(0)
79
+ self.slow_queries.append((sql[:100], duration)) # Truncate SQL to avoid excessive length
80
+
48
81
  def record_error(self, error_type: str):
49
82
  """Record error occurrence
50
83
 
@@ -61,7 +94,73 @@ class ResourceStats:
61
94
  Args:
62
95
  obj: Object to estimate size for
63
96
  """
64
- self.estimated_memory = sys.getsizeof(obj)
97
+ current_memory = sys.getsizeof(obj)
98
+ self.estimated_memory = current_memory
99
+ self.peak_memory = max(self.peak_memory, current_memory)
100
+
101
+ def get_query_time_stats(self) -> dict:
102
+ """Get query time statistics
103
+
104
+ Returns:
105
+ Dictionary with min, max, avg, median query times
106
+ """
107
+ if not self.query_durations:
108
+ return {
109
+ "min": 0,
110
+ "max": 0,
111
+ "avg": 0,
112
+ "median": 0
113
+ }
114
+
115
+ return {
116
+ "min": min(self.query_durations),
117
+ "max": max(self.query_durations),
118
+ "avg": sum(self.query_durations) / len(self.query_durations),
119
+ "median": statistics.median(self.query_durations) if len(self.query_durations) > 0 else 0
120
+ }
121
+
122
+ def get_performance_stats(self) -> str:
123
+ """Get formatted performance statistics
124
+
125
+ Returns:
126
+ Formatted string with performance statistics
127
+ """
128
+ stats = []
129
+ stats.append(f"Database Performance Statistics")
130
+ stats.append(f"-----------------------------")
131
+ stats.append(f"Query Count: {self.query_count}")
132
+
133
+ # Query time statistics
134
+ if self.query_durations:
135
+ time_stats = self.get_query_time_stats()
136
+ stats.append(f"Query Times: avg={time_stats['avg']*1000:.2f}ms, min={time_stats['min']*1000:.2f}ms, max={time_stats['max']*1000:.2f}ms, median={time_stats['median']*1000:.2f}ms")
137
+
138
+ # Query type distribution
139
+ if self.query_types:
140
+ stats.append(f"Query Types:")
141
+ for qtype, count in self.query_types.items():
142
+ percentage = (count / self.query_count) * 100 if self.query_count else 0
143
+ stats.append(f" - {qtype}: {count} ({percentage:.1f}%)")
144
+
145
+ # Slow queries
146
+ if self.slow_queries:
147
+ stats.append(f"Slow Queries:")
148
+ for sql, duration in self.slow_queries:
149
+ stats.append(f" - {duration*1000:.2f}ms: {sql}...")
150
+
151
+ # Error statistics
152
+ if self.error_count > 0:
153
+ error_rate = (self.error_count / self.query_count) * 100 if self.query_count else 0
154
+ stats.append(f"Error Rate: {error_rate:.2f}% ({self.error_count} errors)")
155
+ stats.append(f"Error Types:")
156
+ for etype, count in self.error_types.items():
157
+ stats.append(f" - {etype}: {count}")
158
+
159
+ # Resource usage
160
+ stats.append(f"Memory Usage: current={self.estimated_memory/1024:.2f}KB, peak={self.peak_memory/1024:.2f}KB")
161
+ stats.append(f"Connections: active={self.active_connections}, total={self.total_connections}")
162
+
163
+ return "\n".join(stats)
65
164
 
66
165
  def to_dict(self) -> dict:
67
166
  """Convert stats to dictionary for logging
@@ -74,12 +173,22 @@ class ResourceStats:
74
173
  if self.connection_start_time:
75
174
  connection_duration = (now - self.connection_start_time).total_seconds()
76
175
 
176
+ time_stats = self.get_query_time_stats()
177
+
77
178
  return {
78
179
  "active_connections": self.active_connections,
79
180
  "total_connections": self.total_connections,
80
181
  "connection_duration": connection_duration,
81
182
  "query_count": self.query_count,
183
+ "query_times_ms": {
184
+ "min": time_stats["min"] * 1000 if self.query_durations else 0,
185
+ "max": time_stats["max"] * 1000 if self.query_durations else 0,
186
+ "avg": time_stats["avg"] * 1000 if self.query_durations else 0,
187
+ "median": time_stats["median"] * 1000 if self.query_durations else 0
188
+ },
189
+ "query_types": self.query_types,
82
190
  "error_count": self.error_count,
83
191
  "error_types": self.error_types,
84
- "estimated_memory_bytes": self.estimated_memory
192
+ "estimated_memory_bytes": self.estimated_memory,
193
+ "peak_memory_bytes": self.peak_memory
85
194
  }
@@ -0,0 +1,227 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-dbutils
3
+ Version: 0.10.0
4
+ Summary: MCP Database Utilities Service
5
+ Author: Dong Hao
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: mcp>=1.2.1
10
+ Requires-Dist: psycopg2-binary>=2.9.10
11
+ Requires-Dist: python-dotenv>=1.0.1
12
+ Requires-Dist: pyyaml>=6.0.2
13
+ Provides-Extra: test
14
+ Requires-Dist: aiosqlite>=0.19.0; extra == 'test'
15
+ Requires-Dist: docker>=7.0.0; extra == 'test'
16
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'test'
17
+ Requires-Dist: pytest-cov>=4.1.0; extra == 'test'
18
+ Requires-Dist: pytest-docker>=2.0.0; extra == 'test'
19
+ Requires-Dist: pytest>=7.0.0; extra == 'test'
20
+ Requires-Dist: testcontainers>=3.7.0; extra == 'test'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # MCP Database Utilities
24
+
25
+ ![GitHub Repo stars](https://img.shields.io/github/stars/donghao1393/mcp-dbutils)
26
+ ![PyPI version](https://img.shields.io/pypi/v/mcp-dbutils)
27
+ [![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/donghao1393/bdd0a63ec2a816539ff8c136ceb41e48/raw/coverage.json)](https://github.com/donghao1393/mcp-dbutils/actions)
28
+ ![Python](https://img.shields.io/badge/Python-3.10%2B-blue)
29
+ ![License](https://img.shields.io/github/license/donghao1393/mcp-dbutils)
30
+ [![smithery badge](https://smithery.ai/badge/@donghao1393/mcp-dbutils)](https://smithery.ai/server/@donghao1393/mcp-dbutils)
31
+
32
+ [中文文档](README_CN.md)
33
+
34
+ ## Overview
35
+ MCP Database Utilities is a unified database access service that supports multiple database types (PostgreSQL and SQLite). Through its abstraction layer design, it provides a simple and unified database operation interface for MCP servers.
36
+
37
+ ## Features
38
+ - Unified database access interface
39
+ - Support for multiple database configurations
40
+ - Secure read-only query execution
41
+ - Table structure and schema information retrieval
42
+ - Database tables listing via MCP tools
43
+ - Intelligent connection management and resource cleanup
44
+ - Debug mode support
45
+ - SSL/TLS connection support for PostgreSQL
46
+
47
+ ## Installation and Configuration
48
+
49
+ ### Installation Methods
50
+ #### Installing via Smithery
51
+
52
+ To install Database Utilities for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@donghao1393/mcp-dbutils):
53
+
54
+ ```bash
55
+ npx -y @smithery/cli install @donghao1393/mcp-dbutils --client claude
56
+ ```
57
+
58
+ #### Using uvx (Recommended)
59
+ No installation required, run directly using `uvx`:
60
+ ```bash
61
+ uvx mcp-dbutils --config /path/to/config.yaml
62
+ ```
63
+
64
+ Add to Claude configuration:
65
+ ```json
66
+ "mcpServers": {
67
+ "mcp-dbutils": {
68
+ "command": "uvx",
69
+ "args": [
70
+ "mcp-dbutils",
71
+ "--config",
72
+ "/path/to/config.yaml"
73
+ ],
74
+ "env": {
75
+ "MCP_DEBUG": "1" // Optional: Enable debug mode
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ #### Using pip
82
+ ```bash
83
+ pip install mcp-dbutils
84
+ ```
85
+
86
+ Add to Claude configuration:
87
+ ```json
88
+ "mcpServers": {
89
+ "mcp-dbutils": {
90
+ "command": "python",
91
+ "args": [
92
+ "-m",
93
+ "mcp_dbutils",
94
+ "--config",
95
+ "/path/to/config.yaml"
96
+ ],
97
+ "env": {
98
+ "MCP_DEBUG": "1" // Optional: Enable debug mode
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ #### Using Docker
105
+ ```bash
106
+ docker run -i --rm \
107
+ -v /path/to/config.yaml:/app/config.yaml \
108
+ -v /path/to/sqlite.db:/app/sqlite.db \ # Optional: for SQLite database
109
+ -e MCP_DEBUG=1 \ # Optional: Enable debug mode
110
+ mcp/dbutils --config /app/config.yaml
111
+ ```
112
+
113
+ Add to Claude configuration:
114
+ ```json
115
+ "mcpServers": {
116
+ "mcp-dbutils": {
117
+ "command": "docker",
118
+ "args": [
119
+ "run",
120
+ "-i",
121
+ "--rm",
122
+ "-v",
123
+ "/path/to/config.yaml:/app/config.yaml",
124
+ "-v",
125
+ "/path/to/sqlite.db:/app/sqlite.db", // Optional: for SQLite database
126
+ "mcp/dbutils",
127
+ "--config",
128
+ "/app/config.yaml"
129
+ ],
130
+ "env": {
131
+ "MCP_DEBUG": "1" // Optional: Enable debug mode
132
+ }
133
+ }
134
+ }
135
+ ```
136
+
137
+ > **Note for Docker database connections:**
138
+ > - For SQLite: Mount your database file using `-v /path/to/sqlite.db:/app/sqlite.db`
139
+ > - For PostgreSQL running on host:
140
+ > - On Mac/Windows: Use `host.docker.internal` as host in config
141
+ > - On Linux: Use `172.17.0.1` (docker0 IP) or run with `--network="host"`
142
+
143
+ ### Requirements
144
+ - Python 3.10+
145
+ - PostgreSQL (optional)
146
+ - SQLite3 (optional)
147
+
148
+ ### Configuration File
149
+ The project requires a YAML configuration file, specified via the `--config` parameter. Configuration examples:
150
+
151
+ ```yaml
152
+ connections:
153
+ # SQLite configuration examples
154
+ dev-db:
155
+ type: sqlite
156
+ path: /path/to/dev.db
157
+ # Password is optional
158
+ password:
159
+
160
+ # PostgreSQL standard configuration
161
+ test-db:
162
+ type: postgres
163
+ host: postgres.example.com
164
+ port: 5432
165
+ dbname: test_db
166
+ user: test_user
167
+ password: test_pass
168
+
169
+ # PostgreSQL URL configuration with SSL
170
+ prod-db:
171
+ type: postgres
172
+ url: postgresql://postgres.example.com:5432/prod-db?sslmode=verify-full
173
+ user: prod_user
174
+ password: prod_pass
175
+
176
+ # PostgreSQL full SSL configuration example
177
+ secure-db:
178
+ type: postgres
179
+ host: secure-db.example.com
180
+ port: 5432
181
+ dbname: secure_db
182
+ user: secure_user
183
+ password: secure_pass
184
+ ssl:
185
+ mode: verify-full # disable/require/verify-ca/verify-full
186
+ cert: /path/to/client-cert.pem
187
+ key: /path/to/client-key.pem
188
+ root: /path/to/root.crt
189
+ ```
190
+
191
+ PostgreSQL SSL Configuration Options:
192
+ 1. Using URL parameters:
193
+ ```
194
+ postgresql://host:port/dbname?sslmode=verify-full&sslcert=/path/to/cert.pem
195
+ ```
196
+ 2. Using dedicated SSL configuration section:
197
+ ```yaml
198
+ ssl:
199
+ mode: verify-full # SSL verification mode
200
+ cert: /path/to/cert.pem # Client certificate
201
+ key: /path/to/key.pem # Client private key
202
+ root: /path/to/root.crt # CA certificate
203
+ ```
204
+
205
+ SSL Modes:
206
+ - disable: No SSL
207
+ - require: Use SSL but no certificate verification
208
+ - verify-ca: Verify server certificate is signed by trusted CA
209
+ - verify-full: Verify server certificate and hostname match
210
+
211
+ SQLite Configuration Options:
212
+ 1. Basic configuration with path:
213
+ ```yaml
214
+ type: sqlite
215
+ path: /path/to/db.sqlite
216
+ password: optional_password # Optional encryption
217
+ ```
218
+ 2. Using URI parameters:
219
+ ```yaml
220
+ type: sqlite
221
+ path: /path/to/db.sqlite?mode=ro&cache=shared
222
+ ```
223
+
224
+ ### Debug Mode
225
+ Set environment variable `MCP_DEBUG=1` to enable debug mode for detailed logging output.
226
+
227
+ [Rest of the README content remains unchanged...]
@@ -0,0 +1,18 @@
1
+ mcp_dbutils/__init__.py,sha256=9dqT2BdVvwforQVWOvJq2M0zCHxFamWIYWC4-6aY70k,1905
2
+ mcp_dbutils/base.py,sha256=2fLSPL-BxdgKIOT_eq73wQGla6MZB03jUrWqBRSWVBQ,25474
3
+ mcp_dbutils/config.py,sha256=uFI4Haw4en5gxHfCM9zugUqNCQFikdlHJZU_NTht7gQ,1905
4
+ mcp_dbutils/log.py,sha256=fibVIwsb1HVU5zriGrDZTMEirKjgIuxuN_B_YTdAJ7I,996
5
+ mcp_dbutils/stats.py,sha256=WYD9NAKHH2bFKmUSTOg18-SuIXergpznPq8AtIjtxdI,7101
6
+ mcp_dbutils/postgres/__init__.py,sha256=XanCXw-kVE6ayqFqjuLJ9swWPcCVlcKZXB2Et2Wjl9w,154
7
+ mcp_dbutils/postgres/config.py,sha256=FG0YUV4TtP1IGZT87ov_PK-ouOEjAOHdSaK42klk5OE,7750
8
+ mcp_dbutils/postgres/handler.py,sha256=O45K0MOosNQ68SJ8piXXH3aZ74RVQ981hudk6faj28Y,24397
9
+ mcp_dbutils/postgres/server.py,sha256=FROogvjr3xlKAZcAvJMNHGgyHqT_LaMK9-vYbA0Dbm0,8716
10
+ mcp_dbutils/sqlite/__init__.py,sha256=lTUOkSfSWNw_BohJ_KNxYKMCANdtpGa3JK_ZyJsg_Ls,134
11
+ mcp_dbutils/sqlite/config.py,sha256=2ekTp89rcCu4qQN1O3sZjIcOwjhzWi4tisD20T2WAik,4533
12
+ mcp_dbutils/sqlite/handler.py,sha256=VxBVbwpNSQHyiudQXYFYpcdoScvbK2IGzYMbkcpvFcI,17731
13
+ mcp_dbutils/sqlite/server.py,sha256=49ug43nyXZApclMXo0t5dKCN4wjDyDUZ-vmf746ac8M,7709
14
+ mcp_dbutils-0.10.0.dist-info/METADATA,sha256=TC_KrWktHUIG6vznvxhIsz-pJG_WWxgH8s1HQJO7NgQ,6262
15
+ mcp_dbutils-0.10.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ mcp_dbutils-0.10.0.dist-info/entry_points.txt,sha256=XTjt0QmYRgKOJQT6skR9bp1EMUfIrgpHeZJPZ3CJffs,49
17
+ mcp_dbutils-0.10.0.dist-info/licenses/LICENSE,sha256=1A_CwpWVlbjrKdVEYO77vYfnXlW7oxcilZ8FpA_BzCI,1065
18
+ mcp_dbutils-0.10.0.dist-info/RECORD,,