mcp-dbutils 0.10.0__py3-none-any.whl → 0.10.3__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.
@@ -0,0 +1,467 @@
1
+ """MySQL connection handler implementation"""
2
+
3
+ import mysql.connector
4
+ from mysql.connector.pooling import MySQLConnectionPool
5
+ import mcp.types as types
6
+
7
+ from ..base import ConnectionHandler, ConnectionHandlerError
8
+ from .config import MySQLConfig
9
+
10
+ class MySQLHandler(ConnectionHandler):
11
+ @property
12
+ def db_type(self) -> str:
13
+ return 'mysql'
14
+
15
+ def __init__(self, config_path: str, connection: str, debug: bool = False):
16
+ """Initialize MySQL handler
17
+
18
+ Args:
19
+ config_path: Path to configuration file
20
+ connection: Database connection name
21
+ debug: Enable debug mode
22
+ """
23
+ super().__init__(config_path, connection, debug)
24
+ self.config = MySQLConfig.from_yaml(config_path, connection)
25
+
26
+ # No connection pool creation during initialization
27
+ masked_params = self.config.get_masked_connection_info()
28
+ self.log("debug", f"Configuring connection with parameters: {masked_params}")
29
+ self.pool = None
30
+
31
+ async def get_tables(self) -> list[types.Resource]:
32
+ """Get all table resources"""
33
+ try:
34
+ conn_params = self.config.get_connection_params()
35
+ conn = mysql.connector.connect(**conn_params)
36
+ with conn.cursor(dictionary=True) as cur:
37
+ cur.execute("""
38
+ SELECT
39
+ TABLE_NAME as table_name,
40
+ TABLE_COMMENT as description
41
+ FROM information_schema.tables
42
+ WHERE TABLE_SCHEMA = %s
43
+ """, (self.config.database,))
44
+ tables = cur.fetchall()
45
+ return [
46
+ types.Resource(
47
+ uri=f"mysql://{self.connection}/{table['table_name']}/schema",
48
+ name=f"{table['table_name']} schema",
49
+ description=table['description'] if table['description'] else None,
50
+ mimeType="application/json"
51
+ ) for table in tables
52
+ ]
53
+ except mysql.connector.Error as e:
54
+ error_msg = f"Failed to get tables: {str(e)}"
55
+ self.stats.record_error(e.__class__.__name__)
56
+ raise ConnectionHandlerError(error_msg)
57
+ finally:
58
+ if conn:
59
+ conn.close()
60
+
61
+ async def get_schema(self, table_name: str) -> str:
62
+ """Get table schema information"""
63
+ try:
64
+ conn_params = self.config.get_connection_params()
65
+ conn = mysql.connector.connect(**conn_params)
66
+ with conn.cursor(dictionary=True) as cur:
67
+ # Get column information
68
+ cur.execute("""
69
+ SELECT
70
+ COLUMN_NAME as column_name,
71
+ DATA_TYPE as data_type,
72
+ IS_NULLABLE as is_nullable,
73
+ COLUMN_COMMENT as description
74
+ FROM information_schema.columns
75
+ WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
76
+ ORDER BY ORDINAL_POSITION
77
+ """, (self.config.database, table_name))
78
+ columns = cur.fetchall()
79
+
80
+ # Get constraint information
81
+ cur.execute("""
82
+ SELECT
83
+ CONSTRAINT_NAME as constraint_name,
84
+ CONSTRAINT_TYPE as constraint_type
85
+ FROM information_schema.table_constraints
86
+ WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
87
+ """, (self.config.database, table_name))
88
+ constraints = cur.fetchall()
89
+
90
+ return str({
91
+ 'columns': [{
92
+ 'name': col['column_name'],
93
+ 'type': col['data_type'],
94
+ 'nullable': col['is_nullable'] == 'YES',
95
+ 'description': col['description']
96
+ } for col in columns],
97
+ 'constraints': [{
98
+ 'name': con['constraint_name'],
99
+ 'type': con['constraint_type']
100
+ } for con in constraints]
101
+ })
102
+ except mysql.connector.Error as e:
103
+ error_msg = f"Failed to read table schema: {str(e)}"
104
+ self.stats.record_error(e.__class__.__name__)
105
+ raise ConnectionHandlerError(error_msg)
106
+ finally:
107
+ if conn:
108
+ conn.close()
109
+
110
+ async def _execute_query(self, sql: str) -> str:
111
+ """Execute SQL query"""
112
+ conn = None
113
+ try:
114
+ conn_params = self.config.get_connection_params()
115
+ conn = mysql.connector.connect(**conn_params)
116
+ self.log("debug", f"Executing query: {sql}")
117
+
118
+ with conn.cursor(dictionary=True) as cur:
119
+ # Start read-only transaction
120
+ cur.execute("SET TRANSACTION READ ONLY")
121
+ try:
122
+ cur.execute(sql)
123
+ results = cur.fetchall()
124
+ columns = [desc[0] for desc in cur.description]
125
+
126
+ result_text = str({
127
+ 'type': self.db_type,
128
+ 'columns': columns,
129
+ 'rows': results,
130
+ 'row_count': len(results)
131
+ })
132
+
133
+ self.log("debug", f"Query completed, returned {len(results)} rows")
134
+ return result_text
135
+ finally:
136
+ cur.execute("ROLLBACK")
137
+ except mysql.connector.Error as e:
138
+ error_msg = f"[{self.db_type}] Query execution failed: {str(e)}"
139
+ raise ConnectionHandlerError(error_msg)
140
+ finally:
141
+ if conn:
142
+ conn.close()
143
+
144
+ async def get_table_description(self, table_name: str) -> str:
145
+ """Get detailed table description"""
146
+ conn = None
147
+ try:
148
+ conn_params = self.config.get_connection_params()
149
+ conn = mysql.connector.connect(**conn_params)
150
+ with conn.cursor(dictionary=True) as cur:
151
+ # Get table information and comment
152
+ cur.execute("""
153
+ SELECT
154
+ TABLE_COMMENT as table_comment
155
+ FROM information_schema.tables
156
+ WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
157
+ """, (self.config.database, table_name))
158
+ table_info = cur.fetchone()
159
+ table_comment = table_info['table_comment'] if table_info else None
160
+
161
+ # Get column information
162
+ cur.execute("""
163
+ SELECT
164
+ COLUMN_NAME as column_name,
165
+ DATA_TYPE as data_type,
166
+ COLUMN_DEFAULT as column_default,
167
+ IS_NULLABLE as is_nullable,
168
+ CHARACTER_MAXIMUM_LENGTH as character_maximum_length,
169
+ NUMERIC_PRECISION as numeric_precision,
170
+ NUMERIC_SCALE as numeric_scale,
171
+ COLUMN_COMMENT as column_comment
172
+ FROM information_schema.columns
173
+ WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
174
+ ORDER BY ORDINAL_POSITION
175
+ """, (self.config.database, table_name))
176
+ columns = cur.fetchall()
177
+
178
+ # Format output
179
+ description = [
180
+ f"Table: {table_name}",
181
+ f"Comment: {table_comment or 'No comment'}\n",
182
+ "Columns:"
183
+ ]
184
+
185
+ for col in columns:
186
+ col_info = [
187
+ f" {col['column_name']} ({col['data_type']})",
188
+ f" Nullable: {col['is_nullable']}",
189
+ f" Default: {col['column_default'] or 'None'}"
190
+ ]
191
+
192
+ if col['character_maximum_length']:
193
+ col_info.append(f" Max Length: {col['character_maximum_length']}")
194
+ if col['numeric_precision']:
195
+ col_info.append(f" Precision: {col['numeric_precision']}")
196
+ if col['numeric_scale']:
197
+ col_info.append(f" Scale: {col['numeric_scale']}")
198
+ if col['column_comment']:
199
+ col_info.append(f" Comment: {col['column_comment']}")
200
+
201
+ description.extend(col_info)
202
+ description.append("") # Empty line between columns
203
+
204
+ return "\n".join(description)
205
+
206
+ except mysql.connector.Error as e:
207
+ error_msg = f"Failed to get table description: {str(e)}"
208
+ self.stats.record_error(e.__class__.__name__)
209
+ raise ConnectionHandlerError(error_msg)
210
+ finally:
211
+ if conn:
212
+ conn.close()
213
+
214
+ async def get_table_ddl(self, table_name: str) -> str:
215
+ """Get DDL statement for creating table"""
216
+ conn = None
217
+ try:
218
+ conn_params = self.config.get_connection_params()
219
+ conn = mysql.connector.connect(**conn_params)
220
+ with conn.cursor(dictionary=True) as cur:
221
+ # MySQL provides a SHOW CREATE TABLE statement
222
+ cur.execute(f"SHOW CREATE TABLE {table_name}")
223
+ result = cur.fetchone()
224
+ if result:
225
+ return result['Create Table']
226
+ return f"Failed to get DDL for table {table_name}"
227
+
228
+ except mysql.connector.Error as e:
229
+ error_msg = f"Failed to get table DDL: {str(e)}"
230
+ self.stats.record_error(e.__class__.__name__)
231
+ raise ConnectionHandlerError(error_msg)
232
+ finally:
233
+ if conn:
234
+ conn.close()
235
+
236
+ async def get_table_indexes(self, table_name: str) -> str:
237
+ """Get index information for table"""
238
+ conn = None
239
+ try:
240
+ conn_params = self.config.get_connection_params()
241
+ conn = mysql.connector.connect(**conn_params)
242
+ with conn.cursor(dictionary=True) as cur:
243
+ # Get index information
244
+ cur.execute("""
245
+ SELECT
246
+ INDEX_NAME as index_name,
247
+ COLUMN_NAME as column_name,
248
+ NON_UNIQUE as non_unique,
249
+ INDEX_TYPE as index_type,
250
+ INDEX_COMMENT as index_comment
251
+ FROM information_schema.statistics
252
+ WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
253
+ ORDER BY INDEX_NAME, SEQ_IN_INDEX
254
+ """, (self.config.database, table_name))
255
+ indexes = cur.fetchall()
256
+
257
+ if not indexes:
258
+ return f"No indexes found on table {table_name}"
259
+
260
+ # Group by index name
261
+ current_index = None
262
+ formatted_indexes = []
263
+ index_info = []
264
+
265
+ for idx in indexes:
266
+ if current_index != idx['index_name']:
267
+ if index_info:
268
+ formatted_indexes.extend(index_info)
269
+ formatted_indexes.append("")
270
+ current_index = idx['index_name']
271
+ index_info = [
272
+ f"Index: {idx['index_name']}",
273
+ f"Type: {'UNIQUE' if not idx['non_unique'] else 'INDEX'}",
274
+ f"Method: {idx['index_type']}",
275
+ "Columns:",
276
+ ]
277
+ if idx['index_comment']:
278
+ index_info.insert(1, f"Comment: {idx['index_comment']}")
279
+
280
+ index_info.append(f" - {idx['column_name']}")
281
+
282
+ if index_info:
283
+ formatted_indexes.extend(index_info)
284
+
285
+ return "\n".join(formatted_indexes)
286
+
287
+ except mysql.connector.Error as e:
288
+ error_msg = f"Failed to get index information: {str(e)}"
289
+ self.stats.record_error(e.__class__.__name__)
290
+ raise ConnectionHandlerError(error_msg)
291
+ finally:
292
+ if conn:
293
+ conn.close()
294
+
295
+ async def get_table_stats(self, table_name: str) -> str:
296
+ """Get table statistics information"""
297
+ conn = None
298
+ try:
299
+ conn_params = self.config.get_connection_params()
300
+ conn = mysql.connector.connect(**conn_params)
301
+ with conn.cursor(dictionary=True) as cur:
302
+ # Get table statistics
303
+ cur.execute("""
304
+ SELECT
305
+ TABLE_ROWS as table_rows,
306
+ AVG_ROW_LENGTH as avg_row_length,
307
+ DATA_LENGTH as data_length,
308
+ INDEX_LENGTH as index_length,
309
+ DATA_FREE as data_free
310
+ FROM information_schema.tables
311
+ WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
312
+ """, (self.config.database, table_name))
313
+ stats = cur.fetchone()
314
+
315
+ if not stats:
316
+ return f"No statistics found for table {table_name}"
317
+
318
+ # Get column statistics
319
+ cur.execute("""
320
+ SELECT
321
+ COLUMN_NAME as column_name,
322
+ DATA_TYPE as data_type,
323
+ COLUMN_TYPE as column_type
324
+ FROM information_schema.columns
325
+ WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
326
+ ORDER BY ORDINAL_POSITION
327
+ """, (self.config.database, table_name))
328
+ columns = cur.fetchall()
329
+
330
+ # Format the output
331
+ output = [
332
+ f"Table Statistics for {table_name}:",
333
+ f" Estimated Row Count: {stats['table_rows']:,}",
334
+ f" Average Row Length: {stats['avg_row_length']} bytes",
335
+ f" Data Length: {stats['data_length']:,} bytes",
336
+ f" Index Length: {stats['index_length']:,} bytes",
337
+ f" Data Free: {stats['data_free']:,} bytes\n",
338
+ "Column Information:"
339
+ ]
340
+
341
+ for col in columns:
342
+ col_info = [
343
+ f" {col['column_name']}:",
344
+ f" Data Type: {col['data_type']}",
345
+ f" Column Type: {col['column_type']}"
346
+ ]
347
+ output.extend(col_info)
348
+ output.append("") # Empty line between columns
349
+
350
+ return "\n".join(output)
351
+
352
+ except mysql.connector.Error as e:
353
+ error_msg = f"Failed to get table statistics: {str(e)}"
354
+ self.stats.record_error(e.__class__.__name__)
355
+ raise ConnectionHandlerError(error_msg)
356
+ finally:
357
+ if conn:
358
+ conn.close()
359
+
360
+ async def get_table_constraints(self, table_name: str) -> str:
361
+ """Get constraint information for table"""
362
+ conn = None
363
+ try:
364
+ conn_params = self.config.get_connection_params()
365
+ conn = mysql.connector.connect(**conn_params)
366
+ with conn.cursor(dictionary=True) as cur:
367
+ # Get constraint information
368
+ cur.execute("""
369
+ SELECT
370
+ k.CONSTRAINT_NAME as constraint_name,
371
+ t.CONSTRAINT_TYPE as constraint_type,
372
+ k.COLUMN_NAME as column_name,
373
+ k.REFERENCED_TABLE_NAME as referenced_table_name,
374
+ k.REFERENCED_COLUMN_NAME as referenced_column_name
375
+ FROM information_schema.key_column_usage k
376
+ JOIN information_schema.table_constraints t
377
+ ON k.CONSTRAINT_NAME = t.CONSTRAINT_NAME
378
+ AND k.TABLE_SCHEMA = t.TABLE_SCHEMA
379
+ AND k.TABLE_NAME = t.TABLE_NAME
380
+ WHERE k.TABLE_SCHEMA = %s
381
+ AND k.TABLE_NAME = %s
382
+ ORDER BY t.CONSTRAINT_TYPE, k.CONSTRAINT_NAME, k.ORDINAL_POSITION
383
+ """, (self.config.database, table_name))
384
+ constraints = cur.fetchall()
385
+
386
+ if not constraints:
387
+ return f"No constraints found on table {table_name}"
388
+
389
+ # Format constraints by type
390
+ output = [f"Constraints for {table_name}:"]
391
+ current_constraint = None
392
+ constraint_info = []
393
+
394
+ for con in constraints:
395
+ if current_constraint != con['constraint_name']:
396
+ if constraint_info:
397
+ output.extend(constraint_info)
398
+ output.append("")
399
+ current_constraint = con['constraint_name']
400
+ constraint_info = [
401
+ f"\n{con['constraint_type']} Constraint: {con['constraint_name']}",
402
+ "Columns:"
403
+ ]
404
+
405
+ col_info = f" - {con['column_name']}"
406
+ if con['referenced_table_name']:
407
+ col_info += f" -> {con['referenced_table_name']}.{con['referenced_column_name']}"
408
+ constraint_info.append(col_info)
409
+
410
+ if constraint_info:
411
+ output.extend(constraint_info)
412
+
413
+ return "\n".join(output)
414
+
415
+ except mysql.connector.Error as e:
416
+ error_msg = f"Failed to get constraint information: {str(e)}"
417
+ self.stats.record_error(e.__class__.__name__)
418
+ raise ConnectionHandlerError(error_msg)
419
+ finally:
420
+ if conn:
421
+ conn.close()
422
+
423
+ async def explain_query(self, sql: str) -> str:
424
+ """Get query execution plan"""
425
+ conn = None
426
+ try:
427
+ conn_params = self.config.get_connection_params()
428
+ conn = mysql.connector.connect(**conn_params)
429
+ with conn.cursor(dictionary=True) as cur:
430
+ # Get EXPLAIN output
431
+ cur.execute(f"EXPLAIN FORMAT=TREE {sql}")
432
+ explain_result = cur.fetchall()
433
+
434
+ # Get EXPLAIN ANALYZE output
435
+ cur.execute(f"EXPLAIN ANALYZE {sql}")
436
+ analyze_result = cur.fetchall()
437
+
438
+ output = [
439
+ "Query Execution Plan:",
440
+ "==================",
441
+ "\nEstimated Plan:",
442
+ "----------------"
443
+ ]
444
+ for row in explain_result:
445
+ output.append(str(row['EXPLAIN']))
446
+
447
+ output.extend([
448
+ "\nActual Plan (ANALYZE):",
449
+ "----------------------"
450
+ ])
451
+ for row in analyze_result:
452
+ output.append(str(row['EXPLAIN']))
453
+
454
+ return "\n".join(output)
455
+
456
+ except mysql.connector.Error as e:
457
+ error_msg = f"Failed to explain query: {str(e)}"
458
+ self.stats.record_error(e.__class__.__name__)
459
+ raise ConnectionHandlerError(error_msg)
460
+ finally:
461
+ if conn:
462
+ conn.close()
463
+
464
+ async def cleanup(self):
465
+ """Cleanup resources"""
466
+ # Log final stats before cleanup
467
+ self.log("info", f"Final MySQL handler stats: {self.stats.to_dict()}")
@@ -0,0 +1,216 @@
1
+ """MySQL MCP server implementation"""
2
+ import mysql.connector
3
+ from mysql.connector.pooling import MySQLConnectionPool, PooledMySQLConnection
4
+ from typing import Optional, List
5
+ import mcp.types as types
6
+ from importlib.metadata import metadata
7
+ from ..base import ConnectionServer
8
+ from ..log import create_logger
9
+ from .config import MySQLConfig
10
+
11
+ # 获取包信息用于日志命名
12
+ pkg_meta = metadata("mcp-dbutils")
13
+
14
+ class MySQLServer(ConnectionServer):
15
+ def __init__(self, config: MySQLConfig, config_path: Optional[str] = None):
16
+ """初始化MySQL服务器
17
+ Args:
18
+ config: 数据库配置
19
+ config_path: 配置文件路径(可选)
20
+ """
21
+ super().__init__(config_path, config.debug)
22
+ self.config = config
23
+ self.config_path = config_path
24
+ self.log = create_logger(f"{pkg_meta['Name']}.db.mysql", config.debug)
25
+ # 创建连接池
26
+ try:
27
+ conn_params = config.get_connection_params()
28
+ masked_params = config.get_masked_connection_info()
29
+ self.log("debug", f"正在连接数据库,参数: {masked_params}")
30
+
31
+ # 测试连接
32
+ test_conn = mysql.connector.connect(**conn_params)
33
+ test_conn.close()
34
+ self.log("info", "测试连接成功")
35
+
36
+ # 创建连接池配置
37
+ pool_config = {
38
+ 'pool_name': 'mypool',
39
+ 'pool_size': 5,
40
+ **conn_params
41
+ }
42
+ self.pool = MySQLConnectionPool(**pool_config)
43
+ self.log("info", "连接池创建成功")
44
+ except mysql.connector.Error as e:
45
+ self.log("error", f"连接失败: {str(e)}")
46
+ raise
47
+
48
+ async def list_resources(self) -> list[types.Resource]:
49
+ """列出所有表资源"""
50
+ try:
51
+ conn = self.pool.get_connection()
52
+ with conn.cursor(dictionary=True) as cur:
53
+ cur.execute("""
54
+ SELECT
55
+ table_name,
56
+ table_comment as description
57
+ FROM information_schema.tables
58
+ WHERE table_schema = %s
59
+ """, (self.config.database,))
60
+ tables = cur.fetchall()
61
+ return [
62
+ types.Resource(
63
+ uri=f"mysql://{self.config.host}/{table['table_name']}/schema",
64
+ name=f"{table['table_name']} schema",
65
+ description=table['description'] if table['description'] else None,
66
+ mimeType="application/json"
67
+ ) for table in tables
68
+ ]
69
+ except mysql.connector.Error as e:
70
+ error_msg = f"获取表列表失败: {str(e)}"
71
+ self.log("error", error_msg)
72
+ raise
73
+ finally:
74
+ conn.close()
75
+
76
+ async def read_resource(self, uri: str) -> str:
77
+ """读取表结构信息"""
78
+ try:
79
+ table_name = uri.split('/')[-2]
80
+ conn = self.pool.get_connection()
81
+ with conn.cursor(dictionary=True) as cur:
82
+ # 获取列信息
83
+ cur.execute("""
84
+ SELECT
85
+ column_name,
86
+ data_type,
87
+ is_nullable,
88
+ column_comment as description
89
+ FROM information_schema.columns
90
+ WHERE table_schema = %s AND table_name = %s
91
+ ORDER BY ordinal_position
92
+ """, (self.config.database, table_name))
93
+ columns = cur.fetchall()
94
+
95
+ # 获取约束信息
96
+ cur.execute("""
97
+ SELECT
98
+ constraint_name,
99
+ constraint_type
100
+ FROM information_schema.table_constraints
101
+ WHERE table_schema = %s AND table_name = %s
102
+ """, (self.config.database, table_name))
103
+ constraints = cur.fetchall()
104
+
105
+ return str({
106
+ 'columns': [{
107
+ 'name': col['column_name'],
108
+ 'type': col['data_type'],
109
+ 'nullable': col['is_nullable'] == 'YES',
110
+ 'description': col['description']
111
+ } for col in columns],
112
+ 'constraints': [{
113
+ 'name': con['constraint_name'],
114
+ 'type': con['constraint_type']
115
+ } for con in constraints]
116
+ })
117
+ except mysql.connector.Error as e:
118
+ error_msg = f"读取表结构失败: {str(e)}"
119
+ self.log("error", error_msg)
120
+ raise
121
+ finally:
122
+ conn.close()
123
+
124
+ def get_tools(self) -> list[types.Tool]:
125
+ """获取可用工具列表"""
126
+ return [
127
+ types.Tool(
128
+ name="query",
129
+ description="执行只读SQL查询",
130
+ inputSchema={
131
+ "type": "object",
132
+ "properties": {
133
+ "connection": {
134
+ "type": "string",
135
+ "description": "数据库连接名称(可选)"
136
+ },
137
+ "sql": {
138
+ "type": "string",
139
+ "description": "SQL查询语句(仅支持SELECT)"
140
+ }
141
+ },
142
+ "required": ["sql"]
143
+ }
144
+ )
145
+ ]
146
+
147
+ async def call_tool(self, name: str, arguments: dict) -> list[types.TextContent]:
148
+ """执行工具调用"""
149
+ if name != "query":
150
+ raise ValueError(f"未知工具: {name}")
151
+ sql = arguments.get("sql", "").strip()
152
+ if not sql:
153
+ raise ValueError("SQL查询不能为空")
154
+ # 仅允许SELECT语句
155
+ if not sql.lower().startswith("select"):
156
+ raise ValueError("仅支持SELECT查询")
157
+
158
+ connection = arguments.get("connection")
159
+ use_pool = True
160
+ conn = None
161
+ try:
162
+ if connection and self.config_path:
163
+ # 使用指定的数据库连接
164
+ config = MySQLConfig.from_yaml(self.config_path, connection)
165
+ conn_params = config.get_connection_params()
166
+ masked_params = config.get_masked_connection_info()
167
+ self.log("info", f"使用配置 {connection} 连接数据库: {masked_params}")
168
+ conn = mysql.connector.connect(**conn_params)
169
+ use_pool = False
170
+ else:
171
+ # 使用现有连接池
172
+ conn = self.pool.get_connection()
173
+
174
+ self.log("info", f"执行查询: {sql}")
175
+ with conn.cursor(dictionary=True) as cur:
176
+ # 设置只读事务
177
+ cur.execute("SET TRANSACTION READ ONLY")
178
+ try:
179
+ cur.execute(sql)
180
+ results = cur.fetchall()
181
+ columns = [desc[0] for desc in cur.description]
182
+ result_text = str({
183
+ 'type': 'mysql',
184
+ 'config_name': connection or 'default',
185
+ 'query_result': {
186
+ 'columns': columns,
187
+ 'rows': results,
188
+ 'row_count': len(results)
189
+ }
190
+ })
191
+ self.log("info", f"查询完成,返回{len(results)}行结果")
192
+ return [types.TextContent(type="text", text=result_text)]
193
+ finally:
194
+ cur.execute("ROLLBACK")
195
+ except Exception as e:
196
+ error = f"查询执行失败: {str(e)}"
197
+ error_msg = str({
198
+ 'type': 'mysql',
199
+ 'config_name': connection or 'default',
200
+ 'error': error
201
+ })
202
+ self.log("error", error_msg)
203
+ return [types.TextContent(type="text", text=error_msg)]
204
+ finally:
205
+ if conn:
206
+ if isinstance(conn, PooledMySQLConnection):
207
+ conn.close() # 返回到连接池
208
+ else:
209
+ conn.close() # 关闭独立连接
210
+
211
+ async def cleanup(self):
212
+ """清理资源"""
213
+ if hasattr(self, 'pool'):
214
+ self.log("info", "关闭连接池")
215
+ # MySQL连接池没有直接的closeall方法
216
+ # 当对象被销毁时,连接池会自动关闭