mcp-dbutils 0.7.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 +387 -62
- 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.7.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.7.0.dist-info/RECORD +0 -18
- {mcp_dbutils-0.7.0.dist-info → mcp_dbutils-0.9.0.dist-info}/WHEEL +0 -0
- {mcp_dbutils-0.7.0.dist-info → mcp_dbutils-0.9.0.dist-info}/entry_points.txt +0 -0
- {mcp_dbutils-0.7.0.dist-info → mcp_dbutils-0.9.0.dist-info}/licenses/LICENSE +0 -0
mcp_dbutils/__init__.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
"""MCP
|
1
|
+
"""MCP Connection Utilities Service"""
|
2
2
|
|
3
3
|
import asyncio
|
4
4
|
import argparse
|
@@ -9,7 +9,7 @@ import yaml
|
|
9
9
|
from importlib.metadata import metadata
|
10
10
|
|
11
11
|
from .log import create_logger
|
12
|
-
from .base import
|
12
|
+
from .base import ConnectionServer
|
13
13
|
|
14
14
|
# 获取包信息
|
15
15
|
pkg_meta = metadata("mcp-dbutils")
|
@@ -19,7 +19,7 @@ log = create_logger(pkg_meta["Name"])
|
|
19
19
|
|
20
20
|
async def run_server():
|
21
21
|
"""服务器运行逻辑"""
|
22
|
-
parser = argparse.ArgumentParser(description='MCP
|
22
|
+
parser = argparse.ArgumentParser(description='MCP Connection Server')
|
23
23
|
parser.add_argument('--config', required=True, help='YAML配置文件路径')
|
24
24
|
parser.add_argument('--local-host', help='本地主机地址')
|
25
25
|
|
@@ -32,7 +32,7 @@ async def run_server():
|
|
32
32
|
global log
|
33
33
|
log = create_logger(pkg_meta["Name"], debug)
|
34
34
|
|
35
|
-
log("info", f"MCP
|
35
|
+
log("info", f"MCP Connection Utilities Service v{pkg_meta['Version']}")
|
36
36
|
if debug:
|
37
37
|
log("debug", "Debug模式已开启")
|
38
38
|
|
@@ -40,11 +40,11 @@ async def run_server():
|
|
40
40
|
try:
|
41
41
|
with open(args.config, 'r') as f:
|
42
42
|
config = yaml.safe_load(f)
|
43
|
-
if not config or '
|
44
|
-
log("error", "配置文件必须包含
|
43
|
+
if not config or 'connections' not in config:
|
44
|
+
log("error", "配置文件必须包含 connections 配置")
|
45
45
|
sys.exit(1)
|
46
|
-
if not config['
|
47
|
-
log("error", "
|
46
|
+
if not config['connections']:
|
47
|
+
log("error", "配置文件必须包含至少一个连接配置")
|
48
48
|
sys.exit(1)
|
49
49
|
except Exception as e:
|
50
50
|
log("error", f"读取配置文件失败: {str(e)}")
|
@@ -52,7 +52,7 @@ async def run_server():
|
|
52
52
|
|
53
53
|
# 创建并运行服务器
|
54
54
|
try:
|
55
|
-
server =
|
55
|
+
server = ConnectionServer(args.config, debug)
|
56
56
|
await server.run()
|
57
57
|
except KeyboardInterrupt:
|
58
58
|
log("info", "服务器已停止")
|
mcp_dbutils/base.py
CHANGED
@@ -1,14 +1,14 @@
|
|
1
|
-
"""
|
1
|
+
"""Connection server base class"""
|
2
2
|
|
3
|
-
class
|
4
|
-
"""Base exception for
|
3
|
+
class ConnectionHandlerError(Exception):
|
4
|
+
"""Base exception for connection errors"""
|
5
5
|
pass
|
6
6
|
|
7
|
-
class ConfigurationError(
|
7
|
+
class ConfigurationError(ConnectionHandlerError):
|
8
8
|
"""Configuration related errors"""
|
9
9
|
pass
|
10
10
|
|
11
|
-
class ConnectionError(
|
11
|
+
class ConnectionError(ConnectionHandlerError):
|
12
12
|
"""Connection related errors"""
|
13
13
|
pass
|
14
14
|
|
@@ -17,6 +17,8 @@ from typing import Any, List, Optional, AsyncContextManager
|
|
17
17
|
from contextlib import asynccontextmanager
|
18
18
|
import json
|
19
19
|
import yaml
|
20
|
+
import time
|
21
|
+
from datetime import datetime
|
20
22
|
from importlib.metadata import metadata
|
21
23
|
from mcp.server import Server, NotificationOptions
|
22
24
|
import mcp.server.stdio
|
@@ -29,21 +31,21 @@ from .stats import ResourceStats
|
|
29
31
|
# 获取包信息用于日志命名
|
30
32
|
pkg_meta = metadata("mcp-dbutils")
|
31
33
|
|
32
|
-
class
|
33
|
-
"""Abstract base class defining common interface for
|
34
|
+
class ConnectionHandler(ABC):
|
35
|
+
"""Abstract base class defining common interface for connection handlers"""
|
34
36
|
|
35
|
-
def __init__(self, config_path: str,
|
36
|
-
"""Initialize
|
37
|
+
def __init__(self, config_path: str, connection: str, debug: bool = False):
|
38
|
+
"""Initialize connection handler
|
37
39
|
|
38
40
|
Args:
|
39
41
|
config_path: Path to configuration file
|
40
|
-
|
42
|
+
connection: Database connection name
|
41
43
|
debug: Enable debug mode
|
42
44
|
"""
|
43
45
|
self.config_path = config_path
|
44
|
-
self.
|
46
|
+
self.connection = connection
|
45
47
|
self.debug = debug
|
46
|
-
self.log = create_logger(f"{pkg_meta['Name']}.handler.{
|
48
|
+
self.log = create_logger(f"{pkg_meta['Name']}.handler.{connection}", debug)
|
47
49
|
self.stats = ResourceStats()
|
48
50
|
|
49
51
|
@property
|
@@ -54,7 +56,7 @@ class DatabaseHandler(ABC):
|
|
54
56
|
|
55
57
|
@abstractmethod
|
56
58
|
async def get_tables(self) -> list[types.Resource]:
|
57
|
-
"""Get list of table resources from database"""
|
59
|
+
"""Get list of table resources from database connection"""
|
58
60
|
pass
|
59
61
|
|
60
62
|
@abstractmethod
|
@@ -68,28 +70,144 @@ class DatabaseHandler(ABC):
|
|
68
70
|
pass
|
69
71
|
|
70
72
|
async def execute_query(self, sql: str) -> str:
|
71
|
-
"""Execute SQL query"""
|
73
|
+
"""Execute SQL query with performance tracking"""
|
74
|
+
start_time = datetime.now()
|
72
75
|
try:
|
73
76
|
self.stats.record_query()
|
74
77
|
result = await self._execute_query(sql)
|
78
|
+
duration = (datetime.now() - start_time).total_seconds()
|
79
|
+
self.stats.record_query_duration(sql, duration)
|
75
80
|
self.stats.update_memory_usage(result)
|
76
|
-
self.log("info", f"Resource stats: {json.dumps(self.stats.to_dict())}")
|
81
|
+
self.log("info", f"Query executed in {duration*1000:.2f}ms. Resource stats: {json.dumps(self.stats.to_dict())}")
|
77
82
|
return result
|
78
83
|
except Exception as e:
|
84
|
+
duration = (datetime.now() - start_time).total_seconds()
|
79
85
|
self.stats.record_error(e.__class__.__name__)
|
80
|
-
self.log("error", f"Query error - {str(e)}\nResource stats: {json.dumps(self.stats.to_dict())}")
|
86
|
+
self.log("error", f"Query error after {duration*1000:.2f}ms - {str(e)}\nResource stats: {json.dumps(self.stats.to_dict())}")
|
81
87
|
raise
|
82
88
|
|
89
|
+
@abstractmethod
|
90
|
+
async def get_table_description(self, table_name: str) -> str:
|
91
|
+
"""Get detailed table description including columns, types, and comments
|
92
|
+
|
93
|
+
Args:
|
94
|
+
table_name: Name of the table to describe
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
Formatted table description
|
98
|
+
"""
|
99
|
+
pass
|
100
|
+
|
101
|
+
@abstractmethod
|
102
|
+
async def get_table_ddl(self, table_name: str) -> str:
|
103
|
+
"""Get DDL statement for table including columns, constraints and indexes
|
104
|
+
|
105
|
+
Args:
|
106
|
+
table_name: Name of the table to get DDL for
|
107
|
+
|
108
|
+
Returns:
|
109
|
+
DDL statement as string
|
110
|
+
"""
|
111
|
+
pass
|
112
|
+
|
113
|
+
@abstractmethod
|
114
|
+
async def get_table_indexes(self, table_name: str) -> str:
|
115
|
+
"""Get index information for table
|
116
|
+
|
117
|
+
Args:
|
118
|
+
table_name: Name of the table to get indexes for
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
Formatted index information
|
122
|
+
"""
|
123
|
+
pass
|
124
|
+
|
125
|
+
@abstractmethod
|
126
|
+
async def get_table_stats(self, table_name: str) -> str:
|
127
|
+
"""Get table statistics information
|
128
|
+
|
129
|
+
Args:
|
130
|
+
table_name: Name of the table to get statistics for
|
131
|
+
|
132
|
+
Returns:
|
133
|
+
Formatted statistics information including row count, size, etc.
|
134
|
+
"""
|
135
|
+
pass
|
136
|
+
|
137
|
+
@abstractmethod
|
138
|
+
async def get_table_constraints(self, table_name: str) -> str:
|
139
|
+
"""Get constraint information for table
|
140
|
+
|
141
|
+
Args:
|
142
|
+
table_name: Name of the table to get constraints for
|
143
|
+
|
144
|
+
Returns:
|
145
|
+
Formatted constraint information including primary keys, foreign keys, etc.
|
146
|
+
"""
|
147
|
+
pass
|
148
|
+
|
149
|
+
@abstractmethod
|
150
|
+
async def explain_query(self, sql: str) -> str:
|
151
|
+
"""Get query execution plan
|
152
|
+
|
153
|
+
Args:
|
154
|
+
sql: SQL query to explain
|
155
|
+
|
156
|
+
Returns:
|
157
|
+
Formatted query execution plan with cost estimates
|
158
|
+
"""
|
159
|
+
pass
|
160
|
+
|
83
161
|
@abstractmethod
|
84
162
|
async def cleanup(self):
|
85
163
|
"""Cleanup resources"""
|
86
164
|
pass
|
87
165
|
|
88
|
-
|
89
|
-
|
166
|
+
async def execute_tool_query(self, tool_name: str, table_name: str = "", sql: str = "") -> str:
|
167
|
+
"""Execute a tool query and return formatted result
|
168
|
+
|
169
|
+
Args:
|
170
|
+
tool_name: Name of the tool to execute
|
171
|
+
table_name: Name of the table to query (for table-related tools)
|
172
|
+
sql: SQL query (for query-related tools)
|
173
|
+
|
174
|
+
Returns:
|
175
|
+
Formatted query result
|
176
|
+
"""
|
177
|
+
try:
|
178
|
+
self.stats.record_query()
|
179
|
+
|
180
|
+
if tool_name == "dbutils-describe-table":
|
181
|
+
result = await self.get_table_description(table_name)
|
182
|
+
elif tool_name == "dbutils-get-ddl":
|
183
|
+
result = await self.get_table_ddl(table_name)
|
184
|
+
elif tool_name == "dbutils-list-indexes":
|
185
|
+
result = await self.get_table_indexes(table_name)
|
186
|
+
elif tool_name == "dbutils-get-stats":
|
187
|
+
result = await self.get_table_stats(table_name)
|
188
|
+
elif tool_name == "dbutils-list-constraints":
|
189
|
+
result = await self.get_table_constraints(table_name)
|
190
|
+
elif tool_name == "dbutils-explain-query":
|
191
|
+
if not sql:
|
192
|
+
raise ValueError("SQL query required for explain-query tool")
|
193
|
+
result = await self.explain_query(sql)
|
194
|
+
else:
|
195
|
+
raise ValueError(f"Unknown tool: {tool_name}")
|
196
|
+
|
197
|
+
self.stats.update_memory_usage(result)
|
198
|
+
self.log("info", f"Resource stats: {json.dumps(self.stats.to_dict())}")
|
199
|
+
return f"[{self.db_type}]\n{result}"
|
200
|
+
|
201
|
+
except Exception as e:
|
202
|
+
self.stats.record_error(e.__class__.__name__)
|
203
|
+
self.log("error", f"Tool error - {str(e)}\nResource stats: {json.dumps(self.stats.to_dict())}")
|
204
|
+
raise
|
205
|
+
|
206
|
+
class ConnectionServer:
|
207
|
+
"""Unified connection server class"""
|
90
208
|
|
91
209
|
def __init__(self, config_path: str, debug: bool = False):
|
92
|
-
"""Initialize
|
210
|
+
"""Initialize connection server
|
93
211
|
|
94
212
|
Args:
|
95
213
|
config_path: Path to configuration file
|
@@ -120,27 +238,27 @@ class DatabaseServer:
|
|
120
238
|
raise
|
121
239
|
|
122
240
|
@asynccontextmanager
|
123
|
-
async def get_handler(self,
|
124
|
-
"""Get
|
241
|
+
async def get_handler(self, connection: str) -> AsyncContextManager[ConnectionHandler]:
|
242
|
+
"""Get connection handler
|
125
243
|
|
126
|
-
Get appropriate
|
244
|
+
Get appropriate connection handler based on connection name
|
127
245
|
|
128
246
|
Args:
|
129
|
-
|
247
|
+
connection: Database connection name
|
130
248
|
|
131
249
|
Returns:
|
132
|
-
AsyncContextManager[
|
250
|
+
AsyncContextManager[ConnectionHandler]: Context manager for connection handler
|
133
251
|
"""
|
134
252
|
# Read configuration file to determine database type
|
135
253
|
with open(self.config_path, 'r') as f:
|
136
254
|
config = yaml.safe_load(f)
|
137
|
-
if not config or '
|
138
|
-
raise ConfigurationError("Configuration file must contain '
|
139
|
-
if
|
140
|
-
|
141
|
-
raise ConfigurationError(f"
|
255
|
+
if not config or 'connections' not in config:
|
256
|
+
raise ConfigurationError("Configuration file must contain 'connections' section")
|
257
|
+
if connection not in config['connections']:
|
258
|
+
available_connections = list(config['connections'].keys())
|
259
|
+
raise ConfigurationError(f"Connection not found: {connection}. Available connections: {available_connections}")
|
142
260
|
|
143
|
-
db_config = config['
|
261
|
+
db_config = config['connections'][connection]
|
144
262
|
|
145
263
|
handler = None
|
146
264
|
try:
|
@@ -150,16 +268,16 @@ class DatabaseServer:
|
|
150
268
|
db_type = db_config['type']
|
151
269
|
self.logger("debug", f"Creating handler for database type: {db_type}")
|
152
270
|
if db_type == 'sqlite':
|
153
|
-
from .sqlite.handler import
|
154
|
-
handler =
|
271
|
+
from .sqlite.handler import SQLiteHandler
|
272
|
+
handler = SQLiteHandler(self.config_path, connection, self.debug)
|
155
273
|
elif db_type == 'postgres':
|
156
|
-
from .postgres.handler import
|
157
|
-
handler =
|
274
|
+
from .postgres.handler import PostgreSQLHandler
|
275
|
+
handler = PostgreSQLHandler(self.config_path, connection, self.debug)
|
158
276
|
else:
|
159
277
|
raise ConfigurationError(f"Unsupported database type: {db_type}")
|
160
278
|
|
161
279
|
handler.stats.record_connection_start()
|
162
|
-
self.logger("debug", f"Handler created successfully for {
|
280
|
+
self.logger("debug", f"Handler created successfully for {connection}")
|
163
281
|
self.logger("info", f"Resource stats: {json.dumps(handler.stats.to_dict())}")
|
164
282
|
yield handler
|
165
283
|
except yaml.YAMLError as e:
|
@@ -168,7 +286,7 @@ class DatabaseServer:
|
|
168
286
|
raise ConfigurationError(f"Failed to import handler for {db_type}: {str(e)}")
|
169
287
|
finally:
|
170
288
|
if handler:
|
171
|
-
self.logger("debug", f"Cleaning up handler for {
|
289
|
+
self.logger("debug", f"Cleaning up handler for {connection}")
|
172
290
|
handler.stats.record_connection_end()
|
173
291
|
self.logger("info", f"Final resource stats: {json.dumps(handler.stats.to_dict())}")
|
174
292
|
await handler.cleanup()
|
@@ -177,75 +295,215 @@ class DatabaseServer:
|
|
177
295
|
"""Setup MCP handlers"""
|
178
296
|
@self.server.list_resources()
|
179
297
|
async def handle_list_resources(arguments: dict | None = None) -> list[types.Resource]:
|
180
|
-
if not arguments or '
|
181
|
-
# Return empty list when no
|
298
|
+
if not arguments or 'connection' not in arguments:
|
299
|
+
# Return empty list when no connection specified
|
182
300
|
return []
|
183
301
|
|
184
|
-
|
185
|
-
async with self.get_handler(
|
302
|
+
connection = arguments['connection']
|
303
|
+
async with self.get_handler(connection) as handler:
|
186
304
|
return await handler.get_tables()
|
187
305
|
|
188
306
|
@self.server.read_resource()
|
189
307
|
async def handle_read_resource(uri: str, arguments: dict | None = None) -> str:
|
190
|
-
if not arguments or '
|
191
|
-
raise ConfigurationError("
|
308
|
+
if not arguments or 'connection' not in arguments:
|
309
|
+
raise ConfigurationError("Connection name must be specified")
|
192
310
|
|
193
311
|
parts = uri.split('/')
|
194
312
|
if len(parts) < 3:
|
195
313
|
raise ConfigurationError("Invalid resource URI format")
|
196
314
|
|
197
|
-
|
315
|
+
connection = arguments['connection']
|
198
316
|
table_name = parts[-2] # URI format: xxx/table_name/schema
|
199
317
|
|
200
|
-
async with self.get_handler(
|
318
|
+
async with self.get_handler(connection) as handler:
|
201
319
|
return await handler.get_schema(table_name)
|
202
320
|
|
203
321
|
@self.server.list_tools()
|
204
322
|
async def handle_list_tools() -> list[types.Tool]:
|
205
323
|
return [
|
206
324
|
types.Tool(
|
207
|
-
name="query",
|
208
|
-
description="Execute read-only SQL query",
|
325
|
+
name="dbutils-run-query",
|
326
|
+
description="Execute read-only SQL query on database connection",
|
209
327
|
inputSchema={
|
210
328
|
"type": "object",
|
211
329
|
"properties": {
|
212
|
-
"
|
330
|
+
"connection": {
|
213
331
|
"type": "string",
|
214
|
-
"description": "Database
|
332
|
+
"description": "Database connection name"
|
215
333
|
},
|
216
334
|
"sql": {
|
217
335
|
"type": "string",
|
218
336
|
"description": "SQL query (SELECT only)"
|
219
337
|
}
|
220
338
|
},
|
221
|
-
"required": ["
|
339
|
+
"required": ["connection", "sql"]
|
340
|
+
}
|
341
|
+
),
|
342
|
+
types.Tool(
|
343
|
+
name="dbutils-list-tables",
|
344
|
+
description="List all available tables in the specified database connection",
|
345
|
+
inputSchema={
|
346
|
+
"type": "object",
|
347
|
+
"properties": {
|
348
|
+
"connection": {
|
349
|
+
"type": "string",
|
350
|
+
"description": "Database connection name"
|
351
|
+
}
|
352
|
+
},
|
353
|
+
"required": ["connection"]
|
222
354
|
}
|
223
355
|
),
|
224
356
|
types.Tool(
|
225
|
-
name="
|
226
|
-
description="
|
357
|
+
name="dbutils-describe-table",
|
358
|
+
description="Get detailed information about a table's structure",
|
227
359
|
inputSchema={
|
228
360
|
"type": "object",
|
229
361
|
"properties": {
|
230
|
-
"
|
362
|
+
"connection": {
|
231
363
|
"type": "string",
|
232
|
-
"description": "Database
|
364
|
+
"description": "Database connection name"
|
365
|
+
},
|
366
|
+
"table": {
|
367
|
+
"type": "string",
|
368
|
+
"description": "Table name to describe"
|
233
369
|
}
|
234
370
|
},
|
235
|
-
"required": ["
|
371
|
+
"required": ["connection", "table"]
|
372
|
+
}
|
373
|
+
),
|
374
|
+
types.Tool(
|
375
|
+
name="dbutils-get-ddl",
|
376
|
+
description="Get DDL statement for creating the table",
|
377
|
+
inputSchema={
|
378
|
+
"type": "object",
|
379
|
+
"properties": {
|
380
|
+
"connection": {
|
381
|
+
"type": "string",
|
382
|
+
"description": "Database connection name"
|
383
|
+
},
|
384
|
+
"table": {
|
385
|
+
"type": "string",
|
386
|
+
"description": "Table name to get DDL for"
|
387
|
+
}
|
388
|
+
},
|
389
|
+
"required": ["connection", "table"]
|
390
|
+
}
|
391
|
+
),
|
392
|
+
types.Tool(
|
393
|
+
name="dbutils-list-indexes",
|
394
|
+
description="List all indexes on the specified table",
|
395
|
+
inputSchema={
|
396
|
+
"type": "object",
|
397
|
+
"properties": {
|
398
|
+
"connection": {
|
399
|
+
"type": "string",
|
400
|
+
"description": "Database connection name"
|
401
|
+
},
|
402
|
+
"table": {
|
403
|
+
"type": "string",
|
404
|
+
"description": "Table name to list indexes for"
|
405
|
+
}
|
406
|
+
},
|
407
|
+
"required": ["connection", "table"]
|
408
|
+
}
|
409
|
+
),
|
410
|
+
types.Tool(
|
411
|
+
name="dbutils-get-stats",
|
412
|
+
description="Get table statistics like row count and size",
|
413
|
+
inputSchema={
|
414
|
+
"type": "object",
|
415
|
+
"properties": {
|
416
|
+
"connection": {
|
417
|
+
"type": "string",
|
418
|
+
"description": "Database connection name"
|
419
|
+
},
|
420
|
+
"table": {
|
421
|
+
"type": "string",
|
422
|
+
"description": "Table name to get statistics for"
|
423
|
+
}
|
424
|
+
},
|
425
|
+
"required": ["connection", "table"]
|
426
|
+
}
|
427
|
+
),
|
428
|
+
types.Tool(
|
429
|
+
name="dbutils-list-constraints",
|
430
|
+
description="List all constraints (primary key, foreign keys, etc) on the table",
|
431
|
+
inputSchema={
|
432
|
+
"type": "object",
|
433
|
+
"properties": {
|
434
|
+
"connection": {
|
435
|
+
"type": "string",
|
436
|
+
"description": "Database connection name"
|
437
|
+
},
|
438
|
+
"table": {
|
439
|
+
"type": "string",
|
440
|
+
"description": "Table name to list constraints for"
|
441
|
+
}
|
442
|
+
},
|
443
|
+
"required": ["connection", "table"]
|
444
|
+
}
|
445
|
+
),
|
446
|
+
types.Tool(
|
447
|
+
name="dbutils-explain-query",
|
448
|
+
description="Get execution plan for a SQL query",
|
449
|
+
inputSchema={
|
450
|
+
"type": "object",
|
451
|
+
"properties": {
|
452
|
+
"connection": {
|
453
|
+
"type": "string",
|
454
|
+
"description": "Database connection name"
|
455
|
+
},
|
456
|
+
"sql": {
|
457
|
+
"type": "string",
|
458
|
+
"description": "SQL query to explain"
|
459
|
+
}
|
460
|
+
},
|
461
|
+
"required": ["connection", "sql"]
|
462
|
+
}
|
463
|
+
),
|
464
|
+
types.Tool(
|
465
|
+
name="dbutils-get-performance",
|
466
|
+
description="Get database performance statistics",
|
467
|
+
inputSchema={
|
468
|
+
"type": "object",
|
469
|
+
"properties": {
|
470
|
+
"connection": {
|
471
|
+
"type": "string",
|
472
|
+
"description": "Database connection name"
|
473
|
+
}
|
474
|
+
},
|
475
|
+
"required": ["connection"]
|
476
|
+
}
|
477
|
+
),
|
478
|
+
types.Tool(
|
479
|
+
name="dbutils-analyze-query",
|
480
|
+
description="Analyze a SQL query for performance",
|
481
|
+
inputSchema={
|
482
|
+
"type": "object",
|
483
|
+
"properties": {
|
484
|
+
"connection": {
|
485
|
+
"type": "string",
|
486
|
+
"description": "Database connection name"
|
487
|
+
},
|
488
|
+
"sql": {
|
489
|
+
"type": "string",
|
490
|
+
"description": "SQL query to analyze"
|
491
|
+
}
|
492
|
+
},
|
493
|
+
"required": ["connection", "sql"]
|
236
494
|
}
|
237
495
|
)
|
238
496
|
]
|
239
497
|
|
240
498
|
@self.server.call_tool()
|
241
499
|
async def handle_call_tool(name: str, arguments: dict) -> list[types.TextContent]:
|
242
|
-
if "
|
243
|
-
raise ConfigurationError("
|
244
|
-
|
245
|
-
|
500
|
+
if "connection" not in arguments:
|
501
|
+
raise ConfigurationError("Connection name must be specified")
|
502
|
+
|
503
|
+
connection = arguments["connection"]
|
246
504
|
|
247
|
-
if name == "
|
248
|
-
async with self.get_handler(
|
505
|
+
if name == "dbutils-list-tables":
|
506
|
+
async with self.get_handler(connection) as handler:
|
249
507
|
tables = await handler.get_tables()
|
250
508
|
if not tables:
|
251
509
|
# 空表列表的情况也返回数据库类型
|
@@ -260,7 +518,7 @@ class DatabaseServer:
|
|
260
518
|
])
|
261
519
|
# 添加数据库类型前缀
|
262
520
|
return [types.TextContent(type="text", text=f"[{handler.db_type}]\n{formatted_tables}")]
|
263
|
-
elif name == "query":
|
521
|
+
elif name == "dbutils-run-query":
|
264
522
|
sql = arguments.get("sql", "").strip()
|
265
523
|
if not sql:
|
266
524
|
raise ConfigurationError("SQL query cannot be empty")
|
@@ -269,9 +527,76 @@ class DatabaseServer:
|
|
269
527
|
if not sql.lower().startswith("select"):
|
270
528
|
raise ConfigurationError("Only SELECT queries are supported for security reasons")
|
271
529
|
|
272
|
-
async with self.get_handler(
|
530
|
+
async with self.get_handler(connection) as handler:
|
273
531
|
result = await handler.execute_query(sql)
|
274
532
|
return [types.TextContent(type="text", text=result)]
|
533
|
+
elif name in ["dbutils-describe-table", "dbutils-get-ddl", "dbutils-list-indexes",
|
534
|
+
"dbutils-get-stats", "dbutils-list-constraints"]:
|
535
|
+
table = arguments.get("table", "").strip()
|
536
|
+
if not table:
|
537
|
+
raise ConfigurationError("Table name cannot be empty")
|
538
|
+
|
539
|
+
async with self.get_handler(connection) as handler:
|
540
|
+
result = await handler.execute_tool_query(name, table_name=table)
|
541
|
+
return [types.TextContent(type="text", text=result)]
|
542
|
+
elif name == "dbutils-explain-query":
|
543
|
+
sql = arguments.get("sql", "").strip()
|
544
|
+
if not sql:
|
545
|
+
raise ConfigurationError("SQL query cannot be empty")
|
546
|
+
|
547
|
+
async with self.get_handler(connection) as handler:
|
548
|
+
result = await handler.execute_tool_query(name, sql=sql)
|
549
|
+
return [types.TextContent(type="text", text=result)]
|
550
|
+
elif name == "dbutils-get-performance":
|
551
|
+
async with self.get_handler(connection) as handler:
|
552
|
+
performance_stats = handler.stats.get_performance_stats()
|
553
|
+
return [types.TextContent(type="text", text=f"[{handler.db_type}]\n{performance_stats}")]
|
554
|
+
elif name == "dbutils-analyze-query":
|
555
|
+
sql = arguments.get("sql", "").strip()
|
556
|
+
if not sql:
|
557
|
+
raise ConfigurationError("SQL query cannot be empty")
|
558
|
+
|
559
|
+
async with self.get_handler(connection) as handler:
|
560
|
+
# First get the execution plan
|
561
|
+
explain_result = await handler.explain_query(sql)
|
562
|
+
|
563
|
+
# Then execute the actual query to measure performance
|
564
|
+
start_time = datetime.now()
|
565
|
+
if sql.lower().startswith("select"):
|
566
|
+
try:
|
567
|
+
await handler.execute_query(sql)
|
568
|
+
except Exception as e:
|
569
|
+
# If query fails, we still provide the execution plan
|
570
|
+
self.logger("error", f"Query execution failed during analysis: {str(e)}")
|
571
|
+
duration = (datetime.now() - start_time).total_seconds()
|
572
|
+
|
573
|
+
# Combine analysis results
|
574
|
+
analysis = [
|
575
|
+
f"[{handler.db_type}] Query Analysis",
|
576
|
+
f"SQL: {sql}",
|
577
|
+
f"",
|
578
|
+
f"Execution Time: {duration*1000:.2f}ms",
|
579
|
+
f"",
|
580
|
+
f"Execution Plan:",
|
581
|
+
explain_result
|
582
|
+
]
|
583
|
+
|
584
|
+
# Add optimization suggestions based on execution plan and timing
|
585
|
+
suggestions = []
|
586
|
+
if "seq scan" in explain_result.lower() and duration > 0.1:
|
587
|
+
suggestions.append("- Consider adding an index to avoid sequential scan")
|
588
|
+
if "hash join" in explain_result.lower() and duration > 0.5:
|
589
|
+
suggestions.append("- Consider optimizing join conditions")
|
590
|
+
if duration > 0.5: # 500ms
|
591
|
+
suggestions.append("- Query is slow, consider optimizing or adding caching")
|
592
|
+
if "temporary" in explain_result.lower():
|
593
|
+
suggestions.append("- Query creates temporary tables, consider restructuring")
|
594
|
+
|
595
|
+
if suggestions:
|
596
|
+
analysis.append("\nOptimization Suggestions:")
|
597
|
+
analysis.extend(suggestions)
|
598
|
+
|
599
|
+
return [types.TextContent(type="text", text="\n".join(analysis))]
|
275
600
|
else:
|
276
601
|
raise ConfigurationError(f"Unknown tool: {name}")
|
277
602
|
|