awslabs.postgres-mcp-server 1.0.3__py3-none-any.whl → 1.0.4__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.
@@ -14,4 +14,4 @@
14
14
 
15
15
  """awslabs.postgres-mcp-server"""
16
16
 
17
- __version__ = '0.0.4'
17
+ __version__ = '1.0.4'
@@ -0,0 +1,18 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """aws.postgres-mcp-server.connection"""
16
+
17
+ from awslabs.postgres_mcp_server.connection.db_connection_singleton import DBConnectionSingleton
18
+ from awslabs.postgres_mcp_server.connection.abstract_db_connection import AbstractDBConnection
@@ -0,0 +1,68 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Abstract database connection interface for postgres MCP Server."""
16
+
17
+ from abc import ABC, abstractmethod
18
+ from typing import Any, Dict, List, Optional
19
+
20
+
21
+ class AbstractDBConnection(ABC):
22
+ """Abstract base class for database connections."""
23
+
24
+ def __init__(self, readonly: bool):
25
+ """Initialize the database connection.
26
+
27
+ Args:
28
+ readonly: Whether the connection should be read-only
29
+ """
30
+ self._readonly = readonly
31
+
32
+ @property
33
+ def readonly_query(self) -> bool:
34
+ """Get whether this connection is read-only.
35
+
36
+ Returns:
37
+ bool: True if the connection is read-only, False otherwise
38
+ """
39
+ return self._readonly
40
+
41
+ @abstractmethod
42
+ async def execute_query(
43
+ self, sql: str, parameters: Optional[List[Dict[str, Any]]] = None
44
+ ) -> Dict[str, Any]:
45
+ """Execute a SQL query.
46
+
47
+ Args:
48
+ sql: The SQL query to execute
49
+ parameters: Optional parameters for the query
50
+
51
+ Returns:
52
+ Dict containing query results with column metadata and records
53
+ """
54
+ pass
55
+
56
+ @abstractmethod
57
+ async def close(self) -> None:
58
+ """Close the database connection."""
59
+ pass
60
+
61
+ @abstractmethod
62
+ async def check_connection_health(self) -> bool:
63
+ """Check if the database connection is healthy.
64
+
65
+ Returns:
66
+ bool: True if the connection is healthy, False otherwise
67
+ """
68
+ pass
@@ -0,0 +1,117 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Database connection singleton for postgres MCP Server."""
16
+
17
+ import asyncio
18
+ from awslabs.postgres_mcp_server.connection.rds_api_connection import RDSDataAPIConnection
19
+ from loguru import logger
20
+
21
+
22
+ class DBConnectionSingleton:
23
+ """Manages a single RDS Data API connection instance across the application."""
24
+
25
+ _instance = None
26
+
27
+ def __init__(
28
+ self,
29
+ resource_arn: str,
30
+ secret_arn: str,
31
+ database: str,
32
+ region: str,
33
+ readonly: bool = True,
34
+ is_test: bool = False,
35
+ ):
36
+ """Initialize a new DB connection singleton for RDS Data API.
37
+
38
+ Args:
39
+ resource_arn: The ARN of the RDS cluster
40
+ secret_arn: The ARN of the secret containing credentials
41
+ database: The name of the database to connect to
42
+ region: The AWS region where the RDS instance is located
43
+ readonly: Whether the connection should be read-only (default: True)
44
+ is_test: Whether this is a test connection (default: False)
45
+ """
46
+ if not all([resource_arn, secret_arn, database, region]):
47
+ raise ValueError(
48
+ 'Missing required connection parameters for RDS Data API. '
49
+ 'Please provide resource_arn, secret_arn, database, and region.'
50
+ )
51
+
52
+ self._db_connection = RDSDataAPIConnection(
53
+ cluster_arn=resource_arn,
54
+ secret_arn=secret_arn,
55
+ database=database,
56
+ region=region,
57
+ readonly=readonly,
58
+ is_test=is_test,
59
+ )
60
+
61
+ @classmethod
62
+ def initialize(
63
+ cls,
64
+ resource_arn: str,
65
+ secret_arn: str,
66
+ database: str,
67
+ region: str,
68
+ readonly: bool = True,
69
+ is_test: bool = False,
70
+ ):
71
+ """Initialize the singleton instance if it doesn't exist.
72
+
73
+ Args:
74
+ resource_arn: The ARN of the RDS cluster
75
+ secret_arn: The ARN of the secret containing credentials
76
+ database: The name of the database to connect to
77
+ region: The AWS region where the RDS instance is located
78
+ readonly: Whether the connection should be read-only (default: True)
79
+ is_test: Whether this is a test connection (default: False)
80
+ """
81
+ if cls._instance is None:
82
+ cls._instance = cls(
83
+ resource_arn=resource_arn,
84
+ secret_arn=secret_arn,
85
+ database=database,
86
+ region=region,
87
+ readonly=readonly,
88
+ is_test=is_test,
89
+ )
90
+
91
+ @classmethod
92
+ def get(cls):
93
+ """Get the singleton instance."""
94
+ if cls._instance is None:
95
+ raise RuntimeError('DBConnectionSingleton is not initialized.')
96
+ return cls._instance
97
+
98
+ @property
99
+ def db_connection(self):
100
+ """Get the database connection."""
101
+ return self._db_connection
102
+
103
+ @classmethod
104
+ def cleanup(cls):
105
+ """Clean up resources when shutting down."""
106
+ if cls._instance and cls._instance._db_connection:
107
+ # Handle calling async close method from sync context
108
+ try:
109
+ loop = asyncio.get_event_loop()
110
+ if loop.is_running():
111
+ # If we're in an async context, create a task
112
+ asyncio.create_task(cls._instance._db_connection.close())
113
+ else:
114
+ # If we're in a sync context, run the coroutine to completion
115
+ loop.run_until_complete(cls._instance._db_connection.close())
116
+ except Exception as e:
117
+ logger.error(f'Error during connection cleanup: {str(e)}')
@@ -0,0 +1,287 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Psycopg connector for postgres MCP Server.
16
+
17
+ This connector provides direct connection to PostgreSQL databases using psycopg.
18
+ It supports both Aurora PostgreSQL and RDS PostgreSQL instances via direct connection
19
+ parameters (host, port, database, user, password) or via AWS Secrets Manager.
20
+ """
21
+
22
+ import boto3
23
+ import json
24
+ from awslabs.postgres_mcp_server.connection.abstract_db_connection import AbstractDBConnection
25
+ from loguru import logger
26
+ from psycopg_pool import AsyncConnectionPool
27
+ from typing import Any, Dict, List, Optional, Tuple
28
+
29
+
30
+ class PsycopgPoolConnection(AbstractDBConnection):
31
+ """Class that wraps DB connection using psycopg connection pool.
32
+
33
+ This class can connect directly to any PostgreSQL database, including:
34
+ - Aurora PostgreSQL (using the cluster endpoint)
35
+ - RDS PostgreSQL (using the instance endpoint)
36
+ - Self-hosted PostgreSQL
37
+
38
+ It uses AWS Secrets Manager (secret_arn and region) for authentication.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ host: str,
44
+ port: int,
45
+ database: str,
46
+ readonly: bool,
47
+ secret_arn: str,
48
+ region: str,
49
+ min_size: int = 1,
50
+ max_size: int = 10,
51
+ is_test: bool = False,
52
+ ):
53
+ """Initialize a new DB connection pool.
54
+
55
+ Args:
56
+ host: Database host (Aurora cluster endpoint or RDS instance endpoint)
57
+ port: Database port
58
+ database: Database name
59
+ readonly: Whether connections should be read-only
60
+ secret_arn: ARN of the secret containing credentials
61
+ region: AWS region for Secrets Manager
62
+ min_size: Minimum number of connections in the pool
63
+ max_size: Maximum number of connections in the pool
64
+ is_test: Whether this is a test connection
65
+ """
66
+ super().__init__(readonly)
67
+ self.host = host
68
+ self.port = port
69
+ self.database = database
70
+ self.min_size = min_size
71
+ self.max_size = max_size
72
+ self.pool: Optional['AsyncConnectionPool[Any]'] = None
73
+
74
+ # Get credentials from Secrets Manager
75
+ logger.info(f'Retrieving credentials from Secrets Manager: {secret_arn}')
76
+ self.user, self.password = self._get_credentials_from_secret(secret_arn, region, is_test)
77
+ logger.info(f'Successfully retrieved credentials for user: {self.user}')
78
+
79
+ # Store connection info
80
+ if not is_test:
81
+ self.conninfo = f'host={host} port={port} dbname={database} user={self.user} password={self.password}'
82
+ logger.info('Connection parameters stored')
83
+
84
+ async def initialize_pool(self):
85
+ """Initialize the connection pool."""
86
+ if self.pool is None:
87
+ logger.info(
88
+ f'Initializing connection pool with min_size={self.min_size}, max_size={self.max_size}'
89
+ )
90
+ self.pool = AsyncConnectionPool(
91
+ self.conninfo, min_size=self.min_size, max_size=self.max_size, open=True
92
+ )
93
+ logger.info('Connection pool initialized successfully')
94
+
95
+ # Set read-only mode if needed
96
+ if self.readonly_query:
97
+ await self._set_all_connections_readonly()
98
+
99
+ async def _get_connection(self):
100
+ """Get a database connection from the pool."""
101
+ if self.pool is None:
102
+ await self.initialize_pool()
103
+
104
+ if self.pool is None:
105
+ raise ValueError('Failed to initialize connection pool')
106
+
107
+ return self.pool.connection(timeout=15.0)
108
+
109
+ async def _set_all_connections_readonly(self):
110
+ """Set all connections in the pool to read-only mode."""
111
+ if self.pool is None:
112
+ logger.warning('Connection pool is not initialized, cannot set read-only mode')
113
+ return
114
+
115
+ try:
116
+ async with self.pool.connection(timeout=15.0) as conn:
117
+ await conn.execute(
118
+ 'ALTER ROLE CURRENT_USER SET default_transaction_read_only = on'
119
+ ) # type: ignore
120
+ logger.info('Successfully set connection to read-only mode')
121
+ except Exception as e:
122
+ logger.warning(f'Failed to set connections to read-only mode: {str(e)}')
123
+ logger.warning('Continuing without setting read-only mode')
124
+
125
+ async def execute_query(
126
+ self, sql: str, parameters: Optional[List[Dict[str, Any]]] = None
127
+ ) -> Dict[str, Any]:
128
+ """Execute a SQL query using async connection."""
129
+ try:
130
+ async with await self._get_connection() as conn:
131
+ async with conn.transaction():
132
+ if self.readonly_query:
133
+ await conn.execute('SET TRANSACTION READ ONLY') # type: ignore
134
+
135
+ # Create a cursor for better control
136
+ async with conn.cursor() as cursor:
137
+ # Execute the query
138
+ if parameters:
139
+ params = self._convert_parameters(parameters)
140
+ await cursor.execute(sql, params)
141
+ else:
142
+ await cursor.execute(sql)
143
+
144
+ # Check if there are results to fetch by examining the cursor's description
145
+ if cursor.description:
146
+ # Get column names
147
+ columns = [desc[0] for desc in cursor.description]
148
+
149
+ # Fetch all rows
150
+ rows = await cursor.fetchall()
151
+
152
+ # Structure the response to match the interface contract required by server.py
153
+ column_metadata = [{'name': col} for col in columns]
154
+ records = []
155
+
156
+ # Convert each row to the expected format
157
+ for row in rows:
158
+ record = []
159
+ for value in row:
160
+ if value is None:
161
+ record.append({'isNull': True})
162
+ elif isinstance(value, str):
163
+ record.append({'stringValue': value})
164
+ elif isinstance(value, int):
165
+ record.append({'longValue': value})
166
+ elif isinstance(value, float):
167
+ record.append({'doubleValue': value})
168
+ elif isinstance(value, bool):
169
+ record.append({'booleanValue': value})
170
+ elif isinstance(value, bytes):
171
+ record.append({'blobValue': value})
172
+ else:
173
+ # Convert other types to string
174
+ record.append({'stringValue': str(value)})
175
+ records.append(record)
176
+
177
+ return {'columnMetadata': column_metadata, 'records': records}
178
+ else:
179
+ # No results (e.g., for INSERT, UPDATE, etc.)
180
+ return {'columnMetadata': [], 'records': []}
181
+
182
+ except Exception as e:
183
+ logger.error(f'Database connection error: {str(e)}')
184
+ raise e
185
+
186
+ def _convert_parameters(self, parameters: List[Dict[str, Any]]) -> Dict[str, Any]:
187
+ """Transform structured parameter format to psycopg's native parameter format."""
188
+ result = {}
189
+ for param in parameters:
190
+ name = param.get('name')
191
+ value = param.get('value', {})
192
+
193
+ # Extract the value based on its type
194
+ if 'stringValue' in value:
195
+ result[name] = value['stringValue']
196
+ elif 'longValue' in value:
197
+ result[name] = value['longValue']
198
+ elif 'doubleValue' in value:
199
+ result[name] = value['doubleValue']
200
+ elif 'booleanValue' in value:
201
+ result[name] = value['booleanValue']
202
+ elif 'blobValue' in value:
203
+ result[name] = value['blobValue']
204
+ elif 'isNull' in value and value['isNull']:
205
+ result[name] = None
206
+
207
+ return result
208
+
209
+ def _get_credentials_from_secret(
210
+ self, secret_arn: str, region: str, is_test: bool = False
211
+ ) -> Tuple[str, str]:
212
+ """Get database credentials from AWS Secrets Manager."""
213
+ if is_test:
214
+ return 'test_user', 'test_password'
215
+
216
+ try:
217
+ # Create a Secrets Manager client
218
+ logger.info(f'Creating Secrets Manager client in region {region}')
219
+ session = boto3.Session()
220
+ client = session.client(service_name='secretsmanager', region_name=region)
221
+
222
+ # Get the secret value
223
+ logger.info(f'Retrieving secret value for {secret_arn}')
224
+ get_secret_value_response = client.get_secret_value(SecretId=secret_arn)
225
+ logger.info('Successfully retrieved secret value')
226
+
227
+ # Parse the secret string
228
+ if 'SecretString' in get_secret_value_response:
229
+ secret = json.loads(get_secret_value_response['SecretString'])
230
+ logger.info(f'Secret keys: {", ".join(secret.keys())}')
231
+
232
+ # Extract username and password
233
+ username = secret.get('username') or secret.get('user') or secret.get('Username')
234
+ password = secret.get('password') or secret.get('Password')
235
+
236
+ if not username:
237
+ logger.error(
238
+ f'Username not found in secret. Available keys: {", ".join(secret.keys())}'
239
+ )
240
+ raise ValueError(
241
+ f'Secret does not contain username. Available keys: {", ".join(secret.keys())}'
242
+ )
243
+
244
+ if not password:
245
+ logger.error('Password not found in secret')
246
+ raise ValueError(
247
+ f'Secret does not contain password. Available keys: {", ".join(secret.keys())}'
248
+ )
249
+
250
+ logger.info(f'Successfully extracted credentials for user: {username}')
251
+ return username, password
252
+ else:
253
+ logger.error('Secret does not contain a SecretString')
254
+ raise ValueError('Secret does not contain a SecretString')
255
+ except Exception as e:
256
+ logger.error(f'Error retrieving secret: {str(e)}')
257
+ raise ValueError(f'Failed to retrieve credentials from Secrets Manager: {str(e)}')
258
+
259
+ async def close(self) -> None:
260
+ """Close all connections in the pool."""
261
+ if self.pool is not None:
262
+ logger.info('Closing connection pool')
263
+ await self.pool.close()
264
+ self.pool = None
265
+ logger.info('Connection pool closed successfully')
266
+
267
+ async def check_connection_health(self) -> bool:
268
+ """Check if the connection is healthy."""
269
+ try:
270
+ result = await self.execute_query('SELECT 1')
271
+ return len(result.get('records', [])) > 0
272
+ except Exception as e:
273
+ logger.error(f'Connection health check failed: {str(e)}')
274
+ return False
275
+
276
+ def get_pool_stats(self) -> Dict[str, int]:
277
+ """Get current connection pool statistics."""
278
+ if not hasattr(self, 'pool') or self.pool is None:
279
+ return {'size': 0, 'min_size': self.min_size, 'max_size': self.max_size, 'idle': 0}
280
+
281
+ # Access pool attributes safely
282
+ size = getattr(self.pool, 'size', 0)
283
+ min_size = getattr(self.pool, 'min_size', self.min_size)
284
+ max_size = getattr(self.pool, 'max_size', self.max_size)
285
+ idle = getattr(self.pool, 'idle', 0)
286
+
287
+ return {'size': size, 'min_size': min_size, 'max_size': max_size, 'idle': idle}
@@ -0,0 +1,157 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """RDS Data API connector for postgres MCP Server."""
16
+
17
+ import asyncio
18
+ import boto3
19
+ from awslabs.postgres_mcp_server.connection.abstract_db_connection import AbstractDBConnection
20
+ from loguru import logger
21
+ from typing import Any, Dict, List, Optional
22
+
23
+
24
+ class RDSDataAPIConnection(AbstractDBConnection):
25
+ """Class that wraps DB connection client by RDS API."""
26
+
27
+ def __init__(
28
+ self,
29
+ cluster_arn: str,
30
+ secret_arn: str,
31
+ database: str,
32
+ region: str,
33
+ readonly: bool,
34
+ is_test: bool = False,
35
+ ):
36
+ """Initialize a new DB connection.
37
+
38
+ Args:
39
+ cluster_arn: The ARN of the RDS cluster
40
+ secret_arn: The ARN of the secret containing credentials
41
+ database: The name of the database to connect to
42
+ region: The AWS region where the RDS instance is located
43
+ readonly: Whether the connection should be read-only
44
+ is_test: Whether this is a test connection
45
+ """
46
+ super().__init__(readonly)
47
+ self.cluster_arn = cluster_arn
48
+ self.secret_arn = secret_arn
49
+ self.database = database
50
+ if not is_test:
51
+ self.data_client = boto3.client('rds-data', region_name=region)
52
+
53
+ async def execute_query(
54
+ self, sql: str, parameters: Optional[List[Dict[str, Any]]] = None
55
+ ) -> Dict[str, Any]:
56
+ """Execute a SQL query using RDS Data API.
57
+
58
+ Args:
59
+ sql: The SQL query to execute
60
+ parameters: Optional parameters for the query
61
+
62
+ Returns:
63
+ Dict containing query results with column metadata and records
64
+ """
65
+ if self.readonly_query:
66
+ return await asyncio.to_thread(self._execute_readonly_query, sql, parameters)
67
+ else:
68
+ execute_params = {
69
+ 'resourceArn': self.cluster_arn,
70
+ 'secretArn': self.secret_arn,
71
+ 'database': self.database,
72
+ 'sql': sql,
73
+ 'includeResultMetadata': True,
74
+ }
75
+
76
+ if parameters:
77
+ execute_params['parameters'] = parameters
78
+
79
+ return await asyncio.to_thread(self.data_client.execute_statement, **execute_params)
80
+
81
+ def _execute_readonly_query(
82
+ self, query: str, parameters: Optional[List[Dict[str, Any]]] = None
83
+ ) -> Dict[str, Any]:
84
+ """Execute a query under readonly transaction.
85
+
86
+ Args:
87
+ query: query to run
88
+ parameters: parameters
89
+
90
+ Returns:
91
+ Dict containing query results with column metadata and records
92
+ """
93
+ tx_id = ''
94
+ try:
95
+ # Begin read-only transaction
96
+ tx = self.data_client.begin_transaction(
97
+ resourceArn=self.cluster_arn,
98
+ secretArn=self.secret_arn,
99
+ database=self.database,
100
+ )
101
+
102
+ tx_id = tx['transactionId']
103
+
104
+ self.data_client.execute_statement(
105
+ resourceArn=self.cluster_arn,
106
+ secretArn=self.secret_arn,
107
+ database=self.database,
108
+ sql='SET TRANSACTION READ ONLY',
109
+ transactionId=tx_id,
110
+ )
111
+
112
+ execute_params = {
113
+ 'resourceArn': self.cluster_arn,
114
+ 'secretArn': self.secret_arn,
115
+ 'database': self.database,
116
+ 'sql': query,
117
+ 'includeResultMetadata': True,
118
+ 'transactionId': tx_id,
119
+ }
120
+
121
+ if parameters is not None:
122
+ execute_params['parameters'] = parameters
123
+
124
+ result = self.data_client.execute_statement(**execute_params)
125
+
126
+ self.data_client.commit_transaction(
127
+ resourceArn=self.cluster_arn,
128
+ secretArn=self.secret_arn,
129
+ transactionId=tx_id,
130
+ )
131
+ return result
132
+ except Exception as e:
133
+ if tx_id:
134
+ self.data_client.rollback_transaction(
135
+ resourceArn=self.cluster_arn,
136
+ secretArn=self.secret_arn,
137
+ transactionId=tx_id,
138
+ )
139
+ raise e
140
+
141
+ async def close(self) -> None:
142
+ """Close the database connection asynchronously."""
143
+ # RDS Data API doesn't maintain persistent connections
144
+ pass
145
+
146
+ async def check_connection_health(self) -> bool:
147
+ """Check if the RDS Data API connection is healthy.
148
+
149
+ Returns:
150
+ bool: True if the connection is healthy, False otherwise
151
+ """
152
+ try:
153
+ result = await self.execute_query('SELECT 1')
154
+ return len(result.get('records', [])) > 0
155
+ except Exception as e:
156
+ logger.error(f'RDS Data API connection health check failed: {str(e)}')
157
+ return False
@@ -16,8 +16,9 @@
16
16
 
