mcp-dbutils 0.21.0__py3-none-any.whl → 0.23.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 CHANGED
@@ -3,7 +3,7 @@
3
3
  import json
4
4
  from abc import ABC, abstractmethod
5
5
  from contextlib import asynccontextmanager
6
- from datetime import datetime
6
+ from datetime import datetime, timedelta
7
7
  from importlib.metadata import metadata
8
8
  from typing import Any, AsyncContextManager, Dict
9
9
 
@@ -474,6 +474,29 @@ class ConnectionServer:
474
474
  },
475
475
  "required": ["connection", "sql"],
476
476
  },
477
+ annotations={
478
+ "examples": [
479
+ {
480
+ "input": {
481
+ "connection": "example_db",
482
+ "sql": "SELECT id, name, email FROM users LIMIT 10"
483
+ },
484
+ "output": "Results showing first 10 users with their IDs, names, and email addresses"
485
+ },
486
+ {
487
+ "input": {
488
+ "connection": "example_db",
489
+ "sql": "SELECT department, COUNT(*) as employee_count FROM employees GROUP BY department ORDER BY employee_count DESC"
490
+ },
491
+ "output": "Results showing departments and their employee counts in descending order"
492
+ }
493
+ ],
494
+ "usage_tips": [
495
+ "Always use SELECT statements only - other SQL operations are not permitted",
496
+ "Use LIMIT to restrict large result sets",
497
+ "For complex queries, consider using dbutils-explain-query first to understand query execution plan"
498
+ ]
499
+ }
477
500
  ),
478
501
  types.Tool(
479
502
  name="dbutils-list-tables",
@@ -488,6 +511,19 @@ class ConnectionServer:
488
511
  },
489
512
  "required": ["connection"],
490
513
  },
514
+ annotations={
515
+ "examples": [
516
+ {
517
+ "input": {"connection": "example_db"},
518
+ "output": "List of tables in the example_db database with their URIs and descriptions"
519
+ }
520
+ ],
521
+ "usage_tips": [
522
+ "Use this tool first when exploring a new database to understand its structure",
523
+ "After listing tables, use dbutils-describe-table to get detailed information about specific tables",
524
+ "Table URIs can be used with other database tools for further operations"
525
+ ]
526
+ }
491
527
  ),
