Skip to content

Commit 6c9a73d

Browse files
authored
fix(rds_mysql)support the creation of polling instances (#205)
1 parent d004aa9 commit 6c9a73d

3 files changed

Lines changed: 307 additions & 145 deletions

File tree

server/mcp_server_rds_mysql/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description = "MCP server for RDS MySQL"
55
readme = "README.md"
66
requires-python = ">=3.10"
77
dependencies = [
8-
"mcp[cli]>=1.5.0",
8+
"mcp[cli]>=1.10.0",
99
"volcengine-python-sdk>=3.0.1",
1010
"aiohttp>=3.11.14",
1111
"deprecated>=1.2.18",

server/mcp_server_rds_mysql/src/mcp_server_rds_mysql/server.py

Lines changed: 101 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
11
import os
22
import asyncio
3-
from typing import Optional
43
from pydantic import Field
54
import logging
65
import argparse
7-
from typing import Any, Literal
86
from mcp.server.fastmcp import FastMCP
97
from mcp_server_rds_mysql.resource.rds_mysql_resource import RDSMySQLSDK
8+
from typing import List, Dict, Any, Optional
109

1110
# 初始化MCP服务
1211
mcp_server = FastMCP("rds_mysql_mcp_server", port=int(os.getenv("MCP_SERVER_PORT", "8000")))
1312
logger = logging.getLogger("rds_mysql_mcp_server")
1413

1514
rds_mysql_resource = RDSMySQLSDK(
16-
region=os.getenv('VOLCENGINE_REGION'), ak=os.getenv('VOLCENGINE_ACCESS_KEY'), sk=os.getenv('VOLCENGINE_SECRET_KEY'), host=os.getenv('VOLCENGINE_ENDPOINT')
15+
region=os.getenv('VOLCENGINE_REGION',"cn-beijing"), ak=os.getenv('VOLCENGINE_ACCESS_KEY'), sk=os.getenv('VOLCENGINE_SECRET_KEY'), host=os.getenv('VOLCENGINE_ENDPOINT')
1716
)
1817

19-
from typing import List, Dict, Any, Optional
20-
2118
@mcp_server.tool(
2219
name="describe_db_instances",
2320
description="查询RDS MySQL实例列表"
@@ -459,9 +456,9 @@ def modify_db_account_description(
459456

460457
@mcp_server.tool(
461458
name="create_rds_mysql_instance",
462-
description="创建 RDS MySQL 实例"
459+
description="创建RDS MySQL实例,可选择是否等待实例就绪"
463460
)
464-
def create_rds_mysql_instance(
461+
async def create_rds_mysql_instance(
465462
vpc_id: str = Field(title="私有网络 ID", description="需要使用describe_vpcs获取"),
466463
subnet_id: str = Field(title="子网 ID", description="需要使用describe_subnets获取,subnet_id只有一个可用区属性,多可用区时找到一个与主节点或者备节点所在的可用区相同的即可"),
467464
db_engine_version: str = Field(default="MySQL_8_0", description="数据库版本"),
@@ -494,43 +491,21 @@ def create_rds_mysql_instance(
494491
allow_list_ids: Optional[List[str]] = Field(default=None, description="白名单 ID 列表"),
495492
port: int = Field(default=3306, description="默认终端的私网端口"),
496493
instance_tags: Optional[List[Dict]] = Field(default=None, description="实例标签列表"),
497-
maintenance_window: Optional[Dict] = Field(default=None, description="维护窗口配置")
494+
maintenance_window: Optional[Dict] = Field(default=None, description="维护窗口配置"),
495+
wait_for_ready: bool = Field(default=True, description="是否等待实例就绪后再返回,默认为True。如设为False将立即返回创建结果不等待实例就绪"),
496+
max_wait_time: int = Field(default=600, description="等待实例就绪的最长时间(秒),默认为600秒(10分钟)。仅在wait_for_ready=True时有效"),
497+
backoff_strategy: str = Field(default="exponential", description="重试策略,可选值:exponential(指数退避)、fibonacci(斐波那契序列)", choices=["exponential", "fibonacci"])
498498
) -> dict[str, Any]:
499-
"""创建 RDS MySQL 实例
500-
501-
Args:
502-
vpc_id: 私有网络 ID
503-
subnet_id: 子网 ID
504-
db_engine_version: 数据库版本,默认 MySQL_8_0
505-
instance_name: 实例名称
506-
primary_zone: 主节点可用区,默认 cn-beijing-a
507-
primary_spec: 主节点规格,默认 rds.mysql.1c2g
508-
secondary_count: 备节点数量,默认 1
509-
secondary_zone: 备节点可用区,默认与主节点相同
510-
secondary_spec: 备节点规格,默认 rds.mysql.1c2g
511-
read_only_count: 只读节点数量,默认 0
512-
read_only_zone: 只读节点可用区,默认 cn-beijing-a
513-
read_only_spec: 只读节点规格,默认 rds.mysql.1c2g
514-
storage_space: 存储空间大小(GB),默认 20
515-
storage_type: 存储类型,默认 LocalSSD
516-
charge_type: 付费类型,默认 PostPaid
517-
auto_renew: 预付费场景下是否自动续费
518-
period_unit: 预付费场景下的购买周期(Month/Year)
519-
period: 预付费场景下的购买时长
520-
instance_type: 实例类型,默认 DoubleNode
521-
super_account_name: 高权限账号名称
522-
super_account_password: 高权限账号密码
523-
lower_case_table_names: 表名是否区分大小写,默认 1
524-
db_time_zone: 时区
525-
db_param_group_id: 参数模板 ID
526-
project_name: 实例所属项目
527-
allow_list_ids: 白名单 ID 列表
528-
port: 默认终端的私网端口,默认 3306
529-
instance_tags: 实例标签列表
530-
maintenance_window: 维护窗口配置
531-
532-
Returns:
533-
dict: 创建结果,包含实例ID和订单号等信息
499+
"""创建RDS MySQL实例,可选择是否等待实例就绪
500+
501+
此方法默认会在内部处理等待逻辑,只有当实例状态为"Running"时才会返回结果,
502+
无需手动轮询检查实例状态。如果设置wait_for_ready=False,则会立即返回创建结果。
503+
504+
支持两种重试策略:
505+
- exponential: 指数退避策略,等待间隔为 initial_wait * (2^n),如 5, 10, 20, 40秒...
506+
- fibonacci: 斐波那契序列策略,等待间隔为斐波那契序列 * initial_wait,如 5, 5, 10, 15, 25秒...
507+
508+
通过max_wait_time参数可控制最长等待时间,默认为10分钟,一般大多数实例3-5分钟可创建完成,复杂实例可能需要更长时间。
534509
"""
535510
node_info = []
536511

@@ -602,8 +577,85 @@ def create_rds_mysql_instance(
602577
if maintenance_window is not None:
603578
data["maintenance_window"] = maintenance_window
604579

605-
resp = rds_mysql_resource.create_db_instance(data)
606-
return resp.to_dict()
580+
create_resp = rds_mysql_resource.create_db_instance(data)
581+
582+
instance_id = create_resp.instance_id
583+
584+
if instance_id is None:
585+
create_result = create_resp.to_dict()
586+
instance_id = create_result.get("instance_id")
587+
588+
if instance_id is None:
589+
raise ValueError(f"无法获取实例ID,API响应: {create_result}")
590+
591+
# If we don't need to wait for the instance to be ready, return creation result immediately
592+
if not wait_for_ready:
593+
return create_resp.to_dict()
594+
595+
# 设置初始等待参数
596+
initial_wait = 5 # 初始等待5秒
597+
max_interval = 60 # 最大等待间隔60秒
598+
599+
logger.info(f"Waiting for instance {instance_id} to be ready using {backoff_strategy} backoff strategy")
600+
601+
# 对于斐波那契序列,预先计算前20个数,足够我们使用
602+
fibonacci_sequence = [1, 1]
603+
for i in range(2, 20):
604+
fibonacci_sequence.append(fibonacci_sequence[i-1] + fibonacci_sequence[i-2])
605+
606+
time_spent = 0
607+
retry_count = 0
608+
609+
while time_spent < max_wait_time:
610+
# 根据策略计算等待间隔
611+
if backoff_strategy == "exponential":
612+
# 指数退避: initial_wait * 2^n, 上限为max_interval
613+
wait_interval = min(initial_wait * (2 ** retry_count), max_interval)
614+
else: # fibonacci
615+
# 斐波那契序列: 从第3项开始为前两项之和,但从1开始
616+
index = min(retry_count, len(fibonacci_sequence) - 1)
617+
wait_interval = min(initial_wait * fibonacci_sequence[index], max_interval)
618+
619+
# 等待相应时间
620+
await asyncio.sleep(wait_interval)
621+
time_spent += wait_interval
622+
retry_count += 1
623+
624+
try:
625+
logger.info(f"Checking instance status, attempt {retry_count}, " +
626+
f"waited {wait_interval}s, total time: {time_spent}s")
627+
628+
req = {"instance_id": instance_id}
629+
detail_resp = rds_mysql_resource.describe_db_instance_detail(req)
630+
detail = detail_resp.to_dict()
631+
632+
# 从响应中提取实例状态
633+
instance_status = None
634+
635+
if hasattr(detail_resp, 'basic_info') and detail_resp.basic_info is not None:
636+
if hasattr(detail_resp.basic_info, 'instance_status'):
637+
instance_status = detail_resp.basic_info.instance_status
638+
else:
639+
basic_info_dict = detail_resp.basic_info.to_dict() if hasattr(detail_resp.basic_info, 'to_dict') else {}
640+
instance_status = basic_info_dict.get('instance_status')
641+
else:
642+
basic_info = detail.get('basic_info', {})
643+
instance_status = basic_info.get('instance_status')
644+
645+
if instance_status == "Running":
646+
logger.info(f"Instance {instance_id} is now running after {retry_count} attempts, {time_spent}s")
647+
return detail
648+
elif instance_status in ["Error", "Failed"]:
649+
logger.error(f"Instance {instance_id} creation failed with status: {instance_status}")
650+
raise RuntimeError(f"Instance {instance_id} creation failed with status: {instance_status}")
651+
else:
652+
logger.info(f"Instance {instance_id} current status: {instance_status}, continuing to wait...")
653+
except Exception as e:
654+
logger.error(f"Error checking instance status: {str(e)}, retrying...")
655+
656+
# 超时
657+
logger.error(f"Instance {instance_id} creation timed out after {time_spent} seconds")
658+
raise TimeoutError(f"Instance {instance_id} creation timed out after {time_spent} seconds. Please check the instance status manually.")
607659

608660

609661
@mcp_server.tool(
@@ -1012,7 +1064,7 @@ def create_db_account(
10121064

10131065
@mcp_server.tool(
10141066
name="describe_vpcs",
1015-
description="查询满足指定条件的VPC,用于创建实例"
1067+
description="查询VPC 信息,用于创建实例"
10161068
)
10171069
def describe_vpcs(
10181070
page_number: int = Field(default=1, description="当前页页码,最小值为1"),
@@ -1032,7 +1084,7 @@ def describe_vpcs(
10321084

10331085
@mcp_server.tool(
10341086
name="describe_subnets",
1035-
description="查询满足指定条件的子网,用于创建实例"
1087+
description="查询子网信息,用于创建实例"
10361088
)
10371089
def describe_subnets(
10381090
vpc_id: str = Field(
@@ -1058,9 +1110,9 @@ def main():
10581110
parser.add_argument(
10591111
"--transport",
10601112
"-t",
1061-
choices=["sse", "stdio"],
1113+
choices=["sse", "stdio", "streamable-http"],
10621114
default="stdio",
1063-
help="Transport protocol to use (sse or stdio)",
1115+
help="Transport protocol to use (sse, stdio or streamable-http)",
10641116
)
10651117

10661118
args = parser.parse_args()

0 commit comments

Comments
 (0)