mcp-dbutils 0.23.1__py3-none-any.whl → 1.0.1__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/audit.py +269 -0
- mcp_dbutils/base.py +498 -0
- mcp_dbutils/config.py +103 -1
- mcp_dbutils/mysql/config.py +57 -40
- mcp_dbutils/mysql/handler.py +60 -0
- mcp_dbutils/postgres/config.py +40 -22
- mcp_dbutils/postgres/handler.py +60 -0
- mcp_dbutils/sqlite/config.py +8 -1
- mcp_dbutils/sqlite/handler.py +53 -0
- {mcp_dbutils-0.23.1.dist-info → mcp_dbutils-1.0.1.dist-info}/METADATA +1 -1
- mcp_dbutils-1.0.1.dist-info/RECORD +23 -0
- mcp_dbutils-0.23.1.dist-info/RECORD +0 -22
- {mcp_dbutils-0.23.1.dist-info → mcp_dbutils-1.0.1.dist-info}/WHEEL +0 -0
- {mcp_dbutils-0.23.1.dist-info → mcp_dbutils-1.0.1.dist-info}/entry_points.txt +0 -0
- {mcp_dbutils-0.23.1.dist-info → mcp_dbutils-1.0.1.dist-info}/licenses/LICENSE +0 -0
mcp_dbutils/mysql/config.py
CHANGED
@@ -3,7 +3,7 @@ from dataclasses import dataclass
|
|
3
3
|
from typing import Any, Dict, Literal, Optional
|
4
4
|
from urllib.parse import parse_qs, urlparse
|
5
5
|
|
6
|
-
from ..config import ConnectionConfig
|
6
|
+
from ..config import ConnectionConfig, WritePermissions
|
7
7
|
|
8
8
|
|
9
9
|
@dataclass
|
@@ -16,30 +16,30 @@ class SSLConfig:
|
|
16
16
|
|
17
17
|
def parse_url(url: str) -> Dict[str, Any]:
|
18
18
|
"""Parse MySQL URL into connection parameters
|
19
|
-
|
19
|
+
|
20
20
|
Args:
|
21
21
|
url: URL (e.g. mysql://host:port/dbname?ssl-mode=verify_identity)
|
22
|
-
|
22
|
+
|
23
23
|
Returns:
|
24
24
|
Dictionary of connection parameters including SSL settings
|
25
25
|
"""
|
26
26
|
if not url.startswith('mysql://'):
|
27
27
|
raise ValueError("Invalid MySQL URL format")
|
28
|
-
|
28
|
+
|
29
29
|
if '@' in url:
|
30
30
|
raise ValueError("URL should not contain credentials. Please provide username and password separately.")
|
31
|
-
|
31
|
+
|
32
32
|
# Parse URL and query parameters
|
33
33
|
parsed = urlparse(url)
|
34
34
|
query_params = parse_qs(parsed.query)
|
35
|
-
|
35
|
+
|
36
36
|
params = {
|
37
37
|
'host': parsed.hostname or 'localhost',
|
38
38
|
'port': str(parsed.port or 3306),
|
39
39
|
'database': parsed.path.lstrip('/') if parsed.path else '',
|
40
40
|
'charset': query_params.get('charset', ['utf8mb4'])[0]
|
41
41
|
}
|
42
|
-
|
42
|
+
|
43
43
|
if not params['database']:
|
44
44
|
raise ValueError("MySQL database name must be specified in URL")
|
45
45
|
|
@@ -50,17 +50,17 @@ def parse_url(url: str) -> Dict[str, Any]:
|
|
50
50
|
if mode not in ['disabled', 'required', 'verify_ca', 'verify_identity']:
|
51
51
|
raise ValueError(f"Invalid ssl-mode: {mode}")
|
52
52
|
ssl_params['mode'] = mode
|
53
|
-
|
53
|
+
|
54
54
|
if 'ssl-ca' in query_params:
|
55
55
|
ssl_params['ca'] = query_params['ssl-ca'][0]
|
56
56
|
if 'ssl-cert' in query_params:
|
57
57
|
ssl_params['cert'] = query_params['ssl-cert'][0]
|
58
58
|
if 'ssl-key' in query_params:
|
59
59
|
ssl_params['key'] = query_params['ssl-key'][0]
|
60
|
-
|
60
|
+
|
61
61
|
if ssl_params:
|
62
62
|
params['ssl'] = SSLConfig(**ssl_params)
|
63
|
-
|
63
|
+
|
64
64
|
return params
|
65
65
|
|
66
66
|
@dataclass
|
@@ -75,18 +75,20 @@ class MySQLConfig(ConnectionConfig):
|
|
75
75
|
type: Literal['mysql'] = 'mysql'
|
76
76
|
url: Optional[str] = None
|
77
77
|
ssl: Optional[SSLConfig] = None
|
78
|
+
writable: bool = False # Whether write operations are allowed
|
79
|
+
write_permissions: Optional[WritePermissions] = None # Write permissions configuration
|
78
80
|
|
79
81
|
@classmethod
|
80
82
|
def _validate_connection_config(cls, configs: dict, db_name: str) -> dict:
|
81
83
|
"""验证连接配置是否有效
|
82
|
-
|
84
|
+
|
83
85
|
Args:
|
84
86
|
configs: 配置字典
|
85
87
|
db_name: 连接名称
|
86
|
-
|
88
|
+
|
87
89
|
Returns:
|
88
90
|
dict: 数据库配置
|
89
|
-
|
91
|
+
|
90
92
|
Raises:
|
91
93
|
ValueError: 如果配置无效
|
92
94
|
"""
|
@@ -107,17 +109,17 @@ class MySQLConfig(ConnectionConfig):
|
|
107
109
|
raise ValueError("User must be specified in connection configuration")
|
108
110
|
if not db_config.get('password'):
|
109
111
|
raise ValueError("Password must be specified in connection configuration")
|
110
|
-
|
112
|
+
|
111
113
|
return db_config
|
112
|
-
|
114
|
+
|
113
115
|
@classmethod
|
114
116
|
def _create_config_from_url(cls, db_config: dict, local_host: Optional[str] = None) -> 'MySQLConfig':
|
115
117
|
"""从URL创建配置
|
116
|
-
|
118
|
+
|
117
119
|
Args:
|
118
120
|
db_config: 数据库配置
|
119
121
|
local_host: 可选的本地主机地址
|
120
|
-
|
122
|
+
|
121
123
|
Returns:
|
122
124
|
MySQLConfig: 配置对象
|
123
125
|
"""
|
@@ -135,18 +137,18 @@ class MySQLConfig(ConnectionConfig):
|
|
135
137
|
ssl=params.get('ssl')
|
136
138
|
)
|
137
139
|
return config
|
138
|
-
|
140
|
+
|
139
141
|
@classmethod
|
140
142
|
def _create_config_from_params(cls, db_config: dict, local_host: Optional[str] = None) -> 'MySQLConfig':
|
141
143
|
"""从参数创建配置
|
142
|
-
|
144
|
+
|
143
145
|
Args:
|
144
146
|
db_config: 数据库配置
|
145
147
|
local_host: 可选的本地主机地址
|
146
|
-
|
148
|
+
|
147
149
|
Returns:
|
148
150
|
MySQLConfig: 配置对象
|
149
|
-
|
151
|
+
|
150
152
|
Raises:
|
151
153
|
ValueError: 如果缺少必需参数或SSL配置无效
|
152
154
|
"""
|
@@ -156,10 +158,10 @@ class MySQLConfig(ConnectionConfig):
|
|
156
158
|
raise ValueError("Host must be specified in connection configuration")
|
157
159
|
if not db_config.get('port'):
|
158
160
|
raise ValueError("Port must be specified in connection configuration")
|
159
|
-
|
161
|
+
|
160
162
|
# Parse SSL configuration if present
|
161
163
|
ssl_config = cls._parse_ssl_config(db_config)
|
162
|
-
|
164
|
+
|
163
165
|
config = cls(
|
164
166
|
database=db_config['database'],
|
165
167
|
user=db_config['user'],
|
@@ -171,30 +173,30 @@ class MySQLConfig(ConnectionConfig):
|
|
171
173
|
ssl=ssl_config
|
172
174
|
)
|
173
175
|
return config
|
174
|
-
|
176
|
+
|
175
177
|
@classmethod
|
176
178
|
def _parse_ssl_config(cls, db_config: dict) -> Optional[SSLConfig]:
|
177
179
|
"""解析SSL配置
|
178
|
-
|
180
|
+
|
179
181
|
Args:
|
180
182
|
db_config: 数据库配置
|
181
|
-
|
183
|
+
|
182
184
|
Returns:
|
183
185
|
Optional[SSLConfig]: SSL配置或None
|
184
|
-
|
186
|
+
|
185
187
|
Raises:
|
186
188
|
ValueError: 如果SSL配置无效
|
187
189
|
"""
|
188
190
|
if 'ssl' not in db_config:
|
189
191
|
return None
|
190
|
-
|
192
|
+
|
191
193
|
ssl_params = db_config['ssl']
|
192
194
|
if not isinstance(ssl_params, dict):
|
193
195
|
raise ValueError("SSL configuration must be a dictionary")
|
194
|
-
|
196
|
+
|
195
197
|
if ssl_params.get('mode') not in [None, 'disabled', 'required', 'verify_ca', 'verify_identity']:
|
196
198
|
raise ValueError(f"Invalid ssl-mode: {ssl_params.get('mode')}")
|
197
|
-
|
199
|
+
|
198
200
|
return SSLConfig(
|
199
201
|
mode=ssl_params.get('mode', 'disabled'),
|
200
202
|
ca=ssl_params.get('ca'),
|
@@ -212,7 +214,7 @@ class MySQLConfig(ConnectionConfig):
|
|
212
214
|
local_host: Optional local host address
|
213
215
|
"""
|
214
216
|
configs = cls.load_yaml_config(yaml_path)
|
215
|
-
|
217
|
+
|
216
218
|
# Validate connection config
|
217
219
|
db_config = cls._validate_connection_config(configs, db_name)
|
218
220
|
|
@@ -221,26 +223,35 @@ class MySQLConfig(ConnectionConfig):
|
|
221
223
|
config = cls._create_config_from_url(db_config, local_host)
|
222
224
|
else:
|
223
225
|
config = cls._create_config_from_params(db_config, local_host)
|
224
|
-
|
226
|
+
|
227
|
+
# Parse write permissions
|
228
|
+
config.writable = db_config.get('writable', False)
|
229
|
+
if config.writable and 'write_permissions' in db_config:
|
230
|
+
config.write_permissions = WritePermissions(db_config['write_permissions'])
|
231
|
+
|
225
232
|
config.debug = cls.get_debug_mode()
|
226
233
|
return config
|
227
234
|
|
228
235
|
@classmethod
|
229
|
-
def from_url(cls, url: str, user: str, password: str,
|
230
|
-
local_host: Optional[str] = None
|
236
|
+
def from_url(cls, url: str, user: str, password: str,
|
237
|
+
local_host: Optional[str] = None,
|
238
|
+
writable: bool = False,
|
239
|
+
write_permissions: Optional[Dict[str, Any]] = None) -> 'MySQLConfig':
|
231
240
|
"""Create configuration from URL and credentials
|
232
|
-
|
241
|
+
|
233
242
|
Args:
|
234
243
|
url: URL (mysql://host:port/dbname)
|
235
244
|
user: Username for connection
|
236
245
|
password: Password for connection
|
237
246
|
local_host: Optional local host address
|
238
|
-
|
247
|
+
writable: Whether write operations are allowed
|
248
|
+
write_permissions: Write permissions configuration
|
249
|
+
|
239
250
|
Raises:
|
240
251
|
ValueError: If URL format is invalid or required parameters are missing
|
241
252
|
"""
|
242
253
|
params = parse_url(url)
|
243
|
-
|
254
|
+
|
244
255
|
config = cls(
|
245
256
|
database=params['database'],
|
246
257
|
user=user,
|
@@ -250,8 +261,14 @@ class MySQLConfig(ConnectionConfig):
|
|
250
261
|
charset=params['charset'],
|
251
262
|
local_host=local_host,
|
252
263
|
url=url,
|
253
|
-
ssl=params.get('ssl')
|
264
|
+
ssl=params.get('ssl'),
|
265
|
+
writable=writable
|
254
266
|
)
|
267
|
+
|
268
|
+
# Parse write permissions
|
269
|
+
if writable and write_permissions:
|
270
|
+
config.write_permissions = WritePermissions(write_permissions)
|
271
|
+
|
255
272
|
config.debug = cls.get_debug_mode()
|
256
273
|
return config
|
257
274
|
|
@@ -266,7 +283,7 @@ class MySQLConfig(ConnectionConfig):
|
|
266
283
|
'charset': self.charset,
|
267
284
|
'use_unicode': True
|
268
285
|
}
|
269
|
-
|
286
|
+
|
270
287
|
# Add SSL parameters if configured
|
271
288
|
if self.ssl:
|
272
289
|
params['ssl_mode'] = self.ssl.mode
|
@@ -276,7 +293,7 @@ class MySQLConfig(ConnectionConfig):
|
|
276
293
|
params['ssl_cert'] = self.ssl.cert
|
277
294
|
if self.ssl.key:
|
278
295
|
params['ssl_key'] = self.ssl.key
|
279
|
-
|
296
|
+
|
280
297
|
return {k: v for k, v in params.items() if v is not None}
|
281
298
|
|
282
299
|
def get_masked_connection_info(self) -> Dict[str, Any]:
|
mcp_dbutils/mysql/handler.py
CHANGED
@@ -181,6 +181,66 @@ class MySQLHandler(ConnectionHandler):
|
|
181
181
|
if conn:
|
182
182
|
conn.close()
|
183
183
|
|
184
|
+
async def _execute_write_query(self, sql: str) -> str:
|
185
|
+
"""Execute SQL write query
|
186
|
+
|
187
|
+
Args:
|
188
|
+
sql: SQL write query (INSERT, UPDATE, DELETE)
|
189
|
+
|
190
|
+
Returns:
|
191
|
+
str: Execution result
|
192
|
+
|
193
|
+
Raises:
|
194
|
+
ConnectionHandlerError: If query execution fails
|
195
|
+
"""
|
196
|
+
conn = None
|
197
|
+
try:
|
198
|
+
# Check if the query is a write operation
|
199
|
+
sql_upper = sql.strip().upper()
|
200
|
+
is_insert = sql_upper.startswith("INSERT")
|
201
|
+
is_update = sql_upper.startswith("UPDATE")
|
202
|
+
is_delete = sql_upper.startswith("DELETE")
|
203
|
+
is_transaction = sql_upper.startswith(("BEGIN", "COMMIT", "ROLLBACK", "START TRANSACTION"))
|
204
|
+
|
205
|
+
if not (is_insert or is_update or is_delete or is_transaction):
|
206
|
+
raise ConnectionHandlerError("Only INSERT, UPDATE, DELETE, and transaction statements are allowed for write operations")
|
207
|
+
|
208
|
+
conn_params = self.config.get_connection_params()
|
209
|
+
conn = mysql.connector.connect(**conn_params)
|
210
|
+
self.log("debug", f"Executing write operation: {sql}")
|
211
|
+
|
212
|
+
with conn.cursor() as cur:
|
213
|
+
try:
|
214
|
+
# Execute the write operation
|
215
|
+
cur.execute(sql)
|
216
|
+
|
217
|
+
# Get number of affected rows
|
218
|
+
affected_rows = cur.rowcount
|
219
|
+
|
220
|
+
# Commit the transaction if not in a transaction block
|
221
|
+
if not is_transaction:
|
222
|
+
conn.commit()
|
223
|
+
|
224
|
+
self.log("debug", f"Write operation executed successfully, affected {affected_rows} rows")
|
225
|
+
|
226
|
+
# Return result
|
227
|
+
if is_transaction:
|
228
|
+
return f"Transaction operation executed successfully"
|
229
|
+
else:
|
230
|
+
return f"Write operation executed successfully. {affected_rows} row{'s' if affected_rows != 1 else ''} affected."
|
231
|
+
except mysql.connector.Error as e:
|
232
|
+
# Rollback on error
|
233
|
+
if not is_transaction:
|
234
|
+
conn.rollback()
|
235
|
+
self.log("error", f"Write operation error: {str(e)}")
|
236
|
+
raise ConnectionHandlerError(str(e))
|
237
|
+
except mysql.connector.Error as e:
|
238
|
+
error_msg = f"[{self.db_type}] Write operation failed: {str(e)}"
|
239
|
+
raise ConnectionHandlerError(error_msg)
|
240
|
+
finally:
|
241
|
+
if conn:
|
242
|
+
conn.close()
|
243
|
+
|
184
244
|
async def get_table_description(self, table_name: str) -> str:
|
185
245
|
"""Get detailed table description"""
|
186
246
|
conn = None
|
mcp_dbutils/postgres/config.py
CHANGED
@@ -3,7 +3,7 @@ from dataclasses import dataclass
|
|
3
3
|
from typing import Any, Dict, Literal, Optional
|
4
4
|
from urllib.parse import parse_qs, urlparse
|
5
5
|
|
6
|
-
from ..config import ConnectionConfig
|
6
|
+
from ..config import ConnectionConfig, WritePermissions
|
7
7
|
|
8
8
|
|
9
9
|
@dataclass
|
@@ -16,29 +16,29 @@ class SSLConfig:
|
|
16
16
|
|
17
17
|
def parse_url(url: str) -> Dict[str, Any]:
|
18
18
|
"""Parse PostgreSQL URL into connection parameters
|
19
|
-
|
19
|
+
|
20
20
|
Args:
|
21
21
|
url: URL (e.g. postgresql://host:port/dbname?sslmode=verify-full)
|
22
|
-
|
22
|
+
|
23
23
|
Returns:
|
24
24
|
Dictionary of connection parameters including SSL settings
|
25
25
|
"""
|
26
26
|
if not url.startswith('postgresql://'):
|
27
27
|
raise ValueError("Invalid PostgreSQL URL format")
|
28
|
-
|
28
|
+
|
29
29
|
if '@' in url:
|
30
30
|
raise ValueError("URL should not contain credentials. Please provide username and password separately.")
|
31
|
-
|
31
|
+
|
32
32
|
# Parse URL and query parameters
|
33
33
|
parsed = urlparse(url)
|
34
34
|
query_params = parse_qs(parsed.query)
|
35
|
-
|
35
|
+
|
36
36
|
params = {
|
37
37
|
'host': parsed.hostname or 'localhost',
|
38
38
|
'port': str(parsed.port or 5432),
|
39
39
|
'dbname': parsed.path.lstrip('/') if parsed.path else '',
|
40
40
|
}
|
41
|
-
|
41
|
+
|
42
42
|
if not params['dbname']:
|
43
43
|
raise ValueError("PostgreSQL database name must be specified in URL")
|
44
44
|
|
@@ -49,17 +49,17 @@ def parse_url(url: str) -> Dict[str, Any]:
|
|
49
49
|
if mode not in ['disable', 'require', 'verify-ca', 'verify-full']:
|
50
50
|
raise ValueError(f"Invalid sslmode: {mode}")
|
51
51
|
ssl_params['mode'] = mode
|
52
|
-
|
52
|
+
|
53
53
|
if 'sslcert' in query_params:
|
54
54
|
ssl_params['cert'] = query_params['sslcert'][0]
|
55
55
|
if 'sslkey' in query_params:
|
56
56
|
ssl_params['key'] = query_params['sslkey'][0]
|
57
57
|
if 'sslrootcert' in query_params:
|
58
58
|
ssl_params['root'] = query_params['sslrootcert'][0]
|
59
|
-
|
59
|
+
|
60
60
|
if ssl_params:
|
61
61
|
params['ssl'] = SSLConfig(**ssl_params)
|
62
|
-
|
62
|
+
|
63
63
|
return params
|
64
64
|
|
65
65
|
@dataclass
|
@@ -73,6 +73,8 @@ class PostgreSQLConfig(ConnectionConfig):
|
|
73
73
|
type: Literal['postgres'] = 'postgres'
|
74
74
|
url: Optional[str] = None
|
75
75
|
ssl: Optional[SSLConfig] = None
|
76
|
+
writable: bool = False # Whether write operations are allowed
|
77
|
+
write_permissions: Optional[WritePermissions] = None # Write permissions configuration
|
76
78
|
|
77
79
|
@classmethod
|
78
80
|
def from_yaml(cls, yaml_path: str, db_name: str, local_host: Optional[str] = None) -> 'PostgreSQLConfig':
|
@@ -123,24 +125,24 @@ class PostgreSQLConfig(ConnectionConfig):
|
|
123
125
|
raise ValueError("Host must be specified in connection configuration")
|
124
126
|
if not db_config.get('port'):
|
125
127
|
raise ValueError("Port must be specified in connection configuration")
|
126
|
-
|
128
|
+
|
127
129
|
# Parse SSL configuration if present
|
128
130
|
ssl_config = None
|
129
131
|
if 'ssl' in db_config:
|
130
132
|
ssl_params = db_config['ssl']
|
131
133
|
if not isinstance(ssl_params, dict):
|
132
134
|
raise ValueError("SSL configuration must be a dictionary")
|
133
|
-
|
135
|
+
|
134
136
|
if ssl_params.get('mode') not in [None, 'disable', 'require', 'verify-ca', 'verify-full']:
|
135
137
|
raise ValueError(f"Invalid sslmode: {ssl_params.get('mode')}")
|
136
|
-
|
138
|
+
|
137
139
|
ssl_config = SSLConfig(
|
138
140
|
mode=ssl_params.get('mode', 'disable'),
|
139
141
|
cert=ssl_params.get('cert'),
|
140
142
|
key=ssl_params.get('key'),
|
141
143
|
root=ssl_params.get('root')
|
142
144
|
)
|
143
|
-
|
145
|
+
|
144
146
|
config = cls(
|
145
147
|
dbname=db_config['dbname'],
|
146
148
|
user=db_config['user'],
|
@@ -150,25 +152,35 @@ class PostgreSQLConfig(ConnectionConfig):
|
|
150
152
|
local_host=local_host,
|
151
153
|
ssl=ssl_config
|
152
154
|
)
|
155
|
+
|
156
|
+
# Parse write permissions
|
157
|
+
config.writable = db_config.get('writable', False)
|
158
|
+
if config.writable and 'write_permissions' in db_config:
|
159
|
+
config.write_permissions = WritePermissions(db_config['write_permissions'])
|
160
|
+
|
153
161
|
config.debug = cls.get_debug_mode()
|
154
162
|
return config
|
155
163
|
|
156
164
|
@classmethod
|
157
|
-
def from_url(cls, url: str, user: str, password: str,
|
158
|
-
local_host: Optional[str] = None
|
165
|
+
def from_url(cls, url: str, user: str, password: str,
|
166
|
+
local_host: Optional[str] = None,
|
167
|
+
writable: bool = False,
|
168
|
+
write_permissions: Optional[Dict[str, Any]] = None) -> 'PostgreSQLConfig':
|
159
169
|
"""Create configuration from URL and credentials
|
160
|
-
|
170
|
+
|
161
171
|
Args:
|
162
172
|
url: URL (postgresql://host:port/dbname)
|
163
173
|
user: Username for connection
|
164
174
|
password: Password for connection
|
165
175
|
local_host: Optional local host address
|
166
|
-
|
176
|
+
writable: Whether write operations are allowed
|
177
|
+
write_permissions: Write permissions configuration
|
178
|
+
|
167
179
|
Raises:
|
168
180
|
ValueError: If URL format is invalid or required parameters are missing
|
169
181
|
"""
|
170
182
|
params = parse_url(url)
|
171
|
-
|
183
|
+
|
172
184
|
config = cls(
|
173
185
|
dbname=params['dbname'],
|
174
186
|
user=user,
|
@@ -177,8 +189,14 @@ class PostgreSQLConfig(ConnectionConfig):
|
|
177
189
|
port=params['port'],
|
178
190
|
local_host=local_host,
|
179
191
|
url=url,
|
180
|
-
ssl=params.get('ssl')
|
192
|
+
ssl=params.get('ssl'),
|
193
|
+
writable=writable
|
181
194
|
)
|
195
|
+
|
196
|
+
# Parse write permissions
|
197
|
+
if writable and write_permissions:
|
198
|
+
config.write_permissions = WritePermissions(write_permissions)
|
199
|
+
|
182
200
|
config.debug = cls.get_debug_mode()
|
183
201
|
return config
|
184
202
|
|
@@ -191,7 +209,7 @@ class PostgreSQLConfig(ConnectionConfig):
|
|
191
209
|
'host': self.local_host or self.host,
|
192
210
|
'port': self.port
|
193
211
|
}
|
194
|
-
|
212
|
+
|
195
213
|
# Add SSL parameters if configured
|
196
214
|
if self.ssl:
|
197
215
|
params['sslmode'] = self.ssl.mode
|
@@ -201,7 +219,7 @@ class PostgreSQLConfig(ConnectionConfig):
|
|
201
219
|
params['sslkey'] = self.ssl.key
|
202
220
|
if self.ssl.root:
|
203
221
|
params['sslrootcert'] = self.ssl.root
|
204
|
-
|
222
|
+
|
205
223
|
return {k: v for k, v in params.items() if v}
|
206
224
|
|
207
225
|
def get_masked_connection_info(self) -> Dict[str, Any]:
|
mcp_dbutils/postgres/handler.py
CHANGED
@@ -154,6 +154,66 @@ class PostgreSQLHandler(ConnectionHandler):
|
|
154
154
|
if conn:
|
155
155
|
conn.close()
|
156
156
|
|
157
|
+
async def _execute_write_query(self, sql: str) -> str:
|
158
|
+
"""Execute SQL write query
|
159
|
+
|
160
|
+
Args:
|
161
|
+
sql: SQL write query (INSERT, UPDATE, DELETE)
|
162
|
+
|
163
|
+
Returns:
|
164
|
+
str: Execution result
|
165
|
+
|
166
|
+
Raises:
|
167
|
+
ConnectionHandlerError: If query execution fails
|
168
|
+
"""
|
169
|
+
conn = None
|
170
|
+
try:
|
171
|
+
# Check if the query is a write operation
|
172
|
+
sql_upper = sql.strip().upper()
|
173
|
+
is_insert = sql_upper.startswith("INSERT")
|
174
|
+
is_update = sql_upper.startswith("UPDATE")
|
175
|
+
is_delete = sql_upper.startswith("DELETE")
|
176
|
+
is_transaction = sql_upper.startswith(("BEGIN", "COMMIT", "ROLLBACK"))
|
177
|
+
|
178
|
+
if not (is_insert or is_update or is_delete or is_transaction):
|
179
|
+
raise ConnectionHandlerError("Only INSERT, UPDATE, DELETE, and transaction statements are allowed for write operations")
|
180
|
+
|
181
|
+
conn_params = self.config.get_connection_params()
|
182
|
+
conn = psycopg2.connect(**conn_params)
|
183
|
+
self.log("debug", f"Executing write operation: {sql}")
|
184
|
+
|
185
|
+
with conn.cursor() as cur:
|
186
|
+
try:
|
187
|
+
# Execute the write operation
|
188
|
+
cur.execute(sql)
|
189
|
+
|
190
|
+
# Get number of affected rows
|
191
|
+
affected_rows = cur.rowcount
|
192
|
+
|
193
|
+
# Commit the transaction if not in a transaction block
|
194
|
+
if not is_transaction:
|
195
|
+
conn.commit()
|
196
|
+
|
197
|
+
self.log("debug", f"Write operation executed successfully, affected {affected_rows} rows")
|
198
|
+
|
199
|
+
# Return result
|
200
|
+
if is_transaction:
|
201
|
+
return f"Transaction operation executed successfully"
|
202
|
+
else:
|
203
|
+
return f"Write operation executed successfully. {affected_rows} row{'s' if affected_rows != 1 else ''} affected."
|
204
|
+
except psycopg2.Error as e:
|
205
|
+
# Rollback on error
|
206
|
+
if not is_transaction:
|
207
|
+
conn.rollback()
|
208
|
+
self.log("error", f"Write operation error: [Code: {e.pgcode}] {e.pgerror or str(e)}")
|
209
|
+
raise ConnectionHandlerError(f"[Code: {e.pgcode}] {e.pgerror or str(e)}")
|
210
|
+
except psycopg2.Error as e:
|
211
|
+
error_msg = f"[{self.db_type}] Write operation failed: [Code: {e.pgcode}] {e.pgerror or str(e)}"
|
212
|
+
raise ConnectionHandlerError(error_msg)
|
213
|
+
finally:
|
214
|
+
if conn:
|
215
|
+
conn.close()
|
216
|
+
|
157
217
|
async def get_table_description(self, table_name: str) -> str:
|
158
218
|
"""Get detailed table description"""
|
159
219
|
conn = None
|
mcp_dbutils/sqlite/config.py
CHANGED
@@ -5,7 +5,7 @@ from pathlib import Path
|
|
5
5
|
from typing import Any, Dict, Literal, Optional
|
6
6
|
from urllib.parse import parse_qs, urlparse
|
7
7
|
|
8
|
-
from ..config import ConnectionConfig
|
8
|
+
from ..config import ConnectionConfig, WritePermissions
|
9
9
|
|
10
10
|
|
11
11
|
def parse_jdbc_url(jdbc_url: str) -> Dict[str, str]:
|
@@ -55,6 +55,8 @@ class SQLiteConfig(ConnectionConfig):
|
|
55
55
|
password: Optional[str] = None
|
56
56
|
uri: bool = True # Enable URI mode to support parameters like password
|
57
57
|
type: Literal['sqlite'] = 'sqlite'
|
58
|
+
writable: bool = False # Whether write operations are allowed
|
59
|
+
write_permissions: Optional[WritePermissions] = None # Write permissions configuration
|
58
60
|
|
59
61
|
@classmethod
|
60
62
|
def from_jdbc_url(cls, jdbc_url: str, password: Optional[str] = None) -> 'SQLiteConfig':
|
@@ -147,5 +149,10 @@ class SQLiteConfig(ConnectionConfig):
|
|
147
149
|
uri=True
|
148
150
|
)
|
149
151
|
|
152
|
+
# Parse write permissions
|
153
|
+
config.writable = db_config.get('writable', False)
|
154
|
+
if config.writable and 'write_permissions' in db_config:
|
155
|
+
config.write_permissions = WritePermissions(db_config['write_permissions'])
|
156
|
+
|
150
157
|
config.debug = cls.get_debug_mode()
|
151
158
|
return config
|
mcp_dbutils/sqlite/handler.py
CHANGED
@@ -128,6 +128,59 @@ class SQLiteHandler(ConnectionHandler):
|
|
128
128
|
error_msg = f"[{self.db_type}] Query execution failed: {str(e)}"
|
129
129
|
raise ConnectionHandlerError(error_msg)
|
130
130
|
|
131
|
+
async def _execute_write_query(self, sql: str) -> str:
|
132
|
+
"""Execute SQL write query
|
133
|
+
|
134
|
+
Args:
|
135
|
+
sql: SQL write query (INSERT, UPDATE, DELETE)
|
136
|
+
|
137
|
+
Returns:
|
138
|
+
str: Execution result
|
139
|
+
|
140
|
+
Raises:
|
141
|
+
ConnectionHandlerError: If query execution fails
|
142
|
+
"""
|
143
|
+
try:
|
144
|
+
# Check if the query is a write operation
|
145
|
+
sql_upper = sql.strip().upper()
|
146
|
+
is_insert = sql_upper.startswith("INSERT")
|
147
|
+
is_update = sql_upper.startswith("UPDATE")
|
148
|
+
is_delete = sql_upper.startswith("DELETE")
|
149
|
+
is_transaction = sql_upper.startswith(("BEGIN", "COMMIT", "ROLLBACK"))
|
150
|
+
|
151
|
+
if not (is_insert or is_update or is_delete or is_transaction):
|
152
|
+
raise ConnectionHandlerError("Only INSERT, UPDATE, DELETE, and transaction statements are allowed for write operations")
|
153
|
+
|
154
|
+
conn = sqlite3.connect(self.config.path)
|
155
|
+
cur = conn.cursor()
|
156
|
+
|
157
|
+
try:
|
158
|
+
start_time = time.time()
|
159
|
+
cur.execute(sql)
|
160
|
+
conn.commit()
|
161
|
+
end_time = time.time()
|
162
|
+
elapsed_ms = (end_time - start_time) * 1000
|
163
|
+
|
164
|
+
# Get number of affected rows
|
165
|
+
affected_rows = cur.rowcount
|
166
|
+
|
167
|
+
self.log("debug", f"Write operation executed in {elapsed_ms:.2f}ms, affected {affected_rows} rows")
|
168
|
+
|
169
|
+
# Return result
|
170
|
+
if is_transaction:
|
171
|
+
return f"Transaction operation executed successfully"
|
172
|
+
else:
|
173
|
+
return f"Write operation executed successfully. {affected_rows} row{'s' if affected_rows != 1 else ''} affected."
|
174
|
+
except sqlite3.Error as e:
|
175
|
+
self.log("error", f"Write operation error: {str(e)}")
|
176
|
+
raise ConnectionHandlerError(str(e))
|
177
|
+
finally:
|
178
|
+
cur.close()
|
179
|
+
conn.close()
|
180
|
+
except sqlite3.Error as e:
|
181
|
+
error_msg = f"[{self.db_type}] Write operation failed: {str(e)}"
|
182
|
+
raise ConnectionHandlerError(error_msg)
|
183
|
+
|
131
184
|
async def get_table_description(self, table_name: str) -> str:
|
132
185
|
"""Get detailed table description"""
|
133
186
|
try:
|