mcp-dbutils 0.3.0__tar.gz → 0.4.0__tar.gz
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-0.3.0 → mcp_dbutils-0.4.0}/CHANGELOG.md +14 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/PKG-INFO +1 -1
- mcp_dbutils-0.4.0/config.yaml.example +29 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/pyproject.toml +1 -1
- mcp_dbutils-0.4.0/src/mcp_dbutils/postgres/config.py +150 -0
- mcp_dbutils-0.4.0/tests/integration/test_postgres_config.py +129 -0
- mcp_dbutils-0.3.0/config.yaml.example +0 -18
- mcp_dbutils-0.3.0/src/mcp_dbutils/postgres/config.py +0 -66
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/.coveragerc +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/.github/workflows/release.yml +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/.github/workflows/test.yml +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/.gitignore +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/Dockerfile +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/LICENSE +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/README.md +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/README_CN.md +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/smithery.yaml +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/__init__.py +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/base.py +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/config.py +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/log.py +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/postgres/__init__.py +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/postgres/handler.py +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/postgres/server.py +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/sqlite/__init__.py +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/sqlite/config.py +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/sqlite/handler.py +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/sqlite/server.py +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/stats.py +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/tests/conftest.py +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/tests/integration/test_monitoring.py +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/tests/integration/test_postgres.py +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/tests/integration/test_prompts.py +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/tests/integration/test_sqlite.py +0 -0
- {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/tests/unit/test_stats.py +0 -0
@@ -1,6 +1,20 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
3
|
|
4
|
+
## v0.4.0 (2025-03-01)
|
5
|
+
|
6
|
+
### Features
|
7
|
+
|
8
|
+
- **config**: Add JDBC URL support for PostgreSQL
|
9
|
+
([#3](https://github.com/donghao1393/mcp-dbutils/pull/3),
|
10
|
+
[`4f148f3`](https://github.com/donghao1393/mcp-dbutils/commit/4f148f31d5dc623b8b39201f0270d8f523e65238))
|
11
|
+
|
12
|
+
- Add JDBC URL parsing with strict security measures - Require credentials to be provided separately
|
13
|
+
- Implement validation for all required parameters - Add comprehensive test coverage
|
14
|
+
|
15
|
+
Closes #2
|
16
|
+
|
17
|
+
|
4
18
|
## v0.3.0 (2025-02-16)
|
5
19
|
|
6
20
|
### Bug Fixes
|
@@ -0,0 +1,29 @@
|
|
1
|
+
databases:
|
2
|
+
# SQLite configuration examples
|
3
|
+
dev-db:
|
4
|
+
type: sqlite
|
5
|
+
path: /path/to/dev.db
|
6
|
+
password:
|
7
|
+
|
8
|
+
# PostgreSQL configuration examples
|
9
|
+
# Standard configuration
|
10
|
+
test-db:
|
11
|
+
type: postgres
|
12
|
+
host: postgres.example.com
|
13
|
+
port: 5432
|
14
|
+
dbname: test_db
|
15
|
+
user: test_user
|
16
|
+
password: test_pass
|
17
|
+
|
18
|
+
# JDBC URL configuration
|
19
|
+
# Note: When using JDBC URL in code, provide credentials separately:
|
20
|
+
# PostgresConfig.from_jdbc_url(
|
21
|
+
# "jdbc:postgresql://prod.example.com:5432/prod_db",
|
22
|
+
# user="prod_user",
|
23
|
+
# password="your@special#password"
|
24
|
+
# )
|
25
|
+
prod-db:
|
26
|
+
type: postgres
|
27
|
+
jdbc_url: jdbc:postgresql://postgres.example.com:5432/prod-db
|
28
|
+
user: prod_user
|
29
|
+
password: prod_pass
|
@@ -0,0 +1,150 @@
|
|
1
|
+
"""PostgreSQL configuration module"""
|
2
|
+
from dataclasses import dataclass
|
3
|
+
from typing import Optional, Dict, Any, Literal
|
4
|
+
from urllib.parse import urlparse
|
5
|
+
from ..config import DatabaseConfig
|
6
|
+
|
7
|
+
def parse_jdbc_url(jdbc_url: str) -> Dict[str, str]:
|
8
|
+
"""Parse JDBC URL into connection parameters
|
9
|
+
|
10
|
+
Args:
|
11
|
+
jdbc_url: JDBC URL (e.g. jdbc:postgresql://host:port/dbname)
|
12
|
+
|
13
|
+
Returns:
|
14
|
+
Dictionary of connection parameters
|
15
|
+
"""
|
16
|
+
if not jdbc_url.startswith('jdbc:postgresql://'):
|
17
|
+
raise ValueError("Invalid PostgreSQL JDBC URL format")
|
18
|
+
|
19
|
+
# Remove jdbc: prefix and ensure no credentials in URL
|
20
|
+
url = jdbc_url[5:]
|
21
|
+
if '@' in url:
|
22
|
+
raise ValueError("JDBC URL should not contain credentials. Please provide username and password separately.")
|
23
|
+
|
24
|
+
# Parse URL
|
25
|
+
parsed = urlparse(url)
|
26
|
+
|
27
|
+
params = {
|
28
|
+
'host': parsed.hostname or 'localhost',
|
29
|
+
'port': str(parsed.port or 5432),
|
30
|
+
'dbname': parsed.path.lstrip('/') if parsed.path else '',
|
31
|
+
}
|
32
|
+
|
33
|
+
if not params['dbname']:
|
34
|
+
raise ValueError("Database name must be specified in URL")
|
35
|
+
|
36
|
+
return params
|
37
|
+
|
38
|
+
@dataclass
|
39
|
+
class PostgresConfig(DatabaseConfig):
|
40
|
+
dbname: str
|
41
|
+
user: str
|
42
|
+
password: str
|
43
|
+
host: str = 'localhost'
|
44
|
+
port: str = '5432'
|
45
|
+
local_host: Optional[str] = None
|
46
|
+
type: Literal['postgres'] = 'postgres'
|
47
|
+
|
48
|
+
@classmethod
|
49
|
+
def from_yaml(cls, yaml_path: str, db_name: str, local_host: Optional[str] = None) -> 'PostgresConfig':
|
50
|
+
"""Create configuration from YAML file
|
51
|
+
|
52
|
+
Args:
|
53
|
+
yaml_path: Path to YAML configuration file
|
54
|
+
db_name: Database configuration name to use
|
55
|
+
local_host: Optional local host address
|
56
|
+
"""
|
57
|
+
configs = cls.load_yaml_config(yaml_path)
|
58
|
+
if not db_name:
|
59
|
+
raise ValueError("Database name must be specified")
|
60
|
+
if db_name not in configs:
|
61
|
+
available_dbs = list(configs.keys())
|
62
|
+
raise ValueError(f"Database configuration not found: {db_name}. Available configurations: {available_dbs}")
|
63
|
+
|
64
|
+
db_config = configs[db_name]
|
65
|
+
if 'type' not in db_config:
|
66
|
+
raise ValueError("Database configuration must include 'type' field")
|
67
|
+
if db_config['type'] != 'postgres':
|
68
|
+
raise ValueError(f"Configuration is not PostgreSQL type: {db_config['type']}")
|
69
|
+
|
70
|
+
# Check required credentials
|
71
|
+
if not db_config.get('user'):
|
72
|
+
raise ValueError("User must be specified in database configuration")
|
73
|
+
if not db_config.get('password'):
|
74
|
+
raise ValueError("Password must be specified in database configuration")
|
75
|
+
|
76
|
+
# Get connection parameters
|
77
|
+
if 'jdbc_url' in db_config:
|
78
|
+
# Parse JDBC URL for connection parameters
|
79
|
+
params = parse_jdbc_url(db_config['jdbc_url'])
|
80
|
+
config = cls(
|
81
|
+
dbname=params['dbname'],
|
82
|
+
user=db_config['user'],
|
83
|
+
password=db_config['password'],
|
84
|
+
host=params['host'],
|
85
|
+
port=params['port'],
|
86
|
+
local_host=local_host,
|
87
|
+
)
|
88
|
+
else:
|
89
|
+
if not db_config.get('dbname'):
|
90
|
+
raise ValueError("Database name must be specified in configuration")
|
91
|
+
if not db_config.get('host'):
|
92
|
+
raise ValueError("Host must be specified in configuration")
|
93
|
+
if not db_config.get('port'):
|
94
|
+
raise ValueError("Port must be specified in configuration")
|
95
|
+
config = cls(
|
96
|
+
dbname=db_config['dbname'],
|
97
|
+
user=db_config['user'],
|
98
|
+
password=db_config['password'],
|
99
|
+
host=db_config['host'],
|
100
|
+
port=str(db_config['port']),
|
101
|
+
local_host=local_host,
|
102
|
+
)
|
103
|
+
config.debug = cls.get_debug_mode()
|
104
|
+
return config
|
105
|
+
|
106
|
+
@classmethod
|
107
|
+
def from_jdbc_url(cls, jdbc_url: str, user: str, password: str,
|
108
|
+
local_host: Optional[str] = None) -> 'PostgresConfig':
|
109
|
+
"""Create configuration from JDBC URL and credentials
|
110
|
+
|
111
|
+
Args:
|
112
|
+
jdbc_url: JDBC URL (jdbc:postgresql://host:port/dbname)
|
113
|
+
user: Username for database connection
|
114
|
+
password: Password for database connection
|
115
|
+
local_host: Optional local host address
|
116
|
+
|
117
|
+
Raises:
|
118
|
+
ValueError: If URL format is invalid or required parameters are missing
|
119
|
+
"""
|
120
|
+
params = parse_jdbc_url(jdbc_url)
|
121
|
+
|
122
|
+
config = cls(
|
123
|
+
dbname=params['dbname'],
|
124
|
+
user=user,
|
125
|
+
password=password,
|
126
|
+
host=params['host'],
|
127
|
+
port=params['port'],
|
128
|
+
local_host=local_host,
|
129
|
+
)
|
130
|
+
config.debug = cls.get_debug_mode()
|
131
|
+
return config
|
132
|
+
|
133
|
+
def get_connection_params(self) -> Dict[str, Any]:
|
134
|
+
"""Get psycopg2 connection parameters"""
|
135
|
+
params = {
|
136
|
+
'dbname': self.dbname,
|
137
|
+
'user': self.user,
|
138
|
+
'password': self.password,
|
139
|
+
'host': self.local_host or self.host,
|
140
|
+
'port': self.port
|
141
|
+
}
|
142
|
+
return {k: v for k, v in params.items() if v}
|
143
|
+
|
144
|
+
def get_masked_connection_info(self) -> Dict[str, Any]:
|
145
|
+
"""Return masked connection information for logging"""
|
146
|
+
return {
|
147
|
+
'dbname': self.dbname,
|
148
|
+
'host': self.local_host or self.host,
|
149
|
+
'port': self.port
|
150
|
+
}
|
@@ -0,0 +1,129 @@
|
|
1
|
+
"""Test PostgreSQL configuration functionality"""
|
2
|
+
import pytest
|
3
|
+
import tempfile
|
4
|
+
import yaml
|
5
|
+
from mcp_dbutils.postgres.config import PostgresConfig, parse_jdbc_url
|
6
|
+
|
7
|
+
def test_parse_jdbc_url():
|
8
|
+
"""Test JDBC URL parsing"""
|
9
|
+
# Test valid URL
|
10
|
+
url = "jdbc:postgresql://localhost:5432/testdb"
|
11
|
+
params = parse_jdbc_url(url)
|
12
|
+
assert params["host"] == "localhost"
|
13
|
+
assert params["port"] == "5432"
|
14
|
+
assert params["dbname"] == "testdb"
|
15
|
+
|
16
|
+
# Test URL with credentials (should fail)
|
17
|
+
with pytest.raises(ValueError, match="should not contain credentials"):
|
18
|
+
parse_jdbc_url("jdbc:postgresql://user:pass@localhost:5432/testdb")
|
19
|
+
|
20
|
+
# Test invalid format
|
21
|
+
with pytest.raises(ValueError, match="Invalid PostgreSQL JDBC URL format"):
|
22
|
+
parse_jdbc_url("postgresql://localhost:5432/testdb")
|
23
|
+
|
24
|
+
# Test missing database name
|
25
|
+
with pytest.raises(ValueError, match="Database name must be specified"):
|
26
|
+
parse_jdbc_url("jdbc:postgresql://localhost:5432")
|
27
|
+
|
28
|
+
def test_from_jdbc_url():
|
29
|
+
"""Test PostgresConfig creation from JDBC URL"""
|
30
|
+
url = "jdbc:postgresql://localhost:5432/testdb"
|
31
|
+
config = PostgresConfig.from_jdbc_url(
|
32
|
+
url,
|
33
|
+
user="test_user",
|
34
|
+
password="test_pass"
|
35
|
+
)
|
36
|
+
|
37
|
+
assert config.dbname == "testdb"
|
38
|
+
assert config.host == "localhost"
|
39
|
+
assert config.port == "5432"
|
40
|
+
assert config.user == "test_user"
|
41
|
+
assert config.password == "test_pass"
|
42
|
+
assert config.type == "postgres"
|
43
|
+
|
44
|
+
def test_from_yaml_with_jdbc_url(tmp_path):
|
45
|
+
"""Test PostgresConfig creation from YAML with JDBC URL"""
|
46
|
+
config_data = {
|
47
|
+
"databases": {
|
48
|
+
"test_db": {
|
49
|
+
"type": "postgres",
|
50
|
+
"jdbc_url": "jdbc:postgresql://localhost:5432/testdb",
|
51
|
+
"user": "test_user",
|
52
|
+
"password": "test_pass"
|
53
|
+
}
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
config_file = tmp_path / "config.yaml"
|
58
|
+
with open(config_file, "w") as f:
|
59
|
+
yaml.dump(config_data, f)
|
60
|
+
|
61
|
+
config = PostgresConfig.from_yaml(str(config_file), "test_db")
|
62
|
+
assert config.dbname == "testdb"
|
63
|
+
assert config.host == "localhost"
|
64
|
+
assert config.port == "5432"
|
65
|
+
assert config.user == "test_user"
|
66
|
+
assert config.password == "test_pass"
|
67
|
+
assert config.type == "postgres"
|
68
|
+
|
69
|
+
def test_required_fields_validation(tmp_path):
|
70
|
+
"""Test validation of required configuration fields"""
|
71
|
+
# Missing user
|
72
|
+
config_data = {
|
73
|
+
"databases": {
|
74
|
+
"test_db": {
|
75
|
+
"type": "postgres",
|
76
|
+
"host": "localhost",
|
77
|
+
"port": 5432,
|
78
|
+
"dbname": "testdb",
|
79
|
+
"password": "test_pass"
|
80
|
+
}
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
config_file = tmp_path / "config.yaml"
|
85
|
+
with open(config_file, "w") as f:
|
86
|
+
yaml.dump(config_data, f)
|
87
|
+
|
88
|
+
with pytest.raises(ValueError, match="User must be specified"):
|
89
|
+
PostgresConfig.from_yaml(str(config_file), "test_db")
|
90
|
+
|
91
|
+
# Missing password
|
92
|
+
config_data["databases"]["test_db"]["user"] = "test_user"
|
93
|
+
del config_data["databases"]["test_db"]["password"]
|
94
|
+
|
95
|
+
with open(config_file, "w") as f:
|
96
|
+
yaml.dump(config_data, f)
|
97
|
+
|
98
|
+
with pytest.raises(ValueError, match="Password must be specified"):
|
99
|
+
PostgresConfig.from_yaml(str(config_file), "test_db")
|
100
|
+
|
101
|
+
# Missing host
|
102
|
+
config_data["databases"]["test_db"]["password"] = "test_pass"
|
103
|
+
del config_data["databases"]["test_db"]["host"]
|
104
|
+
|
105
|
+
with open(config_file, "w") as f:
|
106
|
+
yaml.dump(config_data, f)
|
107
|
+
|
108
|
+
with pytest.raises(ValueError, match="Host must be specified"):
|
109
|
+
PostgresConfig.from_yaml(str(config_file), "test_db")
|
110
|
+
|
111
|
+
# Missing port
|
112
|
+
config_data["databases"]["test_db"]["host"] = "localhost"
|
113
|
+
del config_data["databases"]["test_db"]["port"]
|
114
|
+
|
115
|
+
with open(config_file, "w") as f:
|
116
|
+
yaml.dump(config_data, f)
|
117
|
+
|
118
|
+
with pytest.raises(ValueError, match="Port must be specified"):
|
119
|
+
PostgresConfig.from_yaml(str(config_file), "test_db")
|
120
|
+
|
121
|
+
# Missing database name
|
122
|
+
config_data["databases"]["test_db"]["port"] = 5432
|
123
|
+
del config_data["databases"]["test_db"]["dbname"]
|
124
|
+
|
125
|
+
with open(config_file, "w") as f:
|
126
|
+
yaml.dump(config_data, f)
|
127
|
+
|
128
|
+
with pytest.raises(ValueError, match="Database name must be specified"):
|
129
|
+
PostgresConfig.from_yaml(str(config_file), "test_db")
|
@@ -1,18 +0,0 @@
|
|
1
|
-
databases:
|
2
|
-
dev-db:
|
3
|
-
type: sqlite
|
4
|
-
path: /path/to/dev.db
|
5
|
-
password:
|
6
|
-
|
7
|
-
test-db:
|
8
|
-
type: sqlite
|
9
|
-
path: /path/to/test.db
|
10
|
-
password: test_pass
|
11
|
-
|
12
|
-
prod-db:
|
13
|
-
type: postgres
|
14
|
-
host: prod.example.com
|
15
|
-
port: 5432
|
16
|
-
dbname: prod_db
|
17
|
-
user: prod_user
|
18
|
-
password: prod_pass
|
@@ -1,66 +0,0 @@
|
|
1
|
-
"""PostgreSQL configuration module"""
|
2
|
-
from dataclasses import dataclass
|
3
|
-
from typing import Optional, Dict, Any, Literal
|
4
|
-
from ..config import DatabaseConfig
|
5
|
-
|
6
|
-
@dataclass
|
7
|
-
class PostgresConfig(DatabaseConfig):
|
8
|
-
dbname: str
|
9
|
-
user: str
|
10
|
-
password: str
|
11
|
-
host: str = 'localhost'
|
12
|
-
port: str = '5432'
|
13
|
-
local_host: Optional[str] = None
|
14
|
-
type: Literal['postgres'] = 'postgres'
|
15
|
-
|
16
|
-
@classmethod
|
17
|
-
def from_yaml(cls, yaml_path: str, db_name: str, local_host: Optional[str] = None) -> 'PostgresConfig':
|
18
|
-
"""Create configuration from YAML file
|
19
|
-
|
20
|
-
Args:
|
21
|
-
yaml_path: Path to YAML configuration file
|
22
|
-
db_name: Database configuration name to use
|
23
|
-
local_host: Optional local host address
|
24
|
-
"""
|
25
|
-
configs = cls.load_yaml_config(yaml_path)
|
26
|
-
if not db_name:
|
27
|
-
raise ValueError("Database name must be specified")
|
28
|
-
if db_name not in configs:
|
29
|
-
available_dbs = list(configs.keys())
|
30
|
-
raise ValueError(f"Database configuration not found: {db_name}. Available configurations: {available_dbs}")
|
31
|
-
|
32
|
-
db_config = configs[db_name]
|
33
|
-
if 'type' not in db_config:
|
34
|
-
raise ValueError("Database configuration must include 'type' field")
|
35
|
-
if db_config['type'] != 'postgres':
|
36
|
-
raise ValueError(f"Configuration is not PostgreSQL type: {db_config['type']}")
|
37
|
-
|
38
|
-
config = cls(
|
39
|
-
dbname=db_config.get('dbname', ''),
|
40
|
-
user=db_config.get('user', ''),
|
41
|
-
password=db_config.get('password', ''),
|
42
|
-
host=db_config.get('host', 'localhost'),
|
43
|
-
port=str(db_config.get('port', 5432)),
|
44
|
-
local_host=local_host,
|
45
|
-
)
|
46
|
-
config.debug = cls.get_debug_mode()
|
47
|
-
return config
|
48
|
-
|
49
|
-
def get_connection_params(self) -> Dict[str, Any]:
|
50
|
-
"""Get psycopg2 connection parameters"""
|
51
|
-
params = {
|
52
|
-
'dbname': self.dbname,
|
53
|
-
'user': self.user,
|
54
|
-
'password': self.password,
|
55
|
-
'host': self.local_host or self.host,
|
56
|
-
'port': self.port
|
57
|
-
}
|
58
|
-
return {k: v for k, v in params.items() if v}
|
59
|
-
|
60
|
-
def get_masked_connection_info(self) -> Dict[str, Any]:
|
61
|
-
"""Return masked connection information for logging"""
|
62
|
-
return {
|
63
|
-
'dbname': self.dbname,
|
64
|
-
'host': self.local_host or self.host,
|
65
|
-
'port': self.port
|
66
|
-
}
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|