17
17
  import argparse
18
18
  import asyncio
19
- import boto3
20
19
  import sys
20
+ from awslabs.postgres_mcp_server.connection import DBConnectionSingleton
21
+ from awslabs.postgres_mcp_server.connection.psycopg_pool_connection import PsycopgPoolConnection
21
22
  from awslabs.postgres_mcp_server.mutable_sql_detector import (
22
23
  check_sql_injection_risk,
23
24
  detect_mutating_keywords,
@@ -49,104 +50,6 @@ class DummyCtx:
49
50
  pass
50
51
 
51
52
 
52
- class DBConnection:
53
- """Class that wraps DB connection client by RDS API."""
54
-
55
- def __init__(self, cluster_arn, secret_arn, database, region, readonly, is_test=False):
56
- """Initialize a new DB connection.
57
-
58
- Args:
59
- cluster_arn: The ARN of the RDS cluster
60
- secret_arn: The ARN of the secret containing credentials
61
- database: The name of the database to connect to
62
- region: The AWS region where the RDS instance is located
63
- readonly: Whether the connection should be read-only
64
- is_test: Whether this is a test connection
65
- """
66
- self.cluster_arn = cluster_arn
67
- self.secret_arn = secret_arn
68
- self.database = database
69
- self.readonly = readonly
70
- if not is_test:
71
- self.data_client = boto3.client('rds-data', region_name=region)
72
-
73
- @property
74
- def readonly_query(self):
75
- """Get whether this connection is read-only.
76
-
77
- Returns:
78
- bool: True if the connection is read-only, False otherwise
79
- """
80
- return self.readonly
81
-
82
-
83
- class DBConnectionSingleton:
84
- """Manages a single DBConnection instance across the application.
85
-
86
- This singleton ensures that only one DBConnection is created and reused.
87
- """
88
-
89
- _instance = None
90
-
91
- def __init__(self, resource_arn, secret_arn, database, region, readonly, is_test=False):
92
- """Initialize a new DB connection singleton.
93
-
94
- Args:
95
- resource_arn: The ARN of the RDS resource
96
- secret_arn: The ARN of the secret containing credentials
97
- database: The name of the database to connect to
98
- region: The AWS region where the RDS instance is located
99
- readonly: Whether the connection should be read-only
100
- is_test: Whether this is a test connection
101
- """
102
- if not all([resource_arn, secret_arn, database, region]):
103
- raise ValueError(
104
- 'Missing required connection parameters. '
105
- 'Please provide resource_arn, secret_arn, database, and region.'
106
- )
107
- self._db_connection = DBConnection(
108
- resource_arn, secret_arn, database, region, readonly, is_test
109
- )
110
-
111
- @classmethod
112
- def initialize(cls, resource_arn, secret_arn, database, region, readonly, is_test=False):
113
- """Initialize the singleton instance if it doesn't exist.
114
-
115
- Args:
116
- resource_arn: The ARN of the RDS resource
117
- secret_arn: The ARN of the secret containing credentials
118
- database: The name of the database to connect to
119
- region: The AWS region where the RDS instance is located
120
- readonly: Whether the connection should be read-only
121
- is_test: Whether this is a test connection
122
- """
123
- if cls._instance is None:
124
- cls._instance = cls(resource_arn, secret_arn, database, region, readonly, is_test)
125
-
126
- @classmethod
127
- def get(cls):
128
- """Get the singleton instance.
129
-
130
- Returns:
131
- DBConnectionSingleton: The singleton instance
132
-
133
- Raises:
134
- RuntimeError: If the singleton has not been initialized
135
- """
136
- if cls._instance is None:
137
- raise RuntimeError('DBConnectionSingleton is not initialized.')
138
- return cls._instance
139
-
140
- @property
141
- def db_connection(self):
142
- """Get the database connection.
143
-
144
- Returns:
145
- DBConnection: The database connection instance
146
- """
147
- return self._db_connection
148
-
149
-
150
53
  def extract_cell(cell: dict):
151
54
  """Extracts the scalar or array value from a single cell."""
152
55
  if cell.get('isNull'):
@@ -177,14 +80,14 @@ def parse_execute_response(response: dict) -> list[dict]:
177
80
 
178
81
 
179
82
  mcp = FastMCP(
180
- 'apg-mcp MCP server. This is the starting point for all solutions created',
83
+ 'pg-mcp MCP server. This is the starting point for all solutions created',
181
84
  dependencies=[
182
85
  'loguru',
183
86
  ],
184
87
  )
185
88
 
186
89
 
187
- @mcp.tool(name='run_query', description='Run a SQL query using boto3 execute_statement')
90
+ @mcp.tool(name='run_query', description='Run a SQL query against PostgreSQL')
188
91
  async def run_query(
189
92
  sql: Annotated[str, Field(description='The SQL query to run')],
190
93
  ctx: Context,
@@ -193,12 +96,12 @@ async def run_query(
193
96
  Optional[List[Dict[str, Any]]], Field(description='Parameters for the SQL query')
194
97
  ] = None,
195
98
  ) -> list[dict]: # type: ignore
196
- """Run a SQL query using boto3 execute_statement.
99
+ """Run a SQL query against PostgreSQL.
197
100
 
198
101
  Args:
199
102
  sql: The sql statement to run
200
103
  ctx: MCP context for logging and state management
201
- db_connection: DB connection object passed by unit test. It should be None if if called by MCP server.
104
+ db_connection: DB connection object passed by unit test. It should be None if called by MCP server.
202
105
  query_parameters: Parameters for the SQL query
203
106
 
204
107
  Returns:
@@ -209,7 +112,17 @@ async def run_query(
209
112
  global write_query_prohibited_key
210
113
 
211
114
  if db_connection is None:
212
- db_connection = DBConnectionSingleton.get().db_connection
115
+ try:
116
+ # Try to get the connection from the singleton
117
+ db_connection = DBConnectionSingleton.get().db_connection
118
+ except RuntimeError:
119
+ # If the singleton is not initialized, this might be a direct connection
120
+ logger.error('No database connection available')
121
+ await ctx.error('No database connection available')
122
+ return [{'error': 'No database connection available'}]
123
+
124
+ if db_connection is None:
125
+ raise AssertionError('db_connection should never be None')
213
126
 
214
127
  if db_connection.readonly_query:
215
128
  matches = detect_mutating_keywords(sql)
@@ -233,27 +146,10 @@ async def run_query(
233
146
  try:
234
147
  logger.info(f'run_query: readonly:{db_connection.readonly_query}, SQL:{sql}')
235
148
 
236
- if db_connection.readonly_query:
237
- response = await asyncio.to_thread(
238
- execute_readonly_query, db_connection, sql, query_parameters
239
- )
240
- else:
241
- execute_params = {
242
- 'resourceArn': db_connection.cluster_arn,
243
- 'secretArn': db_connection.secret_arn,
244
- 'database': db_connection.database,
245
- 'sql': sql,
246
- 'includeResultMetadata': True,
247
- }
248
-
249
- if query_parameters:
250
- execute_params['parameters'] = query_parameters
251
-
252
- response = await asyncio.to_thread(
253
- db_connection.data_client.execute_statement, **execute_params
254
- )
149
+ # Execute the query using the abstract connection interface
150
+ response = await db_connection.execute_query(sql, query_parameters)
255
151
 
256
- logger.success('run_query successfully executed query:{}', sql)
152
+ logger.success(f'run_query successfully executed query:{sql}')
257
153
  return parse_execute_response(response)
258
154
  except ClientError as e:
259
155
  logger.exception(client_error_code_key)
@@ -270,7 +166,7 @@ async def run_query(
270
166
 
271
167
  @mcp.tool(
272
168
  name='get_table_schema',
273
- description='Fetch table columns and comments from Postgres using RDS Data API',
169
+ description='Fetch table columns and comments from Postgres',
274
170
  )
275
171
  async def get_table_schema(
276
172
  table_name: Annotated[str, Field(description='name of the table')], ctx: Context
@@ -305,68 +201,6 @@ async def get_table_schema(
305
201
  return await run_query(sql=sql, ctx=ctx, query_parameters=params)
306
202
 
307
203
 
308
- def execute_readonly_query(
309
- db_connection: DBConnection, query: str, parameters: Optional[List[Dict[str, Any]]] = None
310
- ) -> dict:
311
- """Execute a query under readonly transaction.
312
-
313
- Args:
314
- db_connection: connection object
315
- query: query to run
316
- parameters: parameters
317
-
318
- Returns:
319
- List of dictionary that contains query response rows
320
- """
321
- tx_id = ''
322
- try:
323
- # Begin read-only transaction
324
- tx = db_connection.data_client.begin_transaction(
325
- resourceArn=db_connection.cluster_arn,
326
- secretArn=db_connection.secret_arn,
327
- database=db_connection.database,
328
- )
329
-
330
- tx_id = tx['transactionId']
331
-
332
- db_connection.data_client.execute_statement(
333
- resourceArn=db_connection.cluster_arn,
334
- secretArn=db_connection.secret_arn,
335
- database=db_connection.database,
336
- sql='SET TRANSACTION READ ONLY',
337
- transactionId=tx_id,
338
- )
339
-
340
- execute_params = {
341
- 'resourceArn': db_connection.cluster_arn,
342
- 'secretArn': db_connection.secret_arn,
343
- 'database': db_connection.database,
344
- 'sql': query,
345
- 'includeResultMetadata': True,
346
- 'transactionId': tx_id,
347
- }
348
-
349
- if parameters is not None:
350
- execute_params['parameters'] = parameters
351
-
352
- result = db_connection.data_client.execute_statement(**execute_params)
353
-
354
- db_connection.data_client.commit_transaction(
355
- resourceArn=db_connection.cluster_arn,
356
- secretArn=db_connection.secret_arn,
357
- transactionId=tx_id,
358
- )
359
- return result
360
- except Exception as e:
361
- if tx_id:
362
- db_connection.data_client.rollback_transaction(
363
- resourceArn=db_connection.cluster_arn,
364
- secretArn=db_connection.secret_arn,
365
- transactionId=tx_id,
366
- )
367
- raise e
368
-
369
-
370
204
  def main():
371
205
  """Main entry point for the MCP server application."""
372
206
  global client_error_code_key
@@ -375,51 +209,111 @@ def main():
375
209
  parser = argparse.ArgumentParser(
376
210
  description='An AWS Labs Model Context Protocol (MCP) server for postgres'
377
211
  )
378
- parser.add_argument('--resource_arn', required=True, help='ARN of the RDS cluster')
212
+
213
+ # Connection method 1: RDS Data API
214
+ parser.add_argument('--resource_arn', help='ARN of the RDS cluster (for RDS Data API)')
215
+
216
+ # Connection method 2: Psycopg Direct Connection
217
+ parser.add_argument('--hostname', help='Database hostname (for direct PostgreSQL connection)')
218
+ parser.add_argument('--port', type=int, default=5432, help='Database port (default: 5432)')
219
+
220
+ # Common parameters
379
221
  parser.add_argument(
380
222
  '--secret_arn',
381
223
  required=True,
382
224
  help='ARN of the Secrets Manager secret for database credentials',
383
225
  )
384
226
  parser.add_argument('--database', required=True, help='Database name')
385
- parser.add_argument(
386
- '--region', required=True, help='AWS region for RDS Data API (default: us-west-2)'
387
- )
388
- parser.add_argument(
389
- '--readonly', required=True, help='Enforce NL to SQL to only allow readonly sql statement'
390
- )
227
+ parser.add_argument('--region', required=True, help='AWS region')
228
+ parser.add_argument('--readonly', required=True, help='Enforce readonly SQL statements')
229
+
391
230
  args = parser.parse_args()
392
231
 
393
- logger.info(
394
- 'Postgres MCP init with CLUSTER_ARN:{}, SECRET_ARN:{}, REGION:{}, DATABASE:{}, READONLY:{}',
395
- args.resource_arn,
396
- args.secret_arn,
397
- args.region,
398
- args.database,
399
- args.readonly,
400
- )
232
+ # Validate connection parameters
233
+ if not args.resource_arn and not args.hostname:
234
+ parser.error(
235
+ 'Either --resource_arn (for RDS Data API) or '
236
+ '--hostname (for direct PostgreSQL) must be provided'
237
+ )
401
238
 
402
- try:
403
- DBConnectionSingleton.initialize(
404
- args.resource_arn, args.secret_arn, args.database, args.region, args.readonly
239
+ if args.resource_arn and args.hostname:
240
+ parser.error(
241
+ 'Cannot specify both --resource_arn and --hostname. Choose one connection method.'
405
242
  )
406
- except BotoCoreError:
407
- logger.exception('Failed to RDS API client object for Postgres. Exit the MCP server')
243
+
244
+ # Convert args to dict for easier handling
245
+ connection_params = vars(args)
246
+
247
+ # Convert readonly string to boolean
248
+ connection_params['readonly'] = args.readonly.lower() == 'true'
249
+
250
+ # Log connection information
251
+ connection_target = args.resource_arn if args.resource_arn else f'{args.hostname}:{args.port}'
252
+
253
+ if args.resource_arn:
254
+ logger.info(
255
+ f'Postgres MCP init with RDS Data API: CONNECTION_TARGET:{connection_target}, SECRET_ARN:{args.secret_arn}, REGION:{args.region}, DATABASE:{args.database}, READONLY:{args.readonly}'
256
+ )
257
+ else:
258
+ logger.info(
259
+ f'Postgres MCP init with psycopg: CONNECTION_TARGET:{connection_target}, PORT:{args.port}, DATABASE:{args.database}, READONLY:{args.readonly}'
260
+ )
261
+
262
+ # Create the appropriate database connection based on the provided parameters
263
+ db_connection = None
264
+
265
+ try:
266
+ if args.resource_arn:
267
+ # Use RDS Data API with singleton pattern
268
+ try:
269
+ # Initialize the RDS Data API connection singleton
270
+ DBConnectionSingleton.initialize(
271
+ resource_arn=args.resource_arn,
272
+ secret_arn=args.secret_arn,
273
+ database=args.database,
274
+ region=args.region,
275
+ readonly=connection_params['readonly'],
276
+ )
277
+
278
+ # Get the connection from the singleton
279
+ db_connection = DBConnectionSingleton.get().db_connection
280
+ except Exception as e:
281
+ logger.exception(f'Failed to create RDS Data API connection: {str(e)}')
282
+ sys.exit(1)
283
+
284
+ else:
285
+ # Use Direct PostgreSQL connection using psycopg connection pool
286
+ try:
287
+ # Create a direct PostgreSQL connection pool
288
+ db_connection = PsycopgPoolConnection(
289
+ host=args.hostname,
290
+ port=args.port,
291
+ database=args.database,
292
+ readonly=connection_params['readonly'],
293
+ secret_arn=args.secret_arn,
294
+ region=args.region,
295
+ )
296
+ except Exception as e:
297
+ logger.exception(f'Failed to create PostgreSQL connection: {str(e)}')
298
+ sys.exit(1)
299
+
300
+ except BotoCoreError as e:
301
+ logger.exception(f'Failed to create database connection: {str(e)}')
408
302
  sys.exit(1)
409
303
 
410
- # Test RDS API connection
304
+ # Test database connection
411
305
  ctx = DummyCtx()
412
- response = asyncio.run(run_query('SELECT 1', ctx))
306
+ response = asyncio.run(run_query('SELECT 1', ctx, db_connection))
413
307
  if (
414
308
  isinstance(response, list)
415
309
  and len(response) == 1
416
310
  and isinstance(response[0], dict)
417
311
  and 'error' in response[0]
418
312
  ):
419
- logger.error('Failed to validate RDS API db connection to Postgres. Exit the MCP server')
313
+ logger.error('Failed to validate database connection to Postgres. Exit the MCP server')
420
314
  sys.exit(1)
421
315
 
422
- logger.success('Successfully validated RDS API db connection to Postgres')
316
+ logger.success('Successfully validated database connection to Postgres')
423
317
 
424
318
  logger.info('Starting Postgres MCP server')
425
319
  mcp.run()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: awslabs.postgres-mcp-server
3
- Version: 1.0.3
3
+ Version: 1.0.4
4
4
  Summary: An AWS Labs Model Context Protocol (MCP) server for postgres
5
5
  Project-URL: homepage, https://awslabs.github.io/mcp/
6
6
  Project-URL: docs, https://awslabs.github.io/mcp/servers/postgres-mcp-server/
@@ -25,6 +25,7 @@ Requires-Dist: boto3>=1.38.5
25
25
  Requires-Dist: botocore>=1.38.5
26
26
  Requires-Dist: loguru>=0.7.0
27
27
  Requires-Dist: mcp[cli]>=1.11.0
28
+ Requires-Dist: psycopg[pool]>=3.1.12
28
29
  Requires-Dist: pydantic>=2.10.6
29
30
  Description-Content-Type: text/markdown
30
31
 
@@ -58,6 +59,8 @@ An AWS Labs Model Context Protocol (MCP) server for Aurora Postgres
58
59
 
59
60
  Configure the MCP server in your MCP client configuration (e.g., for Amazon Q Developer CLI, edit `~/.aws/amazonq/mcp.json`):
60
61
 
62
+ ### Option 1: Using RDS Data API Connection (for Aurora Postgres)
63
+
61
64
  ```json
62
65
  {
63
66
  "mcpServers": {
@@ -83,6 +86,35 @@ Configure the MCP server in your MCP client configuration (e.g., for Amazon Q De
83
86
  }
84
87
  ```
85
88
 
89
+ ### Option 2: Using Direct PostgreSQL(psycopg) Connection (for Aurora Postgres and RDS Postgres)
90
+
91
+ ```json
92
+ {
93
+ "mcpServers": {
94
+ "awslabs.postgres-mcp-server": {
95
+ "command": "uvx",
96
+ "args": [
97
+ "awslabs.postgres-mcp-server@latest",
98
+ "--hostname", "[your data]",
99
+ "--secret_arn", "[your data]",
100
+ "--database", "[your data]",
101
+ "--region", "[your data]",
102
+ "--readonly", "True"
103
+ ],
104
+ "env": {
105
+ "AWS_PROFILE": "your-aws-profile",
106
+ "AWS_REGION": "us-east-1",
107
+ "FASTMCP_LOG_LEVEL": "ERROR"
108
+ },
109
+ "disabled": false,
110
+ "autoApprove": []
111
+ }
112
+ }
113
+ }
114
+ ```
115
+
116
+ Note: The `--port` parameter is optional and defaults to 5432 (the standard PostgreSQL port). You only need to specify it if your PostgreSQL instance uses a non-standard port.
117
+
86
118
  ### Build and install docker image locally on the same host of your LLM client
87
119
 
88
120
  1. 'git clone https://github.com/awslabs/mcp.git'
@@ -91,6 +123,8 @@ Configure the MCP server in your MCP client configuration (e.g., for Amazon Q De
91
123
 
92
124
  ### Add or update your LLM client's config with following:
93
125
 
126
+ #### Option 1: Using RDS Data API Connection (for Aurora Postgres)
127
+
94
128
  ```json
95
129
  {
96
130
  "mcpServers": {
@@ -115,8 +149,46 @@ Configure the MCP server in your MCP client configuration (e.g., for Amazon Q De
115
149
  }
116
150
  ```
117
151
 
152
+ #### Option 2: Using Direct PostgreSQL (psycopg) Connection (for Aurora Postgres and RDS Postgres)
153
+
154
+ ```
155
+ {
156
+ "mcpServers": {
157
+ "awslabs.postgres-mcp-server": {
158
+ "command": "docker",
159
+ "args": [
160
+ "run",
161
+ "-i",
162
+ "--rm",
163
+ "-e", "AWS_ACCESS_KEY_ID=[your data]",
164
+ "-e", "AWS_SECRET_ACCESS_KEY=[your data]",
165
+ "-e", "AWS_REGION=[your data]",
166
+ "awslabs/postgres-mcp-server:latest",
167
+ "--hostname", "[your data]",
168
+ "--secret_arn", "[your data]",
169
+ "--database", "[your data]",
170
+ "--region", "[your data]",
171
+ "--readonly", "True"
172
+ ]
173
+ }
174
+ }
175
+ }
176
+ ```
177
+
178
+ Note: The `--port` parameter is optional and defaults to 5432 (the standard PostgreSQL port). You only need to specify it if your PostgreSQL instance uses a non-standard port.
179
+
118
180
  NOTE: By default, only read-only queries are allowed and it is controlled by --readonly parameter above. Set it to False if you also want to allow writable DML or DDL.
119
181
 
182
+ ## Connection Methods
183
+
184
+ This MCP server supports two connection methods:
185
+
186
+ 1. **RDS Data API Connection** (using `--resource_arn`): Uses the AWS RDS Data API to connect to Aurora PostgreSQL. This method requires that your Aurora cluster has the Data API enabled.
187
+
188
+ 2. **Direct PostgreSQL Connection** (using `--hostname`): Uses psycopg to connect directly to any PostgreSQL database, including Aurora PostgreSQL, RDS PostgreSQL, or self-hosted PostgreSQL instances. This method provides better performance for frequent queries but requires direct network access to the database.
189
+
190
+ Choose the connection method that best fits your environment and requirements.
191
+
120
192
  ### AWS Authentication
121
193
 
122
194
  The MCP server uses the AWS profile specified in the `AWS_PROFILE` environment variable. If not provided, it defaults to the "default" profile in your AWS configuration file.
@@ -0,0 +1,15 @@
1
+ awslabs/__init__.py,sha256=WuqxdDgUZylWNmVoPKiK7qGsTB_G4UmuXIrJ-VBwDew,731
2
+ awslabs/postgres_mcp_server/__init__.py,sha256=Hmj3ZFqpAA8HzLCrR48AtfJthOFX7HH6_iNzE_E_2Q4,673
3
+ awslabs/postgres_mcp_server/mutable_sql_detector.py,sha256=bivLNglukgfi0IWcQSfDcwxFae4Q8CSvb--yWW617l0,2717
4
+ awslabs/postgres_mcp_server/server.py,sha256=wenII8rWhhUVVo-m2C2cPWVwS1S_Us3LUKmKQrr9ebw,11621
5
+ awslabs/postgres_mcp_server/connection/__init__.py,sha256=9j_gZeLcWoXu_Fny1PkC3RsnDMOHr9M3OM9EWvlGR7o,850
6
+ awslabs/postgres_mcp_server/connection/abstract_db_connection.py,sha256=BGdyRoF1fgjLoFSiWg-XdS0KJ1UKuNgV03gGcRXOG8U,2080
7
+ awslabs/postgres_mcp_server/connection/db_connection_singleton.py,sha256=Fwm6uTwB-bhprx-UgiOttDFtczeMUfW5M79wGL0x1Go,4279
8
+ awslabs/postgres_mcp_server/connection/psycopg_pool_connection.py,sha256=qHbRR8-P__v5_-_Q-AQdZVD_nTHj-2Y1p-j1RnKRmrs,12593
9
+ awslabs/postgres_mcp_server/connection/rds_api_connection.py,sha256=1G4qfik5Nu-KbAxRLFNshQ5DJxFnJQI6eqLJPXp-Qn4,5419
10
+ awslabs_postgres_mcp_server-1.0.4.dist-info/METADATA,sha256=aiAGagfCfoskIxv-rVx9mPqTt7rVbqhAf_Oq_DJUlcg,8410
11
+ awslabs_postgres_mcp_server-1.0.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
+ awslabs_postgres_mcp_server-1.0.4.dist-info/entry_points.txt,sha256=qHCxq_MTANxTB8-mA7Wl6H71qWEMwmG-I6_koII4AXY,88
13
+ awslabs_postgres_mcp_server-1.0.4.dist-info/licenses/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142
14
+ awslabs_postgres_mcp_server-1.0.4.dist-info/licenses/NOTICE,sha256=qMIIe3h7I1Y4-CKejn50wbSKXEZLWhYHdKaRwKdXN9M,95
15
+ awslabs_postgres_mcp_server-1.0.4.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- awslabs/__init__.py,sha256=WuqxdDgUZylWNmVoPKiK7qGsTB_G4UmuXIrJ-VBwDew,731
2
- awslabs/postgres_mcp_server/__init__.py,sha256=OpiWDc6HV8EoZVGVKb6ebzcIm7TUbZeALyFkLdm7YWg,673
3
- awslabs/postgres_mcp_server/mutable_sql_detector.py,sha256=bivLNglukgfi0IWcQSfDcwxFae4Q8CSvb--yWW617l0,2717
4
- awslabs/postgres_mcp_server/server.py,sha256=Hw8qPrjlqoKoPatP2485RN_ntXfwb09OIPYOAKyRtrk,14500
5
- awslabs_postgres_mcp_server-1.0.3.dist-info/METADATA,sha256=N7XIGheT6KY3uZguuG-1VpdJchn37MMX5Trc6w7uI90,5952
6
- awslabs_postgres_mcp_server-1.0.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
- awslabs_postgres_mcp_server-1.0.3.dist-info/entry_points.txt,sha256=qHCxq_MTANxTB8-mA7Wl6H71qWEMwmG-I6_koII4AXY,88
8
- awslabs_postgres_mcp_server-1.0.3.dist-info/licenses/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142
9
- awslabs_postgres_mcp_server-1.0.3.dist-info/licenses/NOTICE,sha256=qMIIe3h7I1Y4-CKejn50wbSKXEZLWhYHdKaRwKdXN9M,95
10
- awslabs_postgres_mcp_server-1.0.3.dist-info/RECORD,,