mcp-dbutils 0.8.0__py3-none-any.whl → 0.9.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.
@@ -1,133 +1,417 @@
1
- """SQLite database handler implementation"""
1
+ """SQLite connection handler implementation"""
2
2
 
3
3
  import sqlite3
4
- from pathlib import Path
5
- from contextlib import closing
4
+ import json
5
+ from typing import Any
6
6
  import mcp.types as types
7
7
 
8
- from ..base import DatabaseHandler, DatabaseError
9
- from .config import SqliteConfig
8
+ from ..base import ConnectionHandler, ConnectionHandlerError
9
+ from .config import SQLiteConfig
10
10
 
11
- class SqliteHandler(DatabaseHandler):
11
+ class SQLiteHandler(ConnectionHandler):
12
12
  @property
13
13
  def db_type(self) -> str:
14
14
  return 'sqlite'
15
15
 
16
- def __init__(self, config_path: str, database: str, debug: bool = False):
16
+ def __init__(self, config_path: str, connection: str, debug: bool = False):
17
17
  """Initialize SQLite handler
18
18
 
19
19
  Args:
20
20
  config_path: Path to configuration file
21
- database: Database configuration name
21
+ connection: Database connection name
22
22
  debug: Enable debug mode
23
23
  """
24
- super().__init__(config_path, database, debug)
25
- self.config = SqliteConfig.from_yaml(config_path, database)
26
-
27
- # Ensure database directory exists
28
- db_file = Path(self.config.absolute_path)
29
- db_file.parent.mkdir(parents=True, exist_ok=True)
30
-
31
- # No connection test during initialization
32
- self.log("debug", f"Configuring database: {self.config.get_masked_connection_info()}")
33
-
34
- def _get_connection(self):
35
- """Get database connection"""
36
- connection_params = self.config.get_connection_params()
37
- conn = sqlite3.connect(**connection_params)
38
- conn.row_factory = sqlite3.Row
39
- return conn
24
+ super().__init__(config_path, connection, debug)
25
+ self.config = SQLiteConfig.from_yaml(config_path, connection)
40
26
 
41
27
  async def get_tables(self) -> list[types.Resource]:
42
28
  """Get all table resources"""
43
29
  try:
44
- with closing(self._get_connection()) as conn:
45
- cursor = conn.execute(
46
- "SELECT name FROM sqlite_master WHERE type='table'"
47
- )
48
- tables = cursor.fetchall()
49
-
30
+ with sqlite3.connect(self.config.path) as conn:
31
+ cur = conn.cursor()
32
+ cur.execute("SELECT name FROM sqlite_master WHERE type='table'")
33
+ tables = cur.fetchall()
50
34
  return [
51
35
  types.Resource(
52
- uri=f"sqlite://{self.database}/{table[0]}/schema",
36
+ uri=f"sqlite://{self.connection}/{table[0]}/schema",
53
37
  name=f"{table[0]} schema",
54
38
  mimeType="application/json"
55
39
  ) for table in tables
56
40
  ]
57
41
  except sqlite3.Error as e:
58
42
  error_msg = f"Failed to get table list: {str(e)}"
59
- self.log("error", error_msg)
60
- raise
43
+ self.stats.record_error(e.__class__.__name__)
44
+ raise ConnectionHandlerError(error_msg)
61
45
 
62
46
  async def get_schema(self, table_name: str) -> str:
63
47
  """Get table schema information"""
64
48
  try:
65
- with closing(self._get_connection()) as conn:
66
- # Get table structure
67
- cursor = conn.execute(f"PRAGMA table_info({table_name})")
68
- columns = cursor.fetchall()
49
+ with sqlite3.connect(self.config.path) as conn:
50
+ cur = conn.cursor()
51
+ cur.execute(f"PRAGMA table_info({table_name})")
52
+ columns = cur.fetchall()
69
53
 
70
- # Get index information
71
- cursor = conn.execute(f"PRAGMA index_list({table_name})")
72
- indexes = cursor.fetchall()
54
+ cur.execute(f"PRAGMA index_list({table_name})")
55
+ indexes = cur.fetchall()
73
56
 
