mcp-dbutils 0.16.0__py3-none-any.whl → 0.17.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/base.py +441 -323
- mcp_dbutils/mysql/config.py +122 -54
- mcp_dbutils/mysql/handler.py +69 -22
- mcp_dbutils/mysql/server.py +3 -3
- mcp_dbutils/sqlite/handler.py +66 -24
- mcp_dbutils/sqlite/server.py +2 -2
- mcp_dbutils-0.17.0.dist-info/METADATA +308 -0
- {mcp_dbutils-0.16.0.dist-info → mcp_dbutils-0.17.0.dist-info}/RECORD +11 -11
- mcp_dbutils-0.16.0.dist-info/METADATA +0 -572
- {mcp_dbutils-0.16.0.dist-info → mcp_dbutils-0.17.0.dist-info}/WHEEL +0 -0
- {mcp_dbutils-0.16.0.dist-info → mcp_dbutils-0.17.0.dist-info}/entry_points.txt +0 -0
- {mcp_dbutils-0.16.0.dist-info → mcp_dbutils-0.17.0.dist-info}/licenses/LICENSE +0 -0
mcp_dbutils/mysql/config.py
CHANGED
@@ -77,15 +77,19 @@ class MySQLConfig(ConnectionConfig):
|
|
77
77
|
ssl: Optional[SSLConfig] = None
|
78
78
|
|
79
79
|
@classmethod
|
80
|
-
def
|
81
|
-
"""
|
82
|
-
|
80
|
+
def _validate_connection_config(cls, configs: dict, db_name: str) -> dict:
|
81
|
+
"""验证连接配置是否有效
|
82
|
+
|
83
83
|
Args:
|
84
|
-
|
85
|
-
db_name:
|
86
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
|
mcp_dbutils/mysql/handler.py
CHANGED
@@ -31,12 +31,41 @@ class MySQLHandler(ConnectionHandler):
|
|
31
31
|
self.log("debug", f"Configuring connection with parameters: {masked_params}")
|
32
32
|
self.pool = None
|
33
33
|
|
34
|
+
async def _check_table_exists(self, cursor, table_name: str) -> None:
|
35
|
+
"""检查表是否存在
|
36
|
+
|
37
|
+
Args:
|
38
|
+
cursor: 数据库游标
|
39
|
+
table_name: 表名
|
40
|
+
|
41
|
+
Raises:
|
42
|
+
ConnectionHandlerError: 如果表不存在
|
43
|
+
"""
|
44
|
+
cursor.execute("""
|
45
|
+
SELECT COUNT(*) as count
|
46
|
+
FROM information_schema.tables
|
47
|
+
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
|
48
|
+
""", (self.config.database, table_name))
|
49
|
+
table_exists = cursor.fetchone()
|
50
|
+
|
51
|
+
# Handle different formats of cursor results (dict or tuple)
|
52
|
+
if not table_exists:
|
53
|
+
raise ConnectionHandlerError(f"Table '{self.config.database}.{table_name}' doesn't exist")
|
54
|
+
|
55
|
+
# If fetchone returns a dictionary (dictionary=True was used)
|
56
|
+
if isinstance(table_exists, dict) and 'count' in table_exists and table_exists['count'] == 0:
|
57
|
+
raise ConnectionHandlerError(f"Table '{self.config.database}.{table_name}' doesn't exist")
|
58
|
+
|
59
|
+
# If fetchone returns a tuple
|
60
|
+
if isinstance(table_exists, tuple) and table_exists[0] == 0:
|
61
|
+
raise ConnectionHandlerError(f"Table '{self.config.database}.{table_name}' doesn't exist")
|
62
|
+
|
34
63
|
async def get_tables(self) -> list[types.Resource]:
|
35
64
|
"""Get all table resources"""
|
36
65
|
try:
|
37
66
|
conn_params = self.config.get_connection_params()
|
38
67
|
conn = mysql.connector.connect(**conn_params)
|
39
|
-
with conn.cursor(dictionary=True) as cur:
|
68
|
+
with conn.cursor(dictionary=True) as cur: # NOSONAR
|
40
69
|
cur.execute("""
|
41
70
|
SELECT
|
42
71
|
TABLE_NAME as table_name,
|
@@ -66,7 +95,7 @@ class MySQLHandler(ConnectionHandler):
|
|
66
95
|
try:
|
67
96
|
conn_params = self.config.get_connection_params()
|
68
97
|
conn = mysql.connector.connect(**conn_params)
|
69
|
-
with conn.cursor(dictionary=True) as cur:
|
98
|
+
with conn.cursor(dictionary=True) as cur: # NOSONAR
|
70
99
|
# Get column information
|
71
100
|
cur.execute("""
|
72
101
|
SELECT
|
@@ -118,25 +147,31 @@ class MySQLHandler(ConnectionHandler):
|
|
118
147
|
conn = mysql.connector.connect(**conn_params)
|
119
148
|
self.log("debug", f"Executing query: {sql}")
|
120
149
|
|
121
|
-
with conn.cursor(dictionary=True) as cur:
|
122
|
-
#
|
123
|
-
|
150
|
+
with conn.cursor(dictionary=True) as cur: # NOSONAR
|
151
|
+
# Check if the query is a SELECT statement
|
152
|
+
sql_upper = sql.strip().upper()
|
153
|
+
is_select = sql_upper.startswith("SELECT")
|
154
|
+
|
155
|
+
# Only set read-only transaction for SELECT statements
|
156
|
+
if is_select:
|
157
|
+
cur.execute("SET TRANSACTION READ ONLY")
|
124
158
|
try:
|
125
159
|
cur.execute(sql)
|
126
|
-
|
160
|
+
if not is_select:
|
161
|
+
conn.commit()
|
162
|
+
results = cur.fetchall() if is_select else []
|
163
|
+
if cur.description is None: # DDL statements
|
164
|
+
return "Query executed successfully"
|
127
165
|
columns = [desc[0] for desc in cur.description]
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
'columns': columns,
|
132
|
-
'rows': results,
|
133
|
-
'row_count': len(results)
|
166
|
+
return str({
|
167
|
+
"columns": columns,
|
168
|
+
"rows": results
|
134
169
|
})
|
135
|
-
|
136
|
-
self.log("
|
137
|
-
|
170
|
+
except mysql.connector.Error as e:
|
171
|
+
self.log("error", f"Query error: {str(e)}")
|
172
|
+
raise ConnectionHandlerError(str(e))
|
138
173
|
finally:
|
139
|
-
cur.
|
174
|
+
cur.close()
|
140
175
|
except mysql.connector.Error as e:
|
141
176
|
error_msg = f"[{self.db_type}] Query execution failed: {str(e)}"
|
142
177
|
raise ConnectionHandlerError(error_msg)
|
@@ -150,7 +185,10 @@ class MySQLHandler(ConnectionHandler):
|
|
150
185
|
try:
|
151
186
|
conn_params = self.config.get_connection_params()
|
152
187
|
conn = mysql.connector.connect(**conn_params)
|
153
|
-
with conn.cursor(dictionary=True) as cur:
|
188
|
+
with conn.cursor(dictionary=True) as cur: # NOSONAR
|
189
|
+
# Check if table exists
|
190
|
+
await self._check_table_exists(cur, table_name)
|
191
|
+
|
154
192
|
# Get table information and comment
|
155
193
|
cur.execute("""
|
156
194
|
SELECT
|
@@ -220,7 +258,7 @@ class MySQLHandler(ConnectionHandler):
|
|
220
258
|
try:
|
221
259
|
conn_params = self.config.get_connection_params()
|
222
260
|
conn = mysql.connector.connect(**conn_params)
|
223
|
-
with conn.cursor(dictionary=True) as cur:
|
261
|
+
with conn.cursor(dictionary=True) as cur: # NOSONAR
|
224
262
|
# MySQL provides a SHOW CREATE TABLE statement
|
225
263
|
cur.execute(f"SHOW CREATE TABLE {table_name}")
|
226
264
|
result = cur.fetchone()
|
@@ -242,7 +280,10 @@ class MySQLHandler(ConnectionHandler):
|
|
242
280
|
try:
|
243
281
|
conn_params = self.config.get_connection_params()
|
244
282
|
conn = mysql.connector.connect(**conn_params)
|
245
|
-
with conn.cursor(dictionary=True) as cur:
|
283
|
+
with conn.cursor(dictionary=True) as cur: # NOSONAR
|
284
|
+
# Check if table exists
|
285
|
+
await self._check_table_exists(cur, table_name)
|
286
|
+
|
246
287
|
# Get index information
|
247
288
|
cur.execute("""
|
248
289
|
SELECT
|
@@ -301,7 +342,10 @@ class MySQLHandler(ConnectionHandler):
|
|
301
342
|
try:
|
302
343
|
conn_params = self.config.get_connection_params()
|
303
344
|
conn = mysql.connector.connect(**conn_params)
|
304
|
-
with conn.cursor(dictionary=True) as cur:
|
345
|
+
with conn.cursor(dictionary=True) as cur: # NOSONAR
|
346
|
+
# Check if table exists
|
347
|
+
await self._check_table_exists(cur, table_name)
|
348
|
+
|
305
349
|
# Get table statistics
|
306
350
|
cur.execute("""
|
307
351
|
SELECT
|
@@ -366,7 +410,10 @@ class MySQLHandler(ConnectionHandler):
|
|
366
410
|
try:
|
367
411
|
conn_params = self.config.get_connection_params()
|
368
412
|
conn = mysql.connector.connect(**conn_params)
|
369
|
-
with conn.cursor(dictionary=True) as cur:
|
413
|
+
with conn.cursor(dictionary=True) as cur: # NOSONAR
|
414
|
+
# Check if table exists
|
415
|
+
await self._check_table_exists(cur, table_name)
|
416
|
+
|
370
417
|
# Get constraint information
|
371
418
|
cur.execute("""
|
372
419
|
SELECT
|
@@ -429,7 +476,7 @@ class MySQLHandler(ConnectionHandler):
|
|
429
476
|
try:
|
430
477
|
conn_params = self.config.get_connection_params()
|
431
478
|
conn = mysql.connector.connect(**conn_params)
|
432
|
-
with conn.cursor(dictionary=True) as cur:
|
479
|
+
with conn.cursor(dictionary=True) as cur: # NOSONAR
|
433
480
|
# Get EXPLAIN output
|
434
481
|
cur.execute(f"EXPLAIN FORMAT=TREE {sql}")
|
435
482
|
explain_result = cur.fetchall()
|
mcp_dbutils/mysql/server.py
CHANGED
@@ -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:
|
mcp_dbutils/sqlite/handler.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
"""SQLite connection handler implementation"""
|
2
2
|
|
3
3
|
import sqlite3
|
4
|
+
import time
|
4
5
|
|
5
6
|
import mcp.types as types
|
6
7
|
|
@@ -78,28 +79,51 @@ class SQLiteHandler(ConnectionHandler):
|
|
78
79
|
async def _execute_query(self, sql: str) -> str:
|
79
80
|
"""Execute SQL query"""
|
80
81
|
try:
|
81
|
-
#
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
conn.row_factory = sqlite3.Row
|
87
|
-
cur = conn.cursor()
|
88
|
-
self.log("debug", f"Executing query: {sql}")
|
89
|
-
|
90
|
-
cur.execute(sql)
|
91
|
-
results = cur.fetchall()
|
92
|
-
rows = [dict(row) for row in results]
|
82
|
+
# Check if the query is a DDL statement
|
83
|
+
sql_upper = sql.strip().upper()
|
84
|
+
is_ddl = sql_upper.startswith(("CREATE", "DROP", "ALTER", "TRUNCATE"))
|
85
|
+
is_dml = sql_upper.startswith(("INSERT", "UPDATE", "DELETE"))
|
86
|
+
is_select = sql_upper.startswith("SELECT")
|
93
87
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
})
|
88
|
+
if not (is_select or is_ddl or is_dml):
|
89
|
+
raise ConnectionHandlerError("Only SELECT, DDL, and DML statements are allowed")
|
90
|
+
|
91
|
+
conn = sqlite3.connect(self.config.path)
|
92
|
+
cur = conn.cursor()
|
100
93
|
|
101
|
-
|
102
|
-
|
94
|
+
try:
|
95
|
+
start_time = time.time()
|
96
|
+
cur.execute(sql)
|
97
|
+
conn.commit()
|
98
|
+
end_time = time.time()
|
99
|
+
elapsed_ms = (end_time - start_time) * 1000
|
100
|
+
self.log("debug", f"Query executed in {elapsed_ms:.2f}ms")
|
101
|
+
|
102
|
+
if is_select:
|
103
|
+
# Get column names
|
104
|
+
columns = [description[0] for description in cur.description]
|
105
|
+
# Fetch results and convert to dictionaries
|
106
|
+
results = []
|
107
|
+
for row in cur.fetchall():
|
108
|
+
# Convert each row to a dictionary
|
109
|
+
row_dict = {}
|
110
|
+
for i, col_name in enumerate(columns):
|
111
|
+
row_dict[col_name] = row[i]
|
112
|
+
results.append(row_dict)
|
113
|
+
|
114
|
+
return str({
|
115
|
+
"columns": columns,
|
116
|
+
"rows": results
|
117
|
+
})
|
118
|
+
else:
|
119
|
+
# For DDL/DML statements
|
120
|
+
return "Query executed successfully"
|
121
|
+
except sqlite3.Error as e:
|
122
|
+
self.log("error", f"Query error: {str(e)}")
|
123
|
+
raise ConnectionHandlerError(str(e))
|
124
|
+
finally:
|
125
|
+
cur.close()
|
126
|
+
conn.close()
|
103
127
|
except sqlite3.Error as e:
|
104
128
|
error_msg = f"[{self.db_type}] Query execution failed: {str(e)}"
|
105
129
|
raise ConnectionHandlerError(error_msg)
|
@@ -142,7 +166,7 @@ class SQLiteHandler(ConnectionHandler):
|
|
142
166
|
with sqlite3.connect(self.config.path) as conn:
|
143
167
|
cur = conn.cursor()
|
144
168
|
# SQLite provides the complete CREATE statement
|
145
|
-
cur.execute(
|
169
|
+
cur.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
|
146
170
|
result = cur.fetchone()
|
147
171
|
|
148
172
|
if not result:
|
@@ -151,7 +175,7 @@ class SQLiteHandler(ConnectionHandler):
|
|
151
175
|
ddl = result[0]
|
152
176
|
|
153
177
|
# Get indexes
|
154
|
-
cur.execute(
|
178
|
+
cur.execute("SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name=?", (table_name,))
|
155
179
|
indexes = cur.fetchall()
|
156
180
|
|
157
181
|
# Add index definitions
|
@@ -173,6 +197,12 @@ class SQLiteHandler(ConnectionHandler):
|
|
173
197
|
try:
|
174
198
|
with sqlite3.connect(self.config.path) as conn:
|
175
199
|
cur = conn.cursor()
|
200
|
+
|
201
|
+
# Check if table exists
|
202
|
+
cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
|
203
|
+
if not cur.fetchone():
|
204
|
+
raise ConnectionHandlerError(f"Table '{table_name}' doesn't exist")
|
205
|
+
|
176
206
|
# 获取索引列表
|
177
207
|
cur.execute(f"PRAGMA index_list({table_name})")
|
178
208
|
indexes = cur.fetchall()
|
@@ -221,6 +251,11 @@ class SQLiteHandler(ConnectionHandler):
|
|
221
251
|
with sqlite3.connect(self.config.path) as conn:
|
222
252
|
cur = conn.cursor()
|
223
253
|
|
254
|
+
# Check if table exists
|
255
|
+
cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
|
256
|
+
if not cur.fetchone():
|
257
|
+
raise ConnectionHandlerError(f"Table '{table_name}' doesn't exist")
|
258
|
+
|
224
259
|
# Get basic table information
|
225
260
|
cur.execute(f"PRAGMA table_info({table_name})")
|
226
261
|
columns = cur.fetchall()
|
@@ -234,9 +269,9 @@ class SQLiteHandler(ConnectionHandler):
|
|
234
269
|
indexes = cur.fetchall()
|
235
270
|
|
236
271
|
# Get page count and size
|
237
|
-
cur.execute(
|
272
|
+
cur.execute("PRAGMA page_count")
|
238
273
|
page_count = cur.fetchone()[0]
|
239
|
-
cur.execute(
|
274
|
+
cur.execute("PRAGMA page_size")
|
240
275
|
page_size = cur.fetchone()[0]
|
241
276
|
|
242
277
|
# Calculate total size
|
@@ -382,6 +417,13 @@ class SQLiteHandler(ConnectionHandler):
|
|
382
417
|
with sqlite3.connect(self.config.path) as conn:
|
383
418
|
cur = conn.cursor()
|
384
419
|
|
420
|
+
# Check if the query is valid by preparing it
|
421
|
+
try:
|
422
|
+
# Use prepare to validate the query without executing it
|
423
|
+
conn.execute(f"EXPLAIN {sql}")
|
424
|
+
except sqlite3.Error as e:
|
425
|
+
raise ConnectionHandlerError(f"Failed to explain query: {str(e)}")
|
426
|
+
|
385
427
|
# Get EXPLAIN output
|
386
428
|
cur.execute(f"EXPLAIN QUERY PLAN {sql}")
|
387
429
|
plan = cur.fetchall()
|
mcp_dbutils/sqlite/server.py
CHANGED
@@ -54,7 +54,7 @@ class SQLiteServer(ConnectionServer):
|
|
54
54
|
# 使用默认连接
|
55
55
|
conn = self._get_connection()
|
56
56
|
|
57
|
-
with closing(conn) as
|
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
|
158
|
+
with closing(conn) as _:
|
159
159
|
self.log("info", f"执行查询: {sql}")
|
160
160
|
cursor = conn.execute(sql)
|
161
161
|
results = cursor.fetchall()
|