492
528
  types.Tool(
493
529
  name="dbutils-describe-table",
@@ -999,5 +1035,8 @@ class ConnectionServer:
999
1035
  """Run server"""
1000
1036
  async with mcp.server.stdio.stdio_server() as streams:
1001
1037
  await self.server.run(
1002
- streams[0], streams[1], self.server.create_initialization_options()
1038
+ streams[0],
1039
+ streams[1],
1040
+ self.server.create_initialization_options(),
1041
+ read_timeout_seconds=timedelta(seconds=30) # 设置30秒超时
1003
1042
  )
@@ -537,3 +537,15 @@ class MySQLHandler(ConnectionHandler):
537
537
  """Cleanup resources"""
538
538
  # Log final stats before cleanup
539
539
  self.log("info", f"Final MySQL handler stats: {self.stats.to_dict()}")
540
+
541
+ # 主动关闭连接
542
+ if hasattr(self, '_connection') and self._connection:
543
+ try:
544
+ self.log("debug", "Closing MySQL connection")
545
+ self._connection.close()
546
+ self._connection = None
547
+ except Exception as e:
548
+ self.log("warning", f"Error closing MySQL connection: {str(e)}")
549
+
550
+ # 清理其他资源
551
+ self.log("debug", "MySQL handler cleanup complete")
@@ -27,12 +27,12 @@ class MySQLServer(ConnectionServer):
27
27
  conn_params = config.get_connection_params()
28
28
  masked_params = config.get_masked_connection_info()
29
29
  self.log("debug", f"正在连接数据库,参数: {masked_params}")
30
-
30
+
31
31
  # 测试连接
32
32
  test_conn = mysql.connector.connect(**conn_params)
33
33
  test_conn.close()
34
34
  self.log("info", "测试连接成功")
35
-
35
+
36
36
  # 创建连接池配置
37
37
  pool_config = {
38
38
  'pool_name': 'mypool',
@@ -51,7 +51,7 @@ class MySQLServer(ConnectionServer):
51
51
  conn = self.pool.get_connection()
52
52
  with conn.cursor(dictionary=True) as cur: # NOSONAR - dictionary参数是正确的,用于返回字典格式的结果
53
53
  cur.execute("""
54
- SELECT
54
+ SELECT
55
55
  table_name,
56
56
  table_comment as description
57
57
  FROM information_schema.tables
@@ -81,7 +81,7 @@ class MySQLServer(ConnectionServer):
81
81
  with conn.cursor(dictionary=True) as cur: # NOSONAR - dictionary参数是正确的,用于返回字典格式的结果
82
82
  # 获取列信息
83
83
  cur.execute("""
84
- SELECT
84
+ SELECT
85
85
  column_name,
86
86
  data_type,
87
87
  is_nullable,
@@ -157,6 +157,9 @@ class MySQLServer(ConnectionServer):
157
157
 
158
158
  connection = arguments.get("connection")
159
159
  conn = None
160
+ results = []
161
+ columns = []
162
+
160
163
  try:
161
164
  if connection and self.config_path:
162
165
  # 使用指定的数据库连接
@@ -177,19 +180,22 @@ class MySQLServer(ConnectionServer):
177
180
  cur.execute(sql)
178
181
  results = cur.fetchall()
179
182
  columns = [desc[0] for desc in cur.description]
180
- result_text = str({
181
- 'type': 'mysql',
182
- 'config_name': connection or 'default',
183
- 'query_result': {
184
- 'columns': columns,
185
- 'rows': results,
186
- 'row_count': len(results)
187
- }
188
- })
189
- self.log("info", f"查询完成,返回{len(results)}行结果")
190
- return [types.TextContent(type="text", text=result_text)]
191
183
  finally:
192
184
  cur.execute("ROLLBACK")
185
+
186
+ # 处理结果(在连接操作完成后)
187
+ result_text = str({
188
+ 'type': 'mysql',
189
+ 'config_name': connection or 'default',
190
+ 'query_result': {
191
+ 'columns': columns,
192
+ 'rows': results,
193
+ 'row_count': len(results)
194
+ }
195
+ })
196
+ self.log("info", f"查询完成,返回{len(results)}行结果")
197
+ return [types.TextContent(type="text", text=result_text)]
198
+
193
199
  except Exception as e:
194
200
  error = f"查询执行失败: {str(e)}"
195
201
  error_msg = str({
@@ -200,12 +206,28 @@ class MySQLServer(ConnectionServer):
200
206
  self.log("error", error_msg)
201
207
  return [types.TextContent(type="text", text=error_msg)]
202
208
  finally:
209
+ # 确保连接始终被关闭
203
210
  if conn:
204
- conn.close() # 关闭连接(连接池会自动处理)
211
+ try:
212
+ conn.close() # 关闭连接(连接池会自动处理)
213
+ except Exception as e:
214
+ self.log("warning", f"关闭连接时出错: {str(e)}")
205
215
 
206
216
  async def cleanup(self):
207
217
  """清理资源"""
208
218
  if hasattr(self, 'pool'):
209
- self.log("info", "关闭连接池")
210
- # MySQL连接池没有直接的closeall方法
211
- # 当对象被销毁时,连接池会自动关闭
219
+ self.log("info", "关闭MySQL连接池")
220
+ # MySQL连接池没有直接的closeall方法,但我们可以主动关闭连接
221
+ try:
222
+ # 尝试获取并关闭所有活动连接
223
+ for _ in range(5): # 假设最多有5个连接在池中
224
+ try:
225
+ conn = self.pool.get_connection()
226
+ conn.close()
227
+ except Exception as e:
228
+ # 如果没有更多连接或出现其他错误,跳出循环
229
+ self.log("debug", f"连接池清理: {str(e)}")
230
+ break
231
+ self.log("info", "MySQL连接池清理完成")
232
+ except Exception as e:
233
+ self.log("warning", f"清理MySQL连接池时出错: {str(e)}")
@@ -610,3 +610,15 @@ class PostgreSQLHandler(ConnectionHandler):
610
610
  """Cleanup resources"""
611
611
  # Log final stats before cleanup
612
612
  self.log("info", f"Final PostgreSQL handler stats: {self.stats.to_dict()}")
613
+
614
+ # 主动关闭连接
615
+ if hasattr(self, '_connection') and self._connection:
616
+ try:
617
+ self.log("debug", "Closing PostgreSQL connection")
618
+ self._connection.close()
619
+ self._connection = None
620
+ except Exception as e:
621
+ self.log("warning", f"Error closing PostgreSQL connection: {str(e)}")
622
+
623
+ # 清理其他资源
624
+ self.log("debug", "PostgreSQL handler cleanup complete")
@@ -148,9 +148,14 @@ class PostgreSQLServer(ConnectionServer):
148
148
  # 仅允许SELECT语句
149
149
  if not sql.lower().startswith("select"):
150
150
  raise ValueError("仅支持SELECT查询")
151
+
151
152
  connection = arguments.get("connection")
152
153
  use_pool = True
153
154
  conn = None
155
+ results = []
156
+ columns = []
157
+ formatted_results = []
158
+
154
159
  try:
155
160
  if connection and self.config_path:
156
161
  # 使用指定的数据库连接
@@ -163,6 +168,7 @@ class PostgreSQLServer(ConnectionServer):
163
168
  else:
164
169
  # 使用现有连接池
165
170
  conn = self.pool.getconn()
171
+
166
172
  self.log("info", f"执行查询: {sql}")
167
173
  with conn.cursor() as cur:
168
174
  # 启动只读事务
@@ -172,19 +178,22 @@ class PostgreSQLServer(ConnectionServer):
172
178
  results = cur.fetchall()
173
179
  columns = [desc[0] for desc in cur.description]
174
180
  formatted_results = [dict(zip(columns, row)) for row in results]
175
- result_text = str({
176
- 'type': 'postgres',
177
- 'config_name': connection or 'default',
178
- 'query_result': {
179
- 'columns': columns,
180
- 'rows': formatted_results,
181
- 'row_count': len(results)
182
- }
183
- })
184
- self.log("info", f"查询完成,返回{len(results)}行结果")
185
- return [types.TextContent(type="text", text=result_text)]
186
181
  finally:
187
182
  cur.execute("ROLLBACK")
183
+
184
+ # 处理结果(在连接操作完成后)
185
+ result_text = str({
186
+ 'type': 'postgres',
187
+ 'config_name': connection or 'default',
188
+ 'query_result': {
189
+ 'columns': columns,
190
+ 'rows': formatted_results,
191
+ 'row_count': len(results)
192
+ }
193
+ })
194
+ self.log("info", f"查询完成,返回{len(results)}行结果")
195
+ return [types.TextContent(type="text", text=result_text)]
196
+
188
197
  except Exception as e:
189
198
  if isinstance(e, psycopg2.Error):
190
199
  error = f"查询执行失败: [Code: {e.pgcode}] {e.pgerror or str(e)}"
@@ -198,11 +207,15 @@ class PostgreSQLServer(ConnectionServer):
198
207
  self.log("error", error_msg)
199
208
  return [types.TextContent(type="text", text=error_msg)]
200
209
  finally:
210
+ # 确保连接始终被正确处理
201
211
  if conn:
202
- if use_pool:
203
- self.pool.putconn(conn)
204
- else:
205
- conn.close()
212
+ try:
213
+ if use_pool:
214
+ self.pool.putconn(conn)
215
+ else:
216
+ conn.close()
217
+ except Exception as e:
218
+ self.log("warning", f"关闭连接时出错: {str(e)}")
206
219
  async def cleanup(self):
207
220
  """清理资源"""
208
221
  if hasattr(self, 'pool'):
@@ -475,3 +475,15 @@ class SQLiteHandler(ConnectionHandler):
475
475
  """Cleanup resources"""
476
476
  # Log final stats
477
477
  self.log("info", f"Final SQLite handler stats: {self.stats.to_dict()}")
478
+
479
+ # 主动关闭连接
480
+ if hasattr(self, '_connection') and self._connection:
481
+ try:
482
+ self.log("debug", "Closing SQLite connection")
483
+ self._connection.close()
484
+ self._connection = None
485
+ except Exception as e:
486
+ self.log("warning", f"Error closing SQLite connection: {str(e)}")
487
+
488
+ # 清理其他资源
489
+ self.log("debug", "SQLite handler cleanup complete")
@@ -141,8 +141,13 @@ class SQLiteServer(ConnectionServer):
141
141
  raise ValueError("仅支持SELECT查询")
142
142
 
143
143
  conn = None
144
+ connection = arguments.get("connection")
145
+ config_name = connection if isinstance(connection, str) else 'default'
146
+ results = []
147
+ columns = []
148
+ formatted_results = []
149
+
144
150
  try:
145
- connection = arguments.get("connection")
146
151
  if connection and self.config_path:
147
152
  # 使用指定的数据库连接
148
153
  config = SQLiteConfig.from_yaml(self.config_path, connection)
@@ -159,28 +164,24 @@ class SQLiteServer(ConnectionServer):
159
164
  self.log("info", f"执行查询: {sql}")
160
165
  cursor = conn.execute(sql)
161
166
  results = cursor.fetchall()
162
-
163
167
  columns = [desc[0] for desc in cursor.description]
164
168
  formatted_results = [dict(zip(columns, row)) for row in results]
165
169
 
166
- # 使用更通用的方法确定配置名称
167
- config_name = connection if isinstance(connection, str) else 'default'
168
- result_text = json.dumps({
169
- 'type': 'sqlite',
170
- 'config_name': config_name,
171
- 'query_result': {
172
- 'columns': columns,
173
- 'rows': formatted_results,
174
- 'row_count': len(results)
175
- }
176
- })
177
-
178
- self.log("info", f"查询完成,返回{len(results)}行结果")
179
- return [types.TextContent(type="text", text=result_text)]
170
+ # 处理结果(在连接操作完成后)
171
+ result_text = json.dumps({
172
+ 'type': 'sqlite',
173
+ 'config_name': config_name,
174
+ 'query_result': {
175
+ 'columns': columns,
176
+ 'rows': formatted_results,
177
+ 'row_count': len(results)
178
+ }
179
+ })
180
+
181
+ self.log("info", f"查询完成,返回{len(results)}行结果")
182
+ return [types.TextContent(type="text", text=result_text)]
180
183
 
181
184
  except sqlite3.Error as e:
182
- # 使用更通用的方法确定配置名称
183
- config_name = connection if isinstance(connection, str) else 'default'
184
185
  error_msg = json.dumps({
185
186
  'type': 'sqlite',
186
187
  'config_name': config_name,
@@ -188,6 +189,13 @@ class SQLiteServer(ConnectionServer):
188
189
  })
189
190
  self.log("error", error_msg)
190
191
  return [types.TextContent(type="text", text=error_msg)]
192
+ finally:
193
+ # 确保连接被正确关闭(如果不是使用with语句)
194
+ if conn and not isinstance(connection, str):
195
+ try:
196
+ conn.close()
197
+ except Exception as e:
198
+ self.log("warning", f"关闭连接时出错: {str(e)}")
191
199
 
192
200
  async def cleanup(self):
193
201
  """清理资源"""
@@ -1,16 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-dbutils
3
- Version: 0.21.0
3
+ Version: 0.23.0
4
4
  Summary: MCP Database Utilities Service
5
5
  Author: Dong Hao
6
6
  License-Expression: MIT
7
7
  License-File: LICENSE
8
8
  Requires-Python: >=3.10
9
- Requires-Dist: mcp>=1.2.1
9
+ Requires-Dist: mcp>=1.7.1
10
10
  Requires-Dist: mysql-connector-python>=8.2.0
11
11
  Requires-Dist: psycopg2-binary>=2.9.10
12
12
  Requires-Dist: python-dotenv>=1.0.1
13
13
  Requires-Dist: pyyaml>=6.0.2
14
+ Requires-Dist: typer>=0.9.0
14
15
  Provides-Extra: test
15
16
  Requires-Dist: aiosqlite>=0.19.0; extra == 'test'
16
17
  Requires-Dist: docker>=7.0.0; extra == 'test'
@@ -1,22 +1,22 @@
1
1
  mcp_dbutils/__init__.py,sha256=6LLccQv7je2L4IpY_I3OzSJZcK32VUDJv2IY31y6eYg,1900
2
- mcp_dbutils/base.py,sha256=7UCw9gBZdzS_LnW3_QflWPfEV00eUFpUMDoaQJCN2aA,40012
2
+ mcp_dbutils/base.py,sha256=A9p79jqKxzCmOErFTwabQtUzIftkl1_qMFQ2rlnFB3g,42147
3
3
  mcp_dbutils/config.py,sha256=bmXpOd1fyYfoyUS75I035ChT6t3wP5AyEnJ06e2ZS2o,1848
4
4
  mcp_dbutils/log.py,sha256=mqxi6I_IL-MF1F_pxBtnYZQKOHbGBJ74gsvZHVelr1w,823
5
5
  mcp_dbutils/stats.py,sha256=wMqWPfGnEOg9v5YBtTsARV-1YsFUMM_pKdzitzSU9x4,7137
6
6
  mcp_dbutils/mysql/__init__.py,sha256=gNhoHaxK1qhvMAH5AVl1vfV1rUpcbV9KZWUQb41aaQk,129
7
7
  mcp_dbutils/mysql/config.py,sha256=BTPPFqlhoTp7EBFIeLJZh8x6bCn3q9NivHYz9yZHziw,9820
8
- mcp_dbutils/mysql/handler.py,sha256=Nu2YySImnYKOSLYe04ipHYWW1kNndFKiP-gdX5L5wpU,22325
9
- mcp_dbutils/mysql/server.py,sha256=1bWAu7qHYXVeTZu4wdEpS6gSVB0RoXKI3Smy_ix-y8A,8586
8
+ mcp_dbutils/mysql/handler.py,sha256=CJl6Mcs8kloojtS7i-2yD_-InRSmtU09LfYnHDZDhn0,22783
9
+ mcp_dbutils/mysql/server.py,sha256=4kWkrZby8fZq-Em65Fobs5KDDPJnrOxbsK1zLK1jzp0,9412
10
10
  mcp_dbutils/postgres/__init__.py,sha256=-2zYuEJEQ2AMvmGhH5Z_umerSvt7S4xOa_XV4wgvGfI,154
11
11
  mcp_dbutils/postgres/config.py,sha256=NyQOVhkXJ1S-JD0w-ePNjTKI1Ja-aZQkDUdHi6U7Vl4,7752
12
- mcp_dbutils/postgres/handler.py,sha256=rHSvgz93UXIfnVzHFOTGhgnMMZy-s2tB6nR40p2G26Q,24710
13
- mcp_dbutils/postgres/server.py,sha256=_CiJC9PitpI1NB99Q1Bcs5TYADNgDpYMwv88fRHQunE,8640
12
+ mcp_dbutils/postgres/handler.py,sha256=SnzgXjcO3iuvpQ-dzggNOF0bewAKB5BwL3QS11a5qWw,25183
13
+ mcp_dbutils/postgres/server.py,sha256=qden51sQu5Ht2ibl2jSDyxg7r_N9qDVhg83Pc-q98yc,8887
14
14
  mcp_dbutils/sqlite/__init__.py,sha256=fK_3-WylCBYpBAzwuopi8hlwoIGJm2TPAlwcPWG46I0,134
15
15
  mcp_dbutils/sqlite/config.py,sha256=j67TJ8mQJ2D886MthSa-zYMtvUUYyyxYLMlNxkYoqZE,4509
16
- mcp_dbutils/sqlite/handler.py,sha256=nCgBeBp3zjpE2HNohMbh3Jpz5tNeMczt5K87JOhVWzY,19320
17
- mcp_dbutils/sqlite/server.py,sha256=jqpE8d9vJETMs5xYGB7P0tvNDPes6Yn5ZM_iCCF7Tv4,7181
18
- mcp_dbutils-0.21.0.dist-info/METADATA,sha256=0ZPTcDD34ecEh38aATtkvhGRj4Z1GNwX1Dp2cE9dSMY,8413
19
- mcp_dbutils-0.21.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
- mcp_dbutils-0.21.0.dist-info/entry_points.txt,sha256=XTjt0QmYRgKOJQT6skR9bp1EMUfIrgpHeZJPZ3CJffs,49
21
- mcp_dbutils-0.21.0.dist-info/licenses/LICENSE,sha256=1A_CwpWVlbjrKdVEYO77vYfnXlW7oxcilZ8FpA_BzCI,1065
22
- mcp_dbutils-0.21.0.dist-info/RECORD,,
16
+ mcp_dbutils/sqlite/handler.py,sha256=jH0xi_0PxiIBIVnXAGkfNoTVazTr7ol_P1XgBVDRkrU,19781
17
+ mcp_dbutils/sqlite/server.py,sha256=EBKNKz_wTvChwg6BZlvZIBA1H5mmE2NiNEMOgu_CMy4,7373
18
+ mcp_dbutils-0.23.0.dist-info/METADATA,sha256=8GKU0Z9Z-TQFAVJqjeouk5VRNrZaJfedBktVujVg3b4,8441
19
+ mcp_dbutils-0.23.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
+ mcp_dbutils-0.23.0.dist-info/entry_points.txt,sha256=XTjt0QmYRgKOJQT6skR9bp1EMUfIrgpHeZJPZ3CJffs,49
21
+ mcp_dbutils-0.23.0.dist-info/licenses/LICENSE,sha256=1A_CwpWVlbjrKdVEYO77vYfnXlW7oxcilZ8FpA_BzCI,1065
22
+ mcp_dbutils-0.23.0.dist-info/RECORD,,