Spaces:
Sleeping
Sleeping
updated with help from Gemini
Browse files
app.py
CHANGED
|
@@ -1,39 +1,29 @@
|
|
| 1 |
-
|
| 2 |
-
"""
|
| 3 |
-
Complete Clean Terraform MCP Server
|
| 4 |
-
Single responsibility: Terraform code generation, validation, and workflows
|
| 5 |
-
No external dependencies beyond MCP and standard library
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
import logging
|
| 9 |
-
logging.basicConfig(level=logging.DEBUG,
|
| 10 |
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 11 |
-
logger = logging.getLogger(__name__)
|
| 12 |
-
|
| 13 |
-
logger.debug("Script started: Setting DEBUG log level.") # This should be the very first log
|
| 14 |
-
|
| 15 |
-
|
| 16 |
import asyncio
|
| 17 |
import json
|
| 18 |
import re
|
| 19 |
-
import
|
| 20 |
from typing import Dict, List, Optional, Any
|
| 21 |
from dataclasses import dataclass
|
| 22 |
from datetime import datetime
|
| 23 |
import logging
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
# MCP Server imports
|
| 26 |
import mcp.types as types
|
| 27 |
-
from mcp.server
|
| 28 |
-
from mcp.server import NotificationOptions, Server
|
| 29 |
-
from mcp.server.stdio import stdio_server
|
| 30 |
|
| 31 |
# Configure logging
|
| 32 |
-
|
|
|
|
|
|
|
| 33 |
logger = logging.getLogger(__name__)
|
| 34 |
|
| 35 |
# ============================================================================
|
| 36 |
-
# TERRAFORM CODE TEMPLATES AND GENERATORS
|
| 37 |
# ============================================================================
|
| 38 |
|
| 39 |
class TerraformTemplates:
|
|
@@ -140,7 +130,7 @@ variable "environment" {
|
|
| 140 |
default = "dev"
|
| 141 |
|
| 142 |
validation {
|
| 143 |
-
condition
|
| 144 |
error_message = "Environment must be dev, staging, or prod."
|
| 145 |
}
|
| 146 |
}'''
|
|
@@ -163,7 +153,7 @@ variable "environment" {
|
|
| 163 |
default = "dev"
|
| 164 |
|
| 165 |
validation {
|
| 166 |
-
condition
|
| 167 |
error_message = "Environment must be dev, staging, or prod."
|
| 168 |
}
|
| 169 |
}'''
|
|
@@ -203,7 +193,7 @@ variable "vpc_cidr" {{
|
|
| 203 |
default = "{cidr}"
|
| 204 |
|
| 205 |
validation {{
|
| 206 |
-
condition
|
| 207 |
error_message = "VPC CIDR must be a valid IPv4 CIDR block."
|
| 208 |
}}
|
| 209 |
}}
|
|
@@ -219,10 +209,10 @@ module "{name}_vpc" {{
|
|
| 219 |
public_subnets = [for k, v in {azs} : cidrsubnet(var.vpc_cidr, 8, k)]
|
| 220 |
private_subnets = [for k, v in {azs} : cidrsubnet(var.vpc_cidr, 8, k + 10)]
|
| 221 |
|
| 222 |
-
enable_nat_gateway
|
| 223 |
-
enable_vpn_gateway
|
| 224 |
enable_dns_hostnames = true
|
| 225 |
-
enable_dns_support
|
| 226 |
|
| 227 |
public_subnet_tags = {{
|
| 228 |
Type = "Public"
|
|
@@ -329,7 +319,7 @@ module "{name}_instance" {{
|
|
| 329 |
|
| 330 |
name = "{name}-instance"
|
| 331 |
|
| 332 |
-
instance_type
|
| 333 |
ami = data.aws_ami.app_ami.id
|
| 334 |
key_name = var.key_pair_name
|
| 335 |
monitoring = true
|
|
@@ -500,7 +490,7 @@ module "{name}_s3_bucket" {{
|
|
| 500 |
lifecycle_configuration = {{
|
| 501 |
rule = [
|
| 502 |
{{
|
| 503 |
-
id
|
| 504 |
status = "Enabled"
|
| 505 |
|
| 506 |
abort_incomplete_multipart_upload = {{
|
|
@@ -508,7 +498,7 @@ module "{name}_s3_bucket" {{
|
|
| 508 |
}}
|
| 509 |
}},
|
| 510 |
{{
|
| 511 |
-
id
|
| 512 |
status = "Enabled"
|
| 513 |
|
| 514 |
transition = [
|
|
@@ -647,10 +637,10 @@ module "vpc" {{
|
|
| 647 |
public_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
|
| 648 |
private_subnets = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"]
|
| 649 |
|
| 650 |
-
enable_nat_gateway
|
| 651 |
-
single_nat_gateway
|
| 652 |
enable_dns_hostnames = true
|
| 653 |
-
enable_dns_support
|
| 654 |
|
| 655 |
public_subnet_tags = {{
|
| 656 |
"kubernetes.io/role/elb" = "1"
|
|
@@ -669,11 +659,11 @@ module "vpc" {{
|
|
| 669 |
module "eks" {{
|
| 670 |
source = "terraform-aws-modules/eks/aws"
|
| 671 |
|
| 672 |
-
cluster_name
|
| 673 |
cluster_version = var.kubernetes_version
|
| 674 |
|
| 675 |
-
vpc_id
|
| 676 |
-
subnet_ids
|
| 677 |
cluster_endpoint_public_access = true
|
| 678 |
|
| 679 |
cluster_addons = {{
|
|
@@ -697,9 +687,9 @@ module "eks" {{
|
|
| 697 |
|
| 698 |
instance_types = [var.node_instance_type]
|
| 699 |
|
| 700 |
-
min_size
|
| 701 |
-
max_size
|
| 702 |
-
desired_size
|
| 703 |
|
| 704 |
disk_size = 50
|
| 705 |
|
|
@@ -790,7 +780,7 @@ variable "address_space" {{
|
|
| 790 |
|
| 791 |
# Resource Group
|
| 792 |
resource "azurerm_resource_group" "{name}_rg" {{
|
| 793 |
-
name
|
| 794 |
location = var.location
|
| 795 |
|
| 796 |
tags = {{
|
|
@@ -804,9 +794,9 @@ module "{name}_vnet" {{
|
|
| 804 |
source = "Azure/vnet/azurerm"
|
| 805 |
|
| 806 |
resource_group_name = azurerm_resource_group.{name}_rg.name
|
| 807 |
-
location
|
| 808 |
-
vnet_name
|
| 809 |
-
address_space
|
| 810 |
|
| 811 |
subnet_prefixes = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
|
| 812 |
subnet_names = ["subnet1", "subnet2", "subnet3"]
|
|
@@ -839,7 +829,7 @@ output "resource_group_name" {{
|
|
| 839 |
}}'''
|
| 840 |
|
| 841 |
# ============================================================================
|
| 842 |
-
# TERRAFORM VALIDATION AND UTILITIES
|
| 843 |
# ============================================================================
|
| 844 |
|
| 845 |
class TerraformValidator:
|
|
@@ -1045,217 +1035,20 @@ class TerraformModuleConverter:
|
|
| 1045 |
return '\n\n'.join(outputs)
|
| 1046 |
|
| 1047 |
# ============================================================================
|
| 1048 |
-
#
|
|
|
|
|
|
|
| 1049 |
# ============================================================================
|
| 1050 |
|
| 1051 |
-
class
|
| 1052 |
-
"""
|
| 1053 |
|
| 1054 |
def __init__(self):
|
| 1055 |
-
self.server = Server("terraform-mcp-server")
|
| 1056 |
self.code_generator = TerraformCodeGenerator()
|
| 1057 |
self.validator = TerraformValidator()
|
| 1058 |
self.workflows = TerraformWorkflows()
|
| 1059 |
self.module_converter = TerraformModuleConverter()
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
def _setup_handlers(self):
|
| 1063 |
-
"""Setup MCP server handlers"""
|
| 1064 |
-
|
| 1065 |
-
@self.server.list_tools()
|
| 1066 |
-
async def handle_list_tools() -> list[types.Tool]:
|
| 1067 |
-
logger.debug("Handling ListToolsRequest") # Add debug
|
| 1068 |
-
"""List all available Terraform tools"""
|
| 1069 |
-
return [
|
| 1070 |
-
types.Tool(
|
| 1071 |
-
name="generate_terraform_config",
|
| 1072 |
-
description="Generate complete Terraform configuration for infrastructure resources",
|
| 1073 |
-
inputSchema={
|
| 1074 |
-
"type": "object",
|
| 1075 |
-
"properties": {
|
| 1076 |
-
"resource_type": {
|
| 1077 |
-
"type": "string",
|
| 1078 |
-
"description": "Type of infrastructure resource",
|
| 1079 |
-
"enum": ["vpc", "ec2", "s3", "eks", "rds", "vnet", "compute", "aks"]
|
| 1080 |
-
},
|
| 1081 |
-
"provider": {
|
| 1082 |
-
"type": "string",
|
| 1083 |
-
"description": "Cloud provider",
|
| 1084 |
-
"enum": ["aws", "azurerm", "google"]
|
| 1085 |
-
},
|
| 1086 |
-
"name": {
|
| 1087 |
-
"type": "string",
|
| 1088 |
-
"description": "Name for the infrastructure resources"
|
| 1089 |
-
},
|
| 1090 |
-
"options": {
|
| 1091 |
-
"type": "object",
|
| 1092 |
-
"description": "Resource-specific configuration options",
|
| 1093 |
-
"properties": {
|
| 1094 |
-
"cidr": {"type": "string", "description": "CIDR block for VPC"},
|
| 1095 |
-
"azs": {"type": "array", "items": {"type": "string"}, "description": "Availability zones"},
|
| 1096 |
-
"instance_type": {"type": "string", "description": "EC2 instance type"},
|
| 1097 |
-
"key_pair_name": {"type": "string", "description": "SSH key pair name"},
|
| 1098 |
-
"enable_nat_gateway": {"type": "boolean", "description": "Enable NAT gateway"},
|
| 1099 |
-
"versioning": {"type": "boolean", "description": "Enable S3 versioning"},
|
| 1100 |
-
"encryption": {"type": "boolean", "description": "Enable encryption"},
|
| 1101 |
-
"node_instance_type": {"type": "string", "description": "EKS node instance type"},
|
| 1102 |
-
"min_nodes": {"type": "integer", "description": "Minimum nodes"},
|
| 1103 |
-
"max_nodes": {"type": "integer", "description": "Maximum nodes"},
|
| 1104 |
-
"desired_nodes": {"type": "integer", "description": "Desired nodes"},
|
| 1105 |
-
"kubernetes_version": {"type": "string", "description": "Kubernetes version"}
|
| 1106 |
-
}
|
| 1107 |
-
}
|
| 1108 |
-
},
|
| 1109 |
-
"required": ["resource_type", "provider", "name"]
|
| 1110 |
-
}
|
| 1111 |
-
),
|
| 1112 |
-
types.Tool(
|
| 1113 |
-
name="validate_terraform_config",
|
| 1114 |
-
description="Validate Terraform configuration syntax and best practices",
|
| 1115 |
-
inputSchema={
|
| 1116 |
-
"type": "object",
|
| 1117 |
-
"properties": {
|
| 1118 |
-
"config_content": {
|
| 1119 |
-
"type": "string",
|
| 1120 |
-
"description": "Terraform configuration content to validate"
|
| 1121 |
-
},
|
| 1122 |
-
"check_best_practices": {
|
| 1123 |
-
"type": "boolean",
|
| 1124 |
-
"description": "Include best practices validation",
|
| 1125 |
-
"default": True
|
| 1126 |
-
},
|
| 1127 |
-
"strict_mode": {
|
| 1128 |
-
"type": "boolean",
|
| 1129 |
-
"description": "Enable strict validation rules",
|
| 1130 |
-
"default": False
|
| 1131 |
-
}
|
| 1132 |
-
},
|
| 1133 |
-
"required": ["config_content"]
|
| 1134 |
-
}
|
| 1135 |
-
),
|
| 1136 |
-
types.Tool(
|
| 1137 |
-
name="get_deployment_workflow",
|
| 1138 |
-
description="Generate step-by-step Terraform deployment workflow",
|
| 1139 |
-
inputSchema={
|
| 1140 |
-
"type": "object",
|
| 1141 |
-
"properties": {
|
| 1142 |
-
"workflow_type": {
|
| 1143 |
-
"type": "string",
|
| 1144 |
-
"description": "Type of deployment workflow",
|
| 1145 |
-
"enum": ["basic", "production", "module_development"],
|
| 1146 |
-
"default": "basic"
|
| 1147 |
-
},
|
| 1148 |
-
"backend_type": {
|
| 1149 |
-
"type": "string",
|
| 1150 |
-
"description": "Backend storage type",
|
| 1151 |
-
"enum": ["local", "s3", "azurerm", "gcs"],
|
| 1152 |
-
"default": "local"
|
| 1153 |
-
},
|
| 1154 |
-
"environment": {
|
| 1155 |
-
"type": "string",
|
| 1156 |
-
"description": "Target environment",
|
| 1157 |
-
"enum": ["development", "staging", "production"],
|
| 1158 |
-
"default": "development"
|
| 1159 |
-
}
|
| 1160 |
-
}
|
| 1161 |
-
}
|
| 1162 |
-
),
|
| 1163 |
-
types.Tool(
|
| 1164 |
-
name="convert_to_module",
|
| 1165 |
-
description="Convert Terraform configuration to reusable module structure",
|
| 1166 |
-
inputSchema={
|
| 1167 |
-
"type": "object",
|
| 1168 |
-
"properties": {
|
| 1169 |
-
"config_content": {
|
| 1170 |
-
"type": "string",
|
| 1171 |
-
"description": "Terraform configuration to convert"
|
| 1172 |
-
},
|
| 1173 |
-
"module_name": {
|
| 1174 |
-
"type": "string",
|
| 1175 |
-
"description": "Name for the module"
|
| 1176 |
-
},
|
| 1177 |
-
"extract_variables": {
|
| 1178 |
-
"type": "boolean",
|
| 1179 |
-
"description": "Extract hardcoded values as variables",
|
| 1180 |
-
"default": True
|
| 1181 |
-
},
|
| 1182 |
-
"generate_examples": {
|
| 1183 |
-
"type": "boolean",
|
| 1184 |
-
"description": "Generate usage examples",
|
| 1185 |
-
"default": True
|
| 1186 |
-
}
|
| 1187 |
-
},
|
| 1188 |
-
"required": ["config_content", "module_name"]
|
| 1189 |
-
}
|
| 1190 |
-
),
|
| 1191 |
-
types.Tool(
|
| 1192 |
-
name="format_terraform_code",
|
| 1193 |
-
description="Format and standardize Terraform code",
|
| 1194 |
-
inputSchema={
|
| 1195 |
-
"type": "object",
|
| 1196 |
-
"properties": {
|
| 1197 |
-
"config_content": {
|
| 1198 |
-
"type": "string",
|
| 1199 |
-
"description": "Terraform configuration to format"
|
| 1200 |
-
},
|
| 1201 |
-
"sort_blocks": {
|
| 1202 |
-
"type": "boolean",
|
| 1203 |
-
"description": "Sort resource blocks alphabetically",
|
| 1204 |
-
"default": True
|
| 1205 |
-
}
|
| 1206 |
-
},
|
| 1207 |
-
"required": ["config_content"]
|
| 1208 |
-
}
|
| 1209 |
-
),
|
| 1210 |
-
types.Tool(
|
| 1211 |
-
name="generate_terraform_docs",
|
| 1212 |
-
description="Generate documentation for Terraform configuration",
|
| 1213 |
-
inputSchema={
|
| 1214 |
-
"type": "object",
|
| 1215 |
-
"properties": {
|
| 1216 |
-
"config_content": {
|
| 1217 |
-
"type": "string",
|
| 1218 |
-
"description": "Terraform configuration to document"
|
| 1219 |
-
},
|
| 1220 |
-
"module_name": {
|
| 1221 |
-
"type": "string",
|
| 1222 |
-
"description": "Name of the module"
|
| 1223 |
-
},
|
| 1224 |
-
"include_examples": {
|
| 1225 |
-
"type": "boolean",
|
| 1226 |
-
"description": "Include usage examples",
|
| 1227 |
-
"default": True
|
| 1228 |
-
}
|
| 1229 |
-
},
|
| 1230 |
-
"required": ["config_content"]
|
| 1231 |
-
}
|
| 1232 |
-
)
|
| 1233 |
-
]
|
| 1234 |
-
|
| 1235 |
-
@self.server.call_tool()
|
| 1236 |
-
async def handle_call_tool(name: str, arguments: dict) -> list[types.TextContent]:
|
| 1237 |
-
logger.debug("Handling CallToolRequest for tool: {name} ") # Add debug here
|
| 1238 |
-
"""Handle tool calls"""
|
| 1239 |
-
try:
|
| 1240 |
-
if name == "generate_terraform_config":
|
| 1241 |
-
return await self._handle_generate_config(arguments)
|
| 1242 |
-
elif name == "validate_terraform_config":
|
| 1243 |
-
return await self._handle_validate_config(arguments)
|
| 1244 |
-
elif name == "get_deployment_workflow":
|
| 1245 |
-
return await self._handle_deployment_workflow(arguments)
|
| 1246 |
-
elif name == "convert_to_module":
|
| 1247 |
-
return await self._handle_convert_to_module(arguments)
|
| 1248 |
-
elif name == "format_terraform_code":
|
| 1249 |
-
return await self._handle_format_code(arguments)
|
| 1250 |
-
elif name == "generate_terraform_docs":
|
| 1251 |
-
return await self._handle_generate_docs(arguments)
|
| 1252 |
-
else:
|
| 1253 |
-
return [types.TextContent(type="text", text=f"β Unknown tool: {name}")]
|
| 1254 |
-
|
| 1255 |
-
except Exception as e:
|
| 1256 |
-
logger.error(f"Tool execution error: {e}")
|
| 1257 |
-
return [types.TextContent(type="text", text=f"β Error executing {name}: {str(e)}")]
|
| 1258 |
-
|
| 1259 |
async def _handle_generate_config(self, args: dict) -> list[types.TextContent]:
|
| 1260 |
"""Handle Terraform configuration generation"""
|
| 1261 |
resource_type = args.get("resource_type")
|
|
@@ -1471,15 +1264,15 @@ Converting Terraform configuration to a reusable module structure.
|
|
| 1471 |
|
| 1472 |
```
|
| 1473 |
modules/{module_name}/
|
| 1474 |
-
βββ main.tf
|
| 1475 |
-
βββ variables.tf
|
| 1476 |
-
βββ outputs.tf
|
| 1477 |
-
βββ versions.tf
|
| 1478 |
-
βββ README.md
|
| 1479 |
βββ examples/
|
| 1480 |
βββ basic/
|
| 1481 |
-
βββ main.tf
|
| 1482 |
-
βββ README.md
|
| 1483 |
```
|
| 1484 |
|
| 1485 |
"""
|
|
@@ -1755,62 +1548,315 @@ module "{module_name}" {{
|
|
| 1755 |
|
| 1756 |
return [types.TextContent(type="text", text=response)]
|
| 1757 |
|
|
|
|
| 1758 |
# ============================================================================
|
| 1759 |
-
#
|
| 1760 |
# ============================================================================
|
| 1761 |
|
| 1762 |
-
|
| 1763 |
-
|
| 1764 |
-
logger.info("π§ Available tools: generate_terraform_config, validate_terraform_config, get_deployment_workflow, convert_to_module, format_terraform_code, generate_terraform_docs")
|
| 1765 |
-
logger.info("π¦ No external dependencies - pure Terraform tooling")
|
| 1766 |
|
| 1767 |
-
|
|
|
|
| 1768 |
|
| 1769 |
-
|
| 1770 |
-
|
| 1771 |
-
async with stdio_server() as (read_stream, write_stream):
|
| 1772 |
-
logger.debug("stdio_server context entered successfully. Attempting to run MCP server...")
|
| 1773 |
-
server_task = asyncio.create_task(
|
| 1774 |
-
terraform_server.server.run(
|
| 1775 |
-
read_stream,
|
| 1776 |
-
write_stream,
|
| 1777 |
-
InitializationOptions(
|
| 1778 |
-
server_name="terraform-mcp-server",
|
| 1779 |
-
server_version="1.0.0",
|
| 1780 |
-
capabilities=terraform_server.server.get_capabilities(
|
| 1781 |
-
notification_options=NotificationOptions(),
|
| 1782 |
-
experimental_capabilities={}
|
| 1783 |
-
)
|
| 1784 |
-
)
|
| 1785 |
-
)
|
| 1786 |
-
)
|
| 1787 |
-
# This is the crucial part: keep the main event loop running.
|
| 1788 |
-
# The server_task is now running in the background.
|
| 1789 |
-
# We need to explicitly await it or another task that keeps the event loop alive.
|
| 1790 |
-
# asyncio.Future() creates a Future that never completes, effectively blocking main.
|
| 1791 |
-
# Or you can await the server_task if it's meant to run indefinitely.
|
| 1792 |
-
# Given your log "MCP server.run() completed", it suggests it exits.
|
| 1793 |
-
# So, we need something *after* it that blocks.
|
| 1794 |
-
|
| 1795 |
-
# Option 1: Await the server task, assuming it will block indefinitely (but your logs contradict this)
|
| 1796 |
-
# await server_task
|
| 1797 |
-
|
| 1798 |
-
# Option 2: Keep the event loop running indefinitely if server_task completes
|
| 1799 |
-
logger.debug("MCP server task created. Keeping event loop alive...")
|
| 1800 |
-
await asyncio.Future() # This creates a future that never resolves, keeping the event loop running
|
| 1801 |
-
|
| 1802 |
-
logger.debug("This line should theoretically not be reached if asyncio.Future() keeps the loop alive.")
|
| 1803 |
|
| 1804 |
-
|
| 1805 |
-
|
| 1806 |
-
|
| 1807 |
|
| 1808 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1809 |
try:
|
| 1810 |
-
|
| 1811 |
-
|
| 1812 |
-
|
| 1813 |
-
|
| 1814 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1815 |
except Exception as e:
|
| 1816 |
-
logger.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import asyncio
|
| 3 |
import json
|
| 4 |
import re
|
| 5 |
+
import uvicorn # Used for running the FastAPI application
|
| 6 |
from typing import Dict, List, Optional, Any
|
| 7 |
from dataclasses import dataclass
|
| 8 |
from datetime import datetime
|
| 9 |
import logging
|
| 10 |
|
| 11 |
+
# FastAPI imports
|
| 12 |
+
from fastapi import FastAPI, Request
|
| 13 |
+
from fastapi.responses import StreamingResponse
|
| 14 |
+
|
| 15 |
# MCP Server imports
|
| 16 |
import mcp.types as types
|
| 17 |
+
from mcp.server import Server, NotificationOptions, InitializationOptions
|
|
|
|
|
|
|
| 18 |
|
| 19 |
# Configure logging
|
| 20 |
+
# Set a very low level for debugging if needed, usually INFO for production
|
| 21 |
+
logging.basicConfig(level=logging.INFO,
|
| 22 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 23 |
logger = logging.getLogger(__name__)
|
| 24 |
|
| 25 |
# ============================================================================
|
| 26 |
+
# TERRAFORM CODE TEMPLATES AND GENERATORS (Existing code, slightly adjusted formatting for consistency)
|
| 27 |
# ============================================================================
|
| 28 |
|
| 29 |
class TerraformTemplates:
|
|
|
|
| 130 |
default = "dev"
|
| 131 |
|
| 132 |
validation {
|
| 133 |
+
condition = contains(["dev", "staging", "prod"], var.environment)
|
| 134 |
error_message = "Environment must be dev, staging, or prod."
|
| 135 |
}
|
| 136 |
}'''
|
|
|
|
| 153 |
default = "dev"
|
| 154 |
|
| 155 |
validation {
|
| 156 |
+
condition = contains(["dev", "staging", "prod"], var.environment)
|
| 157 |
error_message = "Environment must be dev, staging, or prod."
|
| 158 |
}
|
| 159 |
}'''
|
|
|
|
| 193 |
default = "{cidr}"
|
| 194 |
|
| 195 |
validation {{
|
| 196 |
+
condition = can(cidrhost(var.vpc_cidr, 0))
|
| 197 |
error_message = "VPC CIDR must be a valid IPv4 CIDR block."
|
| 198 |
}}
|
| 199 |
}}
|
|
|
|
| 209 |
public_subnets = [for k, v in {azs} : cidrsubnet(var.vpc_cidr, 8, k)]
|
| 210 |
private_subnets = [for k, v in {azs} : cidrsubnet(var.vpc_cidr, 8, k + 10)]
|
| 211 |
|
| 212 |
+
enable_nat_gateway = {str(enable_nat).lower()}
|
| 213 |
+
enable_vpn_gateway = false
|
| 214 |
enable_dns_hostnames = true
|
| 215 |
+
enable_dns_support = true
|
| 216 |
|
| 217 |
public_subnet_tags = {{
|
| 218 |
Type = "Public"
|
|
|
|
| 319 |
|
| 320 |
name = "{name}-instance"
|
| 321 |
|
| 322 |
+
instance_type = var.instance_type
|
| 323 |
ami = data.aws_ami.app_ami.id
|
| 324 |
key_name = var.key_pair_name
|
| 325 |
monitoring = true
|
|
|
|
| 490 |
lifecycle_configuration = {{
|
| 491 |
rule = [
|
| 492 |
{{
|
| 493 |
+
id = "delete_incomplete_multipart_uploads"
|
| 494 |
status = "Enabled"
|
| 495 |
|
| 496 |
abort_incomplete_multipart_upload = {{
|
|
|
|
| 498 |
}}
|
| 499 |
}},
|
| 500 |
{{
|
| 501 |
+
id = "transition_to_ia"
|
| 502 |
status = "Enabled"
|
| 503 |
|
| 504 |
transition = [
|
|
|
|
| 637 |
public_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
|
| 638 |
private_subnets = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"]
|
| 639 |
|
| 640 |
+
enable_nat_gateway = true
|
| 641 |
+
single_nat_gateway = true
|
| 642 |
enable_dns_hostnames = true
|
| 643 |
+
enable_dns_support = true
|
| 644 |
|
| 645 |
public_subnet_tags = {{
|
| 646 |
"kubernetes.io/role/elb" = "1"
|
|
|
|
| 659 |
module "eks" {{
|
| 660 |
source = "terraform-aws-modules/eks/aws"
|
| 661 |
|
| 662 |
+
cluster_name = var.cluster_name
|
| 663 |
cluster_version = var.kubernetes_version
|
| 664 |
|
| 665 |
+
vpc_id = module.vpc.vpc_id
|
| 666 |
+
subnet_ids = module.vpc.private_subnets
|
| 667 |
cluster_endpoint_public_access = true
|
| 668 |
|
| 669 |
cluster_addons = {{
|
|
|
|
| 687 |
|
| 688 |
instance_types = [var.node_instance_type]
|
| 689 |
|
| 690 |
+
min_size = var.node_group_min_size
|
| 691 |
+
max_size = var.node_group_max_size
|
| 692 |
+
desired_size = var.node_group_desired_size
|
| 693 |
|
| 694 |
disk_size = 50
|
| 695 |
|
|
|
|
| 780 |
|
| 781 |
# Resource Group
|
| 782 |
resource "azurerm_resource_group" "{name}_rg" {{
|
| 783 |
+
name = "${{var.environment}}-{name}-rg"
|
| 784 |
location = var.location
|
| 785 |
|
| 786 |
tags = {{
|
|
|
|
| 794 |
source = "Azure/vnet/azurerm"
|
| 795 |
|
| 796 |
resource_group_name = azurerm_resource_group.{name}_rg.name
|
| 797 |
+
location = azurerm_resource_group.{name}_rg.location
|
| 798 |
+
vnet_name = "${{var.environment}}-{name}-vnet"
|
| 799 |
+
address_space = var.address_space
|
| 800 |
|
| 801 |
subnet_prefixes = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
|
| 802 |
subnet_names = ["subnet1", "subnet2", "subnet3"]
|
|
|
|
| 829 |
}}'''
|
| 830 |
|
| 831 |
# ============================================================================
|
| 832 |
+
# TERRAFORM VALIDATION AND UTILITIES (Existing code)
|
| 833 |
# ============================================================================
|
| 834 |
|
| 835 |
class TerraformValidator:
|
|
|
|
| 1035 |
return '\n\n'.join(outputs)
|
| 1036 |
|
| 1037 |
# ============================================================================
|
| 1038 |
+
# TERRAFORM AGENT HANDLERS (Core Logic, adapted from CleanTerraformMCPServer)
|
| 1039 |
+
# This class now holds the business logic, separate from the MCP Server instance
|
| 1040 |
+
# and FastAPI endpoints.
|
| 1041 |
# ============================================================================
|
| 1042 |
|
| 1043 |
+
class TerraformAgentHandlers:
|
| 1044 |
+
"""Handles the core logic for Terraform tool operations."""
|
| 1045 |
|
| 1046 |
def __init__(self):
|
|
|
|
| 1047 |
self.code_generator = TerraformCodeGenerator()
|
| 1048 |
self.validator = TerraformValidator()
|
| 1049 |
self.workflows = TerraformWorkflows()
|
| 1050 |
self.module_converter = TerraformModuleConverter()
|
| 1051 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1052 |
async def _handle_generate_config(self, args: dict) -> list[types.TextContent]:
|
| 1053 |
"""Handle Terraform configuration generation"""
|
| 1054 |
resource_type = args.get("resource_type")
|
|
|
|
| 1264 |
|
| 1265 |
```
|
| 1266 |
modules/{module_name}/
|
| 1267 |
+
βββ main.tf # Main configuration
|
| 1268 |
+
βββ variables.tf # Input variables
|
| 1269 |
+
βββ outputs.tf # Output values
|
| 1270 |
+
βββ versions.tf # Provider requirements
|
| 1271 |
+
βββ README.md # Documentation
|
| 1272 |
βββ examples/
|
| 1273 |
βββ basic/
|
| 1274 |
+
βββ main.tf # Example usage
|
| 1275 |
+
βββ README.md # Example documentation
|
| 1276 |
```
|
| 1277 |
|
| 1278 |
"""
|
|
|
|
| 1548 |
|
| 1549 |
return [types.TextContent(type="text", text=response)]
|
| 1550 |
|
| 1551 |
+
|
| 1552 |
# ============================================================================
|
| 1553 |
+
# FASTAPI APP AND MCP SERVER INSTANCE
|
| 1554 |
# ============================================================================
|
| 1555 |
|
| 1556 |
+
# Create FastAPI app
|
| 1557 |
+
app = FastAPI(title="Terraform MCP Server")
|
|
|
|
|
|
|
| 1558 |
|
| 1559 |
+
# Create an instance of the TerraformAgentHandlers to manage tool logic
|
| 1560 |
+
terraform_agent_handlers = TerraformAgentHandlers()
|
| 1561 |
|
| 1562 |
+
# Create MCP server instance
|
| 1563 |
+
mcp_server_instance = Server("terraform-mcp-server")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1564 |
|
| 1565 |
+
# ============================================================================
|
| 1566 |
+
# MCP SERVER HANDLERS (Decorated on the global mcp_server_instance)
|
| 1567 |
+
# ============================================================================
|
| 1568 |
|
| 1569 |
+
@mcp_server_instance.list_tools()
|
| 1570 |
+
async def list_tools_for_mcp(): # Renamed to avoid conflict with `list_tools` in the Linux example
|
| 1571 |
+
"""List all available Terraform tools"""
|
| 1572 |
+
logger.debug("MCP ListToolsRequest received.")
|
| 1573 |
+
return [
|
| 1574 |
+
types.Tool(
|
| 1575 |
+
name="generate_terraform_config",
|
| 1576 |
+
description="Generate complete Terraform configuration for infrastructure resources",
|
| 1577 |
+
inputSchema={
|
| 1578 |
+
"type": "object",
|
| 1579 |
+
"properties": {
|
| 1580 |
+
"resource_type": {
|
| 1581 |
+
"type": "string",
|
| 1582 |
+
"description": "Type of infrastructure resource",
|
| 1583 |
+
"enum": ["vpc", "ec2", "s3", "eks", "rds", "vnet", "compute", "aks"]
|
| 1584 |
+
},
|
| 1585 |
+
"provider": {
|
| 1586 |
+
"type": "string",
|
| 1587 |
+
"description": "Cloud provider",
|
| 1588 |
+
"enum": ["aws", "azurerm", "google"]
|
| 1589 |
+
},
|
| 1590 |
+
"name": {
|
| 1591 |
+
"type": "string",
|
| 1592 |
+
"description": "Name for the infrastructure resources"
|
| 1593 |
+
},
|
| 1594 |
+
"options": {
|
| 1595 |
+
"type": "object",
|
| 1596 |
+
"description": "Resource-specific configuration options",
|
| 1597 |
+
"properties": {
|
| 1598 |
+
"cidr": {"type": "string", "description": "CIDR block for VPC"},
|
| 1599 |
+
"azs": {"type": "array", "items": {"type": "string"}, "description": "Availability zones"},
|
| 1600 |
+
"instance_type": {"type": "string", "description": "EC2 instance type"},
|
| 1601 |
+
"key_pair_name": {"type": "string", "description": "SSH key pair name"},
|
| 1602 |
+
"enable_nat_gateway": {"type": "boolean", "description": "Enable NAT gateway"},
|
| 1603 |
+
"versioning": {"type": "boolean", "description": "Enable S3 versioning"},
|
| 1604 |
+
"encryption": {"type": "boolean", "description": "Enable encryption"},
|
| 1605 |
+
"node_instance_type": {"type": "string", "description": "EKS node instance type"},
|
| 1606 |
+
"min_nodes": {"type": "integer", "description": "Minimum nodes"},
|
| 1607 |
+
"max_nodes": {"type": "integer", "description": "Maximum nodes"},
|
| 1608 |
+
"desired_nodes": {"type": "integer", "description": "Desired nodes"},
|
| 1609 |
+
"kubernetes_version": {"type": "string", "description": "Kubernetes version"}
|
| 1610 |
+
}
|
| 1611 |
+
}
|
| 1612 |
+
},
|
| 1613 |
+
"required": ["resource_type", "provider", "name"]
|
| 1614 |
+
}
|
| 1615 |
+
),
|
| 1616 |
+
types.Tool(
|
| 1617 |
+
name="validate_terraform_config",
|
| 1618 |
+
description="Validate Terraform configuration syntax and best practices",
|
| 1619 |
+
inputSchema={
|
| 1620 |
+
"type": "object",
|
| 1621 |
+
"properties": {
|
| 1622 |
+
"config_content": {
|
| 1623 |
+
"type": "string",
|
| 1624 |
+
"description": "Terraform configuration content to validate"
|
| 1625 |
+
},
|
| 1626 |
+
"check_best_practices": {
|
| 1627 |
+
"type": "boolean",
|
| 1628 |
+
"description": "Include best practices validation",
|
| 1629 |
+
"default": True
|
| 1630 |
+
},
|
| 1631 |
+
"strict_mode": {
|
| 1632 |
+
"type": "boolean",
|
| 1633 |
+
"description": "Enable strict validation rules",
|
| 1634 |
+
"default": False
|
| 1635 |
+
}
|
| 1636 |
+
},
|
| 1637 |
+
"required": ["config_content"]
|
| 1638 |
+
}
|
| 1639 |
+
),
|
| 1640 |
+
types.Tool(
|
| 1641 |
+
name="get_deployment_workflow",
|
| 1642 |
+
description="Generate step-by-step Terraform deployment workflow",
|
| 1643 |
+
inputSchema={
|
| 1644 |
+
"type": "object",
|
| 1645 |
+
"properties": {
|
| 1646 |
+
"workflow_type": {
|
| 1647 |
+
"type": "string",
|
| 1648 |
+
"description": "Type of deployment workflow",
|
| 1649 |
+
"enum": ["basic", "production", "module_development"],
|
| 1650 |
+
"default": "basic"
|
| 1651 |
+
},
|
| 1652 |
+
"backend_type": {
|
| 1653 |
+
"type": "string",
|
| 1654 |
+
"description": "Backend storage type",
|
| 1655 |
+
"enum": ["local", "s3", "azurerm", "gcs"],
|
| 1656 |
+
"default": "local"
|
| 1657 |
+
},
|
| 1658 |
+
"environment": {
|
| 1659 |
+
"type": "string",
|
| 1660 |
+
"description": "Target environment",
|
| 1661 |
+
"enum": ["development", "staging", "production"],
|
| 1662 |
+
"default": "development"
|
| 1663 |
+
}
|
| 1664 |
+
}
|
| 1665 |
+
}
|
| 1666 |
+
),
|
| 1667 |
+
types.Tool(
|
| 1668 |
+
name="convert_to_module",
|
| 1669 |
+
description="Convert Terraform configuration to reusable module structure",
|
| 1670 |
+
inputSchema={
|
| 1671 |
+
"type": "object",
|
| 1672 |
+
"properties": {
|
| 1673 |
+
"config_content": {
|
| 1674 |
+
"type": "string",
|
| 1675 |
+
"description": "Terraform configuration to convert"
|
| 1676 |
+
},
|
| 1677 |
+
"module_name": {
|
| 1678 |
+
"type": "string",
|
| 1679 |
+
"description": "Name for the module"
|
| 1680 |
+
},
|
| 1681 |
+
"extract_variables": {
|
| 1682 |
+
"type": "boolean",
|
| 1683 |
+
"description": "Extract hardcoded values as variables",
|
| 1684 |
+
"default": True
|
| 1685 |
+
},
|
| 1686 |
+
"generate_examples": {
|
| 1687 |
+
"type": "boolean",
|
| 1688 |
+
"description": "Generate usage examples",
|
| 1689 |
+
"default": True
|
| 1690 |
+
}
|
| 1691 |
+
},
|
| 1692 |
+
"required": ["config_content", "module_name"]
|
| 1693 |
+
}
|
| 1694 |
+
),
|
| 1695 |
+
types.Tool(
|
| 1696 |
+
name="format_terraform_code",
|
| 1697 |
+
description="Format and standardize Terraform code",
|
| 1698 |
+
inputSchema={
|
| 1699 |
+
"type": "object",
|
| 1700 |
+
"properties": {
|
| 1701 |
+
"config_content": {
|
| 1702 |
+
"type": "string",
|
| 1703 |
+
"description": "Terraform configuration to format"
|
| 1704 |
+
},
|
| 1705 |
+
"sort_blocks": {
|
| 1706 |
+
"type": "boolean",
|
| 1707 |
+
"description": "Sort resource blocks alphabetically",
|
| 1708 |
+
"default": True
|
| 1709 |
+
}
|
| 1710 |
+
},
|
| 1711 |
+
"required": ["config_content"]
|
| 1712 |
+
}
|
| 1713 |
+
),
|
| 1714 |
+
types.Tool(
|
| 1715 |
+
name="generate_terraform_docs",
|
| 1716 |
+
description="Generate documentation for Terraform configuration",
|
| 1717 |
+
inputSchema={
|
| 1718 |
+
"type": "object",
|
| 1719 |
+
"properties": {
|
| 1720 |
+
"config_content": {
|
| 1721 |
+
"type": "string",
|
| 1722 |
+
"description": "Terraform configuration to document"
|
| 1723 |
+
},
|
| 1724 |
+
"module_name": {
|
| 1725 |
+
"type": "string",
|
| 1726 |
+
"description": "Name of the module"
|
| 1727 |
+
},
|
| 1728 |
+
"include_examples": {
|
| 1729 |
+
"type": "boolean",
|
| 1730 |
+
"description": "Include usage examples",
|
| 1731 |
+
"default": True
|
| 1732 |
+
}
|
| 1733 |
+
},
|
| 1734 |
+
"required": ["config_content"]
|
| 1735 |
+
}
|
| 1736 |
+
)
|
| 1737 |
+
]
|
| 1738 |
+
|
| 1739 |
+
@mcp_server_instance.call_tool()
|
| 1740 |
+
async def call_tool_for_mcp(name: str, arguments: dict) -> list[types.TextContent]: # Renamed
|
| 1741 |
+
"""Handle tool calls from MCP client via SSE"""
|
| 1742 |
+
logger.info(f"MCP CallToolRequest received for tool: {name}")
|
| 1743 |
try:
|
| 1744 |
+
if name == "generate_terraform_config":
|
| 1745 |
+
return await terraform_agent_handlers._handle_generate_config(arguments)
|
| 1746 |
+
elif name == "validate_terraform_config":
|
| 1747 |
+
return await terraform_agent_handlers._handle_validate_config(arguments)
|
| 1748 |
+
elif name == "get_deployment_workflow":
|
| 1749 |
+
return await terraform_agent_handlers._handle_deployment_workflow(arguments)
|
| 1750 |
+
elif name == "convert_to_module":
|
| 1751 |
+
return await terraform_agent_handlers._handle_convert_to_module(arguments)
|
| 1752 |
+
elif name == "format_terraform_code":
|
| 1753 |
+
return await terraform_agent_handlers._handle_format_code(arguments)
|
| 1754 |
+
elif name == "generate_terraform_docs":
|
| 1755 |
+
return await terraform_agent_handlers._handle_generate_docs(arguments)
|
| 1756 |
+
else:
|
| 1757 |
+
return [types.TextContent(type="text", text=f"β Unknown tool: {name}")]
|
| 1758 |
except Exception as e:
|
| 1759 |
+
logger.exception(f"Error executing Terraform tool {name}")
|
| 1760 |
+
return [types.TextContent(type="text", text=f"β Error executing {name}: {str(e)}")]
|
| 1761 |
+
|
| 1762 |
+
# ============================================================================
|
| 1763 |
+
# FASTAPI WEB ENDPOINTS
|
| 1764 |
+
# ============================================================================
|
| 1765 |
+
|
| 1766 |
+
@app.get("/")
|
| 1767 |
+
async def root():
|
| 1768 |
+
"""Status endpoint"""
|
| 1769 |
+
logger.info("GET / requested - sending status.")
|
| 1770 |
+
return {
|
| 1771 |
+
"service": "terraform-mcp-server",
|
| 1772 |
+
"status": "running",
|
| 1773 |
+
"mcp_endpoint": "/mcp/sse",
|
| 1774 |
+
"tools": len(await list_tools_for_mcp()), # Dynamically list tools
|
| 1775 |
+
"categories": ["infrastructure_generation", "validation", "workflow_management", "module_conversion", "documentation"]
|
| 1776 |
+
}
|
| 1777 |
+
|
| 1778 |
+
@app.get("/health")
|
| 1779 |
+
async def health():
|
| 1780 |
+
"""Health check endpoint for Hugging Face Spaces"""
|
| 1781 |
+
logger.debug("GET /health requested - sending healthy status.")
|
| 1782 |
+
return {"status": "healthy", "service": "terraform-mcp-server"}
|
| 1783 |
+
|
| 1784 |
+
@app.get("/mcp/sse")
|
| 1785 |
+
async def mcp_sse_endpoint(request: Request):
|
| 1786 |
+
"""MCP SSE endpoint for agent connections"""
|
| 1787 |
+
logger.info("GET /mcp/sse requested - starting SSE stream.")
|
| 1788 |
+
|
| 1789 |
+
async def event_stream():
|
| 1790 |
+
# MCP uses this internally for server initialization and handling client requests
|
| 1791 |
+
# The mcp_server_instance will manage the SSE communication within its run() method.
|
| 1792 |
+
# This wrapper is needed to hook mcp_server_instance.run() into FastAPI's StreamingResponse.
|
| 1793 |
+
|
| 1794 |
+
# Create dummy readers/writers for mcp_server_instance.run()
|
| 1795 |
+
# In a real-world scenario, mcp.server might provide an HTTP/SSE specific run method
|
| 1796 |
+
# or expect a custom transport. For this setup, we'll try to adapt.
|
| 1797 |
+
|
| 1798 |
+
# The MCP library's Server.run() method expects streams, so we need to mock or
|
| 1799 |
+
# adapt it to FastAPI's request/response cycle.
|
| 1800 |
+
# However, the most direct approach for MCP and FastAPI is to let MCP handle the HTTP
|
| 1801 |
+
# part if it has a specific HTTP/SSE server implementation, or have FastAPI
|
| 1802 |
+
# call MCP's core logic directly (as I did in the Gradio example).
|
| 1803 |
+
|
| 1804 |
+
# Given the Linux example, the MCP server is directly serving the /mcp/sse.
|
| 1805 |
+
# This implies mcp.server.Server has a mechanism to do this, or the StreamingResponse
|
| 1806 |
+
# content generator is the key.
|
| 1807 |
+
|
| 1808 |
+
# Let's align with the Linux code provided:
|
| 1809 |
+
# The Linux example *does not* call `mcp_app.run()` inside the SSE endpoint directly.
|
| 1810 |
+
# Instead, it uses `mcp_app.list_tools()` and `mcp_app.call_tool()` as decorators
|
| 1811 |
+
# on global functions, and the FastAPI endpoint just sends keepalives.
|
| 1812 |
+
# The communication over SSE is typically initiated by a client *sending* an MCP message
|
| 1813 |
+
# to the server, and the server *sending* responses back.
|
| 1814 |
+
|
| 1815 |
+
# For a simple SSE endpoint that only sends keepalives and doesn't handle incoming MCP calls via SSE directly
|
| 1816 |
+
# (which is what your Linux /mcp/sse seems to do with just keepalives):
|
| 1817 |
+
try:
|
| 1818 |
+
while True:
|
| 1819 |
+
if await request.is_disconnected():
|
| 1820 |
+
logger.info("Client disconnected from /mcp/sse.")
|
| 1821 |
+
break
|
| 1822 |
+
|
| 1823 |
+
# Send a keepalive signal to maintain the connection
|
| 1824 |
+
yield f"data: {json.dumps({'type': 'keepalive'})}\n\n"
|
| 1825 |
+
await asyncio.sleep(30) # Send keepalive every 30 seconds
|
| 1826 |
+
|
| 1827 |
+
except asyncio.CancelledError:
|
| 1828 |
+
logger.info("SSE event stream cancelled.")
|
| 1829 |
+
except Exception as e:
|
| 1830 |
+
logger.error(f"Error in SSE event stream: {e}", exc_info=True)
|
| 1831 |
+
|
| 1832 |
+
|
| 1833 |
+
return StreamingResponse(
|
| 1834 |
+
event_stream(),
|
| 1835 |
+
media_type="text/event-stream",
|
| 1836 |
+
headers={
|
| 1837 |
+
"Cache-Control": "no-cache",
|
| 1838 |
+
"Connection": "keep-alive",
|
| 1839 |
+
"Access-Control-Allow-Origin": "*", # Important for CORS if consumed by a frontend
|
| 1840 |
+
}
|
| 1841 |
+
)
|
| 1842 |
+
|
| 1843 |
+
# ============================================================================
|
| 1844 |
+
# MAIN ENTRY POINT FOR FASTAPI/UVICORN
|
| 1845 |
+
# ============================================================================
|
| 1846 |
+
|
| 1847 |
+
def main():
|
| 1848 |
+
"""Main entry point to run the FastAPI application with Uvicorn."""
|
| 1849 |
+
port = int(os.getenv("PORT", 7860)) # Default to 7860, used by Hugging Face Spaces
|
| 1850 |
+
|
| 1851 |
+
logger.info(f"π Starting Terraform MCP Server with FastAPI/Uvicorn on port {port}")
|
| 1852 |
+
print(f"π Status Endpoint: http://0.0.0.0:{port}/")
|
| 1853 |
+
print(f"β€οΈ Health Check: http://0.0.0.0:{port}/health")
|
| 1854 |
+
print(f"π MCP SSE Endpoint: http://0.0.0.0:{port}/mcp/sse")
|
| 1855 |
+
print(f"π οΈ Available Tools: generate_terraform_config, validate_terraform_config, get_deployment_workflow, convert_to_module, format_terraform_code, generate_terraform_docs")
|
| 1856 |
+
|
| 1857 |
+
# Run the FastAPI application using Uvicorn
|
| 1858 |
+
# This will bind to the specified host and port, handling HTTP requests.
|
| 1859 |
+
uvicorn.run(app, host="0.0.0.0", port=port, log_level="info")
|
| 1860 |
+
|
| 1861 |
+
if __name__ == "__main__":
|
| 1862 |
+
main()
|