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.
@@ -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
 
@@ -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
- # Start read-only transaction
123
- cur.execute("SET TRANSACTION READ ONLY")
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
- results = cur.fetchall()
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
- result_text = str({
130
- 'type': self.db_type,
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("debug", f"Query completed, returned {len(results)} rows")
137
- return result_text
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.execute("ROLLBACK")
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()
@@ -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:
@@ -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
- # Only allow SELECT statements
82
- if not sql.strip().upper().startswith("SELECT"):
83
- raise ConnectionHandlerError("cannot execute DELETE statement")
84
-
85
- with sqlite3.connect(self.config.path) as conn:
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
- result_text = str({
95
- 'type': self.db_type,
96
- 'columns': list(rows[0].keys()) if rows else [],
97
- 'rows': rows,
98
- 'row_count': len(rows)
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
- self.log("debug", f"Query completed, returned {len(rows)} rows")
102
- return result_text
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(f"SELECT sql FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
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(f"SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name=?", (table_name,))
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(f"PRAGMA page_count")
272
+ cur.execute("PRAGMA page_count")
238
273
  page_count = cur.fetchone()[0]
239
- cur.execute(f"PRAGMA page_size")
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()
@@ -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()