adbpg-mcp-server 1.0.0__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 @@
1
+ name = "adbpg-mcp-server"
@@ -0,0 +1,426 @@
1
+ import asyncio
2
+ import logging
3
+ import os
4
+ import sys
5
+ import psycopg
6
+ from psycopg import OperationalError as Error
7
+ from mcp.server import Server
8
+ from mcp.types import Resource, Tool, TextContent, ResourceTemplate
9
+ from pydantic import AnyUrl
10
+ from dotenv import load_dotenv
11
+ from mcp.server.stdio import stdio_server
12
+
13
+ # 配置日志,输出到标准错误
14
+ logging.basicConfig(
15
+ level=logging.DEBUG,
16
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
17
+ stream=sys.stderr
18
+ )
19
+ logger = logging.getLogger("adbpg-mcp-server")
20
+
21
+ # 加载环境变量
22
+ try:
23
+ load_dotenv()
24
+ logger.info("Environment variables loaded")
25
+
26
+ # 检查必要的环境变量
27
+ required_vars = ["ADBPG_HOST", "ADBPG_PORT", "ADBPG_USER", "ADBPG_PASSWORD", "ADBPG_DATABASE"]
28
+ missing_vars = [var for var in required_vars if not os.getenv(var)]
29
+ if missing_vars:
30
+ error_msg = f"Missing required environment variables: {', '.join(missing_vars)}"
31
+ logger.error(error_msg)
32
+ raise ValueError(error_msg)
33
+
34
+ logger.info("All required environment variables are set")
35
+ except Exception as e:
36
+ logger.error(f"Error loading environment variables: {e}")
37
+ sys.exit(1)
38
+
39
+ SERVER_VERSION = "0.1.0"
40
+
41
+ def get_db_config():
42
+ """从环境变量获取数据库配置信息"""
43
+ try:
44
+ config = {
45
+ "host": os.getenv("ADBPG_HOST", "localhost"),
46
+ "port": os.getenv("ADBPG_PORT"),
47
+ "user": os.getenv("ADBPG_USER"),
48
+ "password": os.getenv("ADBPG_PASSWORD"),
49
+ "dbname": os.getenv("ADBPG_DATABASE"),
50
+ "application_name": f"adbpg-mcp-server-{SERVER_VERSION}"
51
+ }
52
+
53
+ # 记录配置信息(不包含密码)
54
+ logger.info(f"Database config: host={config['host']}, port={config['port']}, user={config['user']}, dbname={config['dbname']}")
55
+
56
+ return config
57
+ except Exception as e:
58
+ logger.error(f"Error getting database config: {str(e)}")
59
+ raise
60
+
61
+ # 初始化服务器
62
+ try:
63
+ app = Server("adbpg-mcp-server")
64
+ logger.info("MCP server initialized")
65
+ except Exception as e:
66
+ logger.error(f"Error initializing MCP server: {e}")
67
+ sys.exit(1)
68
+
69
+ @app.list_resources()
70
+ async def list_resources() -> list[Resource]:
71
+ """列出可用的基本资源"""
72
+ try:
73
+ return [
74
+ Resource(
75
+ uri="adbpg:///schemas",
76
+ name="All Schemas",
77
+ description="AnalyticDB PostgreSQL schemas. List all schemas in the database",
78
+ mimeType="text/plain"
79
+ )
80
+ ]
81
+ except Exception as e:
82
+ logger.error(f"Error listing resources: {str(e)}")
83
+ raise
84
+
85
+ @app.list_resource_templates()
86
+ async def list_resource_templates() -> list[ResourceTemplate]:
87
+ """
88
+ 定义动态资源模板
89
+
90
+ 返回:
91
+ list[ResourceTemplate]: 资源模板列表
92
+ 包含以下模板:
93
+ - 列出schema中的表
94
+ - 获取表DDL
95
+ - 获取表统计信息
96
+ """
97
+ return [
98
+ ResourceTemplate(
99
+ uriTemplate="adbpg:///{schema}/tables", # 表列表模板
100
+ name="Schema Tables",
101
+ description="List all tables in a specific schema",
102
+ mimeType="text/plain"
103
+ ),
104
+ ResourceTemplate(
105
+ uriTemplate="adbpg:///{schema}/{table}/ddl", # 表DDL模板
106
+ name="Table DDL",
107
+ description="Get the DDL script of a table in a specific schema",
108
+ mimeType="text/plain"
109
+ ),
110
+ ResourceTemplate(
111
+ uriTemplate="adbpg:///{schema}/{table}/statistics", # 表统计信息模板
112
+ name="Table Statistics",
113
+ description="Get statistics information of a table",
114
+ mimeType="text/plain"
115
+ )
116
+ ]
117
+
118
+ @app.read_resource()
119
+ async def read_resource(uri: AnyUrl) -> str:
120
+ """
121
+ 读取资源内容
122
+
123
+ 参数:
124
+ uri (AnyUrl): 资源URI
125
+
126
+ 返回:
127
+ str: 资源内容
128
+
129
+ 支持的URI格式:
130
+ - adbpg:///schemas: 列出所有schema
131
+ - adbpg:///{schema}/tables: 列出指定schema中的表
132
+ - adbpg:///{schema}/{table}/ddl: 获取表的DDL
133
+ - adbpg:///{schema}/{table}/statistics: 获取表的统计信息
134
+ """
135
+ config = get_db_config()
136
+ uri_str = str(uri)
137
+
138
+ if not uri_str.startswith("adbpg:///"):
139
+ raise ValueError(f"Invalid URI scheme: {uri_str}")
140
+
141
+ try:
142
+ with psycopg.connect(**config) as conn: # 建立数据库连接
143
+ conn.autocommit = True # 设置自动提交
144
+ with conn.cursor() as cursor: # 创建游标
145
+ path_parts = uri_str[9:].split('/') # 解析URI路径
146
+
147
+ if path_parts[0] == "schemas":
148
+ # 列出所有schema
149
+ query = """
150
+ SELECT schema_name
151
+ FROM information_schema.schemata
152
+ WHERE schema_name NOT IN ('pg_catalog', 'information_schema')
153
+ ORDER BY schema_name;
154
+ """
155
+ cursor.execute(query)
156
+ schemas = cursor.fetchall()
157
+ return "\n".join([schema[0] for schema in schemas])
158
+
159
+ elif len(path_parts) == 2 and path_parts[1] == "tables":
160
+ # 列出指定schema中的表
161
+ schema = path_parts[0]
162
+ query = f"""
163
+ SELECT table_name, table_type
164
+ FROM information_schema.tables
165
+ WHERE table_schema = %s
166
+ ORDER BY table_name;
167
+ """
168
+ cursor.execute(query, (schema,))
169
+ tables = cursor.fetchall()
170
+ return "\n".join([f"{table[0]} ({table[1]})" for table in tables])
171
+
172
+ elif len(path_parts) == 3 and path_parts[2] == "ddl":
173
+ # 获取表的DDL
174
+ schema = path_parts[0]
175
+ table = path_parts[1]
176
+ query = f"""
177
+ SELECT pg_get_ddl('{schema}.{table}'::regclass);
178
+ """
179
+ cursor.execute(query)
180
+ ddl = cursor.fetchone()
181
+ return ddl[0] if ddl else f"No DDL found for {schema}.{table}"
182
+
183
+ elif len(path_parts) == 3 and path_parts[2] == "statistics":
184
+ # 获取表的统计信息
185
+ schema = path_parts[0]
186
+ table = path_parts[1]
187
+ query = """
188
+ SELECT
189
+ schemaname,
190
+ tablename,
191
+ attname,
192
+ null_frac,
193
+ avg_width,
194
+ n_distinct,
195
+ most_common_vals,
196
+ most_common_freqs
197
+ FROM pg_stats
198
+ WHERE schemaname = %s AND tablename = %s
199
+ ORDER BY attname;
200
+ """
201
+ cursor.execute(query, (schema, table))
202
+ rows = cursor.fetchall()
203
+ if not rows:
204
+ return f"No statistics found for {schema}.{table}"
205
+
206
+ result = []
207
+ for row in rows:
208
+ result.append(f"Column: {row[2]}")
209
+ result.append(f" Null fraction: {row[3]}")
210
+ result.append(f" Average width: {row[4]}")
211
+ result.append(f" Distinct values: {row[5]}")
212
+ if row[6]:
213
+ result.append(f" Most common values: {row[6]}")
214
+ result.append(f" Most common frequencies: {row[7]}")
215
+ result.append("")
216
+ return "\n".join(result)
217
+
218
+ raise ValueError(f"Invalid resource URI format: {uri_str}")
219
+
220
+ except Error as e:
221
+ raise RuntimeError(f"Database error: {str(e)}")
222
+
223
+ @app.list_tools()
224
+ async def list_tools() -> list[Tool]:
225
+ """
226
+ 列出可用的工具
227
+
228
+ 返回:
229
+ list[Tool]: 工具列表
230
+ 包含以下工具:
231
+ - execute_select_sql: 执行SELECT查询
232
+ - execute_dml_sql: 执行DML操作
233
+ - execute_ddl_sql: 执行DDL操作
234
+ - analyze_table: 分析表统计信息
235
+ - explain_query: 获取查询执行计划
236
+ """
237
+ return [
238
+ Tool(
239
+ name="execute_select_sql",
240
+ description="Execute SELECT SQL to query data from ADBPG database.",
241
+ inputSchema={
242
+ "type": "object",
243
+ "properties": {
244
+ "query": {
245
+ "type": "string",
246
+ "description": "The (SELECT) SQL query to execute"
247
+ }
248
+ },
249
+ "required": ["query"]
250
+ }
251
+ ),
252
+ Tool(
253
+ name="execute_dml_sql",
254
+ description="Execute (INSERT, UPDATE, DELETE) SQL to modify data in ADBPG database.",
255
+ inputSchema={
256
+ "type": "object",
257
+ "properties": {
258
+ "query": {
259
+ "type": "string",
260
+ "description": "The DML SQL query to execute"
261
+ }
262
+ },
263
+ "required": ["query"]
264
+ }
265
+ ),
266
+ Tool(
267
+ name="execute_ddl_sql",
268
+ description="Execute (CREATE, ALTER, DROP) SQL statements to manage database objects.",
269
+ inputSchema={
270
+ "type": "object",
271
+ "properties": {
272
+ "query": {
273
+ "type": "string",
274
+ "description": "The DDL SQL query to execute"
275
+ }
276
+ },
277
+ "required": ["query"]
278
+ }
279
+ ),
280
+ Tool(
281
+ name="analyze_table",
282
+ description="Execute ANALYZE command to collect table statistics.",
283
+ inputSchema={
284
+ "type": "object",
285
+ "properties": {
286
+ "schema": {
287
+ "type": "string",
288
+ "description": "Schema name"
289
+ },
290
+ "table": {
291
+ "type": "string",
292
+ "description": "Table name"
293
+ }
294
+ },
295
+ "required": ["schema", "table"]
296
+ }
297
+ ),
298
+ Tool(
299
+ name="explain_query",
300
+ description="Get query execution plan.",
301
+ inputSchema={
302
+ "type": "object",
303
+ "properties": {
304
+ "query": {
305
+ "type": "string",
306
+ "description": "The SQL query to analyze"
307
+ }
308
+ },
309
+ "required": ["query"]
310
+ }
311
+ )
312
+ ]
313
+
314
+ @app.call_tool()
315
+ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
316
+ """
317
+ 执行工具操作
318
+
319
+ 参数:
320
+ name (str): 工具名称
321
+ arguments (dict): 工具参数
322
+
323
+ 返回:
324
+ list[TextContent]: 执行结果
325
+
326
+ 支持的工具:
327
+ - execute_select_sql: 执行SELECT查询
328
+ - execute_dml_sql: 执行DML操作
329
+ - execute_ddl_sql: 执行DDL操作
330
+ - analyze_table: 分析表统计信息
331
+ - explain_query: 获取查询执行计划
332
+ """
333
+ config = get_db_config()
334
+
335
+ # 根据工具名称处理不同的操作
336
+ if name == "execute_select_sql":
337
+ query = arguments.get("query")
338
+ if not query:
339
+ raise ValueError("Query is required")
340
+ if not query.strip().upper().startswith("SELECT"):
341
+ raise ValueError("Query must be a SELECT statement")
342
+ elif name == "execute_dml_sql":
343
+ query = arguments.get("query")
344
+ if not query:
345
+ raise ValueError("Query is required")
346
+ if not any(query.strip().upper().startswith(keyword) for keyword in ["INSERT", "UPDATE", "DELETE"]):
347
+ raise ValueError("Query must be a DML statement (INSERT, UPDATE, DELETE)")
348
+ elif name == "execute_ddl_sql":
349
+ query = arguments.get("query")
350
+ if not query:
351
+ raise ValueError("Query is required")
352
+ if not any(query.strip().upper().startswith(keyword) for keyword in ["CREATE", "ALTER", "DROP"]):
353
+ raise ValueError("Query must be a DDL statement (CREATE, ALTER, DROP)")
354
+ elif name == "analyze_table":
355
+ schema = arguments.get("schema")
356
+ table = arguments.get("table")
357
+ if not all([schema, table]):
358
+ raise ValueError("Schema and table are required")
359
+ query = f"ANALYZE {schema}.{table}"
360
+ elif name == "explain_query":
361
+ query = arguments.get("query")
362
+ if not query:
363
+ raise ValueError("Query is required")
364
+ query = f"EXPLAIN {query}"
365
+ else:
366
+ raise ValueError(f"Unknown tool: {name}")
367
+
368
+ try:
369
+ with psycopg.connect(**config) as conn:
370
+ conn.autocommit = True
371
+ with conn.cursor() as cursor:
372
+ cursor.execute(query)
373
+
374
+ if name == "analyze_table":
375
+ return [TextContent(type="text", text=f"Successfully analyzed table {schema}.{table}")]
376
+
377
+ if cursor.description:
378
+ columns = [desc[0] for desc in cursor.description]
379
+ rows = cursor.fetchall()
380
+ result = [",".join(map(str, row)) for row in rows]
381
+ return [TextContent(type="text", text="\n".join([",".join(columns)] + result))]
382
+ else:
383
+ return [TextContent(type="text", text="Query executed successfully")]
384
+ except Exception as e:
385
+ return [TextContent(type="text", text=f"Error executing query: {str(e)}")]
386
+
387
+ async def main():
388
+ """服务器主入口点"""
389
+ try:
390
+ config = get_db_config()
391
+ logger.info("Starting ADBPG MCP server...")
392
+
393
+ # 测试数据库连接
394
+ try:
395
+ with psycopg.connect(**config) as conn:
396
+ logger.info("Successfully connected to database")
397
+ except Exception as e:
398
+ logger.error(f"Failed to connect to database: {e}")
399
+ sys.exit(1)
400
+
401
+ # 使用 stdio 传输
402
+ async with stdio_server() as (read_stream, write_stream):
403
+ try:
404
+ logger.info("Running MCP server with stdio transport...")
405
+ await app.run(
406
+ read_stream=read_stream,
407
+ write_stream=write_stream,
408
+ initialization_options=app.create_initialization_options()
409
+ )
410
+ except Exception as e:
411
+ logger.error(f"Error running server: {str(e)}")
412
+ raise
413
+ except Exception as e:
414
+ logger.error(f"Server initialization error: {str(e)}")
415
+ raise
416
+
417
+ def run():
418
+ """同步运行入口点"""
419
+ try:
420
+ asyncio.run(main())
421
+ except Exception as e:
422
+ logger.error(f"Fatal error: {e}")
423
+ sys.exit(1)
424
+
425
+ if __name__ == "__main__":
426
+ run()
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: adbpg-mcp-server
3
+ Version: 1.0.0
4
+ Summary: ADBPG MCP Server
5
+ Home-page: https://github.com/aliyun/alibabacloud-adbpg-mcp-server
6
+ Author: Yutian Qiu
7
+ Author-email: qiuytian@gmail.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: psycopg>=3.1.0
15
+ Requires-Dist: mcp>=1.4.0
16
+ Requires-Dist: pydantic>=2.0.0
17
+ Requires-Dist: python-dotenv>=1.0.0
18
+ Dynamic: author
19
+ Dynamic: author-email
20
+ Dynamic: classifier
21
+ Dynamic: description
22
+ Dynamic: description-content-type
23
+ Dynamic: home-page
24
+ Dynamic: license-file
25
+ Dynamic: requires-dist
26
+ Dynamic: requires-python
27
+ Dynamic: summary
28
+
29
+ # AnalyticDB PostgreSQL MCP Server
30
+
31
+ AnalyticDB PostgreSQL MCP Server serves as a universal interface between AI Agents and AnalyticDB PostgreSQL databases. It enables seamless communication between AI Agents and AnalyticDB PostgreSQL, helping AI Agents retrieve database metadata and execute SQL operations.
32
+
33
+ ## Configuration
34
+
35
+
36
+
37
+ #### Download
38
+
39
+ Download from Github
40
+
41
+ ```shell
42
+ git clone https://github.com/aliyun/alibabacloud-adbpg-mcp-server.git
43
+ ```
44
+
45
+ #### MCP Integration
46
+
47
+ Add the following configuration to the MCP client configuration file:
48
+
49
+ ```json
50
+ "mcpServers": {
51
+ "adbpg-mcp-server": {
52
+ "command": "uv",
53
+ "args": [
54
+ "--directory",
55
+ "/path/to/adbpg-mcp-server",
56
+ "run",
57
+ "adbpg-mcp-server"
58
+ ],
59
+ "env": {
60
+ "ADBPG_HOST": "host",
61
+ "ADBPG_PORT": "port",
62
+ "ADBPG_USER": "username",
63
+ "ADBPG_PASSWORD": "password",
64
+ "ADBPG_DATABASE": "database"
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ ## Components
71
+
72
+ ### Tools
73
+
74
+ * `execute_select_sql`: Execute SELECT SQL queries on the AnalyticDB PostgreSQL server
75
+ * `execute_dml_sql`: Execute DML (INSERT, UPDATE, DELETE) SQL queries on the AnalyticDB PostgreSQL server
76
+ * `execute_ddl_sql`: Execute DDL (CREATE, ALTER, DROP) SQL queries on the AnalyticDB PostgreSQL server
77
+ * `analyze_table`: Collect table statistics
78
+ * `explain_query`: Get query execution plan
79
+
80
+ ### Resources
81
+
82
+ #### Built-in Resources
83
+
84
+ * `adbpg:///schemas`: Get all schemas in the database
85
+
86
+ #### Resource Templates
87
+
88
+ * `adbpg:///{schema}/tables`: List all tables in a specific schema
89
+ * `adbpg:///{schema}/{table}/ddl`: Get table DDL
90
+ * `adbpg:///{schema}/{table}/statistics`: Show table statistics
91
+
92
+ ## Environment Variables
93
+
94
+ MCP Server requires the following environment variables to connect to AnalyticDB PostgreSQL instance:
95
+
96
+ - `ADBPG_HOST`: Database host address
97
+ - `ADBPG_PORT`: Database port
98
+ - `ADBPG_USER`: Database username
99
+ - `ADBPG_PASSWORD`: Database password
100
+ - `ADBPG_DATABASE`: Database name
101
+
102
+ ## Dependencies
103
+
104
+ - Python 3.10 or higher
105
+ - Required packages:
106
+ - mcp >= 1.4.0
107
+ - psycopg >= 3.1.0
108
+ - python-dotenv >= 1.0.0
109
+ - pydantic >= 2.0.0
110
+
111
+ ## Running
112
+
113
+ ```bash
114
+ # Create and activate virtual environment
115
+ uv venv .venv
116
+ source .venv/bin/activate # Linux/Mac
117
+ # or
118
+ .venv\Scripts\activate # Windows
119
+
120
+ # Install dependencies
121
+ uv pip install -e .
122
+
123
+ # Run server
124
+ uv run adbpg-mcp-server
125
+ ```
126
+
127
+
@@ -0,0 +1,7 @@
1
+ adbpg-mcp-server/__init__.py,sha256=89QTK3toQf0AJsdms8fTeUrl7JF41R-i2OR9W_YNIGc,25
2
+ adbpg-mcp-server/adbpg_mcp_server.py,sha256=5QPYmArQYfiLX9iZKnoWJquFgDWUUaxl9EdyW6W5wA8,15350
3
+ adbpg_mcp_server-1.0.0.dist-info/licenses/LICENSE,sha256=KfhlNIQJu0to_CCmMo4whKiv23yAI_yKJSufqkJN1nw,1073
4
+ adbpg_mcp_server-1.0.0.dist-info/METADATA,sha256=qW25E33LvYH-Gus3R-Lq8RtWgYi7Ce-ZJr4BesqCRrI,3107
5
+ adbpg_mcp_server-1.0.0.dist-info/WHEEL,sha256=ooBFpIzZCPdw3uqIQsOo4qqbA4ZRPxHnOH7peeONza0,91
6
+ adbpg_mcp_server-1.0.0.dist-info/top_level.txt,sha256=0LWAscFZd8uAPY8_oR2AL8FUz7kneruC_ZmdgQVk5jE,17
7
+ adbpg_mcp_server-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2025 The Python Packaging Authority
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ adbpg-mcp-server