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.
- mcp_dbutils/__init__.py +9 -9
- mcp_dbutils/base.py +383 -58
- mcp_dbutils/config.py +12 -12
- mcp_dbutils/postgres/__init__.py +3 -3
- mcp_dbutils/postgres/config.py +16 -16
- mcp_dbutils/postgres/handler.py +446 -14
- mcp_dbutils/postgres/server.py +16 -16
- mcp_dbutils/sqlite/__init__.py +3 -3
- mcp_dbutils/sqlite/config.py +12 -12
- mcp_dbutils/sqlite/handler.py +361 -77
- mcp_dbutils/sqlite/server.py +21 -21
- mcp_dbutils/stats.py +112 -3
- {mcp_dbutils-0.8.0.dist-info → mcp_dbutils-0.9.0.dist-info}/METADATA +42 -8
- mcp_dbutils-0.9.0.dist-info/RECORD +18 -0
- mcp_dbutils-0.8.0.dist-info/RECORD +0 -18
- {mcp_dbutils-0.8.0.dist-info → mcp_dbutils-0.9.0.dist-info}/WHEEL +0 -0
- {mcp_dbutils-0.8.0.dist-info → mcp_dbutils-0.9.0.dist-info}/entry_points.txt +0 -0
- {mcp_dbutils-0.8.0.dist-info → mcp_dbutils-0.9.0.dist-info}/licenses/LICENSE +0 -0
mcp_dbutils/sqlite/handler.py
CHANGED
@@ -1,133 +1,417 @@
|
|
1
|
-
"""SQLite
|
1
|
+
"""SQLite connection handler implementation"""
|
2
2
|
|
3
3
|
import sqlite3
|
4
|
-
|
5
|
-
from
|
4
|
+
import json
|
5
|
+
from typing import Any
|
6
6
|
import mcp.types as types
|
7
7
|
|
8
|
-
from ..base import
|
9
|
-
from .config import
|
8
|
+
from ..base import ConnectionHandler, ConnectionHandlerError
|
9
|
+
from .config import SQLiteConfig
|
10
10
|
|
11
|
-
class
|
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,
|
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
|
-
|
21
|
+
connection: Database connection name
|
22
22
|
debug: Enable debug mode
|
23
23
|
"""
|
24
|
-
super().__init__(config_path,
|
25
|
-
self.config =
|
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
|
45
|
-
|
46
|
-
|
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.
|
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.
|
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
|
66
|
-
|
67
|
-
|
68
|
-
columns =
|
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
|
-
|
71
|
-
|
72
|
-
indexes = cursor.fetchall()
|
54
|
+
cur.execute(f"PRAGMA index_list({table_name})")
|
55
|
+
indexes = cur.fetchall()
|
73
56
|
|
74
|
-
|
57
|
+
return str({
|
75
58
|
'columns': [{
|
76
|
-
'name': col[
|
77
|
-
'type': col[
|
78
|
-
'nullable': not col[
|
79
|
-
'
|
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[
|
83
|
-
'unique': bool(idx[
|
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.
|
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
|
-
|
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
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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':
|
119
|
-
'rows':
|
120
|
-
'row_count': len(
|
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(
|
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
|
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
|
416
|
+
# Log final stats
|
133
417
|
self.log("info", f"Final SQLite handler stats: {self.stats.to_dict()}")
|
mcp_dbutils/sqlite/server.py
CHANGED
@@ -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
|
10
|
+
from ..base import ConnectionServer
|
11
11
|
from ..log import create_logger
|
12
|
-
from .config import
|
12
|
+
from .config import SQLiteConfig
|
13
13
|
|
14
14
|
# 获取包信息用于日志命名
|
15
15
|
pkg_meta = metadata("mcp-dbutils")
|
16
16
|
|
17
|
-
class
|
18
|
-
def __init__(self, config:
|
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"
|
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"
|
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
|
-
|
57
|
-
if
|
58
|
-
#
|
59
|
-
config =
|
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"使用配置 {
|
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
|
-
"
|
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
|
-
|
160
|
-
if
|
161
|
-
#
|
162
|
-
config =
|
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"使用配置 {
|
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':
|
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':
|
197
|
+
'config_name': connection or 'default',
|
198
198
|
'error': f"查询执行失败: {str(e)}"
|
199
199
|
})
|
200
200
|
self.log("error", error_msg)
|