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.
Files changed (35) hide show
  1. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/CHANGELOG.md +14 -0
  2. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/PKG-INFO +1 -1
  3. mcp_dbutils-0.4.0/config.yaml.example +29 -0
  4. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/pyproject.toml +1 -1
  5. mcp_dbutils-0.4.0/src/mcp_dbutils/postgres/config.py +150 -0
  6. mcp_dbutils-0.4.0/tests/integration/test_postgres_config.py +129 -0
  7. mcp_dbutils-0.3.0/config.yaml.example +0 -18
  8. mcp_dbutils-0.3.0/src/mcp_dbutils/postgres/config.py +0 -66
  9. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/.coveragerc +0 -0
  10. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/.github/workflows/release.yml +0 -0
  11. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/.github/workflows/test.yml +0 -0
  12. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/.gitignore +0 -0
  13. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/Dockerfile +0 -0
  14. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/LICENSE +0 -0
  15. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/README.md +0 -0
  16. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/README_CN.md +0 -0
  17. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/smithery.yaml +0 -0
  18. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/__init__.py +0 -0
  19. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/base.py +0 -0
  20. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/config.py +0 -0
  21. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/log.py +0 -0
  22. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/postgres/__init__.py +0 -0
  23. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/postgres/handler.py +0 -0
  24. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/postgres/server.py +0 -0
  25. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/sqlite/__init__.py +0 -0
  26. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/sqlite/config.py +0 -0
  27. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/sqlite/handler.py +0 -0
  28. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/sqlite/server.py +0 -0
  29. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/src/mcp_dbutils/stats.py +0 -0
  30. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/tests/conftest.py +0 -0
  31. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/tests/integration/test_monitoring.py +0 -0
  32. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/tests/integration/test_postgres.py +0 -0
  33. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/tests/integration/test_prompts.py +0 -0
  34. {mcp_dbutils-0.3.0 → mcp_dbutils-0.4.0}/tests/integration/test_sqlite.py +0 -0
  35. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-dbutils
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: MCP Database Utilities Service
5
5
  Author: Dong Hao
6
6
  License-Expression: MIT
@@ -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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcp-dbutils"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "MCP Database Utilities Service"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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