74
- schema_info = {
57
+ return str({
75
58
  'columns': [{
76
- 'name': col['name'],
77
- 'type': col['type'],
78
- 'nullable': not col['notnull'],
79
- 'primary_key': bool(col['pk'])
59
+ 'name': col[1],
60
+ 'type': col[2],
61
+ 'nullable': not col[3],
62
+ 'default': col[4],
63
+ 'primary_key': bool(col[5])
80
64
  } for col in columns],
81
65
  'indexes': [{
82
- 'name': idx['name'],
83
- 'unique': bool(idx['unique'])
66
+ 'name': idx[1],
67
+ 'unique': bool(idx[2])
84
68
  } for idx in indexes]
85
- }
86
-
87
- return str(schema_info)
69
+ })
88
70
  except sqlite3.Error as e:
89
71
  error_msg = f"Failed to read table schema: {str(e)}"
90
- self.log("error", error_msg)
91
- raise
72
+ self.stats.record_error(e.__class__.__name__)
73
+ raise ConnectionHandlerError(error_msg)
92
74
 
93
75
  async def _execute_query(self, sql: str) -> str:
94
76
  """Execute SQL query"""
95
- # Check for non-SELECT queries
96
- sql_lower = sql.lower().strip()
97
- if not sql_lower.startswith('select'):
98
- error_msg = "cannot execute DELETE statement"
99
- if sql_lower.startswith('delete'):
100
- error_msg = "cannot execute DELETE statement"
101
- elif sql_lower.startswith('update'):
102
- error_msg = "cannot execute UPDATE statement"
103
- elif sql_lower.startswith('insert'):
104
- error_msg = "cannot execute INSERT statement"
105
- raise DatabaseError(error_msg)
106
-
107
77
  try:
108
- with closing(self._get_connection()) as conn:
78
+ # Only allow SELECT statements
79
+ if not sql.strip().upper().startswith("SELECT"):
80
+ raise ConnectionHandlerError("cannot execute DELETE statement")
81
+
82
+ with sqlite3.connect(self.config.path) as conn:
83
+ conn.row_factory = sqlite3.Row
84
+ cur = conn.cursor()
109
85
  self.log("debug", f"Executing query: {sql}")
110
- cursor = conn.execute(sql)
111
- results = cursor.fetchall()
112
-
113
- columns = [desc[0] for desc in cursor.description]
114
- formatted_results = [dict(zip(columns, row)) for row in results]
86
+
87
+ cur.execute(sql)
88
+ results = cur.fetchall()
89
+ rows = [dict(row) for row in results]
115
90
 
116
91
  result_text = str({
117
92
  'type': self.db_type,
118
- 'columns': columns,
119
- 'rows': formatted_results,
120
- 'row_count': len(results)
93
+ 'columns': list(rows[0].keys()) if rows else [],
94
+ 'rows': rows,
95
+ 'row_count': len(rows)
121
96
  })
122
97
 
123
- self.log("debug", f"Query completed, returned {len(results)} rows")
98
+ self.log("debug", f"Query completed, returned {len(rows)} rows")
124
99
  return result_text
125
-
126
100
  except sqlite3.Error as e:
127
101
  error_msg = f"[{self.db_type}] Query execution failed: {str(e)}"
