awslabs.mysql-mcp-server 1.0.7__py3-none-any.whl → 1.0.9__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.
- awslabs/mysql_mcp_server/__init__.py +1 -1
- awslabs/mysql_mcp_server/connection/__init__.py +18 -0
- awslabs/mysql_mcp_server/connection/abstract_db_connection.py +68 -0
- awslabs/mysql_mcp_server/connection/asyncmy_pool_connection.py +267 -0
- awslabs/mysql_mcp_server/connection/db_connection_singleton.py +142 -0
- awslabs/mysql_mcp_server/connection/rds_data_api_connection.py +93 -0
- awslabs/mysql_mcp_server/server.py +82 -145
- {awslabs_mysql_mcp_server-1.0.7.dist-info → awslabs_mysql_mcp_server-1.0.9.dist-info}/METADATA +44 -1
- awslabs_mysql_mcp_server-1.0.9.dist-info/RECORD +15 -0
- awslabs_mysql_mcp_server-1.0.7.dist-info/RECORD +0 -10
- {awslabs_mysql_mcp_server-1.0.7.dist-info → awslabs_mysql_mcp_server-1.0.9.dist-info}/WHEEL +0 -0
- {awslabs_mysql_mcp_server-1.0.7.dist-info → awslabs_mysql_mcp_server-1.0.9.dist-info}/entry_points.txt +0 -0
- {awslabs_mysql_mcp_server-1.0.7.dist-info → awslabs_mysql_mcp_server-1.0.9.dist-info}/licenses/LICENSE +0 -0
- {awslabs_mysql_mcp_server-1.0.7.dist-info → awslabs_mysql_mcp_server-1.0.9.dist-info}/licenses/NOTICE +0 -0
|
@@ -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.mysql-mcp-server.connection"""
|
|
16
|
+
|
|
17
|
+
from awslabs.mysql_mcp_server.connection.db_connection_singleton import DBConnectionSingleton
|
|
18
|
+
from awslabs.mysql_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 MySQL 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,267 @@
|
|
|
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
|
+
"""Python connector for MySQL and MariaDB MCP Server.
|
|
16
|
+
|
|
17
|
+
This connector provides direct connection to MySQL/MariaDB databases using asyncmy.
|
|
18
|
+
It supports both Aurora MySQL and RDS Mysql/MariaDB instances via direct connection.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import boto3
|
|
22
|
+
import json
|
|
23
|
+
from asyncmy import Pool, create_pool
|
|
24
|
+
from awslabs.mysql_mcp_server.connection.abstract_db_connection import AbstractDBConnection
|
|
25
|
+
from loguru import logger
|
|
26
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AsyncmyPoolConnection(AbstractDBConnection):
|
|
30
|
+
"""Class that wraps DB connection using asyncmy connection pool.
|
|
31
|
+
|
|
32
|
+
This class can connect directly to any MySQL and MariaDB database, including:
|
|
33
|
+
- Aurora MySQL (using the cluster endpoint)
|
|
34
|
+
- RDS MySQL and RDS MariaDB (using the instance endpoint)
|
|
35
|
+
- Self-hosted MySQL and MariaDB
|
|
36
|
+
|
|
37
|
+
It uses AWS Secrets Manager (secret_arn and region) for authentication.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
hostname: str,
|
|
43
|
+
port: int,
|
|
44
|
+
database: str,
|
|
45
|
+
readonly: bool,
|
|
46
|
+
secret_arn: str,
|
|
47
|
+
region: str,
|
|
48
|
+
min_size: int = 1,
|
|
49
|
+
max_size: int = 10,
|
|
50
|
+
):
|
|
51
|
+
"""Initialize a new DB connection pool.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
hostname: Database host (Aurora cluster endpoint or RDS instance endpoint)
|
|
55
|
+
port: Database port (default 3306)
|
|
56
|
+
database: Database name
|
|
57
|
+
readonly: Whether connections should be read-only
|
|
58
|
+
secret_arn: ARN of the secret containing credentials
|
|
59
|
+
region: AWS region for Secrets Manager
|
|
60
|
+
min_size: Minimum number of connections in the pool
|
|
61
|
+
max_size: Maximum number of connections in the pool
|
|
62
|
+
"""
|
|
63
|
+
super().__init__(readonly)
|
|
64
|
+
self.hostname = hostname
|
|
65
|
+
self.port = port
|
|
66
|
+
self.min_size = min_size
|
|
67
|
+
self.max_size = max_size
|
|
68
|
+
self.pool: Optional[Pool] = None
|
|
69
|
+
self.database = database
|
|
70
|
+
|
|
71
|
+
# Get credentials from Secrets Manager
|
|
72
|
+
logger.info(f'Retrieving credentials from Secrets Manager: {secret_arn}')
|
|
73
|
+
self.user, self.password = _get_credentials_from_secret(secret_arn, region)
|
|
74
|
+
logger.info(f'Successfully retrieved credentials for user: {self.user}')
|
|
75
|
+
|
|
76
|
+
async def initialize_pool(self):
|
|
77
|
+
"""Initialize the connection pool."""
|
|
78
|
+
if self.pool is None:
|
|
79
|
+
logger.info(
|
|
80
|
+
f'Initializing connection pool with min_size={self.min_size}, max_size={self.max_size}'
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
self.pool = await create_pool(
|
|
84
|
+
minsize=self.min_size,
|
|
85
|
+
maxsize=self.max_size,
|
|
86
|
+
host=self.hostname,
|
|
87
|
+
port=self.port,
|
|
88
|
+
user=self.user,
|
|
89
|
+
password=self.password,
|
|
90
|
+
db=self.database,
|
|
91
|
+
autocommit=True,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
logger.info('Connection pool initialized successfully')
|
|
95
|
+
|
|
96
|
+
if self._readonly:
|
|
97
|
+
await self._set_all_connections_readonly()
|
|
98
|
+
|
|
99
|
+
async def _set_all_connections_readonly(self):
|
|
100
|
+
"""Set all connections in the pool to read-only mode."""
|
|
101
|
+
if self.pool is None:
|
|
102
|
+
logger.warning('Connection pool is not initialized, cannot set read-only mode')
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
async with self.pool.acquire() as conn:
|
|
107
|
+
async with conn.cursor() as cursor:
|
|
108
|
+
await cursor.execute('SET SESSION TRANSACTION READ ONLY;')
|
|
109
|
+
logger.info('Successfully set connection to read-only mode')
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.warning(f'Failed to set connections to read-only mode: {str(e)}')
|
|
112
|
+
logger.warning('Continuing without setting read-only mode')
|
|
113
|
+
|
|
114
|
+
async def _get_connection(self):
|
|
115
|
+
"""Get a database connection from the pool."""
|
|
116
|
+
if self.pool is None:
|
|
117
|
+
await self.initialize_pool()
|
|
118
|
+
|
|
119
|
+
if self.pool is None:
|
|
120
|
+
raise ValueError('Failed to initialize connection pool')
|
|
121
|
+
|
|
122
|
+
return self.pool.acquire()
|
|
123
|
+
|
|
124
|
+
async def execute_query(
|
|
125
|
+
self, sql: str, parameters: Optional[List[Dict[str, Any]]] = None
|
|
126
|
+
) -> Dict[str, Any]:
|
|
127
|
+
"""Execute a SQL query using async connection."""
|
|
128
|
+
try:
|
|
129
|
+
async with await self._get_connection() as conn:
|
|
130
|
+
async with conn.cursor() as cursor:
|
|
131
|
+
if self._readonly:
|
|
132
|
+
await cursor.execute('SET TRANSACTION READ ONLY')
|
|
133
|
+
# Execute the query
|
|
134
|
+
if parameters:
|
|
135
|
+
params = list(_convert_parameters(self, parameters).values())
|
|
136
|
+
await cursor.execute(sql, params)
|
|
137
|
+
else:
|
|
138
|
+
await cursor.execute(sql)
|
|
139
|
+
|
|
140
|
+
# Check if there are results to fetch by examining the cursor's description
|
|
141
|
+
if cursor.description:
|
|
142
|
+
# Get column names
|
|
143
|
+
columns = [desc[0] for desc in cursor.description]
|
|
144
|
+
|
|
145
|
+
# Fetch all rows
|
|
146
|
+
rows = await cursor.fetchall()
|
|
147
|
+
|
|
148
|
+
# Structure the response to match the interface contract required by server.py
|
|
149
|
+
column_metadata = [{'label': col} for col in columns]
|
|
150
|
+
records = []
|
|
151
|
+
|
|
152
|
+
# Convert each row to the expected format
|
|
153
|
+
for row in rows:
|
|
154
|
+
record = []
|
|
155
|
+
for value in row:
|
|
156
|
+
if value is None:
|
|
157
|
+
record.append({'isNull': True})
|
|
158
|
+
elif isinstance(value, str):
|
|
159
|
+
record.append({'stringValue': value})
|
|
160
|
+
elif isinstance(value, int):
|
|
161
|
+
record.append({'longValue': value})
|
|
162
|
+
elif isinstance(value, float):
|
|
163
|
+
record.append({'doubleValue': value})
|
|
164
|
+
elif isinstance(value, bool):
|
|
165
|
+
record.append({'booleanValue': value})
|
|
166
|
+
elif isinstance(value, bytes):
|
|
167
|
+
record.append({'blobValue': value})
|
|
168
|
+
else:
|
|
169
|
+
# Convert other types to string
|
|
170
|
+
record.append({'stringValue': str(value)})
|
|
171
|
+
records.append(record)
|
|
172
|
+
|
|
173
|
+
return {'columnMetadata': column_metadata, 'records': records}
|
|
174
|
+
else:
|
|
175
|
+
# No results (e.g., for INSERT, UPDATE, etc.)
|
|
176
|
+
return {'columnMetadata': [], 'records': []}
|
|
177
|
+
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.error(f'Database connection error: {str(e)}')
|
|
180
|
+
raise e
|
|
181
|
+
|
|
182
|
+
async def close(self) -> None:
|
|
183
|
+
"""Close all connections in the pool."""
|
|
184
|
+
if self.pool is not None:
|
|
185
|
+
logger.info('Closing connection pool')
|
|
186
|
+
await self.pool.close()
|
|
187
|
+
self.pool = None
|
|
188
|
+
logger.info('Connection pool closed successfully')
|
|
189
|
+
|
|
190
|
+
async def check_connection_health(self) -> bool:
|
|
191
|
+
"""Check if the connection is healthy."""
|
|
192
|
+
try:
|
|
193
|
+
result = await self.execute_query('SELECT 1')
|
|
194
|
+
return len(result.get('records', [])) > 0
|
|
195
|
+
except Exception as e:
|
|
196
|
+
logger.error(f'Connection health check failed: {str(e)}')
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _convert_parameters(self, parameters: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
201
|
+
"""Transform structured parameter format to psycopg's native parameter format."""
|
|
202
|
+
result = {}
|
|
203
|
+
for param in parameters:
|
|
204
|
+
name = param.get('name')
|
|
205
|
+
value = param.get('value', {})
|
|
206
|
+
|
|
207
|
+
# Extract the value based on its type
|
|
208
|
+
if 'stringValue' in value:
|
|
209
|
+
result[name] = value['stringValue']
|
|
210
|
+
elif 'longValue' in value:
|
|
211
|
+
result[name] = value['longValue']
|
|
212
|
+
elif 'doubleValue' in value:
|
|
213
|
+
result[name] = value['doubleValue']
|
|
214
|
+
elif 'booleanValue' in value:
|
|
215
|
+
result[name] = value['booleanValue']
|
|
216
|
+
elif 'blobValue' in value:
|
|
217
|
+
result[name] = value['blobValue']
|
|
218
|
+
elif 'isNull' in value and value['isNull']:
|
|
219
|
+
result[name] = None
|
|
220
|
+
|
|
221
|
+
return result
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _get_credentials_from_secret(secret_arn: str, region: str) -> Tuple[str, str]:
|
|
225
|
+
"""Get database credentials from AWS Secrets Manager."""
|
|
226
|
+
try:
|
|
227
|
+
# Create a Secrets Manager client
|
|
228
|
+
logger.info(f'Creating Secrets Manager client in region {region}')
|
|
229
|
+
session = boto3.Session()
|
|
230
|
+
client = session.client(service_name='secretsmanager', region_name=region)
|
|
231
|
+
|
|
232
|
+
# Get the secret value
|
|
233
|
+
logger.info(f'Retrieving secret value for {secret_arn}')
|
|
234
|
+
get_secret_value_response = client.get_secret_value(SecretId=secret_arn)
|
|
235
|
+
logger.info('Successfully retrieved secret value')
|
|
236
|
+
|
|
237
|
+
# Parse the secret string
|
|
238
|
+
if 'SecretString' in get_secret_value_response:
|
|
239
|
+
secret = json.loads(get_secret_value_response['SecretString'])
|
|
240
|
+
logger.info(f'Secret keys: {", ".join(secret.keys())}')
|
|
241
|
+
|
|
242
|
+
# Extract username and password
|
|
243
|
+
username = secret.get('username') or secret.get('user') or secret.get('Username')
|
|
244
|
+
password = secret.get('password') or secret.get('Password')
|
|
245
|
+
|
|
246
|
+
if not username:
|
|
247
|
+
logger.error(
|
|
248
|
+
f'Username not found in secret. Available keys: {", ".join(secret.keys())}'
|
|
249
|
+
)
|
|
250
|
+
raise ValueError(
|
|
251
|
+
f'Secret does not contain username. Available keys: {", ".join(secret.keys())}'
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
if not password:
|
|
255
|
+
logger.error('Password not found in secret')
|
|
256
|
+
raise ValueError(
|
|
257
|
+
f'Secret does not contain password. Available keys: {", ".join(secret.keys())}'
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
logger.info(f'Successfully extracted credentials for user: {username}')
|
|
261
|
+
return username, password
|
|
262
|
+
else:
|
|
263
|
+
logger.error('Secret does not contain a SecretString')
|
|
264
|
+
raise ValueError('Secret does not contain a SecretString')
|
|
265
|
+
except Exception as e:
|
|
266
|
+
logger.error(f'Error retrieving secret: {str(e)}')
|
|
267
|
+
raise ValueError(f'Failed to retrieve credentials from Secrets Manager: {str(e)}')
|
|
@@ -0,0 +1,142 @@
|
|
|
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 MySQL and MariaDB MCP Server."""
|
|
16
|
+
|
|
17
|
+
from awslabs.mysql_mcp_server.connection.asyncmy_pool_connection import AsyncmyPoolConnection
|
|
18
|
+
from awslabs.mysql_mcp_server.connection.rds_data_api_connection import RDSDataAPIConnection
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DBConnectionSingleton:
|
|
22
|
+
"""Manages a single database connection instance across the application."""
|
|
23
|
+
|
|
24
|
+
_instance = None
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
secret_arn: str,
|
|
29
|
+
database: str,
|
|
30
|
+
region: str,
|
|
31
|
+
readonly: bool = True,
|
|
32
|
+
is_test: bool = False,
|
|
33
|
+
resource_arn: str | None = None,
|
|
34
|
+
hostname: str | None = None,
|
|
35
|
+
port: int | None = None,
|
|
36
|
+
):
|
|
37
|
+
"""Initialize a new DB connection singleton using one of the two connection types.
|
|
38
|
+
|
|
39
|
+
1. RDS Data API Connection by specifying resource ARN
|
|
40
|
+
2. Direct MySQL or MariaDB connection by specifying hostname and port
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
secret_arn: The ARN of the secret containing credentials
|
|
44
|
+
database: The name of the database to connect to
|
|
45
|
+
region: The AWS region where the RDS instance is located
|
|
46
|
+
readonly: Whether the connection should be read-only (default: True)
|
|
47
|
+
resource_arn: The ARN of the RDS cluster (for using RDS Data API)
|
|
48
|
+
hostname: Database hostname (for using direct MySQL connection)
|
|
49
|
+
port: Database port (for using direct MySQL connection)
|
|
50
|
+
is_test: Whether this is a test connection (default: False)
|
|
51
|
+
"""
|
|
52
|
+
if resource_arn:
|
|
53
|
+
if not all([resource_arn, secret_arn, database, region]):
|
|
54
|
+
raise ValueError(
|
|
55
|
+
'Missing required connection parameters for RDS Data API. '
|
|
56
|
+
'Please provide resource_arn, secret_arn, database, and region.'
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
self._db_connection = RDSDataAPIConnection(
|
|
60
|
+
cluster_arn=resource_arn,
|
|
61
|
+
secret_arn=secret_arn,
|
|
62
|
+
database=database,
|
|
63
|
+
region=region,
|
|
64
|
+
readonly=readonly,
|
|
65
|
+
is_test=is_test,
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
# Direct connection to MySQL/MariaDB
|
|
69
|
+
if not all([hostname, port, secret_arn, database, region]):
|
|
70
|
+
raise ValueError(
|
|
71
|
+
'Missing required connection parameters for direct MySQL connection. '
|
|
72
|
+
'Please provide hostname, port, secret_arn, database, and region.'
|
|
73
|
+
)
|
|
74
|
+
assert hostname is not None
|
|
75
|
+
assert port is not None
|
|
76
|
+
|
|
77
|
+
self._db_connection = AsyncmyPoolConnection(
|
|
78
|
+
hostname=hostname,
|
|
79
|
+
port=port,
|
|
80
|
+
secret_arn=secret_arn,
|
|
81
|
+
database=database,
|
|
82
|
+
region=region,
|
|
83
|
+
readonly=readonly,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def initialize(
|
|
88
|
+
cls,
|
|
89
|
+
secret_arn: str,
|
|
90
|
+
database: str,
|
|
91
|
+
region: str,
|
|
92
|
+
readonly: bool = True,
|
|
93
|
+
is_test: bool = False,
|
|
94
|
+
resource_arn: str | None = None,
|
|
95
|
+
hostname: str | None = None,
|
|
96
|
+
port: int | None = None,
|
|
97
|
+
):
|
|
98
|
+
"""Initialize the singleton instance if it doesn't exist.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
resource_arn: The ARN of the RDS cluster (for using RDS Data API)
|
|
102
|
+
hostname: Database hostname (for using direct MySQL/MariaDB connection)
|
|
103
|
+
port: Database port (for using direct MySQL/MariaDB connection)
|
|
104
|
+
secret_arn: The ARN of the secret containing credentials
|
|
105
|
+
database: The name of the database to connect to
|
|
106
|
+
region: The AWS region where the RDS instance is located
|
|
107
|
+
readonly: Whether the connection should be read-only (default: True)
|
|
108
|
+
is_test: Whether this is a test connection (default: False)
|
|
109
|
+
"""
|
|
110
|
+
if cls._instance is None:
|
|
111
|
+
cls._instance = cls(
|
|
112
|
+
secret_arn=secret_arn,
|
|
113
|
+
database=database,
|
|
114
|
+
region=region,
|
|
115
|
+
readonly=readonly,
|
|
116
|
+
resource_arn=resource_arn,
|
|
117
|
+
hostname=hostname,
|
|
118
|
+
port=port,
|
|
119
|
+
is_test=is_test,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def get(cls):
|
|
124
|
+
"""Get the singleton instance.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
DBConnectionSingleton: The singleton instance
|
|
128
|
+
Raises:
|
|
129
|
+
RuntimeError: If the singleton has not been initialized
|
|
130
|
+
"""
|
|
131
|
+
if cls._instance is None:
|
|
132
|
+
raise RuntimeError('DBConnectionSingleton is not initialized.')
|
|
133
|
+
return cls._instance
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def db_connection(self):
|
|
137
|
+
"""Get the database connection.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
DBConnection: The database connection instance
|
|
141
|
+
"""
|
|
142
|
+
return self._db_connection
|
|
@@ -0,0 +1,93 @@
|
|
|
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 MySQL MCP Server."""
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import boto3
|
|
19
|
+
from awslabs.mysql_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 Data 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 Aurora MySQL 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
|
+
Returns:
|
|
62
|
+
Dict containing query results with column metadata and records
|
|
63
|
+
"""
|
|
64
|
+
execute_params = {
|
|
65
|
+
'resourceArn': self.cluster_arn,
|
|
66
|
+
'secretArn': self.secret_arn,
|
|
67
|
+
'database': self.database,
|
|
68
|
+
'sql': sql,
|
|
69
|
+
'includeResultMetadata': True,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if parameters:
|
|
73
|
+
execute_params['parameters'] = parameters
|
|
74
|
+
|
|
75
|
+
return await asyncio.to_thread(self.data_client.execute_statement, **execute_params)
|
|
76
|
+
|
|
77
|
+
async def close(self) -> None:
|
|
78
|
+
"""Close the database connection asynchronously."""
|
|
79
|
+
# RDS Data API doesn't maintain persistent connections
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
async def check_connection_health(self) -> bool:
|
|
83
|
+
"""Check if the RDS Data API connection is healthy.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
bool: True if the connection is healthy, False otherwise
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
result = await self.execute_query('SELECT 1')
|
|
90
|
+
return len(result.get('records', [])) > 0
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.error(f'RDS Data API connection health check failed: {str(e)}')
|
|
93
|
+
return False
|
|
@@ -16,13 +16,14 @@
|
|
|
16
16
|
|
|
17
17
|
import argparse
|
|
18
18
|
import asyncio
|
|
19
|
-
import boto3
|
|
20
19
|
import sys
|
|
20
|
+
from awslabs.mysql_mcp_server.connection import DBConnectionSingleton
|
|
21
|
+
from awslabs.mysql_mcp_server.connection.asyncmy_pool_connection import AsyncmyPoolConnection
|
|
21
22
|
from awslabs.mysql_mcp_server.mutable_sql_detector import (
|
|
22
23
|
check_sql_injection_risk,
|
|
23
24
|
detect_mutating_keywords,
|
|
24
25
|
)
|
|
25
|
-
from botocore.exceptions import
|
|
26
|
+
from botocore.exceptions import ClientError
|
|
26
27
|
from loguru import logger
|
|
27
28
|
from mcp.server.fastmcp import Context, FastMCP
|
|
28
29
|
from pydantic import Field
|
|
@@ -48,104 +49,6 @@ class DummyCtx:
|
|
|
48
49
|
pass
|
|
49
50
|
|
|
50
51
|
|
|
51
|
-
class DBConnection:
|
|
52
|
-
"""Class that wraps DB connection client by RDS API."""
|
|
53
|
-
|
|
54
|
-
def __init__(self, cluster_arn, secret_arn, database, region, readonly, is_test=False):
|
|
55
|
-
"""Initialize a new DB connection.
|
|
56
|
-
|
|
57
|
-
Args:
|
|
58
|
-
cluster_arn: The ARN of the RDS cluster
|
|
59
|
-
secret_arn: The ARN of the secret containing credentials
|
|
60
|
-
database: The name of the database to connect to
|
|
61
|
-
region: The AWS region where the RDS instance is located
|
|
62
|
-
readonly: Whether the connection should be read-only
|
|
63
|
-
is_test: Whether this is a test connection
|
|
64
|
-
"""
|
|
65
|
-
self.cluster_arn = cluster_arn
|
|
66
|
-
self.secret_arn = secret_arn
|
|
67
|
-
self.database = database
|
|
68
|
-
self.readonly = readonly
|
|
69
|
-
if not is_test:
|
|
70
|
-
self.data_client = boto3.client('rds-data', region_name=region)
|
|
71
|
-
|
|
72
|
-
@property
|
|
73
|
-
def readonly_query(self):
|
|
74
|
-
"""Get whether this connection is read-only.
|
|
75
|
-
|
|
76
|
-
Returns:
|
|
77
|
-
bool: True if the connection is read-only, False otherwise
|
|
78
|
-
"""
|
|
79
|
-
return self.readonly
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
class DBConnectionSingleton:
|
|
83
|
-
"""Manages a single DBConnection instance across the application.
|
|
84
|
-
|
|
85
|
-
This singleton ensures that only one DBConnection is created and reused.
|
|
86
|
-
"""
|
|
87
|
-
|
|
88
|
-
_instance = None
|
|
89
|
-
|
|
90
|
-
def __init__(self, resource_arn, secret_arn, database, region, readonly, is_test=False):
|
|
91
|
-
"""Initialize a new DB connection singleton.
|
|
92
|
-
|
|
93
|
-
Args:
|
|
94
|
-
resource_arn: The ARN of the RDS resource
|
|
95
|
-
secret_arn: The ARN of the secret containing credentials
|
|
96
|
-
database: The name of the database to connect to
|
|
97
|
-
region: The AWS region where the RDS instance is located
|
|
98
|
-
readonly: Whether the connection should be read-only
|
|
99
|
-
is_test: Whether this is a test connection
|
|
100
|
-
"""
|
|
101
|
-
if not all([resource_arn, secret_arn, database, region]):
|
|
102
|
-
raise ValueError(
|
|
103
|
-
'Missing required connection parameters. '
|
|
104
|
-
'Please provide resource_arn, secret_arn, database, and region.'
|
|
105
|
-
)
|
|
106
|
-
self._db_connection = DBConnection(
|
|
107
|
-
resource_arn, secret_arn, database, region, readonly, is_test
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
@classmethod
|
|
111
|
-
def initialize(cls, resource_arn, secret_arn, database, region, readonly, is_test=False):
|
|
112
|
-
"""Initialize the singleton instance if it doesn't exist.
|
|
113
|
-
|
|
114
|
-
Args:
|
|
115
|
-
resource_arn: The ARN of the RDS resource
|
|
116
|
-
secret_arn: The ARN of the secret containing credentials
|
|
117
|
-
database: The name of the database to connect to
|
|
118
|
-
region: The AWS region where the RDS instance is located
|
|
119
|
-
readonly: Whether the connection should be read-only
|
|
120
|
-
is_test: Whether this is a test connection
|
|
121
|
-
"""
|
|
122
|
-
if cls._instance is None:
|
|
123
|
-
cls._instance = cls(resource_arn, secret_arn, database, region, readonly, is_test)
|
|
124
|
-
|
|
125
|
-
@classmethod
|
|
126
|
-
def get(cls):
|
|
127
|
-
"""Get the singleton instance.
|
|
128
|
-
|
|
129
|
-
Returns:
|
|
130
|
-
DBConnectionSingleton: The singleton instance
|
|
131
|
-
|
|
132
|
-
Raises:
|
|
133
|
-
RuntimeError: If the singleton has not been initialized
|
|
134
|
-
"""
|
|
135
|
-
if cls._instance is None:
|
|
136
|
-
raise RuntimeError('DBConnectionSingleton is not initialized.')
|
|
137
|
-
return cls._instance
|
|
138
|
-
|
|
139
|
-
@property
|
|
140
|
-
def db_connection(self):
|
|
141
|
-
"""Get the database connection.
|
|
142
|
-
|
|
143
|
-
Returns:
|
|
144
|
-
DBConnection: The database connection instance
|
|
145
|
-
"""
|
|
146
|
-
return self._db_connection
|
|
147
|
-
|
|
148
|
-
|
|
149
52
|
def extract_cell(cell: dict):
|
|
150
53
|
"""Extracts the scalar or array value from a single cell."""
|
|
151
54
|
if cell.get('isNull'):
|
|
@@ -209,6 +112,9 @@ async def run_query(
|
|
|
209
112
|
if db_connection is None:
|
|
210
113
|
db_connection = DBConnectionSingleton.get().db_connection
|
|
211
114
|
|
|
115
|
+
if db_connection is None:
|
|
116
|
+
raise AssertionError('db_connection should never be None')
|
|
117
|
+
|
|
212
118
|
if db_connection.readonly_query:
|
|
213
119
|
matches = detect_mutating_keywords(sql)
|
|
214
120
|
if (bool)(matches):
|
|
@@ -232,20 +138,8 @@ async def run_query(
|
|
|
232
138
|
try:
|
|
233
139
|
logger.info(f'run_query: readonly:{db_connection.readonly_query}, SQL:{sql}')
|
|
234
140
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
'secretArn': db_connection.secret_arn,
|
|
238
|
-
'database': db_connection.database,
|
|
239
|
-
'sql': sql,
|
|
240
|
-
'includeResultMetadata': True,
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if query_parameters:
|
|
244
|
-
execute_params['parameters'] = query_parameters
|
|
245
|
-
|
|
246
|
-
response = await asyncio.to_thread(
|
|
247
|
-
db_connection.data_client.execute_statement, **execute_params
|
|
248
|
-
)
|
|
141
|
+
# Execute the query using the abstract connection interface
|
|
142
|
+
response = await db_connection.execute_query(sql, query_parameters)
|
|
249
143
|
|
|
250
144
|
logger.success('run_query successfully executed query:{}', sql)
|
|
251
145
|
return parse_execute_response(response)
|
|
@@ -300,9 +194,16 @@ async def get_table_schema(
|
|
|
300
194
|
ORDER BY
|
|
301
195
|
ORDINAL_POSITION
|
|
302
196
|
"""
|
|
197
|
+
db_connection = DBConnectionSingleton.get().db_connection
|
|
198
|
+
|
|
199
|
+
if isinstance(db_connection, AsyncmyPoolConnection):
|
|
200
|
+
# Convert to positional parameters for asyncmy
|
|
201
|
+
sql = sql.replace(':database_name', '%s').replace(':table_name', '%s')
|
|
202
|
+
|
|
203
|
+
# Use consistent parameter order matching SQL placeholders
|
|
303
204
|
params = [
|
|
304
|
-
{'name': 'table_name', 'value': {'stringValue': table_name}},
|
|
305
205
|
{'name': 'database_name', 'value': {'stringValue': database_name}},
|
|
206
|
+
{'name': 'table_name', 'value': {'stringValue': table_name}},
|
|
306
207
|
]
|
|
307
208
|
|
|
308
209
|
return await run_query(sql=sql, ctx=ctx, query_parameters=params)
|
|
@@ -316,51 +217,87 @@ def main():
|
|
|
316
217
|
parser = argparse.ArgumentParser(
|
|
317
218
|
description='An AWS Labs Model Context Protocol (MCP) server for MySQL'
|
|
318
219
|
)
|
|
319
|
-
|
|
220
|
+
|
|
221
|
+
# Connection method 1: RDS Data API for Aurora MySQL
|
|
222
|
+
parser.add_argument('--resource_arn', help='ARN of the Aurora MySQL cluster')
|
|
223
|
+
|
|
224
|
+
# Connection method 2: asyncmy for RDS MySQL and RDS MariaDB
|
|
225
|
+
parser.add_argument('--hostname', help='RDS MySQL Database hostname')
|
|
226
|
+
parser.add_argument('--port', type=int, default=3306, help='Database port (default: 3306)')
|
|
227
|
+
|
|
320
228
|
parser.add_argument(
|
|
321
229
|
'--secret_arn',
|
|
322
230
|
required=True,
|
|
323
231
|
help='ARN of the Secrets Manager secret for database credentials',
|
|
324
232
|
)
|
|
325
233
|
parser.add_argument('--database', required=True, help='Database name')
|
|
326
|
-
parser.add_argument(
|
|
327
|
-
'--region', required=True, help='AWS region for RDS Data API (default: us-west-2)'
|
|
328
|
-
)
|
|
234
|
+
parser.add_argument('--region', required=True, help='AWS region')
|
|
329
235
|
parser.add_argument(
|
|
330
236
|
'--readonly', required=True, help='Enforce NL to SQL to only allow readonly sql statement'
|
|
331
237
|
)
|
|
332
238
|
args = parser.parse_args()
|
|
333
239
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
args.secret_arn,
|
|
338
|
-
args.region,
|
|
339
|
-
args.database,
|
|
340
|
-
args.readonly,
|
|
341
|
-
)
|
|
240
|
+
# Validate connection parameters
|
|
241
|
+
if not args.resource_arn and not args.hostname:
|
|
242
|
+
parser.error('Either --resource_arn or --hostname must be provided')
|
|
342
243
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
244
|
+
if args.resource_arn and args.hostname:
|
|
245
|
+
parser.error(
|
|
246
|
+
'Cannot specify both --resource_arn and --hostname. Choose one connection method.'
|
|
346
247
|
)
|
|
347
|
-
except BotoCoreError:
|
|
348
|
-
logger.exception('Failed to RDS API client object for MySQL. Exit the MCP server')
|
|
349
|
-
sys.exit(1)
|
|
350
248
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
249
|
+
if args.resource_arn:
|
|
250
|
+
logger.info(
|
|
251
|
+
f'MySQL MCP init with RDS Data API: CONNECTION_TARGET:{args.resource_arn}, SECRET_ARN:{args.secret_arn}, REGION:{args.region}, DATABASE:{args.database}, READONLY:{args.readonly}'
|
|
252
|
+
)
|
|
253
|
+
else:
|
|
254
|
+
logger.info(
|
|
255
|
+
f'MySQL/MariaDB MCP init with asyncmy: CONNECTION_TARGET:{args.hostname}, PORT:{args.port}, DATABASE:{args.database}, READONLY:{args.readonly}'
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Create the appropriate database connection based on the provided parameters
|
|
259
|
+
try:
|
|
260
|
+
if args.resource_arn:
|
|
261
|
+
# Use RDS Data API with singleton pattern
|
|
262
|
+
DBConnectionSingleton.initialize(
|
|
263
|
+
resource_arn=args.resource_arn,
|
|
264
|
+
secret_arn=args.secret_arn,
|
|
265
|
+
database=args.database,
|
|
266
|
+
region=args.region,
|
|
267
|
+
readonly=args.readonly.lower(),
|
|
268
|
+
)
|
|
362
269
|
|
|
363
|
-
|
|
270
|
+
# Test database connection
|
|
271
|
+
db_connection = DBConnectionSingleton.get().db_connection
|
|
272
|
+
ctx = DummyCtx()
|
|
273
|
+
response = asyncio.run(run_query('SELECT 1', ctx, db_connection))
|
|
274
|
+
|
|
275
|
+
if (
|
|
276
|
+
isinstance(response, list)
|
|
277
|
+
and len(response) == 1
|
|
278
|
+
and isinstance(response[0], dict)
|
|
279
|
+
and 'error' in response[0]
|
|
280
|
+
):
|
|
281
|
+
logger.error(
|
|
282
|
+
'Failed to validate database connection to MySQL. Exit the MCP server'
|
|
283
|
+
)
|
|
284
|
+
sys.exit(1)
|
|
285
|
+
|
|
286
|
+
logger.success('Successfully validated database connection to MySQL')
|
|
287
|
+
else:
|
|
288
|
+
# Use direct MySQL connection singleton with asyncmy
|
|
289
|
+
# note: asyncmy pools are tied to their event loop, so testing DB connection must run inside MCP's loop.
|
|
290
|
+
DBConnectionSingleton.initialize(
|
|
291
|
+
secret_arn=args.secret_arn,
|
|
292
|
+
database=args.database,
|
|
293
|
+
region=args.region,
|
|
294
|
+
readonly=args.readonly.lower(),
|
|
295
|
+
hostname=args.hostname,
|
|
296
|
+
port=args.port,
|
|
297
|
+
)
|
|
298
|
+
except Exception as e:
|
|
299
|
+
logger.exception(f'Failed to create MySQL connection: {str(e)}')
|
|
300
|
+
sys.exit(1)
|
|
364
301
|
|
|
365
302
|
# Run server with appropriate transport
|
|
366
303
|
logger.info('Starting MySQL MCP server')
|
{awslabs_mysql_mcp_server-1.0.7.dist-info → awslabs_mysql_mcp_server-1.0.9.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: awslabs.mysql-mcp-server
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.9
|
|
4
4
|
Summary: An AWS Labs Model Context Protocol (MCP) server for mysql
|
|
5
5
|
Project-URL: homepage, https://awslabs.github.io/mcp/
|
|
6
6
|
Project-URL: docs, https://awslabs.github.io/mcp/servers/mysql-mcp-server/
|
|
@@ -21,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
23
|
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: asyncmy>=0.2.10
|
|
24
25
|
Requires-Dist: boto3>=1.38.14
|
|
25
26
|
Requires-Dist: botocore>=1.38.14
|
|
26
27
|
Requires-Dist: loguru>=0.7.0
|
|
@@ -58,6 +59,18 @@ An AWS Labs Model Context Protocol (MCP) server for Aurora MySQL
|
|
|
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
|
+
## Connection Methods
|
|
63
|
+
|
|
64
|
+
This MCP server supports two connection methods:
|
|
65
|
+
|
|
66
|
+
1. **RDS Data API Connection** (using `--resource_arn`): Uses the AWS RDS Data API to connect to Aurora MySQL. This method requires that your Aurora cluster has the Data API enabled.
|
|
67
|
+
|
|
68
|
+
2. **Direct MySQL Connection** (using `--hostname`): Uses asyncmy to connect directly to any MySQL database, including Aurora MySQL, RDS MySQL, RDS MariaDB, or self-hosted MySQL/MariaDB instances.
|
|
69
|
+
|
|
70
|
+
Choose the connection method that best fits your environment and requirements.
|
|
71
|
+
|
|
72
|
+
### Option 1: Using RDS Data API Connection (for Aurora MySQL)
|
|
73
|
+
|
|
61
74
|
```json
|
|
62
75
|
{
|
|
63
76
|
"mcpServers": {
|
|
@@ -82,6 +95,36 @@ Configure the MCP server in your MCP client configuration (e.g., for Amazon Q De
|
|
|
82
95
|
}
|
|
83
96
|
}
|
|
84
97
|
```
|
|
98
|
+
|
|
99
|
+
### Option 2: Using Direct MySQL Connection (for Aurora MySQL, RDS MySQL, and RDS MariaDB)
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"mcpServers": {
|
|
104
|
+
"awslabs.mysql-mcp-server": {
|
|
105
|
+
"command": "uvx",
|
|
106
|
+
"args": [
|
|
107
|
+
"awslabs.mysql-mcp-server@latest",
|
|
108
|
+
"--hostname", "[your data]",
|
|
109
|
+
"--secret_arn", "[your data]",
|
|
110
|
+
"--database", "[your data]",
|
|
111
|
+
"--region", "[your data]",
|
|
112
|
+
"--readonly", "True"
|
|
113
|
+
],
|
|
114
|
+
"env": {
|
|
115
|
+
"AWS_PROFILE": "your-aws-profile",
|
|
116
|
+
"AWS_REGION": "us-east-1",
|
|
117
|
+
"FASTMCP_LOG_LEVEL": "ERROR"
|
|
118
|
+
},
|
|
119
|
+
"disabled": false,
|
|
120
|
+
"autoApprove": []
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Note: The `--port` parameter is optional and defaults to 3306 (the standard MySQL port). You only need to specify it if your MySQL instance uses a non-default port.
|
|
127
|
+
|
|
85
128
|
### Windows Installation
|
|
86
129
|
|
|
87
130
|
For Windows users, the MCP server configuration format is slightly different:
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
awslabs/__init__.py,sha256=WuqxdDgUZylWNmVoPKiK7qGsTB_G4UmuXIrJ-VBwDew,731
|
|
2
|
+
awslabs/mysql_mcp_server/__init__.py,sha256=4YCpk9z418hrbzBvxvmfiO5CUS2jsotSGe91htH2hTM,670
|
|
3
|
+
awslabs/mysql_mcp_server/mutable_sql_detector.py,sha256=0apKy3GK8WOxKZshj7vKTRcLdSIM7GA6Ee_8-dr41UQ,4224
|
|
4
|
+
awslabs/mysql_mcp_server/server.py,sha256=whlRuMTLOGTh13VRXDq8-LFydRwebEJubBSpZxoHM6Q,10977
|
|
5
|
+
awslabs/mysql_mcp_server/connection/__init__.py,sha256=q63Wdy4cI71kRtNhxh8NHi_O_DWIIZiyv3Cg9usiBcY,841
|
|
6
|
+
awslabs/mysql_mcp_server/connection/abstract_db_connection.py,sha256=OpOxex4gxq8AY7Fp3rztFG6uJmGa_lx3gK9v_7WScqM,2077
|
|
7
|
+
awslabs/mysql_mcp_server/connection/asyncmy_pool_connection.py,sha256=Z6MdLiW7fNHy2EpXcCc7Q4tjupvpSpqKTwfqceluZ5A,11097
|
|
8
|
+
awslabs/mysql_mcp_server/connection/db_connection_singleton.py,sha256=RMJJyAX_IE3S8k_zmXGQ-dqvUUaEKELGAAYieFWjkP0,5371
|
|
9
|
+
awslabs/mysql_mcp_server/connection/rds_data_api_connection.py,sha256=OAQHuRXxXcjrPYY9bJPh-b8q2XvNrv1aXiCO5eDwQKc,3310
|
|
10
|
+
awslabs_mysql_mcp_server-1.0.9.dist-info/METADATA,sha256=E_MjjVxP2Du3xDEl3cyWMdnb9mW9Pwuz3LuYiYeRxlU,8059
|
|
11
|
+
awslabs_mysql_mcp_server-1.0.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
12
|
+
awslabs_mysql_mcp_server-1.0.9.dist-info/entry_points.txt,sha256=dQAG1BpfKE6diUKtCqzxwvWfPxDWGIEa87YcEb0T4rc,82
|
|
13
|
+
awslabs_mysql_mcp_server-1.0.9.dist-info/licenses/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142
|
|
14
|
+
awslabs_mysql_mcp_server-1.0.9.dist-info/licenses/NOTICE,sha256=lpdr2_2JGgD-RqTEnqMJttKCZVvC88gX4rbk0m8UifU,92
|
|
15
|
+
awslabs_mysql_mcp_server-1.0.9.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
awslabs/__init__.py,sha256=WuqxdDgUZylWNmVoPKiK7qGsTB_G4UmuXIrJ-VBwDew,731
|
|
2
|
-
awslabs/mysql_mcp_server/__init__.py,sha256=nt4WYQGNSIByqO4cQ12XSr8WxsWBhV9jfk4DEAT_L_A,670
|
|
3
|
-
awslabs/mysql_mcp_server/mutable_sql_detector.py,sha256=0apKy3GK8WOxKZshj7vKTRcLdSIM7GA6Ee_8-dr41UQ,4224
|
|
4
|
-
awslabs/mysql_mcp_server/server.py,sha256=3K50dr1-snYZwcMIB7OwRI3eF0ietpmCddfF02LBKrI,12502
|
|
5
|
-
awslabs_mysql_mcp_server-1.0.7.dist-info/METADATA,sha256=3G8J-HaXeb-Sa34xMpDdM_QuKn0I0S4GkfqFvWXdFH4,6634
|
|
6
|
-
awslabs_mysql_mcp_server-1.0.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
-
awslabs_mysql_mcp_server-1.0.7.dist-info/entry_points.txt,sha256=dQAG1BpfKE6diUKtCqzxwvWfPxDWGIEa87YcEb0T4rc,82
|
|
8
|
-
awslabs_mysql_mcp_server-1.0.7.dist-info/licenses/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142
|
|
9
|
-
awslabs_mysql_mcp_server-1.0.7.dist-info/licenses/NOTICE,sha256=lpdr2_2JGgD-RqTEnqMJttKCZVvC88gX4rbk0m8UifU,92
|
|
10
|
-
awslabs_mysql_mcp_server-1.0.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|