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.
- mcp_dbutils/__init__.py +62 -0
- mcp_dbutils/base.py +182 -0
- mcp_dbutils/config.py +59 -0
- mcp_dbutils/log.py +33 -0
- mcp_dbutils/postgres/__init__.py +6 -0
- mcp_dbutils/postgres/config.py +66 -0
- mcp_dbutils/postgres/handler.py +150 -0
- mcp_dbutils/postgres/server.py +205 -0
- mcp_dbutils/sqlite/__init__.py +6 -0
- mcp_dbutils/sqlite/config.py +74 -0
- mcp_dbutils/sqlite/handler.py +117 -0
- mcp_dbutils/sqlite/server.py +202 -0
- mcp_dbutils-0.2.3.dist-info/METADATA +218 -0
- mcp_dbutils-0.2.3.dist-info/RECORD +17 -0
- mcp_dbutils-0.2.3.dist-info/WHEEL +4 -0
- mcp_dbutils-0.2.3.dist-info/entry_points.txt +2 -0
- mcp_dbutils-0.2.3.dist-info/licenses/LICENSE +21 -0
@@ -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,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
|