From 1ab6eae8012680ddc8768cba977b103b7a39b4e0 Mon Sep 17 00:00:00 2001 From: Juhui Jung Date: Fri, 27 Jun 2025 17:59:14 +0900 Subject: [PATCH 01/10] feat: add AWS VPC and EKS cluster creation tools for MCP --- server-http-python-lambda/server/app.py | 331 ++++++++++++++++++++++++ 1 file changed, 331 insertions(+) diff --git a/server-http-python-lambda/server/app.py b/server-http-python-lambda/server/app.py index 941579f..cc99ba1 100644 --- a/server-http-python-lambda/server/app.py +++ b/server-http-python-lambda/server/app.py @@ -3,6 +3,8 @@ import random import boto3 import os +import ipaddress +import json # Get session table name from environment variable session_table = os.environ.get('MCP_SESSION_TABLE', 'mcp_sessions') @@ -72,6 +74,335 @@ def update_last_time(s): # Catch-all for unexpected errors return f"Error: An unexpected error occurred: {str(e)}" +@mcp_server.tool() +def create_vpc(params: dict) -> dict: + """ + Generalized VPC and Subnet Creator for OSS contribution. + + Parameters expected in `params`: + - cidr_block: str + - az_list: list[str] + - public_subnets_per_az: int + - private_subnets_per_az: int + - name_tag: str + """ + ec2 = boto3.client("ec2", region_name="us-east-1") + out = {} + + # Extract parameters with defaults + cidr_block = params.get("cidr_block", "10.0.0.0/16") + az_list = params.get("az_list", ["us-east-1a", "us-east-1b"]) + public_subnets_per_az = params.get("public_subnets_per_az", 1) + private_subnets_per_az = params.get("private_subnets_per_az", 1) + name_tag = params.get("name_tag", "mcp-server") + + total_subnets_needed = (public_subnets_per_az + private_subnets_per_az) * len(az_list) + subnet_blocks = list(ipaddress.ip_network(cidr_block).subnets(new_prefix=24)) + if len(subnet_blocks) < total_subnets_needed: + raise Exception("Not enough subnet blocks in CIDR for requested subnets.") + + # Check existing VPC + existing = ec2.describe_vpcs(Filters=[ + {"Name": "tag:Name", "Values": [f"{name_tag}-vpc"]} + ])["Vpcs"] + if existing: + vpc_id = existing[0]["VpcId"] + print(f"[INFO] Existing VPC found: {vpc_id}") + out["VpcId"] = vpc_id + return out + + # Create VPC + vpc_id = ec2.create_vpc(CidrBlock=cidr_block)["Vpc"]["VpcId"] + ec2.modify_vpc_attribute(VpcId=vpc_id, EnableDnsSupport={"Value": True}) + ec2.modify_vpc_attribute(VpcId=vpc_id, EnableDnsHostnames={"Value": True}) + ec2.create_tags(Resources=[vpc_id], + Tags=[{"Key": "Name", "Value": f"{name_tag}-vpc"}]) + out["VpcId"] = vpc_id + + # IGW and main route + igw_id = ec2.create_internet_gateway()["InternetGateway"]["InternetGatewayId"] + ec2.attach_internet_gateway(VpcId=vpc_id, InternetGatewayId=igw_id) + out["InternetGatewayId"] = igw_id + + main_rt_id = ec2.describe_route_tables( + Filters=[ + {"Name": "vpc-id", "Values": [vpc_id]}, + {"Name": "association.main", "Values": ["true"]} + ] + )["RouteTables"][0]["RouteTableId"] + ec2.create_route(RouteTableId=main_rt_id, + DestinationCidrBlock="0.0.0.0/0", + GatewayId=igw_id) + + out["PublicSubnets"] = [] + out["PrivateSubnets"] = [] + + idx = 0 + for az in az_list: + # Create public subnets + for i in range(public_subnets_per_az): + cidr_pub = str(subnet_blocks[idx]) + idx += 1 + pub_subnet = ec2.create_subnet( + VpcId=vpc_id, CidrBlock=cidr_pub, AvailabilityZone=az, + TagSpecifications=[{ + "ResourceType": "subnet", + "Tags": [{"Key": "Name", "Value": f"{name_tag}-pub-{az}-{i}"}] + }] + )["Subnet"] + pub_id = pub_subnet["SubnetId"] + ec2.modify_subnet_attribute(SubnetId=pub_id, MapPublicIpOnLaunch={"Value": True}) + out["PublicSubnets"].append(pub_id) + + # Create NAT Gateway for private subnets + eip = ec2.allocate_address(Domain="vpc")["AllocationId"] + nat = ec2.create_nat_gateway( + SubnetId=pub_id, AllocationId=eip, + TagSpecifications=[{ + "ResourceType": "natgateway", + "Tags": [{"Key": "Name", "Value": f"{name_tag}-nat-{az}"}] + }] + )["NatGateway"] + nat_id = nat["NatGatewayId"] + + waiter = ec2.get_waiter("nat_gateway_available") + print(f"Waiting for NAT Gateway {nat_id} in {az}...") + waiter.wait(NatGatewayIds=[nat_id]) + print(f"NAT Gateway {nat_id} is available.") + + # Create private subnets + for i in range(private_subnets_per_az): + cidr_pri = str(subnet_blocks[idx]) + idx += 1 + pri_subnet = ec2.create_subnet( + VpcId=vpc_id, CidrBlock=cidr_pri, AvailabilityZone=az, + TagSpecifications=[{ + "ResourceType": "subnet", + "Tags": [{"Key": "Name", "Value": f"{name_tag}-pri-{az}-{i}"}] + }] + )["Subnet"] + pri_id = pri_subnet["SubnetId"] + out["PrivateSubnets"].append(pri_id) + + rt_priv = ec2.create_route_table(VpcId=vpc_id)["RouteTable"]["RouteTableId"] + ec2.associate_route_table(RouteTableId=rt_priv, SubnetId=pri_id) + ec2.create_route(RouteTableId=rt_priv, + DestinationCidrBlock="0.0.0.0/0", + NatGatewayId=nat_id) + + return out + +def ensure_eks_access_entries(eks, cluster_name, node_role_arn): + sts = boto3.client("sts") + access_entries = eks.list_access_entries(clusterName=cluster_name)["accessEntries"] + existing_principals = [entry["principalArn"] for entry in access_entries] + print("[INFO] Current Access Entries:", existing_principals) + + # Register Node Role + if node_role_arn not in existing_principals: + eks.create_access_entry( + clusterName=cluster_name, + principalArn=node_role_arn, + type="EC2_LINUX" + ) + print(f"[INFO] Access Entry registered for Node Role: {node_role_arn}") + +def create_eks_cluster_with_nodegroup(params: dict) -> dict: + """ + Generalized EKS cluster + node group creator (OSS contribution ready). + + Accepts: + params: dict containing optional keys: + - cluster_name + - nodegroup_name + - version + - instance_type + - desired_size + - min_size + - max_size + - public_cidrs + - endpoint_public + - endpoint_private + - vpc_id + - subnet_ids + - security_group_ids + - region + - control_role_name + - node_role_name + - install_addons + """ + eks = boto3.client("eks", region_name=params.get("region", "us-east-1")) + ec2 = boto3.client("ec2", region_name=params.get("region", "us-east-1")) + iam = boto3.client("iam", region_name=params.get("region", "us-east-1")) + + cluster_name = params.get("cluster_name", "my-cluster") + nodegroup_name = params.get("nodegroup_name", "default-ng") + version = params.get("version", "1.32") + instance_type = params.get("instance_type", "t3.medium") + desired_size = params.get("desired_size", 2) + min_size = params.get("min_size", 1) + max_size = params.get("max_size", 3) + public_cidrs = params.get("public_cidrs", ["0.0.0.0/0"]) + endpoint_public = params.get("endpoint_public", True) + endpoint_private = params.get("endpoint_private", True) + vpc_id = params.get("vpc_id") + subnet_ids = params.get("subnet_ids") + security_group_ids = params.get("security_group_ids") + control_role_name = params.get("control_role_name", "EKSServiceRole") + node_role_name = params.get("node_role_name", "EKSNodeRole") + install_addons = params.get("install_addons", True) + + # 1️⃣ Check if cluster already exists + if cluster_name in eks.list_clusters()["clusters"]: + print(f"[INFO] EKS cluster '{cluster_name}' already exists. Skipping creation.") + return {"ClusterName": cluster_name, "Status": "EXISTS"} + + # 2️⃣ IAM Roles + try: + control_role_arn = iam.get_role(RoleName=control_role_name)["Role"]["Arn"] + except iam.exceptions.NoSuchEntityException: + print(f"[INFO] Creating IAM role: {control_role_name}") + control_role = iam.create_role( + RoleName=control_role_name, + AssumeRolePolicyDocument=json.dumps({ + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Principal": {"Service": "eks.amazonaws.com"}, + "Action": "sts:AssumeRole" + }] + }), + Description="IAM role for EKS control plane" + ) + control_role_arn = control_role["Role"]["Arn"] + iam.attach_role_policy(RoleName=control_role_name, + PolicyArn="arn:aws:iam::aws:policy/AmazonEKSClusterPolicy") + + try: + node_role_arn = iam.get_role(RoleName=node_role_name)["Role"]["Arn"] + except iam.exceptions.NoSuchEntityException: + print(f"[INFO] Creating IAM role: {node_role_name}") + node_role = iam.create_role( + RoleName=node_role_name, + AssumeRolePolicyDocument=json.dumps({ + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Principal": {"Service": "ec2.amazonaws.com"}, + "Action": "sts:AssumeRole" + }] + }), + Description="IAM role for EKS worker nodes" + ) + node_role_arn = node_role["Role"]["Arn"] + for policy in [ + "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy", + "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly", + "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy" + ]: + iam.attach_role_policy(RoleName=node_role_name, PolicyArn=policy) + + # 3️⃣ VPC, Subnets, SG + if not vpc_id: + vpcs = ec2.describe_vpcs(Filters=[{"Name": "isDefault", "Values": ["true"]}])["Vpcs"] + if not vpcs: + raise Exception("No default VPC found and no vpc_id provided.") + vpc_id = vpcs[0]["VpcId"] + + if not subnet_ids: + subnets = ec2.describe_subnets(Filters=[{"Name": "vpc-id", "Values": [vpc_id]}])["Subnets"] + subnet_ids = [s["SubnetId"] for s in subnets] + if len(subnet_ids) < 2: + raise Exception("At least two subnets are required for EKS cluster creation.") + + if not security_group_ids: + sgs = ec2.describe_security_groups(Filters=[ + {"Name": "vpc-id", "Values": [vpc_id]}, + {"Name": "group-name", "Values": ["default"]} + ])["SecurityGroups"] + if not sgs: + raise Exception("No security group found in the VPC.") + security_group_ids = [sgs[0]["GroupId"]] + + # 4️⃣ Create EKS Cluster + print(f"[INFO] Creating EKS cluster '{cluster_name}'...") + eks.create_cluster( + name=cluster_name, + version=version, + roleArn=control_role_arn, + resourcesVpcConfig={ + "subnetIds": subnet_ids, + "securityGroupIds": security_group_ids, + "endpointPublicAccess": endpoint_public, + "endpointPrivateAccess": endpoint_private, + "publicAccessCidrs": public_cidrs + } + ) + waiter = eks.get_waiter("cluster_active") + print(f"[INFO] Waiting for EKS cluster '{cluster_name}' to become ACTIVE...") + waiter.wait(name=cluster_name) + print(f"[SUCCESS] EKS cluster '{cluster_name}' is ACTIVE.") + + # 5️⃣ Add-ons + if install_addons: + addon_versions = { + "vpc-cni": "v1.19.2-eksbuild.1", + "coredns": "v1.11.4-eksbuild.2", + "kube-proxy": "v1.32.0-eksbuild.2", + "eks-pod-identity-agent": "v1.3.4-eksbuild.1" + } + for addon, version_str in addon_versions.items(): + try: + print(f"[INFO] Installing addon: {addon} ({version_str})...") + eks.create_addon( + clusterName=cluster_name, + addonName=addon, + addonVersion=version_str, + resolveConflicts="OVERWRITE" + ) + except eks.exceptions.ResourceInUseException: + print(f"[WARN] Addon '{addon}' already exists. Skipping.") + except Exception as e: + print(f"[ERROR] Failed to install addon '{addon}': {e}") + + # 6️⃣ Register Access Entry + print("[INFO] Registering EKS Access Entries...") + ensure_eks_access_entries(eks, cluster_name, node_role_arn) + print("[INFO] Access Entry registration completed.") + + waiter = eks.get_waiter('cluster_active') + waiter.wait(name=cluster_name) + + # 7️⃣ Node Group + print(f"[INFO] Creating node group '{nodegroup_name}' in '{cluster_name}'...") + eks.create_nodegroup( + clusterName=cluster_name, + nodegroupName=nodegroup_name, + scalingConfig={ + "minSize": min_size, + "maxSize": max_size, + "desiredSize": desired_size + }, + subnets=subnet_ids, + instanceTypes=[instance_type], + nodeRole=node_role_arn, + amiType="AL2023_x86_64_STANDARD", + diskSize=20, + capacityType="ON_DEMAND" + ) + ng_waiter = eks.get_waiter("nodegroup_active") + print(f"[INFO] Waiting for node group '{nodegroup_name}' to become ACTIVE...") + ng_waiter.wait(clusterName=cluster_name, nodegroupName=nodegroup_name) + print(f"[SUCCESS] Node group '{nodegroup_name}' is ACTIVE.") + + return { + "ClusterName": cluster_name, + "NodegroupName": nodegroup_name, + "VPC": vpc_id, + "Subnets": subnet_ids, + "SecurityGroups": security_group_ids + } + def lambda_handler(event, context): """AWS Lambda handler function.""" return mcp_server.handle_request(event, context) \ No newline at end of file From 78c9edb4e37eae5b38d865b3550ac5fdd35e10ad Mon Sep 17 00:00:00 2001 From: Juhui Jung Date: Tue, 1 Jul 2025 13:25:15 +0900 Subject: [PATCH 02/10] docs: add usage example comments --- server-http-python-lambda/server/app.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/server-http-python-lambda/server/app.py b/server-http-python-lambda/server/app.py index cc99ba1..e96d3af 100644 --- a/server-http-python-lambda/server/app.py +++ b/server-http-python-lambda/server/app.py @@ -85,6 +85,15 @@ def create_vpc(params: dict) -> dict: - public_subnets_per_az: int - private_subnets_per_az: int - name_tag: str + + e.g. + create_vpc({ + "cidr_block": "10.20.0.0/16", + "az_list": ["us-east-1a", "us-east-1b"], + "public_subnets_per_az": 1, + "private_subnets_per_az": 1, + "name_tag": "oss-vpc" + }) """ ec2 = boto3.client("ec2", region_name="us-east-1") out = {} @@ -207,6 +216,7 @@ def ensure_eks_access_entries(eks, cluster_name, node_role_arn): ) print(f"[INFO] Access Entry registered for Node Role: {node_role_arn}") +@mcp_server.tool() def create_eks_cluster_with_nodegroup(params: dict) -> dict: """ Generalized EKS cluster + node group creator (OSS contribution ready). @@ -230,6 +240,15 @@ def create_eks_cluster_with_nodegroup(params: dict) -> dict: - control_role_name - node_role_name - install_addons + e.g. + create_eks_cluster_with_nodegroup({ + "cluster_name": "mcp-eks-cluster", + "nodegroup_name": "mcp-ng", + "desired_size": 2, + "min_size": 1, + "max_size": 2, + "region": "us-east-1" + }) """ eks = boto3.client("eks", region_name=params.get("region", "us-east-1")) ec2 = boto3.client("ec2", region_name=params.get("region", "us-east-1")) From c4e385574e4a2ce0c4fe563c9332939c75f87398 Mon Sep 17 00:00:00 2001 From: Juhui Jung Date: Thu, 3 Jul 2025 15:01:41 +0900 Subject: [PATCH 03/10] fix: edit api gateway url, token & handle string params in create_vpc tool --- client-strands-agents-mcp/main.py | 4 ++-- server-http-python-lambda/server/app.py | 17 ++++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/client-strands-agents-mcp/main.py b/client-strands-agents-mcp/main.py index 784299c..dd40b7d 100644 --- a/client-strands-agents-mcp/main.py +++ b/client-strands-agents-mcp/main.py @@ -4,8 +4,8 @@ from strands.tools.mcp import MCPClient # Add your API Gateway URL here as it outputs from the sam deploy command (include /mcp at the end): -api_gateway_url = "https://1234567890.execute-api.us-east-1.amazonaws.com/Prod/mcp" -auth_token = "1234567890" +api_gateway_url = "https://ma8rga14yj.execute-api.us-west-2.amazonaws.com/Prod/mcp" +auth_token = "juhui" # This uses the Nova Pro model from Amazon Bedrock, in a US region: bedrock_model = BedrockModel( diff --git a/server-http-python-lambda/server/app.py b/server-http-python-lambda/server/app.py index e96d3af..ba927f3 100644 --- a/server-http-python-lambda/server/app.py +++ b/server-http-python-lambda/server/app.py @@ -89,18 +89,21 @@ def create_vpc(params: dict) -> dict: e.g. create_vpc({ "cidr_block": "10.20.0.0/16", - "az_list": ["us-east-1a", "us-east-1b"], + "az_list": ["us-west-2a", "us-west-2b"], "public_subnets_per_az": 1, "private_subnets_per_az": 1, "name_tag": "oss-vpc" }) """ - ec2 = boto3.client("ec2", region_name="us-east-1") + ec2 = boto3.client("ec2", region_name="us-west-2") out = {} + + if isinstance(params, str): + params = json.loads(params) # Extract parameters with defaults cidr_block = params.get("cidr_block", "10.0.0.0/16") - az_list = params.get("az_list", ["us-east-1a", "us-east-1b"]) + az_list = params.get("az_list", ["us-west-2a", "us-west-2b"]) public_subnets_per_az = params.get("public_subnets_per_az", 1) private_subnets_per_az = params.get("private_subnets_per_az", 1) name_tag = params.get("name_tag", "mcp-server") @@ -247,12 +250,12 @@ def create_eks_cluster_with_nodegroup(params: dict) -> dict: "desired_size": 2, "min_size": 1, "max_size": 2, - "region": "us-east-1" + "region": "us-west-2" }) """ - eks = boto3.client("eks", region_name=params.get("region", "us-east-1")) - ec2 = boto3.client("ec2", region_name=params.get("region", "us-east-1")) - iam = boto3.client("iam", region_name=params.get("region", "us-east-1")) + eks = boto3.client("eks", region_name=params.get("region", "us-west-2")) + ec2 = boto3.client("ec2", region_name=params.get("region", "us-west-2")) + iam = boto3.client("iam", region_name=params.get("region", "us-west-2")) cluster_name = params.get("cluster_name", "my-cluster") nodegroup_name = params.get("nodegroup_name", "default-ng") From ee06a08b825055bd1e61dd26e886dd2854f46455 Mon Sep 17 00:00:00 2001 From: Juhui Jung Date: Thu, 3 Jul 2025 15:23:45 +0900 Subject: [PATCH 04/10] chore: increase Lambda timeout to 15 mins --- server-http-python-lambda/template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-http-python-lambda/template.yaml b/server-http-python-lambda/template.yaml index 60d9441..3e1db33 100644 --- a/server-http-python-lambda/template.yaml +++ b/server-http-python-lambda/template.yaml @@ -10,7 +10,7 @@ Parameters: Globals: Function: - Timeout: 60 + Timeout: 900 Runtime: python3.12 Architectures: [x86_64] From 9d3b2d9174b655f570fe0d73d211b86c0bcf4ff8 Mon Sep 17 00:00:00 2001 From: Juhui Jung Date: Thu, 3 Jul 2025 15:26:11 +0900 Subject: [PATCH 05/10] feat: add EC2 and VPC create/list/describe permissions to MCP server function --- server-http-python-lambda/template.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/server-http-python-lambda/template.yaml b/server-http-python-lambda/template.yaml index 3e1db33..c06c6fa 100644 --- a/server-http-python-lambda/template.yaml +++ b/server-http-python-lambda/template.yaml @@ -85,6 +85,20 @@ Resources: - dynamodb:UpdateItem - dynamodb:DescribeTable Resource: !GetAtt McpSessionsTable.Arn + - Effect: Allow + Action: + - ec2:Create* + - ec2:Describe* + - ec2:List* + - ec2:Authorize* + - ec2:Revoke* + Resource: '*' + - Effect: Allow + Action: + - vpc:Create* + - vpc:Describe* + - vpc:List* + Resource: '*' Events: McpAPI: Type: Api From b3858820d3d4adef02c085d7068cb2538a8c8a89 Mon Sep 17 00:00:00 2001 From: Juhui Jung Date: Thu, 3 Jul 2025 15:33:16 +0900 Subject: [PATCH 06/10] fix: add EC2 permissions and delete VPC permissions to MCP server function --- server-http-python-lambda/template.yaml | 30 +++++++++++++++---------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/server-http-python-lambda/template.yaml b/server-http-python-lambda/template.yaml index c06c6fa..70bbfa9 100644 --- a/server-http-python-lambda/template.yaml +++ b/server-http-python-lambda/template.yaml @@ -87,18 +87,24 @@ Resources: Resource: !GetAtt McpSessionsTable.Arn - Effect: Allow Action: - - ec2:Create* - - ec2:Describe* - - ec2:List* - - ec2:Authorize* - - ec2:Revoke* - Resource: '*' - - Effect: Allow - Action: - - vpc:Create* - - vpc:Describe* - - vpc:List* - Resource: '*' + - ec2:CreateVpc + - ec2:ModifyVpcAttribute + - ec2:CreateTags + - ec2:DescribeVpcs + - ec2:CreateInternetGateway + - ec2:AttachInternetGateway + - ec2:DescribeInternetGateways + - ec2:DescribeRouteTables + - ec2:CreateRoute + - ec2:CreateRouteTable + - ec2:AssociateRouteTable + - ec2:CreateSubnet + - ec2:ModifySubnetAttribute + - ec2:DescribeSubnets + - ec2:AllocateAddress + - ec2:CreateNatGateway + - ec2:DescribeNatGateways + Resource: "*" Events: McpAPI: Type: Api From 166de0f6ff2a6fbd8ed6a7c162b6338029432e8e Mon Sep 17 00:00:00 2001 From: Juhui Jung Date: Thu, 3 Jul 2025 17:57:51 +0900 Subject: [PATCH 07/10] docs: update comments with corrected command example --- server-http-python-lambda/server/app.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/server-http-python-lambda/server/app.py b/server-http-python-lambda/server/app.py index ba927f3..179a124 100644 --- a/server-http-python-lambda/server/app.py +++ b/server-http-python-lambda/server/app.py @@ -87,13 +87,7 @@ def create_vpc(params: dict) -> dict: - name_tag: str e.g. - create_vpc({ - "cidr_block": "10.20.0.0/16", - "az_list": ["us-west-2a", "us-west-2b"], - "public_subnets_per_az": 1, - "private_subnets_per_az": 1, - "name_tag": "oss-vpc" - }) + createVpc(params={"cidr_block":"10.20.0.0/16","az_list":["us-west-2a","us-west-2b"],"public_subnets_per_az":1,"private_subnets_per_az":1,"name_tag":"mcp"}) """ ec2 = boto3.client("ec2", region_name="us-west-2") out = {} From 5c4db14f75e1f4205ef5b8343122172c7e661059 Mon Sep 17 00:00:00 2001 From: Juhui Jung Date: Thu, 3 Jul 2025 17:59:33 +0900 Subject: [PATCH 08/10] chore: delete create_eks_cluster_with_nodegroup method --- server-http-python-lambda/server/app.py | 221 ------------------------ 1 file changed, 221 deletions(-) diff --git a/server-http-python-lambda/server/app.py b/server-http-python-lambda/server/app.py index 179a124..25650ca 100644 --- a/server-http-python-lambda/server/app.py +++ b/server-http-python-lambda/server/app.py @@ -198,227 +198,6 @@ def create_vpc(params: dict) -> dict: return out -def ensure_eks_access_entries(eks, cluster_name, node_role_arn): - sts = boto3.client("sts") - access_entries = eks.list_access_entries(clusterName=cluster_name)["accessEntries"] - existing_principals = [entry["principalArn"] for entry in access_entries] - print("[INFO] Current Access Entries:", existing_principals) - - # Register Node Role - if node_role_arn not in existing_principals: - eks.create_access_entry( - clusterName=cluster_name, - principalArn=node_role_arn, - type="EC2_LINUX" - ) - print(f"[INFO] Access Entry registered for Node Role: {node_role_arn}") - -@mcp_server.tool() -def create_eks_cluster_with_nodegroup(params: dict) -> dict: - """ - Generalized EKS cluster + node group creator (OSS contribution ready). - - Accepts: - params: dict containing optional keys: - - cluster_name - - nodegroup_name - - version - - instance_type - - desired_size - - min_size - - max_size - - public_cidrs - - endpoint_public - - endpoint_private - - vpc_id - - subnet_ids - - security_group_ids - - region - - control_role_name - - node_role_name - - install_addons - e.g. - create_eks_cluster_with_nodegroup({ - "cluster_name": "mcp-eks-cluster", - "nodegroup_name": "mcp-ng", - "desired_size": 2, - "min_size": 1, - "max_size": 2, - "region": "us-west-2" - }) - """ - eks = boto3.client("eks", region_name=params.get("region", "us-west-2")) - ec2 = boto3.client("ec2", region_name=params.get("region", "us-west-2")) - iam = boto3.client("iam", region_name=params.get("region", "us-west-2")) - - cluster_name = params.get("cluster_name", "my-cluster") - nodegroup_name = params.get("nodegroup_name", "default-ng") - version = params.get("version", "1.32") - instance_type = params.get("instance_type", "t3.medium") - desired_size = params.get("desired_size", 2) - min_size = params.get("min_size", 1) - max_size = params.get("max_size", 3) - public_cidrs = params.get("public_cidrs", ["0.0.0.0/0"]) - endpoint_public = params.get("endpoint_public", True) - endpoint_private = params.get("endpoint_private", True) - vpc_id = params.get("vpc_id") - subnet_ids = params.get("subnet_ids") - security_group_ids = params.get("security_group_ids") - control_role_name = params.get("control_role_name", "EKSServiceRole") - node_role_name = params.get("node_role_name", "EKSNodeRole") - install_addons = params.get("install_addons", True) - - # 1️⃣ Check if cluster already exists - if cluster_name in eks.list_clusters()["clusters"]: - print(f"[INFO] EKS cluster '{cluster_name}' already exists. Skipping creation.") - return {"ClusterName": cluster_name, "Status": "EXISTS"} - - # 2️⃣ IAM Roles - try: - control_role_arn = iam.get_role(RoleName=control_role_name)["Role"]["Arn"] - except iam.exceptions.NoSuchEntityException: - print(f"[INFO] Creating IAM role: {control_role_name}") - control_role = iam.create_role( - RoleName=control_role_name, - AssumeRolePolicyDocument=json.dumps({ - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Principal": {"Service": "eks.amazonaws.com"}, - "Action": "sts:AssumeRole" - }] - }), - Description="IAM role for EKS control plane" - ) - control_role_arn = control_role["Role"]["Arn"] - iam.attach_role_policy(RoleName=control_role_name, - PolicyArn="arn:aws:iam::aws:policy/AmazonEKSClusterPolicy") - - try: - node_role_arn = iam.get_role(RoleName=node_role_name)["Role"]["Arn"] - except iam.exceptions.NoSuchEntityException: - print(f"[INFO] Creating IAM role: {node_role_name}") - node_role = iam.create_role( - RoleName=node_role_name, - AssumeRolePolicyDocument=json.dumps({ - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Principal": {"Service": "ec2.amazonaws.com"}, - "Action": "sts:AssumeRole" - }] - }), - Description="IAM role for EKS worker nodes" - ) - node_role_arn = node_role["Role"]["Arn"] - for policy in [ - "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy", - "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly", - "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy" - ]: - iam.attach_role_policy(RoleName=node_role_name, PolicyArn=policy) - - # 3️⃣ VPC, Subnets, SG - if not vpc_id: - vpcs = ec2.describe_vpcs(Filters=[{"Name": "isDefault", "Values": ["true"]}])["Vpcs"] - if not vpcs: - raise Exception("No default VPC found and no vpc_id provided.") - vpc_id = vpcs[0]["VpcId"] - - if not subnet_ids: - subnets = ec2.describe_subnets(Filters=[{"Name": "vpc-id", "Values": [vpc_id]}])["Subnets"] - subnet_ids = [s["SubnetId"] for s in subnets] - if len(subnet_ids) < 2: - raise Exception("At least two subnets are required for EKS cluster creation.") - - if not security_group_ids: - sgs = ec2.describe_security_groups(Filters=[ - {"Name": "vpc-id", "Values": [vpc_id]}, - {"Name": "group-name", "Values": ["default"]} - ])["SecurityGroups"] - if not sgs: - raise Exception("No security group found in the VPC.") - security_group_ids = [sgs[0]["GroupId"]] - - # 4️⃣ Create EKS Cluster - print(f"[INFO] Creating EKS cluster '{cluster_name}'...") - eks.create_cluster( - name=cluster_name, - version=version, - roleArn=control_role_arn, - resourcesVpcConfig={ - "subnetIds": subnet_ids, - "securityGroupIds": security_group_ids, - "endpointPublicAccess": endpoint_public, - "endpointPrivateAccess": endpoint_private, - "publicAccessCidrs": public_cidrs - } - ) - waiter = eks.get_waiter("cluster_active") - print(f"[INFO] Waiting for EKS cluster '{cluster_name}' to become ACTIVE...") - waiter.wait(name=cluster_name) - print(f"[SUCCESS] EKS cluster '{cluster_name}' is ACTIVE.") - - # 5️⃣ Add-ons - if install_addons: - addon_versions = { - "vpc-cni": "v1.19.2-eksbuild.1", - "coredns": "v1.11.4-eksbuild.2", - "kube-proxy": "v1.32.0-eksbuild.2", - "eks-pod-identity-agent": "v1.3.4-eksbuild.1" - } - for addon, version_str in addon_versions.items(): - try: - print(f"[INFO] Installing addon: {addon} ({version_str})...") - eks.create_addon( - clusterName=cluster_name, - addonName=addon, - addonVersion=version_str, - resolveConflicts="OVERWRITE" - ) - except eks.exceptions.ResourceInUseException: - print(f"[WARN] Addon '{addon}' already exists. Skipping.") - except Exception as e: - print(f"[ERROR] Failed to install addon '{addon}': {e}") - - # 6️⃣ Register Access Entry - print("[INFO] Registering EKS Access Entries...") - ensure_eks_access_entries(eks, cluster_name, node_role_arn) - print("[INFO] Access Entry registration completed.") - - waiter = eks.get_waiter('cluster_active') - waiter.wait(name=cluster_name) - - # 7️⃣ Node Group - print(f"[INFO] Creating node group '{nodegroup_name}' in '{cluster_name}'...") - eks.create_nodegroup( - clusterName=cluster_name, - nodegroupName=nodegroup_name, - scalingConfig={ - "minSize": min_size, - "maxSize": max_size, - "desiredSize": desired_size - }, - subnets=subnet_ids, - instanceTypes=[instance_type], - nodeRole=node_role_arn, - amiType="AL2023_x86_64_STANDARD", - diskSize=20, - capacityType="ON_DEMAND" - ) - ng_waiter = eks.get_waiter("nodegroup_active") - print(f"[INFO] Waiting for node group '{nodegroup_name}' to become ACTIVE...") - ng_waiter.wait(clusterName=cluster_name, nodegroupName=nodegroup_name) - print(f"[SUCCESS] Node group '{nodegroup_name}' is ACTIVE.") - - return { - "ClusterName": cluster_name, - "NodegroupName": nodegroup_name, - "VPC": vpc_id, - "Subnets": subnet_ids, - "SecurityGroups": security_group_ids - } - def lambda_handler(event, context): """AWS Lambda handler function.""" return mcp_server.handle_request(event, context) \ No newline at end of file From f366ef7a03aeec94dc5bc10ff15fa58207068939 Mon Sep 17 00:00:00 2001 From: Juhui Jung Date: Thu, 3 Jul 2025 18:22:51 +0900 Subject: [PATCH 09/10] chore: update return comments --- server-http-python-lambda/server/app.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/server-http-python-lambda/server/app.py b/server-http-python-lambda/server/app.py index 25650ca..4f106a0 100644 --- a/server-http-python-lambda/server/app.py +++ b/server-http-python-lambda/server/app.py @@ -5,6 +5,7 @@ import os import ipaddress import json +import ast # Get session table name from environment variable session_table = os.environ.get('MCP_SESSION_TABLE', 'mcp_sessions') @@ -94,7 +95,10 @@ def create_vpc(params: dict) -> dict: if isinstance(params, str): - params = json.loads(params) + try: + params = json.loads(params) + except json.JSONDecodeError: + params = ast.literal_eval(params) # Extract parameters with defaults cidr_block = params.get("cidr_block", "10.0.0.0/16") az_list = params.get("az_list", ["us-west-2a", "us-west-2b"]) @@ -196,7 +200,9 @@ def create_vpc(params: dict) -> dict: DestinationCidrBlock="0.0.0.0/0", NatGatewayId=nat_id) - return out + return { + "Message": f"VPC {vpc_id} and related resources created successfully." + } def lambda_handler(event, context): """AWS Lambda handler function.""" From 43ce01be2a677817eb977b468b7b8b45e6390d31 Mon Sep 17 00:00:00 2001 From: Juhui Jung Date: Thu, 3 Jul 2025 18:28:35 +0900 Subject: [PATCH 10/10] chore: delete literal_eval code --- server-http-python-lambda/server/app.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/server-http-python-lambda/server/app.py b/server-http-python-lambda/server/app.py index 4f106a0..22d7d19 100644 --- a/server-http-python-lambda/server/app.py +++ b/server-http-python-lambda/server/app.py @@ -95,10 +95,7 @@ def create_vpc(params: dict) -> dict: if isinstance(params, str): - try: - params = json.loads(params) - except json.JSONDecodeError: - params = ast.literal_eval(params) + params = json.loads(params) # Extract parameters with defaults cidr_block = params.get("cidr_block", "10.0.0.0/16") az_list = params.get("az_list", ["us-west-2a", "us-west-2b"])