mcp-dbutils 0.2.3__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.
@@ -0,0 +1,205 @@
1
+ """PostgreSQL MCP server implementation"""
2
+ import psycopg2
3
+ from psycopg2.pool import SimpleConnectionPool
4
+ from typing import Optional, List
5
+ import mcp.types as types
6
+ from ..base import DatabaseServer
7
+ from ..log import create_logger
8
+ from .config import PostgresConfig
9
+ class PostgresServer(DatabaseServer):
10
+ def __init__(self, config: PostgresConfig, config_path: Optional[str] = None):
11
+ """初始化PostgreSQL服务器
12
+ Args:
13
+ config: 数据库配置
14
+ config_path: 配置文件路径(可选)
15
+ """
16
+ super().__init__("postgres-server", config.debug)
17
+ self.config = config
18
+ self.config_path = config_path
19
+ self.log = create_logger("postgres", config.debug)
20
+ # 创建连接池
21
+ try:
22
+ conn_params = config.get_connection_params()
23
+ masked_params = config.get_masked_connection_info()
24
+ self.log("debug", f"正在连接数据库,参数: {masked_params}")
25
+ # 测试连接
26
+ test_conn = psycopg2.connect(**conn_params)
27
+ test_conn.close()
28
+ self.log("info", "测试连接成功")
29
+ # 创建连接池
30
+ self.pool = SimpleConnectionPool(1, 5, **conn_params)
31
+ self.log("info", "数据库连接池创建成功")
32
+ except psycopg2.Error as e:
33
+ self.log("error", f"数据库连接失败: [Code: {e.pgcode}] {e.pgerror or str(e)}")
34
+ raise
35
+ async def list_resources(self) -> list[types.Resource]:
36
+ """列出所有表资源"""
37
+ try:
38
+ conn = self.pool.getconn()
39
+ with conn.cursor() as cur:
40
+ cur.execute("""
41
+ SELECT
42
+ table_name,
43
+ obj_description(
44
+ (quote_ident(table_schema) || '.' || quote_ident(table_name))::regclass,
45
+ 'pg_class'
46
+ ) as description
47
+ FROM information_schema.tables
48
+ WHERE table_schema = 'public'
49
+ """)
50
+ tables = cur.fetchall()
51
+ return [
52
+ types.Resource(
53
+ uri=f"postgres://{self.config.host}/{table[0]}/schema",
54
+ name=f"{table[0]} schema",
55
+ description=table[1] if table[1] else None,
56
+ mimeType="application/json"
57
+ ) for table in tables
58
+ ]
59
+ except psycopg2.Error as e:
60
+ error_msg = f"获取表列表失败: [Code: {e.pgcode}] {e.pgerror or str(e)}"
61
+ self.log("error", error_msg)
62
+ raise
63
+ finally:
64
+ self.pool.putconn(conn)
65
+ async def read_resource(self, uri: str) -> str:
66
+ """读取表结构信息"""
67
+ try:
68
+ table_name = uri.split('/')[-2]
69
+ conn = self.pool.getconn()
70
+ with conn.cursor() as cur:
71
+ # 获取列信息
72
+ cur.execute("""
73
+ SELECT
74
+ column_name,
75
+ data_type,
76
+ is_nullable,
77
+ col_description(
78
+ (quote_ident(table_schema) || '.' || quote_ident(table_name))::regclass,
79
+ ordinal_position
80
+ ) as description
81
+ FROM information_schema.columns
82
+ WHERE table_name = %s
83
+ ORDER BY ordinal_position
84
+ """, (table_name,))
85
+ columns = cur.fetchall()
86
+ # 获取约束信息
87
+ cur.execute("""
88
+ SELECT
89
+ conname as constraint_name,
90
+ contype as constraint_type
91
+ FROM pg_constraint c
92
+ JOIN pg_class t ON c.conrelid = t.oid
93
+ WHERE t.relname = %s
94
+ """, (table_name,))
95
+ constraints = cur.fetchall()
96
+ return str({
97
+ 'columns': [{
98
+ 'name': col[0],
99
+ 'type': col[1],
100
+ 'nullable': col[2] == 'YES',
101
+ 'description': col[3]
102
+ } for col in columns],
103
+ 'constraints': [{
104
+ 'name': con[0],
105
+ 'type': con[1]
106
+ } for con in constraints]
107
+ })
108
+ except psycopg2.Error as e:
109
+ error_msg = f"读取表结构失败: [Code: {e.pgcode}] {e.pgerror or str(e)}"
110
+ self.log("error", error_msg)
111
+ raise
112
+ finally:
113
+ self.pool.putconn(conn)
114
+ def get_tools(self) -> list[types.Tool]:
115
+ """获取可用工具列表"""
116
+ return [
117
+ types.Tool(
118
+ name="query",
119
+ description="执行只读SQL查询",
120
+ inputSchema={
121
+ "type": "object",
122
+ "properties": {
123
+ "database": {
124
+ "type": "string",
125
+ "description": "数据库配置名称(可选)"
126
+ },
127
+ "sql": {
128
+ "type": "string",
129
+ "description": "SQL查询语句(仅支持SELECT)"
130
+ }
131
+ },
132
+ "required": ["sql"]
133
+ }
134
+ )
135
+ ]
136
+ async def call_tool(self, name: str, arguments: dict) -> list[types.TextContent]:
137
+ """执行工具调用"""
138
+ if name != "query":
139
+ raise ValueError(f"未知工具: {name}")
140
+ sql = arguments.get("sql", "").strip()
141
+ if not sql:
142
+ raise ValueError("SQL查询不能为空")
143
+ # 仅允许SELECT语句
144
+ if not sql.lower().startswith("select"):
145
+ raise ValueError("仅支持SELECT查询")
146
+ database = arguments.get("database")
147
+ use_pool = True
148
+ conn = None
149
+ try:
150
+ if database and self.config_path:
151
+ # 使用指定的数据库配置
152
+ config = PostgresConfig.from_yaml(self.config_path, database)
153
+ conn_params = config.get_connection_params()
154
+ masked_params = config.get_masked_connection_info()
155
+ self.log("info", f"使用配置 {database} 连接数据库: {masked_params}")
156
+ conn = psycopg2.connect(**conn_params)
157
+ use_pool = False
158
+ else:
159
+ # 使用现有连接池
160
+ conn = self.pool.getconn()
161
+ self.log("info", f"执行查询: {sql}")
162
+ with conn.cursor() as cur:
163
+ # 启动只读事务
164
+ cur.execute("BEGIN TRANSACTION READ ONLY")
165
+ try:
166
+ cur.execute(sql)
167
+ results = cur.fetchall()
168
+ columns = [desc[0] for desc in cur.description]
169
+ formatted_results = [dict(zip(columns, row)) for row in results]
170
+ result_text = str({
171
+ 'type': 'postgres',
172
+ 'config_name': database or 'default',
173
+ 'query_result': {
174
+ 'columns': columns,
175
+ 'rows': formatted_results,
176
+ 'row_count': len(results)
177
+ }
178
+ })
179
+ self.log("info", f"查询完成,返回{len(results)}行结果")
180
+ return [types.TextContent(type="text", text=result_text)]
181
+ finally:
182
+ cur.execute("ROLLBACK")
183
+ except Exception as e:
184
+ if isinstance(e, psycopg2.Error):
185
+ error = f"查询执行失败: [Code: {e.pgcode}] {e.pgerror or str(e)}"
186
+ else:
187
+ error = f"查询执行失败: {str(e)}"
188
+ error_msg = str({
189
+ 'type': 'postgres',
190
+ 'config_name': database or 'default',
191
+ 'error': error
192
+ })
193
+ self.log("error", error_msg)
194
+ return [types.TextContent(type="text", text=error_msg)]
195
+ finally:
196
+ if conn:
197
+ if use_pool:
198
+ self.pool.putconn(conn)
199
+ else:
200
+ conn.close()
201
+ async def cleanup(self):
202
+ """清理资源"""
203
+ if hasattr(self, 'pool'):
204
+ self.log("info", "关闭数据库连接池")
205
+ self.pool.closeall()
@@ -0,0 +1,6 @@
1
+ """SQLite module"""
2
+
3
+ from .handler import SqliteHandler
4
+ from .config import SqliteConfig
5
+
6
+ __all__ = ['SqliteHandler', 'SqliteConfig']
@@ -0,0 +1,74 @@
1
+ """SQLite configuration module"""
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Dict, Any, Optional, Literal
6
+ from ..config import DatabaseConfig
7
+
8
+ @dataclass
9
+ class SqliteConfig(DatabaseConfig):
10
+ path: str
11
+ password: Optional[str] = None
12
+ uri: bool = True # Enable URI mode to support parameters like password
13
+ type: Literal['sqlite'] = 'sqlite'
14
+
15
+ @property
16
+ def absolute_path(self) -> str:
17
+ """Return absolute path to database file"""
18
+ return str(Path(self.path).expanduser().resolve())
19
+
20
+ def get_connection_params(self) -> Dict[str, Any]:
21
+ """Get sqlite3 connection parameters"""
22
+ if not self.password:
23
+ return {'database': self.absolute_path, 'uri': self.uri}
24
+
25
+ # Use URI format if password is provided
26
+ uri = f"file:{self.absolute_path}?mode=rw"
27
+ if self.password:
28
+ uri += f"&password={self.password}"
29
+
30
+ return {
31
+ 'database': uri,
32
+ 'uri': True
33
+ }
34
+
35
+ def get_masked_connection_info(self) -> Dict[str, Any]:
36
+ """Return connection information for logging"""
37
+ info = {
38
+ 'database': self.absolute_path,
39
+ 'uri': self.uri
40
+ }
41
+ if self.password:
42
+ info['password'] = '******'
43
+ return info
44
+
45
+ @classmethod
46
+ def from_yaml(cls, yaml_path: str, db_name: str, **kwargs) -> 'SqliteConfig':
47
+ """Create SQLite configuration from YAML
48
+
49
+ Args:
50
+ yaml_path: Path to YAML configuration file
51
+ db_name: Database configuration name
52
+ """
53
+ configs = cls.load_yaml_config(yaml_path)
54
+
55
+ if db_name not in configs:
56
+ available_dbs = list(configs.keys())
57
+ raise ValueError(f"Database configuration not found: {db_name}. Available configurations: {available_dbs}")
58
+
59
+ db_config = configs[db_name]
60
+
61
+ if 'type' not in db_config:
62
+ raise ValueError("Database configuration must include 'type' field")
63
+ if db_config['type'] != 'sqlite':
64
+ raise ValueError(f"Configuration is not SQLite type: {db_config['type']}")
65
+ if 'path' not in db_config:
66
+ raise ValueError("SQLite configuration must include 'path' field")
67
+
68
+ config = cls(
69
+ path=db_config['path'],
70
+ password=db_config.get('password'),
71
+ uri=True
72
+ )
73
+ config.debug = cls.get_debug_mode()
74
+ return config
@@ -0,0 +1,117 @@
1
+ """SQLite database handler implementation"""
2
+
3
+ import sqlite3
4
+ from pathlib import Path
5
+ from contextlib import closing
6
+ import mcp.types as types
7
+
8
+ from ..base import DatabaseHandler
9
+ from .config import SqliteConfig
10
+
11
+ class SqliteHandler(DatabaseHandler):
12
+ def __init__(self, config_path: str, database: str, debug: bool = False):
13
+ """Initialize SQLite handler
14
+
15
+ Args:
16
+ config_path: Path to configuration file
17
+ database: Database configuration name
18
+ debug: Enable debug mode
19
+ """
20
+ super().__init__(config_path, database, debug)
21
+ self.config = SqliteConfig.from_yaml(config_path, database)
22
+
23
+ # Ensure database directory exists
24
+ db_file = Path(self.config.absolute_path)
25
+ db_file.parent.mkdir(parents=True, exist_ok=True)
26
+
27
+ # No connection test during initialization
28
+ self.log("debug", f"Configuring database: {self.config.get_masked_connection_info()}")
29
+
30
+ def _get_connection(self):
31
+ """Get database connection"""
32
+ connection_params = self.config.get_connection_params()
33
+ conn = sqlite3.connect(**connection_params)
34
+ conn.row_factory = sqlite3.Row
35
+ return conn
36
+
37
+ async def get_tables(self) -> list[types.Resource]:
38
+ """Get all table resources"""
39
+ try:
40
+ with closing(self._get_connection()) as conn:
41
+ cursor = conn.execute(
42
+ "SELECT name FROM sqlite_master WHERE type='table'"
43
+ )
44
+ tables = cursor.fetchall()
45
+
46
+ return [
47
+ types.Resource(
48
+ uri=f"sqlite://{self.database}/{table[0]}/schema",
49
+ name=f"{table[0]} schema",
50
+ mimeType="application/json"
51
+ ) for table in tables
52
+ ]
53
+ except sqlite3.Error as e:
54
+ error_msg = f"Failed to get table list: {str(e)}"
55
+ self.log("error", error_msg)
56
+ raise
57
+
58
+ async def get_schema(self, table_name: str) -> str:
59
+ """Get table schema information"""
60
+ try:
61
+ with closing(self._get_connection()) as conn:
62
+ # Get table structure
63
+ cursor = conn.execute(f"PRAGMA table_info({table_name})")
64
+ columns = cursor.fetchall()
65
+
66
+ # Get index information
67
+ cursor = conn.execute(f"PRAGMA index_list({table_name})")
68
+ indexes = cursor.fetchall()
69
+
70
+ schema_info = {
71
+ 'columns': [{
72
+ 'name': col['name'],
73
+ 'type': col['type'],
74
+ 'nullable': not col['notnull'],
75
+ 'primary_key': bool(col['pk'])
76
+ } for col in columns],
77
+ 'indexes': [{
78
+ 'name': idx['name'],
79
+ 'unique': bool(idx['unique'])
80
+ } for idx in indexes]
81
+ }
82
+
83
+ return str(schema_info)
84
+ except sqlite3.Error as e:
85
+ error_msg = f"Failed to read table schema: {str(e)}"
86
+ self.log("error", error_msg)
87
+ raise
88
+
89
+ async def execute_query(self, sql: str) -> str:
90
+ """Execute SQL query"""
91
+ try:
92
+ with closing(self._get_connection()) as conn:
93
+ self.log("info", f"Executing query: {sql}")
94
+ cursor = conn.execute(sql)
95
+ results = cursor.fetchall()
96
+
97
+ columns = [desc[0] for desc in cursor.description]
98
+ formatted_results = [dict(zip(columns, row)) for row in results]
99
+
100
+ result_text = str({
101
+ 'columns': columns,
102
+ 'rows': formatted_results,
103
+ 'row_count': len(results)
104
+ })
105
+
106
+ self.log("info", f"Query completed, returned {len(results)} rows")
107
+ return result_text
108
+
109
+ except sqlite3.Error as e:
110
+ error_msg = f"Query execution failed: {str(e)}"
111
+ self.log("error", error_msg)
112
+ raise
113
+
114
+ async def cleanup(self):
115
+ """Cleanup resources"""
116
+ # No special cleanup needed for SQLite
117
+ pass
@@ -0,0 +1,202 @@
1
+ """SQLite MCP server implementation"""
2
+
3
+ import sqlite3
4
+ from pathlib import Path
5
+ from contextlib import closing
6
+ from typing import Optional, List
7
+ import mcp.types as types
8
+
9
+ from ..base import DatabaseServer
10
+ from ..log import create_logger
11
+ from .config import SqliteConfig
12
+
13
+ class SqliteServer(DatabaseServer):
14
+ def __init__(self, config: SqliteConfig, config_path: Optional[str] = None):
15
+ """初始化 SQLite 服务器
16
+
17
+ Args:
18
+ config: SQLite 配置
19
+ """
20
+ super().__init__("sqlite-server", config.debug)
21
+ self.config = config
22
+ self.config_path = config_path
23
+ self.log = create_logger("sqlite", config.debug)
24
+
25
+ # 确保数据库目录存在
26
+ db_file = Path(self.config.absolute_path)
27
+ db_file.parent.mkdir(parents=True, exist_ok=True)
28
+
29
+ # 测试连接
30
+ try:
31
+ self.log("debug", f"正在连接数据库: {self.config.get_masked_connection_info()}")
32
+ connection_params = self.config.get_connection_params()
33
+ with closing(sqlite3.connect(**connection_params)) as conn:
34
+ conn.row_factory = sqlite3.Row
35
+ self.log("info", "数据库连接测试成功")
36
+ except sqlite3.Error as e:
37
+ self.log("error", f"数据库连接失败: {str(e)}")
38
+ raise
39
+
40
+ def _get_connection(self):
41
+ """获取数据库连接"""
42
+ connection_params = self.config.get_connection_params()
43
+ conn = sqlite3.connect(**connection_params)
44
+ conn.row_factory = sqlite3.Row
45
+ return conn
46
+
47
+ async def list_resources(self) -> list[types.Resource]:
48
+ """列出所有表资源"""
49
+ use_default = True
50
+ conn = None
51
+ try:
52
+ database = arguments.get("database")
53
+ if database and self.config_path:
54
+ # 使用指定的数据库配置
55
+ config = SqliteConfig.from_yaml(self.config_path, database)
56
+ connection_params = config.get_connection_params()
57
+ masked_params = config.get_masked_connection_info()
58
+ self.log("info", f"使用配置 {database} 连接数据库: {masked_params}")
59
+ conn = sqlite3.connect(**connection_params)
60
+ conn.row_factory = sqlite3.Row
61
+ use_default = False
62
+ else:
63
+ # 使用默认连接
64
+ conn = self._get_connection()
65
+
66
+ with closing(conn) as connection:
67
+ cursor = conn.execute(
68
+ "SELECT name FROM sqlite_master WHERE type='table'"
69
+ )
70
+ tables = cursor.fetchall()
71
+
72
+ return [
73
+ types.Resource(
74
+ uri=f"sqlite://{table[0]}/schema",
75
+ name=f"{table[0]} schema",
76
+ mimeType="application/json"
77
+ ) for table in tables
78
+ ]
79
+ except sqlite3.Error as e:
80
+ error_msg = f"获取表列表失败: {str(e)}"
81
+ self.log("error", error_msg)
82
+ raise
83
+
84
+ async def read_resource(self, uri: str) -> str:
85
+ """读取表结构信息"""
86
+ try:
87
+ table_name = uri.split('/')[-2]
88
+ with closing(self._get_connection()) as conn:
89
+ # 获取表结构
90
+ cursor = conn.execute(f"PRAGMA table_info({table_name})")
91
+ columns = cursor.fetchall()
92
+
93
+ # 获取索引信息
94
+ cursor = conn.execute(f"PRAGMA index_list({table_name})")
95
+ indexes = cursor.fetchall()
96
+
97
+ schema_info = {
98
+ 'columns': [{
99
+ 'name': col['name'],
100
+ 'type': col['type'],
101
+ 'nullable': not col['notnull'],
102
+ 'primary_key': bool(col['pk'])
103
+ } for col in columns],
104
+ 'indexes': [{
105
+ 'name': idx['name'],
106
+ 'unique': bool(idx['unique'])
107
+ } for idx in indexes]
108
+ }
109
+
110
+ return str(schema_info)
111
+ except sqlite3.Error as e:
112
+ error_msg = f"读取表结构失败: {str(e)}"
113
+ self.log("error", error_msg)
114
+ raise
115
+
116
+ def get_tools(self) -> list[types.Tool]:
117
+ """获取可用工具列表"""
118
+ return [
119
+ types.Tool(
120
+ name="query",
121
+ description="执行只读SQL查询",
122
+ inputSchema={
123
+ "type": "object",
124
+ "properties": {
125
+ "database": {
126
+ "type": "string",
127
+ "description": "数据库配置名称(可选)"
128
+ },
129
+ "sql": {
130
+ "type": "string",
131
+ "description": "SQL查询语句(仅支持SELECT)"
132
+ }
133
+ },
134
+ "required": ["sql"]
135
+ }
136
+ )
137
+ ]
138
+
139
+ async def call_tool(self, name: str, arguments: dict) -> list[types.TextContent]:
140
+ """执行工具调用"""
141
+ if name != "query":
142
+ raise ValueError(f"未知工具: {name}")
143
+
144
+ sql = arguments.get("sql", "").strip()
145
+ if not sql:
146
+ raise ValueError("SQL查询不能为空")
147
+
148
+ # 仅允许SELECT语句
149
+ if not sql.lower().startswith("select"):
150
+ raise ValueError("仅支持SELECT查询")
151
+
152
+ use_default = True
153
+ conn = None
154
+ try:
155
+ database = arguments.get("database")
156
+ if database and self.config_path:
157
+ # 使用指定的数据库配置
158
+ config = SqliteConfig.from_yaml(self.config_path, database)
159
+ connection_params = config.get_connection_params()
160
+ masked_params = config.get_masked_connection_info()
161
+ self.log("info", f"使用配置 {database} 连接数据库: {masked_params}")
162
+ conn = sqlite3.connect(**connection_params)
163
+ conn.row_factory = sqlite3.Row
164
+ use_default = False
165
+ else:
166
+ # 使用默认连接
167
+ conn = self._get_connection()
168
+
169
+ with closing(conn) as connection:
170
+ self.log("info", f"执行查询: {sql}")
171
+ cursor = conn.execute(sql)
172
+ results = cursor.fetchall()
173
+
174
+ columns = [desc[0] for desc in cursor.description]
175
+ formatted_results = [dict(zip(columns, row)) for row in results]
176
+
177
+ result_text = str({
178
+ 'type': 'sqlite',
179
+ 'config_name': database or 'default',
180
+ 'query_result': {
181
+ 'columns': columns,
182
+ 'rows': formatted_results,
183
+ 'row_count': len(results)
184
+ }
185
+ })
186
+
187
+ self.log("info", f"查询完成,返回{len(results)}行结果")
188
+ return [types.TextContent(type="text", text=result_text)]
189
+
190
+ except sqlite3.Error as e:
191
+ error_msg = str({
192
+ 'type': 'sqlite',
193
+ 'config_name': database or 'default',
194
+ 'error': f"查询执行失败: {str(e)}"
195
+ })
196
+ self.log("error", error_msg)
197
+ return [types.TextContent(type="text", text=error_msg)]
198
+
199
+ async def cleanup(self):
200
+ """清理资源"""
201
+ # SQLite不需要特别的清理操作
202
+ pass