ChrisSacrumCor commited on
Commit
3100adc
Β·
verified Β·
1 Parent(s): e827a48

updated with help from Gemini

Browse files
Files changed (1) hide show
  1. app.py +350 -304
app.py CHANGED
@@ -1,39 +1,29 @@
1
- #!/usr/bin/env python3
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 os
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.models import InitializationOptions
28
- from mcp.server import NotificationOptions, Server
29
- from mcp.server.stdio import stdio_server
30
 
31
  # Configure logging
32
- logging.basicConfig(level=logging.INFO)
 
 
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 = contains(["dev", "staging", "prod"], var.environment)
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 = contains(["dev", "staging", "prod"], var.environment)
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 = can(cidrhost(var.vpc_cidr, 0))
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 = {str(enable_nat).lower()}
223
- enable_vpn_gateway = false
224
  enable_dns_hostnames = true
225
- enable_dns_support = true
226
 
227
  public_subnet_tags = {{
228
  Type = "Public"
@@ -329,7 +319,7 @@ module "{name}_instance" {{
329
 
330
  name = "{name}-instance"
331
 
332
- instance_type = var.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 = "delete_incomplete_multipart_uploads"
504
  status = "Enabled"
505
 
506
  abort_incomplete_multipart_upload = {{
@@ -508,7 +498,7 @@ module "{name}_s3_bucket" {{
508
  }}
509
  }},
510
  {{
511
- id = "transition_to_ia"
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 = true
651
- single_nat_gateway = true
652
  enable_dns_hostnames = true
653
- enable_dns_support = true
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 = var.cluster_name
673
  cluster_version = var.kubernetes_version
674
 
675
- vpc_id = module.vpc.vpc_id
676
- subnet_ids = module.vpc.private_subnets
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 = var.node_group_min_size
701
- max_size = var.node_group_max_size
702
- desired_size = var.node_group_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 = "${{var.environment}}-{name}-rg"
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 = azurerm_resource_group.{name}_rg.location
808
- vnet_name = "${{var.environment}}-{name}-vnet"
809
- address_space = var.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
- # MCP SERVER IMPLEMENTATION
 
 
1049
  # ============================================================================
1050
 
1051
- class CleanTerraformMCPServer:
1052
- """Clean Terraform MCP Server - focused on code generation and validation"""
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
- self._setup_handlers()
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 # Main configuration
1475
- β”œβ”€β”€ variables.tf # Input variables
1476
- β”œβ”€β”€ outputs.tf # Output values
1477
- β”œβ”€β”€ versions.tf # Provider requirements
1478
- β”œβ”€β”€ README.md # Documentation
1479
  └── examples/
1480
  └── basic/
1481
- β”œβ”€β”€ main.tf # Example usage
1482
- └── README.md # Example documentation
1483
  ```
1484
 
1485
  """
@@ -1755,62 +1548,315 @@ module "{module_name}" {{
1755
 
1756
  return [types.TextContent(type="text", text=response)]
1757
 
 
1758
  # ============================================================================
1759
- # MAIN ENTRY POINT FOR HF SPACES
1760
  # ============================================================================
1761
 
1762
- async def main():
1763
- logger.info("πŸš€ Clean Terraform MCP Server starting...")
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
- terraform_server = CleanTerraformMCPServer()
 
1768
 
1769
- logger.debug("Attempting to initialize stdio_server context...")
1770
- try:
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
- except Exception as e:
1805
- logger.critical(f"Unhandled exception during stdio_server or server.run: {e}", exc_info=True)
1806
- raise
1807
 
1808
- if __name__ == "__main__":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1809
  try:
1810
- logger.debug("Script execution started via __main__.")
1811
- asyncio.run(main())
1812
- logger.debug("Script finished execution (normal exit).") # This might not be reached with asyncio.Future()
1813
- except asyncio.CancelledError:
1814
- logger.info("Application received cancellation signal. Exiting gracefully.")
 
 
 
 
 
 
 
 
 
1815
  except Exception as e:
1816
- logger.critical(f"Fatal unhandled exception in main execution block: {e}", exc_info=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()