128
- raise DatabaseError(error_msg)
102
+ raise ConnectionHandlerError(error_msg)
103
+
104
+ async def get_table_description(self, table_name: str) -> str:
105
+ """Get detailed table description"""
106
+ try:
107
+ with sqlite3.connect(self.config.path) as conn:
108
+ cur = conn.cursor()
109
+ # 获取表信息
110
+ cur.execute(f"PRAGMA table_info({table_name})")
111
+ columns = cur.fetchall()
112
+
113
+ # SQLite不支持表级注释,但我们可以获取表的详细信息
114
+ description = [
115
+ f"Table: {table_name}\n",
116
+ "Columns:"
117
+ ]
118
+
119
+ for col in columns:
120
+ col_info = [
121
+ f" {col[1]} ({col[2]})",
122
+ f" Nullable: {'No' if col[3] else 'Yes'}",
123
+ f" Default: {col[4] or 'None'}",
124
+ f" Primary Key: {'Yes' if col[5] else 'No'}"
125
+ ]
126
+ description.extend(col_info)
127
+ description.append("") # Empty line between columns
128
+
129
+ return "\n".join(description)
130
+
131
+ except sqlite3.Error as e:
132
+ error_msg = f"Failed to get table description: {str(e)}"
133
+ self.stats.record_error(e.__class__.__name__)
134
+ raise ConnectionHandlerError(error_msg)
135
+
136
+ async def get_table_ddl(self, table_name: str) -> str:
137
+ """Get DDL statement for creating table"""
138
+ try:
139
+ with sqlite3.connect(self.config.path) as conn:
140
+ cur = conn.cursor()
141
+ # SQLite provides the complete CREATE statement
142
+ cur.execute(f"SELECT sql FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
143
+ result = cur.fetchone()
144
+
145
+ if not result:
146
+ return f"Table {table_name} not found"
147
+
148
+ ddl = result[0]
149
+
150
+ # Get indexes
151
+ cur.execute(f"SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name=?", (table_name,))
152
+ indexes = cur.fetchall()
153
+
154
+ # Add index definitions
155
+ if indexes:
156
+ ddl = ddl + "\n\n-- Indexes:"
157
+ for idx in indexes:
158
+ if idx[0]: # Some internal indexes might have NULL sql
159
+ ddl = ddl + "\n" + idx[0] + ";"
160
+
161
+ return ddl
162
+
163
+ except sqlite3.Error as e:
164
+ error_msg = f"Failed to get table DDL: {str(e)}"
165
+ self.stats.record_error(e.__class__.__name__)
166
+ raise ConnectionHandlerError(error_msg)
167
+
168
+ async def get_table_indexes(self, table_name: str) -> str:
169
+ """Get index information for table"""
170
+ try:
171
+ with sqlite3.connect(self.config.path) as conn:
172
+ cur = conn.cursor()
173
+ # 获取索引列表
174
+ cur.execute(f"PRAGMA index_list({table_name})")
175
+ indexes = cur.fetchall()
176
+
177
+ if not indexes:
178
+ return f"No indexes found on table {table_name}"
179
+
180
+ formatted_indexes = [f"Indexes for {table_name}:"]
181
+
182
+ for idx in indexes:
183
+ # 获取索引详细信息
184
+ cur.execute(f"PRAGMA index_info({idx[1]})")
185
+ index_info = cur.fetchall()
186
+
187
+ # 获取索引的SQL定义
188
+ cur.execute("SELECT sql FROM sqlite_master WHERE type='index' AND name=?", (idx[1],))
189
+ sql = cur.fetchone()
190
+
191
+ index_details = [
192
+ f"\nIndex: {idx[1]}",
193
+ f"Type: {'UNIQUE' if idx[2] else 'INDEX'}",
194
+ "Columns:"
195
+ ]
196
+
197
+ for col in index_info:
198
+ index_details.append(f" - {col[2]}")
199
+
200
+ if sql and sql[0]:
201
+ index_details.extend([
202
+ "Definition:",
203
+ f" {sql[0]}"
204
+ ])
205
+
206
+ formatted_indexes.extend(index_details)
207
+
208
+ return "\n".join(formatted_indexes)
209
+
210
+ except sqlite3.Error as e:
211
+ error_msg = f"Failed to get index information: {str(e)}"
212
+ self.stats.record_error(e.__class__.__name__)
213
+ raise ConnectionHandlerError(error_msg)
214
+
215
+ async def get_table_stats(self, table_name: str) -> str:
216
+ """Get table statistics information"""
217
+ try:
218
+ with sqlite3.connect(self.config.path) as conn:
219
+ cur = conn.cursor()
220
+
221
+ # Get basic table information
222
+ cur.execute(f"PRAGMA table_info({table_name})")
223
+ columns = cur.fetchall()
224
+
225
+ # Count rows
226
+ cur.execute(f"SELECT COUNT(*) FROM {table_name}")
227
+ row_count = cur.fetchone()[0]
228
+
229
+ # Get index information
230
+ cur.execute(f"PRAGMA index_list({table_name})")
231
+ indexes = cur.fetchall()
232
+
233
+ # Get page count and size
234
+ cur.execute(f"PRAGMA page_count")
235
+ page_count = cur.fetchone()[0]
236
+ cur.execute(f"PRAGMA page_size")
237
+ page_size = cur.fetchone()[0]
238
+
239
+ # Calculate total size
240
+ total_size = page_count * page_size
241
+
242
+ # Format size in human readable format
243
+ def format_size(size):
244
+ for unit in ['B', 'KB', 'MB', 'GB']:
245
+ if size < 1024:
246
+ return f"{size:.2f} {unit}"
247
+ size /= 1024
248
+ return f"{size:.2f} TB"
249
+
250
+ # Get column statistics
251
+ column_stats = []
252
+ for col in columns:
253
+ col_name = col[1]
254
+ # Get null count
255
+ cur.execute(f"SELECT COUNT(*) FROM {table_name} WHERE {col_name} IS NULL")
256
+ null_count = cur.fetchone()[0]
257
+ # Get distinct value count
258
+ cur.execute(f"SELECT COUNT(DISTINCT {col_name}) FROM {table_name}")
259
+ distinct_count = cur.fetchone()[0]
260
+
261
+ column_stats.append({
262
+ 'name': col_name,
263
+ 'type': col[2],
264
+ 'null_count': null_count,
265
+ 'null_percent': (null_count / row_count * 100) if row_count > 0 else 0,
266
+ 'distinct_count': distinct_count
267
+ })
268
+
269
+ # Format output
270
+ output = [
271
+ f"Table Statistics for {table_name}:",
272
+ f" Row Count: {row_count:,}",
273
+ f" Total Size: {format_size(total_size)}",
274
+ f" Page Count: {page_count:,}",
275
+ f" Page Size: {format_size(page_size)}",
276
+ f" Index Count: {len(indexes)}\n",
277
+ "Column Statistics:"
278
+ ]
279
+
280
+ for stat in column_stats:
281
+ col_info = [
282
+ f" {stat['name']} ({stat['type']}):",
283
+ f" Null Values: {stat['null_count']:,} ({stat['null_percent']:.1f}%)",
284
+ f" Distinct Values: {stat['distinct_count']:,}"
285
+ ]
286
+ output.extend(col_info)
287
+ output.append("") # Empty line between columns
288
+
289
+ return "\n".join(output)
290
+
291
+ except sqlite3.Error as e:
292
+ error_msg = f"Failed to get table statistics: {str(e)}"
293
+ self.stats.record_error(e.__class__.__name__)
294
+ raise ConnectionHandlerError(error_msg)
295
+
296
+ async def get_table_constraints(self, table_name: str) -> str:
297
+ """Get constraint information for table"""
298
+ try:
299
+ with sqlite3.connect(self.config.path) as conn:
300
+ cur = conn.cursor()
301
+
302
+ # Get table info (includes PRIMARY KEY)
303
+ cur.execute(f"PRAGMA table_info({table_name})")
304
+ columns = cur.fetchall()
305
+
306
+ # Get foreign keys
307
+ cur.execute(f"PRAGMA foreign_key_list({table_name})")
308
+ foreign_keys = cur.fetchall()
309
+
310
+ # Get indexes (for UNIQUE constraints)
311
+ cur.execute(f"PRAGMA index_list({table_name})")
312
+ indexes = cur.fetchall()
313
+
314
+ output = [f"Constraints for {table_name}:"]
315
+
316
+ # Primary Key constraints
317
+ pk_columns = [col[1] for col in columns if col[5]] # col[5] is pk flag
318
+ if pk_columns:
319
+ output.extend([
320
+ "\nPrimary Key Constraints:",
321
+ f" PRIMARY KEY ({', '.join(pk_columns)})"
322
+ ])
323
+
324
+ # Foreign Key constraints
325
+ if foreign_keys:
326
+ output.append("\nForeign Key Constraints:")
327
+ current_fk = None
328
+ fk_columns = []
329
+
330
+ for fk in foreign_keys:
331
+ # SQLite foreign_key_list format:
332
+ # id, seq, table, from, to, on_update, on_delete, match
333
+ if current_fk != fk[0]:
334
+ if fk_columns:
335
+ output.append(f" ({', '.join(fk_columns)})")
336
+ current_fk = fk[0]
337
+ fk_columns = []
338
+ output.append(f" FOREIGN KEY:")
339
+ output.append(f" Referenced Table: {fk[2]}")
340
+ fk_columns.append(f"{fk[3]} -> {fk[2]}.{fk[4]}")
341
+ if fk[5]: # on_update
342
+ output.append(f" ON UPDATE: {fk[5]}")
343
+ if fk[6]: # on_delete
344
+ output.append(f" ON DELETE: {fk[6]}")
345
+
346
+ if fk_columns:
347
+ output.append(f" ({', '.join(fk_columns)})")
348
+
349
+ # Unique constraints (from indexes)
350
+ unique_indexes = [idx for idx in indexes if idx[2]] # idx[2] is unique flag
351
+ if unique_indexes:
352
+ output.append("\nUnique Constraints:")
353
+ for idx in unique_indexes:
354
+ # Get columns in the unique index
355
+ cur.execute(f"PRAGMA index_info({idx[1]})")
356
+ index_info = cur.fetchall()
357
+ columns = [info[2] for info in index_info] # info[2] is column name
358
+ output.append(f" UNIQUE ({', '.join(columns)})")
359
+
360
+ # Check constraints
361
+ # Note: SQLite doesn't provide direct access to CHECK constraints through PRAGMA
362
+ # We need to parse the table creation SQL
363
+ cur.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
364
+ create_sql = cur.fetchone()[0]
365
+ if "CHECK" in create_sql.upper():
366
+ output.append("\nCheck Constraints:")
367
+ output.append(" See table DDL for CHECK constraints")
368
+
369
+ return "\n".join(output)
370
+
371
+ except sqlite3.Error as e:
372
+ error_msg = f"Failed to get constraint information: {str(e)}"
373
+ self.stats.record_error(e.__class__.__name__)
374
+ raise ConnectionHandlerError(error_msg)
375
+
376
+ async def explain_query(self, sql: str) -> str:
377
+ """Get query execution plan"""
378
+ try:
379
+ with sqlite3.connect(self.config.path) as conn:
380
+ cur = conn.cursor()
381
+
382
+ # Get EXPLAIN output
383
+ cur.execute(f"EXPLAIN QUERY PLAN {sql}")
384
+ plan = cur.fetchall()
385
+
386
+ # Format the output
387
+ output = [
388
+ "Query Execution Plan:",
389
+ "==================\n",
390
+ "Details:",
391
+ "--------"
392
+ ]
393
+
394
+ for step in plan:
395
+ # EXPLAIN QUERY PLAN format:
396
+ # id | parent | notused | detail
397
+ indent = " " * (step[0] - step[1] if step[1] >= 0 else step[0])
398
+ output.append(f"{indent}{step[3]}")
399
+
400
+ # Add query statistics
401
+ output.extend([
402
+ "\nNote: SQLite's EXPLAIN QUERY PLAN provides a high-level overview.",
403
+ "For detailed execution statistics, consider using EXPLAIN (not QUERY PLAN)",
404
+ "which shows the virtual machine instructions."
405
+ ])
406
+
407
+ return "\n".join(output)
408
+
409
+ except sqlite3.Error as e:
410
+ error_msg = f"Failed to explain query: {str(e)}"
411
+ self.stats.record_error(e.__class__.__name__)
412
+ raise ConnectionHandlerError(error_msg)
129
413
 
130
414
  async def cleanup(self):
131
415
  """Cleanup resources"""
132
- # Log final stats before cleanup
416
+ # Log final stats
133
417
  self.log("info", f"Final SQLite handler stats: {self.stats.to_dict()}")
@@ -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)