#
tokens: 28755/50000 16/16 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── BUILD.md
├── Dockerfile
├── glama.json
├── LAMBDA.md
├── LICENSE
├── logo.png
├── MCP.md
├── PRICING.md
├── README.md
├── requirements.txt
├── smithery.yaml
└── src
    ├── lambda
    │   ├── __init__.py
    │   ├── lambda_handler.py
    │   ├── requirements.txt
    │   ├── test_event.json
    │   └── test_lambda.py
    └── server.py
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Python
 2 | __pycache__/
 3 | *.py[cod]
 4 | *$py.class
 5 | *.so
 6 | .Python
 7 | build/
 8 | develop-eggs/
 9 | dist/
10 | downloads/
11 | eggs/
12 | .eggs/
13 | lib/
14 | lib64/
15 | parts/
16 | sdist/
17 | var/
18 | wheels/
19 | *.egg-info/
20 | .installed.cfg
21 | *.egg
22 | MANIFEST
23 | 
24 | # Virtual Environment
25 | venv/
26 | env/
27 | ENV/
28 | .env
29 | 
30 | # IDE files
31 | .idea/
32 | .vscode/
33 | *.swp
34 | *.swo
35 | .DS_Store
36 | 
37 | # Docker
38 | .dockerignore
39 | docker-compose.override.yml
40 | 
41 | # Logs
42 | *.log
43 | logs/
44 | 
45 | # Cache
46 | .cache/
47 | .pytest_cache/
48 | .coverage
49 | htmlcov/
50 | 
51 | # Data
52 | *.csv
53 | ec2_pricing.json
54 | 
55 | # Jupyter Notebooks
56 | .ipynb_checkpoints
57 | 
58 | .aws-sam
59 | samconfig.toml
60 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # AWS Pricing MCP
 2 | 
 3 | A Model Context Protocol (MCP) server that provides AWS EC2 instance pricing data. This project includes both a traditional server implementation and a serverless Lambda function.
 4 | 
 5 | ## Quick Start
 6 | 
 7 | ### Lambda Deployment (Recommended)
 8 | 
 9 | The Lambda function provides the same functionality as the server but with serverless benefits:
10 | 
11 | ```bash
12 | # Build and deploy
13 | sam build
14 | sam deploy --guided
15 | 
16 | # Get the Function URL
17 | aws cloudformation describe-stacks \
18 |   --stack-name aws-pricing-mcp \
19 |   --query 'Stacks[0].Outputs[?OutputKey==`FunctionUrl`].OutputValue' \
20 |   --output text
21 | ```
22 | 
23 | For detailed Lambda documentation, see [LAMBDA.md](LAMBDA.md).
24 | 
25 | ### Server Deployment
26 | 
27 | ```bash
28 | # Install dependencies
29 | pip install -r requirements.txt
30 | 
31 | # Run the server
32 | python src/server.py
33 | ```
34 | 
35 | ## Features
36 | 
37 | - **EC2 Pricing Data**: Find the cheapest EC2 instances based on specifications
38 | - **Multiple Pricing Models**: On Demand, Reserved Instances, CloudFix RightSpend
39 | - **Flexible Filtering**: Region, platform, tenancy, vCPU, RAM, GPU, etc.
40 | - **JSON-RPC 2.0**: Full MCP protocol compliance
41 | - **Serverless Option**: Lambda function with Function URL
42 | - **Dynamic Data**: Always up-to-date pricing from S3
43 | 
44 | ## Documentation
45 | 
46 | - [LAMBDA.md](LAMBDA.md) - Comprehensive Lambda documentation
47 | - [MCP.md](MCP.md) - MCP protocol examples
48 | - [PRICING.md](PRICING.md) - Pricing data format and sources
49 | - [BUILD.md](BUILD.md) - Build instructions
50 | 
51 | ## License
52 | 
53 | [LICENSE](LICENSE)
54 | 
```

--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------

```
1 | fastmcp
```

--------------------------------------------------------------------------------
/src/lambda/__init__.py:
--------------------------------------------------------------------------------

```python
1 | # AWS Pricing MCP Lambda Handler Package 
```

--------------------------------------------------------------------------------
/glama.json:
--------------------------------------------------------------------------------

```json
1 | {
2 |     "$schema": "https://glama.ai/mcp/schemas/server.json",
3 |     "maintainers": ["ai-1st"]
4 | }
```

--------------------------------------------------------------------------------
/src/lambda/requirements.txt:
--------------------------------------------------------------------------------

```
1 | # AWS Pricing MCP Lambda Handler Requirements
2 | # No external dependencies required - uses only Python standard library
3 | 
4 | # If you need to add any external dependencies in the future, add them here
5 | # Example:
6 | # requests==2.31.0
7 | # boto3==1.34.0 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | 
 2 | # Smithery.ai configuration
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     {}
 7 |   commandFunction:
 8 |     # A function that produces the CLI command to start the MCP on stdio.
 9 |     |-
10 |     (config) => ({
11 |       "command": "python",
12 |       "args": ["server.py"],
13 |       "env": { }
14 |     })
```

--------------------------------------------------------------------------------
/src/lambda/test_event.json:
--------------------------------------------------------------------------------

```json
1 | {
2 |   "requestContext": {
3 |     "http": {
4 |       "method": "POST"
5 |     }
6 |   },
7 |   "body": "{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"initialize\", \"params\": {\"protocolVersion\": \"2025-03-26\", \"capabilities\": {}, \"clientInfo\": {\"name\": \"TestClient\", \"version\": \"1.0.0\"}}}"
8 | } 
```

--------------------------------------------------------------------------------
/BUILD.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Building the AWS EC2 Pricing MCP Server
 2 | 
 3 | To build the Docker image for multiple platforms (e.g., x86_64 and ARM64):
 4 | 
 5 | 1. Enable Docker BuildKit:
 6 | ```bash
 7 | export DOCKER_BUILDKIT=1
 8 | ```
 9 | 
10 | 2. Create a builder that supports multi-platform builds:
11 | ```bash
12 | docker buildx create --name multiplatform-builder --driver docker-container --use
13 | ```
14 | 
15 | 3. Log in to Docker Hub:
16 | ```bash
17 | docker login
18 | ```
19 | 
20 | 4. Push the multi-platform image to Docker Hub:
21 | ```bash
22 | docker buildx build --platform linux/amd64,linux/arm64 -t ai1st/aws-pricing-mcp:latest --push . --build-arg BUILD_DATE=$(date +%Y-%m-%d)
23 | ```
24 | 
25 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Use Python 3.9 as the base image
 2 | FROM python:3.13-slim
 3 | 
 4 | # Set working directory in the container
 5 | WORKDIR /app
 6 | 
 7 | # Install curl for downloading files
 8 | RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
 9 | 
10 | # Copy requirements file and install dependencies
11 | COPY requirements.txt .
12 | RUN pip install --no-cache-dir -r requirements.txt
13 | 
14 | # Copy the MCP server code
15 | COPY src/server.py .
16 | 
17 | # Invalidate pricing cache with this arg
18 | ARG BUILD_DATE=2025-05-08
19 | 
20 | # Fetch pricing data from public S3 - bust cache daily
21 | RUN echo $BUILD_DATE && curl https://cloudfix-public-aws-pricing.s3.us-east-1.amazonaws.com/pricing/ec2_pricing.json.gz | gunzip > ec2_pricing.json
22 | 
23 | # Run the server when the container starts
24 | CMD ["python", "server.py"]
```

--------------------------------------------------------------------------------
/PRICING.md:
--------------------------------------------------------------------------------

```markdown
  1 | ## Pricing Data JSON Format
  2 | 
  3 | The pricing data JSON file has the following structure:
  4 | 
  5 | ```json
  6 | {
  7 |   "instance_family_name": {
  8 |     "Current Generation": "Yes/No",
  9 |     "Instance Family": "General Purpose/Compute Optimized/etc.",
 10 |     "Physical Processor": "Intel Xeon/AMD EPYC/etc.",
 11 |     "Clock Speed": 2.5,
 12 |     "Processor Features": "AVX, AVX2, etc.",
 13 |     "Enhanced Networking Supported": "Yes/No",
 14 |     "sizes": {
 15 |       "instance_size": {
 16 |         "vCPU": 2,
 17 |         "Memory": 8.0,
 18 |         "Ephemeral Storage": 0,
 19 |         "Network Performance": 5000,
 20 |         "Dedicated EBS Throughput": 650,
 21 |         "GPU": 0,
 22 |         "GPU Memory": 0,
 23 |         "operations": {
 24 |           "operation_code": {
 25 |             "region": "price1,price2,price3,..."
 26 |           }
 27 |         }
 28 |       }
 29 |     }
 30 |   }
 31 | }
 32 | ```
 33 | 
 34 | ### Field Descriptions
 35 | 
 36 | #### Instance Family Level Fields
 37 | 
 38 | - **Instance Family Name**: The name of the instance family (e.g., "t3a")
 39 | - **Current Generation**: Indicates if the instance family is current generation ("Yes") or previous generation ("No")
 40 | - **Instance Family**: The AWS classification of the instance family (e.g., "General Purpose", "Compute Optimized")
 41 | - **Physical Processor**: The CPU manufacturer and model (e.g., "Intel Xeon Platinum 8175", "AMD EPYC 7R13")
 42 | - **Clock Speed**: The processor clock speed in GHz (e.g., 2.5)
 43 | - **Processor Features**: Special CPU features or instruction sets (e.g., "AVX, AVX2, Intel AVX-512")
 44 | - **Enhanced Networking Supported**: Whether enhanced networking is supported ("Yes" or "No")
 45 | 
 46 | #### Instance Size Level Fields
 47 | 
 48 | - **Instance Size Name**: The name of the instance size (e.g., "2xlarge")
 49 | - **vCPU**: Number of virtual CPUs
 50 | - **Memory**: Amount of RAM in GiB
 51 | - **Ephemeral Storage**: Amount of instance store storage in GB (0 for EBS-only instances)
 52 | - **Network Performance**: Network performance in Mbps
 53 | - **Dedicated EBS Throughput**: EBS throughput in Mbps
 54 | - **GPU**: Number of GPUs (0 if none)
 55 | - **GPU Memory**: Amount of GPU memory in GB (0 if no GPU)
 56 | 
 57 | #### Operations and Pricing
 58 | 
 59 | The `operations` object contains mappings from operation codes to region-specific pricing. Each region has a comma-separated string of prices with the following positions:
 60 | 
 61 | - Position 0: OnDemand price for Shared tenancy
 62 | - Position 1: No Upfront 1yr Compute Savings Plan price for Shared tenancy
 63 | - Position 2: Partial Upfront 1yr Compute Savings Plan price for Shared tenancy
 64 | - Position 3: All Upfront 1yr Compute Savings Plan price for Shared tenancy
 65 | - Position 4: No Upfront 3yr Compute Savings Plan price for Shared tenancy
 66 | - Position 5: Partial Upfront 3yr Compute Savings Plan price for Shared tenancy
 67 | - Position 6: All Upfront 3yr Compute Savings Plan price for Shared tenancy
 68 | - Position 7: OnDemand price for Dedicated tenancy
 69 | - Position 8: No Upfront 1yr Compute Savings Plan price for Dedicated tenancy
 70 | - Position 9: Partial Upfront 1yr Compute Savings Plan price for Dedicated tenancy
 71 | - Position 10: All Upfront 1yr Compute Savings Plan price for Dedicated tenancy
 72 | - Position 11: No Upfront 3yr Compute Savings Plan price for Dedicated tenancy
 73 | - Position 12: Partial Upfront 3yr Compute Savings Plan price for Dedicated tenancy
 74 | - Position 13: All Upfront 3yr Compute Savings Plan price for Dedicated tenancy
 75 | 
 76 | Empty string values indicate that no pricing is available for that specific combination.
 77 | 
 78 | ## Operation System to Operation Code Mapping
 79 | 
 80 | The following table shows the mapping between operating systems and their corresponding operation codes:
 81 | 
 82 | | Operating System | Operation Code |
 83 | |------------------|---------------|
 84 | | Linux/UNIX | "" (empty string) |
 85 | | Red Hat BYOL Linux | "00g0" |
 86 | | Red Hat Enterprise Linux | "0010" |
 87 | | Red Hat Enterprise Linux with HA | "1010" |
 88 | | Red Hat Enterprise Linux with SQL Server Standard and HA | "1014" |
 89 | | Red Hat Enterprise Linux with SQL Server Enterprise and HA | "1110" |
 90 | | Red Hat Enterprise Linux with SQL Server Standard | "0014" |
 91 | | Red Hat Enterprise Linux with SQL Server Web | "0210" |
 92 | | Red Hat Enterprise Linux with SQL Server Enterprise | "0110" |
 93 | | Linux with SQL Server Enterprise | "0100" |
 94 | | Linux with SQL Server Standard | "0004" |
 95 | | Linux with SQL Server Web | "0200" |
 96 | | SUSE Linux | "000g" |
 97 | | Windows | "0002" |
 98 | | Windows BYOL | "0800" |
 99 | | Windows with SQL Server Enterprise | "0102" |
100 | | Windows with SQL Server Standard | "0006" |
101 | | Windows with SQL Server Web | "0202" |
102 | 
```

--------------------------------------------------------------------------------
/src/lambda/test_lambda.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Test script for the AWS Pricing MCP Lambda Handler
  3 | 
  4 | This script demonstrates how to use the Lambda handler with sample MCP requests.
  5 | """
  6 | 
  7 | import json
  8 | from lambda_handler import lambda_handler, process_mcp_request
  9 | 
 10 | def test_initialize():
 11 |     """Test the initialize request"""
 12 |     request = {
 13 |         "jsonrpc": "2.0",
 14 |         "id": 1,
 15 |         "method": "initialize",
 16 |         "params": {
 17 |             "protocolVersion": "2025-03-26",
 18 |             "capabilities": {
 19 |                 "roots": {
 20 |                     "listChanged": True
 21 |                 },
 22 |                 "sampling": {}
 23 |             },
 24 |             "clientInfo": {
 25 |                 "name": "TestClient",
 26 |                 "version": "1.0.0"
 27 |             }
 28 |         }
 29 |     }
 30 |     
 31 |     # Test direct MCP processing
 32 |     response = process_mcp_request(request)
 33 |     print("Initialize Response (Direct MCP):")
 34 |     print(json.dumps(response, indent=2))
 35 |     print("\n" + "="*50 + "\n")
 36 |     
 37 |     # Test HTTP Lambda handler
 38 |     http_event = {
 39 |         "requestContext": {
 40 |             "http": {
 41 |                 "method": "POST"
 42 |             }
 43 |         },
 44 |         "body": json.dumps(request)
 45 |     }
 46 |     
 47 |     http_response = lambda_handler(http_event, None)
 48 |     print("Initialize Response (HTTP Lambda):")
 49 |     print(json.dumps(http_response, indent=2))
 50 |     print("\n" + "="*50 + "\n")
 51 | 
 52 | def test_tools_list():
 53 |     """Test the tools/list request"""
 54 |     request = {
 55 |         "jsonrpc": "2.0",
 56 |         "id": 2,
 57 |         "method": "tools/list",
 58 |         "params": {}
 59 |     }
 60 |     
 61 |     # Test direct MCP processing
 62 |     response = process_mcp_request(request)
 63 |     print("Tools List Response (Direct MCP):")
 64 |     print(json.dumps(response, indent=2))
 65 |     print("\n" + "="*50 + "\n")
 66 |     
 67 |     # Test HTTP Lambda handler
 68 |     http_event = {
 69 |         "requestContext": {
 70 |             "http": {
 71 |                 "method": "POST"
 72 |             }
 73 |         },
 74 |         "body": json.dumps(request)
 75 |     }
 76 |     
 77 |     http_response = lambda_handler(http_event, None)
 78 |     print("Tools List Response (HTTP Lambda):")
 79 |     print(json.dumps(http_response, indent=2))
 80 |     print("\n" + "="*50 + "\n")
 81 | 
 82 | def test_tools_call():
 83 |     """Test the tools/call request"""
 84 |     request = {
 85 |         "jsonrpc": "2.0",
 86 |         "id": 3,
 87 |         "method": "tools/call",
 88 |         "params": {
 89 |             "name": "ec2_instances_pricing",
 90 |             "arguments": {
 91 |                 "filter_region": "us-east-1",
 92 |                 "filter_platform": "Linux/UNIX",
 93 |                 "filter_tenancy": "Shared",
 94 |                 "filter_pricing_model": "On Demand",
 95 |                 "filter_min_vcpu": 2,
 96 |                 "filter_min_ram": 4.0,
 97 |                 "filter_max_price_per_hour": 0.1,
 98 |                 "sort_by": "Price",
 99 |                 "sort_order": "Ascending",
100 |                 "page_num": 0
101 |             }
102 |         }
103 |     }
104 |     
105 |     # Test direct MCP processing
106 |     response = process_mcp_request(request)
107 |     print("Tools Call Response (Direct MCP):")
108 |     print(json.dumps(response, indent=2))
109 |     print("\n" + "="*50 + "\n")
110 |     
111 |     # Test HTTP Lambda handler
112 |     http_event = {
113 |         "requestContext": {
114 |             "http": {
115 |                 "method": "POST"
116 |             }
117 |         },
118 |         "body": json.dumps(request)
119 |     }
120 |     
121 |     http_response = lambda_handler(http_event, None)
122 |     print("Tools Call Response (HTTP Lambda):")
123 |     print(json.dumps(http_response, indent=2))
124 |     print("\n" + "="*50 + "\n")
125 | 
126 | def test_ping():
127 |     """Test the ping request"""
128 |     request = {
129 |         "jsonrpc": "2.0",
130 |         "id": "ping-123",
131 |         "method": "ping"
132 |     }
133 |     
134 |     # Test direct MCP processing
135 |     response = process_mcp_request(request)
136 |     print("Ping Response (Direct MCP):")
137 |     print(json.dumps(response, indent=2))
138 |     print("\n" + "="*50 + "\n")
139 |     
140 |     # Test HTTP Lambda handler
141 |     http_event = {
142 |         "requestContext": {
143 |             "http": {
144 |                 "method": "POST"
145 |             }
146 |         },
147 |         "body": json.dumps(request)
148 |     }
149 |     
150 |     http_response = lambda_handler(http_event, None)
151 |     print("Ping Response (HTTP Lambda):")
152 |     print(json.dumps(http_response, indent=2))
153 |     print("\n" + "="*50 + "\n")
154 | 
155 | def test_error_handling():
156 |     """Test error handling with invalid request"""
157 |     request = {
158 |         "jsonrpc": "2.0",
159 |         "id": 4,
160 |         "method": "nonexistent_method",
161 |         "params": {}
162 |     }
163 |     
164 |     # Test direct MCP processing
165 |     response = process_mcp_request(request)
166 |     print("Error Handling Response (Direct MCP):")
167 |     print(json.dumps(response, indent=2))
168 |     print("\n" + "="*50 + "\n")
169 |     
170 |     # Test HTTP Lambda handler
171 |     http_event = {
172 |         "requestContext": {
173 |             "http": {
174 |                 "method": "POST"
175 |             }
176 |         },
177 |         "body": json.dumps(request)
178 |     }
179 |     
180 |     http_response = lambda_handler(http_event, None)
181 |     print("Error Handling Response (HTTP Lambda):")
182 |     print(json.dumps(http_response, indent=2))
183 |     print("\n" + "="*50 + "\n")
184 | 
185 | def test_cors_preflight():
186 |     """Test CORS preflight request"""
187 |     http_event = {
188 |         "requestContext": {
189 |             "http": {
190 |                 "method": "OPTIONS"
191 |             }
192 |         }
193 |     }
194 |     
195 |     http_response = lambda_handler(http_event, None)
196 |     print("CORS Preflight Response:")
197 |     print(json.dumps(http_response, indent=2))
198 |     print("\n" + "="*50 + "\n")
199 | 
200 | def test_invalid_json():
201 |     """Test handling of invalid JSON"""
202 |     http_event = {
203 |         "requestContext": {
204 |             "http": {
205 |                 "method": "POST"
206 |             }
207 |         },
208 |         "body": "invalid json"
209 |     }
210 |     
211 |     http_response = lambda_handler(http_event, None)
212 |     print("Invalid JSON Response:")
213 |     print(json.dumps(http_response, indent=2))
214 |     print("\n" + "="*50 + "\n")
215 | 
216 | if __name__ == "__main__":
217 |     print("Testing AWS Pricing MCP Lambda Handler")
218 |     print("="*50)
219 |     
220 |     try:
221 |         test_initialize()
222 |         test_tools_list()
223 |         test_tools_call()
224 |         test_ping()
225 |         test_error_handling()
226 |         test_cors_preflight()
227 |         test_invalid_json()
228 |         
229 |         print("All tests completed successfully!")
230 |         
231 |     except Exception as e:
232 |         print(f"Test failed with error: {str(e)}")
233 |         import traceback
234 |         traceback.print_exc() 
```

--------------------------------------------------------------------------------
/src/server.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | AWS Pricing MCP Server
  3 | 
  4 | This server exposes AWS EC2 instance pricing data through the Model Context Protocol (MCP).
  5 | It provides tools to find the cheapest EC2 instances based on specified criteria.
  6 | """
  7 | 
  8 | import json
  9 | import os
 10 | import sys
 11 | import traceback
 12 | from typing import List, Dict, Any
 13 | from fastmcp import FastMCP, Context
 14 | 
 15 | # Set up error logging
 16 | def log_error(message):
 17 |     print(f"ERROR: {message}", file=sys.stderr)
 18 | 
 19 | # Load pricing data
 20 | try:
 21 |     PRICING_FILE = os.path.join(os.path.dirname(__file__), "ec2_pricing.json") 
 22 |     if not os.path.exists(PRICING_FILE):
 23 |         log_error(f"Pricing file not found at {PRICING_FILE}")
 24 |         raise FileNotFoundError(f"Could not find ec2_pricing.json")
 25 |     
 26 |     with open(PRICING_FILE, "r") as f:
 27 |         PRICING_DATA = json.load(f)
 28 |         
 29 | except Exception as e:
 30 |     log_error(f"Failed to load pricing data: {str(e)}")
 31 |     log_error(traceback.format_exc())
 32 |     raise
 33 | 
 34 | PLATFORM_TO_OP_CODE = {
 35 |     "Linux/UNIX": "",
 36 |     "Red Hat BYOL Linux": "00g0",
 37 |     "Red Hat Enterprise Linux": "0010",
 38 |     "Red Hat Enterprise Linux with HA": "1010",
 39 |     "Red Hat Enterprise Linux with SQL Server Standard and HA": "1014",
 40 |     "Red Hat Enterprise Linux with SQL Server Enterprise and HA": "1110",
 41 |     "Red Hat Enterprise Linux with SQL Server Standard": "0014",
 42 |     "Red Hat Enterprise Linux with SQL Server Web": "0210",
 43 |     "Red Hat Enterprise Linux with SQL Server Enterprise": "0110",
 44 |     "Linux with SQL Server Enterprise": "0100",
 45 |     "Linux with SQL Server Standard": "0004",
 46 |     "Linux with SQL Server Web": "0200",
 47 |     "SUSE Linux": "000g",
 48 |     "Windows": "0002",
 49 |     "Windows BYOL": "0800",
 50 |     "Windows with SQL Server Enterprise": "0102",
 51 |     "Windows with SQL Server Standard": "0006",
 52 |     "Windows with SQL Server Web": "0202",
 53 | }
 54 | 
 55 | PRICING_MODELS = ["On Demand", "1-yr No Upfront", "1-yr Partial Upfront", "1-yr All Upfront", "3-yr No Upfront", "3-yr Partial Upfront", "3-yr All Upfront"]
 56 | TENANCIES = ["Shared", "Dedicated"]
 57 | 
 58 | # Create an MCP server
 59 | mcp = FastMCP("AWS EC2 Pricing MCP", log_level="ERROR")
 60 | 
 61 | # Define Tools
 62 | @mcp.tool()
 63 | def ec2_instances_pricing(
 64 |     filter_region: str = "us-east-1",
 65 |     filter_platform: str = "Linux/UNIX",
 66 |     filter_tenancy: str = "Shared",
 67 |     filter_pricing_model: str = "On Demand",
 68 |     filter_min_vcpu: int = 0,
 69 |     filter_min_ram: float = 0,
 70 |     filter_min_gpu: int = 0,
 71 |     filter_min_gpu_memory: int = 0,
 72 |     filter_min_cpu_ghz: float = 0,
 73 |     filter_min_network_performance: int = 0,
 74 |     filter_min_ebs_throughput: int = 0,
 75 |     filter_min_ephemeral_storage: int = 0,
 76 |     filter_max_price_per_hour: float = float('inf'),
 77 |     filter_family: str = "",
 78 |     filter_size: str = "",
 79 |     filter_processor: str = "",
 80 |     sort_by: str = "Price",
 81 |     sort_order: str = "Descending",
 82 |     page_num: int = 0
 83 | ) -> List[Dict[str, Any]]:
 84 |     """
 85 |     Find AWS EC2 instances based on specified criteria.
 86 |     
 87 |     Filter Parameters:
 88 |     - region: AWS region (default: us-east-1)
 89 |     - platform: OS platform (one of: Linux/UNIX, Red Hat Enterprise Linux, Red Hat Enterprise Linux with HA, Red Hat Enterprise Linux with SQL Server Standard and HA, Red Hat Enterprise Linux with SQL Server Enterprise and HA, Red Hat Enterprise Linux with SQL Server Standard, Red Hat Enterprise Linux with SQL Server Web, Linux with SQL Server Enterprise, Linux with SQL Server Standard, Linux with SQL Server Web, SUSE Linux, Windows, Windows BYOL, Windows with SQL Server Enterprise, Windows with SQL Server Standard, Windows with SQL Server Web; default: Linux/UNIX)
 90 |     - tenancy: Instance tenancy (one of: Shared, Dedicated; default: Shared)
 91 |     - pricing_model: Pricing model (one of: On Demand, 1-yr No Upfront, 1-yr Partial Upfront, 1-yr All Upfront, 3-yr No Upfront, 3-yr Partial Upfront, 3-yr All Upfront; default: On Demand)
 92 |     - min_vcpu: Minimum number of vCPUs (default: 0)
 93 |     - min_ram: Minimum amount of RAM in GB (default: 0)
 94 |     - min_gpu: Minimum number of GPUs (default: 0)
 95 |     - min_gpu_memory: Minimum GPU memory in GB (default: 0)
 96 |     - min_cpu_ghz: Minimum CPU clock speed in GHz (default: 0)
 97 |     - min_network_performance: Minimum network performance in Mbps (default: 0)
 98 |     - min_ebs_throughput: Minimum dedicated EBS throughput in Mbps (default: 0)
 99 |     - min_ephemeral_storage: Minimum ephemeral storage in GB (default: 0)
100 |     - max_price_per_hour: Maximum price per hour in USD (default: no limit)
101 |     - sort_by: Field to sort by (one of: Price, Clock Speed GHz, vCPU cores, Memory GB, Ephemeral Storage GB, Network Performance Mbps, Dedicated EBS Throughput Mbps, GPU cores, GPU Memory GB; default: Price)
102 |     - sort_order: Sort order (one of: Ascending, Descending; default: Descending)
103 |     - family: Filter by instance family (e.g., "m5", "c6g"; default: "" for all families)
104 |     - size: Filter by instance size (e.g., "large", "2xlarge"; default: "" for all sizes)
105 |     - processor: Filter by physical processor (e.g., "Graviton", "Xeon", "AMD"; default: "" for all processors)
106 |     - page_num: Page number for pagination (default: 0)
107 |     
108 |     Returns:
109 |     - List of instances matching the criteria (5 per page). CloudFix RightSpend pricing is provided when using the flexible cRIs provided by RightSpend (a third-party solution). The benefit of RightSpend is that it 1) eliminates the need for complex forecasting or frequent consultations with engineering about usage fluctuations 2) removes the risk of unused reservations 3) provides 3-yr All Upfront discounts without the need for prepayment.
110 |  
111 |     """
112 |     # Get the operation code for the platform
113 |     if filter_platform not in PLATFORM_TO_OP_CODE:
114 |         raise ValueError(f"Invalid platform: {filter_platform}; valid platforms: {list(PLATFORM_TO_OP_CODE.keys())}")
115 |     filter_op_code = PLATFORM_TO_OP_CODE.get(filter_platform, "")
116 | 
117 |     if filter_tenancy not in TENANCIES:
118 |         raise ValueError(f"Invalid tenancy: {filter_tenancy}; valid tenancies: {list(TENANCIES)}")
119 |     
120 |     if filter_pricing_model not in PRICING_MODELS:
121 |         raise ValueError(f"Invalid pricing model: {filter_pricing_model}; valid pricing models: {list(PRICING_MODELS)}")
122 |     # Find matching instances
123 |     on_demand_price_offset = 7 * TENANCIES.index(filter_tenancy)
124 |     price_offset = on_demand_price_offset + PRICING_MODELS.index(filter_pricing_model)
125 |     
126 |     matching_instances = []
127 |     
128 |     for family_name, family in PRICING_DATA.items():
129 |         # Filter by family if specified
130 |         if filter_family and family_name != filter_family:
131 |             continue
132 |             
133 |         # Filter by processor if specified
134 |         if filter_processor and filter_processor.lower() not in family.get("Physical Processor", "").lower():
135 |             continue
136 |             
137 |         if family["Clock Speed, GHz"] < filter_min_cpu_ghz:
138 |             continue
139 |                 
140 |         for size_name, size in family["sizes"].items():
141 |             # Filter by size if specified
142 |             if filter_size and size_name != filter_size:
143 |                 continue
144 |                 
145 |             # Check if the instance meets the minimum requirements
146 |             if size.get("vCPU, cores", 0) < filter_min_vcpu:
147 |                 continue
148 |                 
149 |             if size.get("Memory, GB", 0) < filter_min_ram:
150 |                 continue
151 |                 
152 |             if size.get("GPU, cores", 0) < filter_min_gpu:
153 |                 continue
154 |                 
155 |             if size.get("GPU Memory, GB", 0) < filter_min_gpu_memory:
156 |                 continue
157 |                 
158 |             if size.get("Network Performance, Mbps", 0) < filter_min_network_performance:
159 |                 continue
160 |                 
161 |             if size.get("Dedicated EBS Throughput, Mbps", 0) < filter_min_ebs_throughput:
162 |                 continue
163 |                 
164 |             if size.get("Ephemeral Storage, GB", 0) < filter_min_ephemeral_storage:
165 |                 continue
166 |         
167 |             if "operations" not in size:
168 |                 raise ValueError(f"Instance {family_name}.{size_name} does not have operations")
169 |             for op_code, regions in size["operations"].items():
170 |                 if op_code != filter_op_code:
171 |                     continue
172 | 
173 |                 for region, prices in regions.items():
174 |                     if region != filter_region:
175 |                         continue
176 | 
177 |                     price_arr = prices.split(",")
178 |                     if len(price_arr) < price_offset:
179 |                         continue
180 | 
181 |                     price = price_arr[price_offset]
182 |                     if price == "":
183 |                         continue
184 | 
185 |                     on_demand_price = price_arr[on_demand_price_offset]
186 | 
187 |                     instance = {
188 |                         "Instance Type": f"{family_name}.{size_name}",
189 |                         "Region": filter_region,
190 |                         "Platform": filter_platform,
191 |                         "Tenancy": filter_tenancy,
192 |                         "Pricing Model": filter_pricing_model,
193 |                         "Effective Price per hour, USD": float(price),
194 |                         "Effective Price per month, USD": round(float(price) * 24 * 365 / 12, 2),
195 |                         "Effective Price per year, USD": round(float(price) * 24 * 365, 2)
196 |                     }
197 |                     if filter_pricing_model == "1-yr Partial Upfront":
198 |                         instance["Upfront Payment, USD"] = round(float(price) * 24 * 365 / 2, 2)
199 |                     elif filter_pricing_model == "1-yr All Upfront":
200 |                         instance["Upfront Payment, USD"] = round(float(price) * 24 * 365, 2)
201 |                     elif filter_pricing_model == "3-yr Partial Upfront":
202 |                         instance["Upfront Payment, USD"] = round(float(price) * 24 * 365 * 3 / 2, 2)
203 |                     elif filter_pricing_model == "3-yr All Upfront":
204 |                         instance["Upfront Payment, USD"] = round(float(price) * 24 * 365 * 3, 2)
205 |                         
206 |                     if on_demand_price and on_demand_price != price:
207 |                         instance["On-Demand Price per hour, USD"] = float(on_demand_price)
208 |                         instance["Discount Percentage"] = (1 - (float(price) / float(on_demand_price))) * 100
209 | 
210 |                     if len(price_arr) > on_demand_price_offset+6:
211 |                         cloudfix_rightspend_price = price_arr[on_demand_price_offset+6]
212 |                         if cloudfix_rightspend_price != "":
213 |                             instance["CloudFix RightSpend Price per hour, USD"] = float(cloudfix_rightspend_price)
214 |                             instance["CloudFix RightSpend Price per month, USD"] = round(float(cloudfix_rightspend_price) * 24 * 365 / 12, 2)
215 |                             instance["CloudFix RightSpend Price per year, USD"] = round(float(cloudfix_rightspend_price) * 24 * 365, 2)
216 |                             instance["CloudFix RightSpend Upfront Payment, USD"] = round(float(cloudfix_rightspend_price) * 24 * 7, 2)
217 |                     instance.update({key: value for key, value in family.items() if key != "sizes"})
218 |                     instance.update({key: value for key, value in size.items() if key != "operations"})
219 |             
220 |                     matching_instances.append(instance)
221 |     
222 |     # Filter by max price if specified
223 |     if filter_max_price_per_hour != float('inf'):
224 |         matching_instances = [i for i in matching_instances if i["Effective Price per hour, USD"] <= filter_max_price_per_hour]
225 | 
226 |     # Define sort key mapping
227 |     sort_key_map = {
228 |         "Price": "Effective Price per hour, USD",
229 |         "Clock Speed GHz": "Clock Speed, GHz",
230 |         "vCPU cores": "vCPU, cores",
231 |         "Memory GB": "Memory, GB",
232 |         "Ephemeral Storage GB": "Ephemeral Storage, GB",
233 |         "Network Performance Mbps": "Network Performance, Mbps",
234 |         "Dedicated EBS Throughput Mbps": "Dedicated EBS Throughput, Mbps",
235 |         "GPU cores": "GPU, cores",
236 |         "GPU Memory GB": "GPU Memory, GB"
237 |     }
238 | 
239 |     # Sort by selected field
240 |     if sort_by not in sort_key_map:
241 |         raise ValueError(f"Invalid sort by: {sort_by}; valid sort by: {list(sort_key_map.keys())}")
242 |     sort_key = sort_key_map.get(sort_by, "Effective Price per hour, USD")
243 |     
244 |     # Two-pass sorting approach:
245 |     # 1. First sort by price (ascending) - this will be our secondary sort
246 |     matching_instances.sort(key=lambda x: x.get("Effective Price per hour, USD", 0))
247 |     
248 |     # 2. Then sort by the primary field with the specified direction
249 |     # Since Python's sort is stable, when primary fields are equal, the price order is preserved
250 |     matching_instances.sort(
251 |         key=lambda x: x.get(sort_key, 0),
252 |         reverse=(sort_order == "Descending")
253 |     )
254 |     
255 |     # Calculate pagination
256 |     items_per_page = 5
257 |     start_idx = page_num * items_per_page
258 |     end_idx = start_idx + items_per_page
259 |     
260 |     # Return the requested page
261 |     return matching_instances[start_idx:end_idx]
262 | 
263 | # Run the server if executed directly
264 | if __name__ == "__main__":
265 |     # res = find_instances()
266 |     # print(res)
267 |     mcp.run(transport="stdio")
268 | 
```

--------------------------------------------------------------------------------
/MCP.md:
--------------------------------------------------------------------------------

```markdown
  1 | This document provides example JSON-RPC messages for the supported MCP commands.
  2 | 
  3 | # Example JSON-RPC Messages for Anthropic MCP (Stateless HTTP Mode)
  4 | 
  5 | Below are example JSON-RPC 2.0 request and response objects for each relevant Model Context Protocol (MCP) command in stateless HTTP mode. Each example shows a complete JSON structure with realistic field values, based on the MCP specification. (All streaming or SSE-based fields are omitted, as these examples assume a non-streaming HTTP interaction.)
  6 | 
  7 | ## initialize
  8 | 
  9 | **Description:** The client begins a session by sending an `initialize` request with its supported protocol version, capabilities, and client info. The server replies with its own protocol version (which may be negotiated), supported server capabilities (e.g. logging, prompts, resources, tools), server info, and any optional instructions.
 10 | 
 11 | **Request:**
 12 | 
 13 | ```json
 14 | {
 15 |   "jsonrpc": "2.0",
 16 |   "id": 1,
 17 |   "method": "initialize",
 18 |   "params": {
 19 |     "protocolVersion": "2025-03-26",
 20 |     "capabilities": {
 21 |       "roots": {
 22 |         "listChanged": true
 23 |       },
 24 |       "sampling": {}
 25 |     },
 26 |     "clientInfo": {
 27 |       "name": "ExampleClient",
 28 |       "version": "1.0.0"
 29 |     }
 30 |   }
 31 | }
 32 | ```
 33 | 
 34 | 
 35 | 
 36 | **Response:**
 37 | 
 38 | ```json
 39 | {
 40 |   "jsonrpc": "2.0",
 41 |   "id": 1,
 42 |   "result": {
 43 |     "protocolVersion": "2025-03-26",
 44 |     "capabilities": {
 45 |       "logging": {},
 46 |       "prompts": {
 47 |         "listChanged": true
 48 |       },
 49 |       "resources": {
 50 |         "subscribe": true,
 51 |         "listChanged": true
 52 |       },
 53 |       "tools": {
 54 |         "listChanged": true
 55 |       }
 56 |     },
 57 |     "serverInfo": {
 58 |       "name": "ExampleServer",
 59 |       "version": "1.0.0"
 60 |     },
 61 |     "instructions": "Optional instructions for the client"
 62 |   }
 63 | }
 64 | ```
 65 | 
 66 | 
 67 | 
 68 | ## initialized (notification)
 69 | 
 70 | **Description:** After the server responds to `initialize`, the client sends an `initialized` notification to signal that it is ready for normal operations. This is a JSON-RPC notification (no `id` field and no response expected).
 71 | 
 72 | **Notification:**
 73 | 
 74 | ```json
 75 | {
 76 |   "jsonrpc": "2.0",
 77 |   "method": "notifications/initialized"
 78 | }
 79 | ```
 80 | 
 81 | 
 82 | 
 83 | ## ping
 84 | 
 85 | **Description:** Either party can send a `ping` request at any time to check connectivity. The `ping` request has no parameters, and the receiver must promptly return an empty result object if still alive.
 86 | 
 87 | **Request:**
 88 | 
 89 | ```json
 90 | {
 91 |   "jsonrpc": "2.0",
 92 |   "id": "123",
 93 |   "method": "ping"
 94 | }
 95 | ```
 96 | 
 97 | 
 98 | 
 99 | **Response:**
100 | 
101 | ```json
102 | {
103 |   "jsonrpc": "2.0",
104 |   "id": "123",
105 |   "result": {}
106 | }
107 | ```
108 | 
109 | 
110 | 
111 | ## resources/list
112 | 
113 | **Description:** The client requests a list of available resources (files, data, etc.) from the server. The `resources/list` request may include an optional `cursor` for pagination. The response contains an array of resource descriptors (each with fields like `uri`, `name`, `description`, `mimeType`, etc.) and may include a `nextCursor` token if more results are available.
114 | 
115 | **Request:**
116 | 
117 | ```json
118 | {
119 |   "jsonrpc": "2.0",
120 |   "id": 1,
121 |   "method": "resources/list",
122 |   "params": {
123 |     "cursor": "optional-cursor-value"
124 |   }
125 | }
126 | ```
127 | 
128 | 
129 | 
130 | **Response:**
131 | 
132 | ```json
133 | {
134 |   "jsonrpc": "2.0",
135 |   "id": 1,
136 |   "result": {
137 |     "resources": [
138 |       {
139 |         "uri": "file:///project/src/main.rs",
140 |         "name": "main.rs",
141 |         "description": "Primary application entry point",
142 |         "mimeType": "text/x-rust"
143 |       }
144 |     ],
145 |     "nextCursor": "next-page-cursor"
146 |   }
147 | }
148 | ```
149 | 
150 | 
151 | 
152 | ## resources/read
153 | 
154 | **Description:** The client retrieves the contents of a specific resource by sending `resources/read` with the resource's URI. The server's response includes a `contents` array with the resource data. If the resource is text-based, it appears under a `text` field (with an associated MIME type); for binary data, a `blob` (base64 string) would be used instead.
155 | 
156 | **Request:**
157 | 
158 | ```json
159 | {
160 |   "jsonrpc": "2.0",
161 |   "id": 2,
162 |   "method": "resources/read",
163 |   "params": {
164 |     "uri": "file:///project/src/main.rs"
165 |   }
166 | }
167 | ```
168 | 
169 | 
170 | 
171 | **Response:**
172 | 
173 | ```json
174 | {
175 |   "jsonrpc": "2.0",
176 |   "id": 2,
177 |   "result": {
178 |     "contents": [
179 |       {
180 |         "uri": "file:///project/src/main.rs",
181 |         "mimeType": "text/x-rust",
182 |         "text": "fn main() {\n    println!(\"Hello world!\");\n}"
183 |       }
184 |     ]
185 |   }
186 | }
187 | ```
188 | 
189 | 
190 | 
191 | ## resources/templates/list
192 | 
193 | **Description:** The client can query available *resource templates* (parameterized resource URIs) by sending `resources/templates/list`. The response provides a list of resource template definitions, each with a `uriTemplate` (often containing placeholders), a human-readable `name` and `description`, and an optional `mimeType` indicating the type of resource produced.
194 | 
195 | **Request:**
196 | 
197 | ```json
198 | {
199 |   "jsonrpc": "2.0",
200 |   "id": 3,
201 |   "method": "resources/templates/list"
202 | }
203 | ```
204 | 
205 | 
206 | 
207 | **Response:**
208 | 
209 | ```json
210 | {
211 |   "jsonrpc": "2.0",
212 |   "id": 3,
213 |   "result": {
214 |     "resourceTemplates": [
215 |       {
216 |         "uriTemplate": "file:///{path}",
217 |         "name": "Project Files",
218 |         "description": "Access files in the project directory",
219 |         "mimeType": "application/octet-stream"
220 |       }
221 |     ]
222 |   }
223 | }
224 | ```
225 | 
226 | 
227 | 
228 | ## prompts/list
229 | 
230 | **Description:** The client requests a list of available prompt templates by sending `prompts/list`. This may also support pagination via a `cursor`. The server responds with an array of prompt definitions, where each prompt has a unique `name`, a `description` of what it does, and an optional list of expected `arguments` (each argument with a name, description, and whether it's required). A `nextCursor` may be provided if the list is paginated.
231 | 
232 | **Request:**
233 | 
234 | ```json
235 | {
236 |   "jsonrpc": "2.0",
237 |   "id": 1,
238 |   "method": "prompts/list",
239 |   "params": {
240 |     "cursor": "optional-cursor-value"
241 |   }
242 | }
243 | ```
244 | 
245 | 
246 | 
247 | **Response:**
248 | 
249 | ```json
250 | {
251 |   "jsonrpc": "2.0",
252 |   "id": 1,
253 |   "result": {
254 |     "prompts": [
255 |       {
256 |         "name": "code_review",
257 |         "description": "Asks the LLM to analyze code quality and suggest improvements",
258 |         "arguments": [
259 |           {
260 |             "name": "code",
261 |             "description": "The code to review",
262 |             "required": true
263 |           }
264 |         ]
265 |       }
266 |     ],
267 |     "nextCursor": "next-page-cursor"
268 |   }
269 | }
270 | ```
271 | 
272 | 
273 | 
274 | ## prompts/get
275 | 
276 | **Description:** To fetch the content of a specific prompt template (possibly filling in arguments), the client sends `prompts/get` with the prompt's `name` and an `arguments` object providing any required values. The server returns the resolved prompt: typically a `description` and a sequence of `messages` that make up the prompt. Each message has a `role` (e.g. "user" or "assistant") and `content` which could be text or other supported content types.
277 | 
278 | **Request:**
279 | 
280 | ```json
281 | {
282 |   "jsonrpc": "2.0",
283 |   "id": 2,
284 |   "method": "prompts/get",
285 |   "params": {
286 |     "name": "code_review",
287 |     "arguments": {
288 |       "code": "def hello():\n    print('world')"
289 |     }
290 |   }
291 | }
292 | ```
293 | 
294 | 
295 | 
296 | **Response:**
297 | 
298 | ```json
299 | {
300 |   "jsonrpc": "2.0",
301 |   "id": 2,
302 |   "result": {
303 |     "description": "Code review prompt",
304 |     "messages": [
305 |       {
306 |         "role": "user",
307 |         "content": {
308 |           "type": "text",
309 |           "text": "Please review this Python code:\n def hello():\n    print('world')"
310 |         }
311 |       }
312 |     ]
313 |   }
314 | }
315 | ```
316 | 
317 | 
318 | 
319 | ## tools/list
320 | 
321 | **Description:** The client sends `tools/list` to get the list of tools (functions/actions) the server provides. The response includes an array of tool definitions. Each tool has a `name`, a `description` of its functionality, and an `inputSchema` (a JSON Schema object) describing the expected parameters for that tool. The example below shows one tool with a required `location` parameter. A `nextCursor` may appear if the list is paginated.
322 | 
323 | **Request:**
324 | 
325 | ```json
326 | {
327 |   "jsonrpc": "2.0",
328 |   "id": 1,
329 |   "method": "tools/list",
330 |   "params": {
331 |     "cursor": "optional-cursor-value"
332 |   }
333 | }
334 | ```
335 | 
336 | 
337 | 
338 | **Response:**
339 | 
340 | ```json
341 | {
342 |   "jsonrpc": "2.0",
343 |   "id": 1,
344 |   "result": {
345 |     "tools": [
346 |       {
347 |         "name": "get_weather",
348 |         "description": "Get current weather information for a location",
349 |         "inputSchema": {
350 |           "type": "object",
351 |           "properties": {
352 |             "location": {
353 |               "type": "string",
354 |               "description": "City name or zip code"
355 |             }
356 |           },
357 |           "required": ["location"]
358 |         }
359 |       }
360 |     ],
361 |     "nextCursor": "next-page-cursor"
362 |   }
363 | }
364 | ```
365 | 
366 | 
367 | 
368 | ## tools/call
369 | 
370 | **Description:** To execute a specific tool, the client sends a `tools/call` request with the tool's `name` and an `arguments` object providing the needed inputs. The server will run the tool and return a result. The result includes a `content` array (which may contain text or other content types, depending on what the tool returns) and an `isError` boolean indicating whether the tool succeeded. In this example, the tool returns a text result (weather information) and `isError: false` to show success.
371 | 
372 | **Request:**
373 | 
374 | ```json
375 | {
376 |   "jsonrpc": "2.0",
377 |   "id": 2,
378 |   "method": "tools/call",
379 |   "params": {
380 |     "name": "get_weather",
381 |     "arguments": {
382 |       "location": "New York"
383 |     }
384 |   }
385 | }
386 | ```
387 | 
388 | 
389 | 
390 | **Response:**
391 | 
392 | ```json
393 | {
394 |   "jsonrpc": "2.0",
395 |   "id": 2,
396 |   "result": {
397 |     "content": [
398 |       {
399 |         "type": "text",
400 |         "text": "Current weather in New York:\n Temperature: 72°F\n Conditions: Partly cloudy"
401 |       }
402 |     ],
403 |     "isError": false
404 |   }
405 | }
406 | ```
407 | 
408 | 
409 | 
410 | ## notifications/cancelled (notification)
411 | 
412 | **Description:** If either side needs to cancel an in-progress request (e.g. due to a timeout or user action), it sends a `notifications/cancelled` notification. This one-way message includes the `requestId` of the original request to be aborted and an optional `reason` string. The receiver should stop work on that request but does not send any response to the notification.
413 | 
414 | **Notification:**
415 | 
416 | ```json
417 | {
418 |   "jsonrpc": "2.0",
419 |   "method": "notifications/cancelled",
420 |   "params": {
421 |     "requestId": "123",
422 |     "reason": "User requested cancellation"
423 |   }
424 | }
425 | ```
426 | 
427 | 
428 | 
429 | Each JSON example above illustrates the structure and fields defined by the MCP specification for stateless HTTP usage, covering the full request/response cycle (or one-way notification) for that command. These messages can be sent over an HTTP-based JSON-RPC connection to manage the model's context and actions without using server-sent events or streaming protocols. All field names and nesting conform to the MCP spec, ensuring interoperability between MCP clients and servers.
430 | 
431 | 
432 | ## Introduction
433 | 
434 | This specification aims to describe a simple protocol for LLMs to discover and use remote APIs, while minimizing the context window used. While Anthropic's Model Context Protocol (MCP) is primarily used for the purpose, its design is non-optimal. MCP began as a simple STDIO‑based local solution but evolved into a complex, stateful HTTP/SSE system that burdens developers with session management and infrastructure headaches. Maintaining persistent connections conflicts with stateless microservice patterns, leading to scalability and load‑balancing challenges. This specification adopts a minimal, stateless design to avoid these pitfalls.
435 | 
436 | ## Overview
437 | 
438 | Webtools expose a lightweight, HTTP‑based contract that allows consumers to
439 | 
440 | * **Discover** capabilities through self‑describing metadata
441 | * **Validate** inputs and outputs via JSON Schema definitions
442 |   * one schema for the request object (`requestSchema`)
443 |   * one schema for the response object (`responseSchema`)
444 | * **Execute** actions with optional per‑request configuration
445 | * **Consume** predictable, strongly‑typed responses
446 | * **Lock-in** specific API versions to improve security
447 | 
448 | ## Use Case Scenario
449 | 
450 | Webtools are defined by URLs. The typical workflow follows these steps:
451 | 
452 | 1. **Discovery**: A user finds a webtool URL from a tool provider, marketplace, or other source
453 | 2. **Metadata Retrieval**: The user's system issues a GET request to the URL to retrieve the webtool's metadata
454 | 3. **Configuration**: The user fills in configuration data according to the `configSchema` defined in the metadata
455 | 4. **Integration**: The system is now able to use the webtool with LLMs, passing the configuration and handling requests/responses
456 | 
457 | ### Security Considerations
458 | 
459 | After reviewing the schemas and metadata, users may choose to lock-in a specific version by storing the validated metadata on their side and no longer fetching it from the remote server. This prevents potential security risks where malicious instructions could be injected into the LLM context through schema changes in newer versions of the webtool metadata.
460 | 
461 | ## HTTP Methods
462 | 
463 | ### GET {webtoolUrl}/ — Webtool Metadata (latest)
464 | 
465 | Returns metadata about the **latest** version of the webtool.
466 | 
467 | ```json
468 | {
469 |   "name": "webtool_name",
470 |   "description": "Human‑readable description of what this webtool does",
471 |   "version": "2.1.0",
472 |   "actions": [
473 |     {
474 |       "name": "action_name",
475 |       "description": "What this action does",
476 |       "requestSchema": { /* JSON Schema for request */ },
477 |       "responseSchema": { /* JSON Schema for response */ }
478 |     }
479 |   ],
480 |   "configSchema": { /* JSON Schema for configuration */ },
481 |   "defaultConfig": { /* Default configuration values */ }
482 | }
483 | ```
484 | 
485 | ### GET {webtoolUrl}/{version} — Webtool Metadata (specific version)
486 | 
487 | Returns metadata **for the specified semantic version**. Use this to fetch historical versions or pin a client to a stable release. The `version` parameter is optional; if omitted, the server SHOULD default to the latest version.
488 | 
489 | ```http
490 | GET /1.0.0
491 | ```
492 | 
493 | ```json
494 | {
495 |   "name": "weather",
496 |   "description": "Provides weather information",
497 |   "version": "1.0.0",
498 |   "actions": [ /* …as above… */ ],
499 |   "configSchema": { /* … */ },
500 |   "defaultConfig": { /* … */ }
501 | }
502 | ```
503 | 
504 | > **Note**: If the version is not found, the endpoint should return `404 Not Found` with an error envelope identical to the standard error response.
505 | 
506 | ### POST {webtoolUrl}/ — Webtool Execution
507 | 
508 | ```json
509 | {
510 |   "sessionId": "unique-session-identifier",
511 |   "version": "1.0.0",
512 |   "action": "action_name",
513 |   "config": { /* Optional configuration object matching configSchema */ },
514 |   "request": { /* Required data matching requestSchema */ }
515 | }
516 | ```
517 | 
518 | The `sessionId` field is optional and used for maintaining state across multiple requests to the same webtool. The `version` field is optional; if omitted, the server SHOULD default to the latest version. The `config` property contains data that is not generated by the LLM but rather supplied by the environment, allowing minimization of context use, passing security credentials, parameter defaults, or user-configured values.
519 | 
520 | #### Response Examples
521 | 
522 | ##### Successful Response
523 | 
524 | ```json
525 | {
526 |   "status": "ok",
527 |   "data": { /* Action‑specific result */ }
528 | }
529 | ```
530 | 
531 | ##### Error Response
532 | 
533 | ```json
534 | {
535 |   "status": "error",
536 |   "error": {
537 |     "code": "INVALID_INPUT",
538 |     "message": "Validation failed for field 'location'"
539 |   }
540 | }
541 | ```
542 | 
543 | ## Content Types
544 | 
545 | All requests and responses MUST use `application/json` content type. Servers MUST include `Content-Type: application/json` headers in their responses.
546 | 
547 | ## JSON Schema Requirements
548 | 
549 | Webtools MAY use any JSON Schema features. Schemas SHOULD include descriptions and example values to help LLMs understand the expected data structure and format.
550 | 
551 | ## Error Handling
552 | 
553 | The specification defines standard error codes for common validation errors:
554 | - `WEBTOOL_NOT_FOUND` - Requested webtool or version does not exist
555 | - `SCHEMA_ERROR` - Request data does not match the action's requestSchema
556 | - `CONFIG_ERROR` - Configuration data does not match the webtool's configSchema
557 | - `RATE_LIMIT` - Request rate limit exceeded
558 | - `INTERNAL_ERROR` - Unrecoverable server error
559 | 
560 | Webtools MAY define their own custom error codes for domain-specific errors. Error messages SHOULD be human-readable.
561 | 
562 | ## Integration Guides
563 | 
564 | ### Using Webtools with the **Vercel AI SDK**
565 | 
566 | The Vercel AI SDK supports OpenAI‑style *tool calling* out‑of‑the‑box.
567 | 
568 | #### 1 – Fetch Metadata at Build Time
569 | 
570 | ```ts
571 | // lib/tools/weather.ts
572 | import type { Tool } from "ai";
573 | 
574 | export async function getWeatherTool(): Promise<Tool> {
575 |   const res = await fetch("/api/webtools/weather");
576 |   const meta = await res.json();
577 |   return {
578 |     name: meta.name,
579 |     description: meta.description,
580 |     parameters: meta.actions[0].requestSchema, // <-- uses requestSchema
581 |     execute: async (args) => {
582 |       const exec = await fetch("/api/webtools/weather", {
583 |         method: "POST",
584 |         headers: { "content-type": "application/json" },
585 |         body: JSON.stringify({
586 |           sessionId: crypto.randomUUID(),
587 |           action: args.action,
588 |           request: args
589 |         })
590 |       });
591 |       return (await exec.json()).data; // unwrap the envelope
592 |     }
593 |   };
594 | }
595 | ```
596 | 
597 | #### 2 – Create a Tool Caller
598 | 
599 | ```ts
600 | import { createToolCaller } from "ai/tool-caller";
601 | import OpenAI from "@ai-sdk/openai";
602 | import { getWeatherTool } from "@/lib/tools/weather";
603 | 
604 | const toolCaller = createToolCaller([await getWeatherTool()]);
605 | 
606 | export async function chat(messages) {
607 |   const llm = new OpenAI();
608 |   const modelResponse = await llm.chat({ messages, tools: toolCaller.tools });
609 |   const final = await toolCaller.call(modelResponse);
610 |   return final;
611 | }
612 | ```
613 | 
614 | > **Tip:** The AI SDK automatically translates `requestSchema` into the function‑calling format the model expects.
615 | 
616 | ### Using Webtools with **LangChain**
617 | 
618 | LangChain's `StructuredTool` helper lets you wrap a webtool with schema metadata so agents can invoke it.
619 | 
620 | ```python
621 | from langchain_core.tools import StructuredTool
622 | import requests, uuid
623 | 
624 | WEATHER_ENDPOINT = "https://api.example.com/webtools/weather"
625 | 
626 | def run_get_current(location: str, units: str = "metric"):
627 |     body = {
628 |         "sessionId": str(uuid.uuid4()),
629 |         "action": "get_current",
630 |         "request": {"location": location},
631 |         "config": {"units": units}
632 |     }
633 |     return requests.post(WEATHER_ENDPOINT, json=body, timeout=10).json()
634 | 
635 | weather_tool = StructuredTool.from_function(
636 |     func=run_get_current,
637 |     name="get_current_weather",
638 |     description="Return the current weather for a given location via the Weather webtool",
639 |     schema={
640 |         "type": "object",
641 |         "properties": {
642 |             "location": {"type": "string"},
643 |             "units": {"type": "string", "enum": ["metric", "imperial"], "default": "metric"}
644 |         },
645 |         "required": ["location"]
646 |     }
647 | )
648 | ```
649 | 
650 | Then add `weather_tool` to any LCEL runnable or agent:
651 | 
652 | ```python
653 | from langchain_openai import ChatOpenAI
654 | from langchain.agents import AgentExecutor, create_openai_functions_agent
655 | 
656 | llm = ChatOpenAI(model_name="gpt-4o")
657 | agent = create_openai_functions_agent(llm, tools=[weather_tool])
658 | executor = AgentExecutor(agent=agent, tools=[weather_tool])
659 | 
660 | result = executor.invoke("Should I take an umbrella to Paris today?")
661 | print(result)
662 | ```
663 | 
664 | ## Error Handling & Best Practices
665 | 
666 | * Validate client input against each action's `requestSchema` before issuing a POST.
667 | * For recoverable failures (4xx), return `status: "error"` with appropriate error codes.
668 | * For unrecoverable server errors (5xx), set code `INTERNAL_ERROR` and avoid leaking internals.
669 | * Use standard HTTP status codes alongside the JSON response for broad compatibility.
670 | * Rate limiting, quotas, and other implementation details are left to implementers.
671 | 
672 | ## Authentication & Authorization
673 | 
674 | This specification is agnostic regarding authorization schemes. Consumers MAY include an Authorization: Bearer <token> header on every request. Additional details—such as token scopes, expiration, or refresh flows—are considered implementation-specific and are not mandated by this spec.
675 | 
676 | ---
677 | 
678 | **Version:** 2025‑06‑30
679 | 
```

--------------------------------------------------------------------------------
/LAMBDA.md:
--------------------------------------------------------------------------------

```markdown
  1 | # AWS Pricing MCP Lambda Handler
  2 | 
  3 | This document provides comprehensive documentation for the AWS Pricing MCP Lambda handler implementation, including deployment, usage, and technical details.
  4 | 
  5 | ## Table of Contents
  6 | 
  7 | 1. [Overview](#overview)
  8 | 2. [Quick Start](#quick-start)
  9 | 3. [Architecture](#architecture)
 10 | 4. [Usage](#usage)
 11 | 5. [Deployment](#deployment)
 12 | 6. [Configuration](#configuration)
 13 | 7. [Testing](#testing)
 14 | 8. [Monitoring](#monitoring)
 15 | 9. [Troubleshooting](#troubleshooting)
 16 | 10. [Implementation Details](#implementation-details)
 17 | 11. [Performance](#performance)
 18 | 12. [Security](#security)
 19 | 13. [Future Enhancements](#future-enhancements)
 20 | 
 21 | ## Overview
 22 | 
 23 | The Lambda handler implements the Model Context Protocol (MCP) specification as described in the `MCP.md` file, providing a serverless way to access AWS EC2 pricing data through JSON-RPC 2.0 requests. The function automatically downloads the latest pricing data from S3 and caches it for optimal performance. It's deployed using AWS SAM with a Function URL for direct HTTP access.
 24 | 
 25 | ### Key Features
 26 | 
 27 | - **JSON-RPC 2.0**: Full compliance with JSON-RPC 2.0 specification
 28 | - **Dynamic Pricing Data**: Downloads latest pricing data from S3 at runtime
 29 | - **HTTP Function URL**: Direct HTTP access with no authentication required
 30 | - **CORS Support**: Full CORS headers for web browser access
 31 | - **Caching**: Intelligent caching for optimal performance
 32 | - **Error Handling**: Comprehensive error handling and validation
 33 | - **No Dependencies**: Uses only Python standard library modules
 34 | 
 35 | ## Quick Start
 36 | 
 37 | ### Prerequisites
 38 | 
 39 | 1. **AWS CLI** installed and configured
 40 | 2. **AWS SAM CLI** installed
 41 | 3. **Python 3.9+** installed
 42 | 4. **AWS Account** with appropriate permissions
 43 | 
 44 | ### Install Prerequisites
 45 | 
 46 | ```bash
 47 | # Install AWS CLI
 48 | curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
 49 | unzip awscliv2.zip
 50 | sudo ./aws/install
 51 | 
 52 | # Install AWS SAM CLI
 53 | pip install aws-sam-cli
 54 | 
 55 | # Configure AWS CLI
 56 | aws configure
 57 | ```
 58 | 
 59 | ### Deploy the Function
 60 | 
 61 | ```bash
 62 | # From the project root directory
 63 | sam build
 64 | sam deploy --guided
 65 | ```
 66 | 
 67 | The guided deployment will prompt for configuration values. Use the defaults or customize as needed.
 68 | 
 69 | ### Get the Function URL
 70 | 
 71 | After deployment, get the Function URL:
 72 | 
 73 | ```bash
 74 | aws cloudformation describe-stacks \
 75 |   --stack-name aws-pricing-mcp \
 76 |   --query 'Stacks[0].Outputs[?OutputKey==`InvokeUrl`].OutputValue' \
 77 |   --output text
 78 | ```
 79 | 
 80 | ### Test the Function
 81 | 
 82 | ```bash
 83 | # Test with curl
 84 | curl -X POST YOUR_FUNCTION_URL \
 85 |   -H "Content-Type: application/json" \
 86 |   -d '{
 87 |     "jsonrpc": "2.0",
 88 |     "id": 1,
 89 |     "method": "initialize",
 90 |     "params": {
 91 |       "protocolVersion": "2025-03-26",
 92 |       "capabilities": {},
 93 |       "clientInfo": {
 94 |         "name": "TestClient",
 95 |         "version": "1.0.0"
 96 |       }
 97 |     }
 98 |   }'
 99 | ```
100 | 
101 | ## Architecture
102 | 
103 | ### Components
104 | 
105 | 1. **Lambda Function**: Processes MCP protocol requests
106 | 2. **Function URL**: Provides HTTP access without authentication
107 | 3. **IAM Role**: Provides execution permissions
108 | 4. **CloudWatch Logs**: Stores function logs
109 | 
110 | ### Request/Response Flow
111 | 
112 | ```
113 | Client Request (JSON-RPC 2.0)
114 |     ↓
115 | Lambda Handler (lambda_handler function)
116 |     ↓
117 | Method Router (based on "method" field)
118 |     ↓
119 | Specific Handler Function
120 |     ↓
121 | JSON-RPC 2.0 Response
122 | ```
123 | 
124 | ### Function URL Features
125 | 
126 | - **No Authentication**: Public access
127 | - **CORS Enabled**: Web browser compatible
128 | - **HTTP Methods**: GET, POST, OPTIONS
129 | - **Direct Access**: No API Gateway required
130 | 
131 | ## Usage
132 | 
133 | ### Local Testing
134 | 
135 | ```bash
136 | cd src/lambda
137 | python test_lambda.py
138 | ```
139 | 
140 | ### HTTP API Usage
141 | 
142 | Once deployed, the function is accessible via HTTP POST requests:
143 | 
144 | ```bash
145 | # Get the Function URL from SAM outputs
146 | FUNCTION_URL=$(aws cloudformation describe-stacks --stack-name aws-pricing-mcp --query 'Stacks[0].Outputs[?OutputKey==`InvokeUrl`].OutputValue' --output text)
147 | 
148 | # Test the function
149 | curl -X POST $FUNCTION_URL \
150 |   -H "Content-Type: application/json" \
151 |   -d '{
152 |     "jsonrpc": "2.0",
153 |     "id": 1,
154 |     "method": "initialize",
155 |     "params": {
156 |       "protocolVersion": "2025-03-26",
157 |       "capabilities": {},
158 |       "clientInfo": {
159 |         "name": "TestClient",
160 |         "version": "1.0.0"
161 |       }
162 |     }
163 |   }'
164 | ```
165 | 
166 | ### Supported MCP Methods
167 | 
168 | | Method | Handler Function | Status |
169 | |--------|------------------|--------|
170 | | `initialize` | `handle_initialize()` | ✅ Implemented |
171 | | `ping` | `handle_ping()` | ✅ Implemented |
172 | | `tools/list` | `handle_tools_list()` | ✅ Implemented |
173 | | `tools/call` | `handle_tools_call()` | ✅ Implemented |
174 | | `resources/list` | `handle_resources_list()` | ✅ Empty implementation |
175 | | `resources/read` | `handle_resources_read()` | ✅ Empty implementation |
176 | | `prompts/list` | `handle_prompts_list()` | ✅ Empty implementation |
177 | | `prompts/get` | `handle_prompts_get()` | ✅ Empty implementation |
178 | 
179 | ### Sample MCP Requests
180 | 
181 | #### Initialize
182 | ```json
183 | {
184 |   "jsonrpc": "2.0",
185 |   "id": 1,
186 |   "method": "initialize",
187 |   "params": {
188 |     "protocolVersion": "2025-03-26",
189 |     "capabilities": {},
190 |     "clientInfo": {
191 |       "name": "TestClient",
192 |       "version": "1.0.0"
193 |     }
194 |   }
195 | }
196 | ```
197 | 
198 | #### Get Tools List
199 | ```json
200 | {
201 |   "jsonrpc": "2.0",
202 |   "id": 2,
203 |   "method": "tools/list",
204 |   "params": {}
205 | }
206 | ```
207 | 
208 | #### Call EC2 Pricing Tool
209 | ```json
210 | {
211 |   "jsonrpc": "2.0",
212 |   "id": 3,
213 |   "method": "tools/call",
214 |   "params": {
215 |     "name": "ec2_instances_pricing",
216 |     "arguments": {
217 |       "filter_region": "us-east-1",
218 |       "filter_platform": "Linux/UNIX",
219 |       "filter_min_vcpu": 2,
220 |       "filter_min_ram": 4.0,
221 |       "filter_max_price_per_hour": 0.1,
222 |       "sort_by": "Price",
223 |       "sort_order": "Ascending"
224 |     }
225 |   }
226 | }
227 | ```
228 | 
229 | ### EC2 Pricing Tool
230 | 
231 | The main tool provided is `ec2_instances_pricing` which allows filtering and searching EC2 instances based on:
232 | 
233 | - **Region**: AWS region (default: us-east-1)
234 | - **Platform**: OS platform (Linux/UNIX, Windows, Red Hat, etc.)
235 | - **Tenancy**: Shared or Dedicated
236 | - **Pricing Model**: On Demand, Reserved Instances, etc.
237 | - **Specifications**: vCPU, RAM, GPU, network performance, etc.
238 | - **Cost**: Maximum price per hour
239 | - **Sorting**: By price, specifications, etc.
240 | - **Pagination**: 5 results per page
241 | 
242 | ## Deployment
243 | 
244 | ### SAM Template Parameters
245 | 
246 | The `template.yaml` file supports the following parameters:
247 | 
248 | | Parameter | Default | Description |
249 | |-----------|---------|-------------|
250 | | `FunctionName` | `aws-pricing-mcp` | Name of the Lambda function |
251 | | `Runtime` | `python3.12` | Python runtime version |
252 | | `Architecture` | `x86_64` | Lambda function architecture (x86_64 or arm64) |
253 | | `Timeout` | `30` | Function timeout in seconds |
254 | | `MemorySize` | `512` | Function memory size in MB |
255 | 
256 | ### Custom Deployment
257 | 
258 | ```bash
259 | # Deploy with custom parameters
260 | sam deploy \
261 |   --stack-name my-pricing-mcp \
262 |   --parameter-overrides \
263 |     FunctionName=my-pricing-function \
264 |     Runtime=python3.12 \
265 |     Architecture=arm64 \
266 |     Timeout=60 \
267 |     MemorySize=1024
268 | ```
269 | 
270 | ### SAM Template
271 | 
272 | The deployment uses `template.yaml` in the project root which defines:
273 | - Lambda function with Function URL
274 | - IAM roles and policies
275 | - CORS configuration
276 | - Output values for Function URL
277 | 
278 | ## Configuration
279 | 
280 | ### Lambda Function Settings
281 | - **Runtime**: Python 3.12
282 | - **Handler**: `lambda_handler.lambda_handler`
283 | - **Timeout**: 30 seconds (configurable)
284 | - **Memory**: 512 MB (increased for pricing data processing)
285 | - **Architecture**: x86_64 or arm64 (configurable)
286 | 
287 | ### Environment Variables
288 | No environment variables are required.
289 | 
290 | ### IAM Permissions
291 | The Lambda function requires:
292 | - **CloudWatch Logs** for logging
293 | - **Internet access** to download pricing data from S3
294 | - **Basic execution permissions**
295 | 
296 | ### Pricing Data Configuration
297 | - **Download URL**: https://cloudfix-public-aws-pricing.s3.us-east-1.amazonaws.com/pricing/ec2_pricing.json.gz
298 | - **Cache Location**: In-memory (global variable)
299 | - **Cache Duration**: 1 hour (3600 seconds)
300 | - **Compression**: Gzip compressed JSON (decompressed in memory)
301 | 
302 | ### Function URL Configuration
303 | - **Auth Type**: NONE (no authentication required)
304 | - **CORS**: Enabled for all origins
305 | - **Methods**: GET, POST, OPTIONS
306 | - **Headers**: All headers allowed
307 | 
308 | ## Testing
309 | 
310 | The `test_lambda.py` script provides comprehensive testing of all MCP methods with sample requests and expected responses.
311 | 
312 | ### Testing Results
313 | 
314 | The test script successfully validates:
315 | 
316 | 1. **Initialize**: Returns proper MCP capabilities and server info
317 | 2. **Tools List**: Returns complete tool schema with all parameters
318 | 3. **Tools Call**: Successfully executes pricing queries and returns results
319 | 4. **Ping**: Returns empty result (health check)
320 | 5. **Error Handling**: Properly handles invalid methods
321 | 
322 | Sample successful query returned 5 EC2 instances matching criteria:
323 | - t4g.medium ($0.0336/hour)
324 | - t3a.medium ($0.0376/hour)
325 | - t3.medium ($0.0416/hour)
326 | - t2.medium ($0.0464/hour)
327 | - a1.large ($0.051/hour)
328 | 
329 | ## Monitoring
330 | 
331 | ### CloudWatch Logs
332 | 
333 | ```bash
334 | # View function logs
335 | aws logs tail /aws/lambda/aws-pricing-mcp --follow
336 | ```
337 | 
338 | ### CloudWatch Metrics
339 | 
340 | Monitor key metrics:
341 | - **Invocations**: Number of function calls
342 | - **Duration**: Function execution time
343 | - **Errors**: Number of errors
344 | - **Throttles**: Number of throttled requests
345 | 
346 | ### Custom Metrics
347 | 
348 | The function logs important events:
349 | - Pricing data download attempts
350 | - Cache hits/misses
351 | - Error conditions
352 | 
353 | ### Key Metrics to Monitor
354 | - **Download Success Rate**: Percentage of successful pricing data downloads
355 | - **Cache Hit Rate**: Percentage of requests using cached data
356 | - **Response Time**: Time to process requests
357 | - **Error Rate**: Percentage of failed requests
358 | - **HTTP Status Codes**: Monitor 4xx and 5xx errors
359 | 
360 | ## Troubleshooting
361 | 
362 | ### Common Issues
363 | 
364 | 1. **Deployment Fails**
365 |    ```bash
366 |    # Check SAM build
367 |    sam build --debug
368 |    
369 |    # Check CloudFormation events
370 |    aws cloudformation describe-stack-events --stack-name aws-pricing-mcp
371 |    ```
372 | 
373 | 2. **Function URL Not Working**
374 |    ```bash
375 |    # Verify Function URL exists
376 |    aws lambda get-function-url-config --function-name aws-pricing-mcp
377 |    
378 |    # Test with curl
379 |    curl -v YOUR_FUNCTION_URL
380 |    ```
381 | 
382 | 3. **Pricing Data Download Fails**
383 |    ```bash
384 |    # Check function logs
385 |    aws logs tail /aws/lambda/aws-pricing-mcp --since 1h
386 |    
387 |    # Verify internet access
388 |    # Ensure Lambda is not in a private VPC without NAT Gateway
389 |    ```
390 | 
391 | 4. **CORS Issues**
392 |    - Verify CORS headers in function response
393 |    - Check browser console for CORS errors
394 |    - Ensure preflight OPTIONS requests are handled
395 | 
396 | ### Debugging Tips
397 | - Check CloudWatch logs for detailed error messages
398 | - Monitor pricing data download events
399 | - Verify cache file existence and size
400 | - Test with simple requests first
401 | - Use curl or Postman to test HTTP endpoints
402 | 
403 | ## Implementation Details
404 | 
405 | ### Protocol Implementation
406 | 
407 | **Original Server (fastmcp-based):**
408 | - Uses `fastmcp` library for MCP protocol handling
409 | - HTTP/SSE-based communication
410 | - Stateful server with session management
411 | - Complex dependency management
412 | 
413 | **Lambda Handler (standard library):**
414 | - Manual JSON-RPC 2.0 implementation
415 | - Stateless request handling
416 | - Direct method routing
417 | - No external dependencies
418 | 
419 | ### Lambda Handler Function
420 | The `lambda_handler` function is the entry point that:
421 | 1. Handles HTTP requests from Function URL
422 | 2. Parses incoming JSON-RPC requests
423 | 3. Validates request format
424 | 4. Routes to appropriate handler based on method
425 | 5. Returns HTTP response with JSON-RPC response
426 | 
427 | ### Handler Functions
428 | Each MCP method has a dedicated handler function:
429 | - `handle_initialize()` - Protocol initialization
430 | - `handle_tools_list()` - Tool discovery
431 | - `handle_tools_call()` - Tool execution
432 | - `handle_ping()` - Health check
433 | - etc.
434 | 
435 | ### Pricing Data Management
436 | The pricing data is managed through:
437 | - `download_and_cache_pricing_data()` - Downloads and caches pricing data in memory
438 | - **Source**: https://cloudfix-public-aws-pricing.s3.us-east-1.amazonaws.com/pricing/ec2_pricing.json.gz
439 | - **Cache Location**: In-memory global variable
440 | - **Cache Duration**: 1 hour (3600 seconds)
441 | - **Fallback**: Uses cached data if download fails
442 | 
443 | ### Pricing Logic
444 | The `ec2_instances_pricing()` function contains the core pricing logic:
445 | - Ensures pricing data is loaded (downloads if needed)
446 | - Applies filters based on parameters
447 | - Calculates pricing for different models
448 | - Sorts and paginates results
449 | 
450 | ### Error Handling
451 | 
452 | **JSON-RPC Error Codes:**
453 | - `-32700`: Parse error (invalid JSON)
454 | - `-32600`: Invalid request (wrong jsonrpc version)
455 | - `-32601`: Method not found
456 | - `-32603`: Internal error
457 | 
458 | **Validation:**
459 | - Input parameter validation
460 | - Platform/tenancy/pricing model validation
461 | - Graceful handling of missing pricing data
462 | 
463 | ### Dependencies
464 | 
465 | The Lambda handler uses only Python standard library modules:
466 | - `json` - JSON parsing and serialization
467 | - `os` - File system operations
468 | - `sys` - System-specific parameters
469 | - `traceback` - Exception handling
470 | - `typing` - Type hints
471 | - `datetime` - Date/time operations
472 | - `gzip` - Gzip file decompression
473 | - `urllib.request` - HTTP downloads
474 | - `time` - Time-based operations
475 | - `pathlib` - Path operations
476 | 
477 | No external dependencies are required, making deployment simple and lightweight.
478 | 
479 | ## Performance
480 | 
481 | ### Lambda Configuration
482 | - **Cold Start**: ~100-500ms (first request, includes data download)
483 | - **Warm Start**: ~10-50ms (subsequent requests use cached data)
484 | - **Memory Usage**: ~100-200 MB for pricing data in memory
485 | - **Concurrency**: Handles multiple concurrent requests
486 | 
487 | ### Caching Strategy
488 | - **Cache Duration**: 1 hour balances freshness with performance
489 | - **Cache Location**: In-memory global variable
490 | - **Cache Size**: ~100MB uncompressed in memory
491 | - **Cache Hit Rate**: High for typical usage patterns
492 | 
493 | ### Network Considerations
494 | - **Download Time**: ~2-5 seconds for initial data download
495 | - **Retry Logic**: Falls back to cached data if download fails
496 | - **Bandwidth**: ~25MB download per cache refresh
497 | 
498 | ### HTTP Performance
499 | - **Function URL**: Direct HTTP access without API Gateway
500 | - **CORS**: Pre-configured for web browser access
501 | - **Response Time**: Fast JSON-RPC responses
502 | 
503 | ### Performance Characteristics
504 | 
505 | #### Download Performance
506 | - **Download Time**: ~2-5 seconds for initial download
507 | - **Decompression**: ~1-2 seconds for gzip extraction
508 | - **File Size**: ~25MB compressed, ~100MB uncompressed
509 | - **Network**: Requires internet access from Lambda
510 | 
511 | #### Caching Performance
512 | - **Cache Hit**: ~10-50ms response time
513 | - **Cache Miss**: ~3-7 seconds (includes download)
514 | - **Memory Usage**: ~100-200 MB for pricing data
515 | - **Storage**: ~100MB in memory
516 | 
517 | ## Security
518 | 
519 | ### Function URL Security
520 | - **No Authentication**: Function URL is publicly accessible
521 | - **CORS**: Configured for all origins (customize as needed)
522 | - **Rate Limiting**: Consider adding rate limiting for production use
523 | 
524 | ### IAM Permissions
525 | The function uses minimal IAM permissions:
526 | - CloudWatch Logs access
527 | - Basic Lambda execution permissions
528 | - No additional AWS service access required
529 | 
530 | ### Network Security
531 | - **Internet Access**: Required for pricing data download
532 | - **VPC**: If using VPC, ensure NAT Gateway for internet access
533 | - **Security Groups**: Allow outbound HTTPS traffic
534 | 
535 | ## Cost Optimization
536 | 
537 | ### Lambda Costs
538 | - **Memory**: 512 MB (adjust based on usage)
539 | - **Timeout**: 30 seconds (sufficient for most requests)
540 | - **Concurrency**: Auto-scaling based on demand
541 | 
542 | ### Optimization Tips
543 | 1. **Memory Tuning**: Monitor memory usage and adjust
544 | 2. **Timeout Tuning**: Set appropriate timeout values
545 | 3. **Caching**: Function caches pricing data for 1 hour
546 | 4. **Concurrency**: Monitor and adjust if needed
547 | 
548 | ## Updates and Maintenance
549 | 
550 | ### Updating the Function
551 | ```bash
552 | # Deploy updates
553 | sam build
554 | sam deploy
555 | ```
556 | 
557 | ### Updating Configuration
558 | ```bash
559 | # Update parameters
560 | sam deploy --parameter-overrides Timeout=60 MemorySize=1024
561 | ```
562 | 
563 | ### Monitoring Updates
564 | - Monitor pricing data freshness
565 | - Check for new AWS instance types
566 | - Review CloudWatch metrics regularly
567 | 
568 | ## Production Considerations
569 | 
570 | ### High Availability
571 | - **Multi-Region**: Deploy to multiple regions if needed
572 | - **Backup**: Consider backup strategies for critical data
573 | - **Monitoring**: Set up comprehensive monitoring and alerting
574 | 
575 | ### Performance
576 | - **Caching**: Function caches pricing data for optimal performance
577 | - **Cold Starts**: Consider provisioned concurrency for consistent performance
578 | - **Memory**: Monitor and optimize memory usage
579 | 
580 | ### Security
581 | - **Authentication**: Consider adding authentication for production use
582 | - **Rate Limiting**: Implement rate limiting to prevent abuse
583 | - **Monitoring**: Set up security monitoring and alerting
584 | 
585 | ## Future Enhancements
586 | 
587 | ### Potential Improvements
588 | 1. **Multi-Region Support**: Download pricing data for multiple regions
589 | 2. **Incremental Updates**: Only download changed pricing data
590 | 3. **Compression**: Use more efficient compression formats
591 | 4. **CDN Integration**: Use CloudFront for faster downloads
592 | 5. **Background Updates**: Update cache in background threads
593 | 6. **API Gateway**: Add API Gateway for additional features
594 | 7. **Custom Domain**: Use custom domain with Function URL
595 | 
596 | ### Monitoring Enhancements
597 | 1. **Custom Metrics**: Track download performance and cache efficiency
598 | 2. **Alerts**: Set up CloudWatch alarms for download failures
599 | 3. **Dashboard**: Create comprehensive monitoring dashboard
600 | 4. **Health Checks**: Implement pricing data freshness checks
601 | 5. **Performance Monitoring**: Track HTTP response times
602 | 
603 | ## Support
604 | 
605 | For issues and questions:
606 | 
607 | 1. Check CloudWatch Logs for error details
608 | 2. Review this documentation
609 | 3. Check AWS Lambda documentation
610 | 4. Review SAM documentation
611 | 
612 | ## Cleanup
613 | 
614 | To remove the deployment:
615 | 
616 | ```bash
617 | sam delete
618 | ```
619 | 
620 | This will remove:
621 | - Lambda function
622 | - Function URL
623 | - IAM role
624 | - CloudWatch log group
625 | - All associated resources
626 | 
627 | ## File Structure
628 | 
629 | ```
630 | src/lambda/
631 | ├── __init__.py                 # Package initialization
632 | ├── lambda_handler.py          # Main Lambda handler
633 | ├── test_lambda.py             # Test script
634 | ├── test_event.json            # Test event for local testing
635 | ├── requirements.txt           # Dependencies (empty)
636 | └── README.md                  # Documentation (merged into this file)
637 | ```
638 | 
639 | ## Migration Path
640 | 
641 | ### From Original Server
642 | 1. **Replace fastmcp calls** with direct JSON-RPC handling
643 | 2. **Update deployment** to use Lambda instead of HTTP server
644 | 3. **Modify client code** to call Lambda function instead of HTTP endpoint
645 | 4. **Update monitoring** to use CloudWatch instead of server logs
646 | 
647 | ### Client Integration
648 | ```python
649 | # Example client code
650 | import boto3
651 | import json
652 | 
653 | lambda_client = boto3.client('lambda')
654 | 
655 | def call_mcp_server(method, params=None):
656 |     request = {
657 |         "jsonrpc": "2.0",
658 |         "id": 1,
659 |         "method": method,
660 |         "params": params or {}
661 |     }
662 |     
663 |     response = lambda_client.invoke(
664 |         FunctionName='aws-pricing-mcp',
665 |         Payload=json.dumps(request)
666 |     )
667 |     
668 |     return json.loads(response['Payload'].read())
669 | ```
670 | 
671 | ## Conclusion
672 | 
673 | The Lambda handler implementation successfully provides the same functionality as the original fastmcp-based server while offering significant advantages in terms of deployment simplicity, cost-effectiveness, and scalability. The implementation is production-ready and can be deployed immediately to AWS Lambda.
674 | 
675 | The dynamic pricing data download implementation provides significant benefits:
676 | - **Always up-to-date pricing data**
677 | - **Reduced deployment complexity**
678 | - **Improved reliability and error handling**
679 | - **Better performance through intelligent caching**
680 | - **Simplified maintenance and updates**
681 | 
682 | The implementation is production-ready and provides a robust foundation for serving current AWS EC2 pricing data through the MCP protocol. 
```

--------------------------------------------------------------------------------
/src/lambda/lambda_handler.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | AWS Pricing MCP Lambda Handler
  3 | 
  4 | This Lambda function implements the Model Context Protocol (MCP) for AWS EC2 pricing data
  5 | without the fastmcp dependency. It handles JSON-RPC requests and provides tools for
  6 | finding EC2 instances based on specified criteria.
  7 | """
  8 | 
  9 | import json
 10 | import os
 11 | import sys
 12 | import traceback
 13 | import gzip
 14 | import urllib.request
 15 | import time
 16 | from typing import List, Dict, Any, Optional, Union
 17 | from datetime import datetime
 18 | from pathlib import Path
 19 | 
 20 | # Set up error logging
 21 | def log_error(message):
 22 |     print(f"ERROR: {message}", file=sys.stderr)
 23 | 
 24 | # Global variable to store pricing data
 25 | PRICING_DATA = None
 26 | PRICING_DATA_LAST_UPDATE = 0
 27 | CACHE_DURATION = 3600  # Cache for 1 hour (3600 seconds)
 28 | 
 29 | def download_and_cache_pricing_data():
 30 |     """
 31 |     Download pricing data from S3 and store it in memory.
 32 |     Returns the pricing data.
 33 |     """
 34 |     global PRICING_DATA, PRICING_DATA_LAST_UPDATE
 35 |     
 36 |     current_time = time.time()
 37 |     
 38 |     # Check if we have cached data that's still valid
 39 |     if PRICING_DATA is not None and (current_time - PRICING_DATA_LAST_UPDATE) < CACHE_DURATION:
 40 |         return PRICING_DATA
 41 |     
 42 |     # S3 URL for pricing data
 43 |     pricing_url = "https://cloudfix-public-aws-pricing.s3.us-east-1.amazonaws.com/pricing/ec2_pricing.json.gz"
 44 |     
 45 |     try:
 46 |         # Download the gzipped file
 47 |         log_error(f"Downloading pricing data from {pricing_url}")
 48 |         
 49 |         # Download and decompress in memory
 50 |         with urllib.request.urlopen(pricing_url) as response:
 51 |             compressed_data = response.read()
 52 |         
 53 |         # Decompress the gzipped data
 54 |         decompressed_data = gzip.decompress(compressed_data)
 55 |         
 56 |         # Parse the JSON data
 57 |         PRICING_DATA = json.loads(decompressed_data.decode('utf-8'))
 58 |         PRICING_DATA_LAST_UPDATE = current_time
 59 |         
 60 |         log_error(f"Successfully downloaded and cached pricing data. Size: {len(decompressed_data)} bytes")
 61 |         
 62 |         return PRICING_DATA
 63 |         
 64 |     except Exception as e:
 65 |         log_error(f"Failed to download pricing data: {str(e)}")
 66 |         log_error(traceback.format_exc())
 67 |         
 68 |         # If download fails and we have cached data, use it
 69 |         if PRICING_DATA is not None:
 70 |             log_error("Using cached pricing data as fallback")
 71 |             return PRICING_DATA
 72 |         
 73 |         # If all else fails, raise the original error
 74 |         raise
 75 | 
 76 | # Initialize pricing data on module load
 77 | try:
 78 |     download_and_cache_pricing_data()
 79 | except Exception as e:
 80 |     log_error(f"Failed to initialize pricing data: {str(e)}")
 81 |     # Don't raise here - let the handler try to download when needed
 82 | 
 83 | PLATFORM_TO_OP_CODE = {
 84 |     "Linux/UNIX": "",
 85 |     "Red Hat BYOL Linux": "00g0",
 86 |     "Red Hat Enterprise Linux": "0010",
 87 |     "Red Hat Enterprise Linux with HA": "1010",
 88 |     "Red Hat Enterprise Linux with SQL Server Standard and HA": "1014",
 89 |     "Red Hat Enterprise Linux with SQL Server Enterprise and HA": "1110",
 90 |     "Red Hat Enterprise Linux with SQL Server Standard": "0014",
 91 |     "Red Hat Enterprise Linux with SQL Server Web": "0210",
 92 |     "Red Hat Enterprise Linux with SQL Server Enterprise": "0110",
 93 |     "Linux with SQL Server Enterprise": "0100",
 94 |     "Linux with SQL Server Standard": "0004",
 95 |     "Linux with SQL Server Web": "0200",
 96 |     "SUSE Linux": "000g",
 97 |     "Windows": "0002",
 98 |     "Windows BYOL": "0800",
 99 |     "Windows with SQL Server Enterprise": "0102",
100 |     "Windows with SQL Server Standard": "0006",
101 |     "Windows with SQL Server Web": "0202",
102 | }
103 | 
104 | PRICING_MODELS = ["On Demand", "1-yr No Upfront", "1-yr Partial Upfront", "1-yr All Upfront", "3-yr No Upfront", "3-yr Partial Upfront", "3-yr All Upfront"]
105 | TENANCIES = ["Shared", "Dedicated"]
106 | 
107 | # MCP Server Info
108 | SERVER_INFO = {
109 |     "name": "AWS EC2 Pricing MCP",
110 |     "version": "1.0.0"
111 | }
112 | 
113 | # MCP Protocol Version
114 | PROTOCOL_VERSION = "2025-03-26"
115 | 
116 | def create_error_response(request_id: str, code: int, message: str, data: Optional[Dict] = None) -> Dict:
117 |     """Create a JSON-RPC error response"""
118 |     error = {
119 |         "code": code,
120 |         "message": message
121 |     }
122 |     if data:
123 |         error["data"] = data
124 |     
125 |     return {
126 |         "jsonrpc": "2.0",
127 |         "id": request_id,
128 |         "error": error
129 |     }
130 | 
131 | def create_success_response(request_id: str, result: Any) -> Dict:
132 |     """Create a JSON-RPC success response"""
133 |     return {
134 |         "jsonrpc": "2.0",
135 |         "id": request_id,
136 |         "result": result
137 |     }
138 | 
139 | def handle_initialize(params: Dict) -> Dict:
140 |     """Handle MCP initialize request"""
141 |     return {
142 |         "protocolVersion": PROTOCOL_VERSION,
143 |         "capabilities": {
144 |             "logging": {},
145 |             "prompts": {
146 |                 "listChanged": True
147 |             },
148 |             "resources": {
149 |                 "subscribe": True,
150 |                 "listChanged": True
151 |             },
152 |             "tools": {
153 |                 "listChanged": True
154 |             }
155 |         },
156 |         "serverInfo": SERVER_INFO,
157 |         "instructions": "AWS EC2 Pricing MCP Server - Provides tools to find EC2 instances based on pricing and specifications"
158 |     }
159 | 
160 | def handle_ping() -> Dict:
161 |     """Handle MCP ping request"""
162 |     return {}
163 | 
164 | def handle_tools_list(params: Dict) -> Dict:
165 |     """Handle MCP tools/list request"""
166 |     tools = [
167 |         {
168 |             "name": "ec2_instances_pricing",
169 |             "description": "Find AWS EC2 instances based on specified criteria including pricing, specifications, and filters",
170 |             "inputSchema": {
171 |                 "type": "object",
172 |                 "properties": {
173 |                     "filter_region": {
174 |                         "type": "string",
175 |                         "description": "AWS region (default: us-east-1)",
176 |                         "default": "us-east-1"
177 |                     },
178 |                     "filter_platform": {
179 |                         "type": "string",
180 |                         "description": "OS platform",
181 |                         "enum": list(PLATFORM_TO_OP_CODE.keys()),
182 |                         "default": "Linux/UNIX"
183 |                     },
184 |                     "filter_tenancy": {
185 |                         "type": "string",
186 |                         "description": "Instance tenancy",
187 |                         "enum": TENANCIES,
188 |                         "default": "Shared"
189 |                     },
190 |                     "filter_pricing_model": {
191 |                         "type": "string",
192 |                         "description": "Pricing model",
193 |                         "enum": PRICING_MODELS,
194 |                         "default": "On Demand"
195 |                     },
196 |                     "filter_min_vcpu": {
197 |                         "type": "integer",
198 |                         "description": "Minimum number of vCPUs",
199 |                         "default": 0
200 |                     },
201 |                     "filter_min_ram": {
202 |                         "type": "number",
203 |                         "description": "Minimum amount of RAM in GB",
204 |                         "default": 0
205 |                     },
206 |                     "filter_min_gpu": {
207 |                         "type": "integer",
208 |                         "description": "Minimum number of GPUs",
209 |                         "default": 0
210 |                     },
211 |                     "filter_min_gpu_memory": {
212 |                         "type": "integer",
213 |                         "description": "Minimum GPU memory in GB",
214 |                         "default": 0
215 |                     },
216 |                     "filter_min_cpu_ghz": {
217 |                         "type": "number",
218 |                         "description": "Minimum CPU clock speed in GHz",
219 |                         "default": 0
220 |                     },
221 |                     "filter_min_network_performance": {
222 |                         "type": "integer",
223 |                         "description": "Minimum network performance in Mbps",
224 |                         "default": 0
225 |                     },
226 |                     "filter_min_ebs_throughput": {
227 |                         "type": "integer",
228 |                         "description": "Minimum dedicated EBS throughput in Mbps",
229 |                         "default": 0
230 |                     },
231 |                     "filter_min_ephemeral_storage": {
232 |                         "type": "integer",
233 |                         "description": "Minimum ephemeral storage in GB",
234 |                         "default": 0
235 |                     },
236 |                     "filter_max_price_per_hour": {
237 |                         "type": "number",
238 |                         "description": "Maximum price per hour in USD",
239 |                         "default": None
240 |                     },
241 |                     "filter_family": {
242 |                         "type": "string",
243 |                         "description": "Filter by instance family (e.g., 'm5', 'c6g')",
244 |                         "default": ""
245 |                     },
246 |                     "filter_size": {
247 |                         "type": "string",
248 |                         "description": "Filter by instance size (e.g., 'large', '2xlarge')",
249 |                         "default": ""
250 |                     },
251 |                     "filter_processor": {
252 |                         "type": "string",
253 |                         "description": "Filter by physical processor (e.g., 'Graviton', 'Xeon', 'AMD')",
254 |                         "default": ""
255 |                     },
256 |                     "sort_by": {
257 |                         "type": "string",
258 |                         "description": "Field to sort by",
259 |                         "enum": ["Price", "Clock Speed GHz", "vCPU cores", "Memory GB", "Ephemeral Storage GB", "Network Performance Mbps", "Dedicated EBS Throughput Mbps", "GPU cores", "GPU Memory GB"],
260 |                         "default": "Price"
261 |                     },
262 |                     "sort_order": {
263 |                         "type": "string",
264 |                         "description": "Sort order",
265 |                         "enum": ["Ascending", "Descending"],
266 |                         "default": "Descending"
267 |                     },
268 |                     "page_num": {
269 |                         "type": "integer",
270 |                         "description": "Page number for pagination",
271 |                         "default": 0
272 |                     }
273 |                 }
274 |             }
275 |         }
276 |     ]
277 |     
278 |     return {
279 |         "tools": tools
280 |     }
281 | 
282 | def ec2_instances_pricing(
283 |     filter_region: str = "us-east-1",
284 |     filter_platform: str = "Linux/UNIX",
285 |     filter_tenancy: str = "Shared",
286 |     filter_pricing_model: str = "On Demand",
287 |     filter_min_vcpu: int = 0,
288 |     filter_min_ram: float = 0,
289 |     filter_min_gpu: int = 0,
290 |     filter_min_gpu_memory: int = 0,
291 |     filter_min_cpu_ghz: float = 0,
292 |     filter_min_network_performance: int = 0,
293 |     filter_min_ebs_throughput: int = 0,
294 |     filter_min_ephemeral_storage: int = 0,
295 |     filter_max_price_per_hour: Optional[float] = None,
296 |     filter_family: str = "",
297 |     filter_size: str = "",
298 |     filter_processor: str = "",
299 |     sort_by: str = "Price",
300 |     sort_order: str = "Descending",
301 |     page_num: int = 0
302 | ) -> List[Dict[str, Any]]:
303 |     """
304 |     Find AWS EC2 instances based on specified criteria.
305 |     
306 |     Returns:
307 |     - List of instances matching the criteria (5 per page). CloudFix RightSpend pricing is provided when using the flexible cRIs provided by RightSpend (a third-party solution). The benefit of RightSpend is that it 1) eliminates the need for complex forecasting or frequent consultations with engineering about usage fluctuations 2) removes the risk of unused reservations 3) provides 3-yr All Upfront discounts without the need for prepayment.
308 |     """
309 |     global PRICING_DATA
310 |     
311 |     # Ensure we have pricing data
312 |     if PRICING_DATA is None:
313 |         try:
314 |             download_and_cache_pricing_data()
315 |         except Exception as e:
316 |             raise RuntimeError(f"Failed to load pricing data: {str(e)}")
317 |     
318 |     # Get the operation code for the platform
319 |     if filter_platform not in PLATFORM_TO_OP_CODE:
320 |         raise ValueError(f"Invalid platform: {filter_platform}; valid platforms: {list(PLATFORM_TO_OP_CODE.keys())}")
321 |     filter_op_code = PLATFORM_TO_OP_CODE.get(filter_platform, "")
322 | 
323 |     if filter_tenancy not in TENANCIES:
324 |         raise ValueError(f"Invalid tenancy: {filter_tenancy}; valid tenancies: {list(TENANCIES)}")
325 |     
326 |     if filter_pricing_model not in PRICING_MODELS:
327 |         raise ValueError(f"Invalid pricing model: {filter_pricing_model}; valid pricing models: {list(PRICING_MODELS)}")
328 |     
329 |     # Find matching instances
330 |     on_demand_price_offset = 7 * TENANCIES.index(filter_tenancy)
331 |     price_offset = on_demand_price_offset + PRICING_MODELS.index(filter_pricing_model)
332 |     
333 |     matching_instances = []
334 |     
335 |     for family_name, family in PRICING_DATA.items():
336 |         # Filter by family if specified
337 |         if filter_family and family_name != filter_family:
338 |             continue
339 |             
340 |         # Filter by processor if specified
341 |         if filter_processor and filter_processor.lower() not in family.get("Physical Processor", "").lower():
342 |             continue
343 |             
344 |         if family["Clock Speed, GHz"] < filter_min_cpu_ghz:
345 |             continue
346 |                 
347 |         for size_name, size in family["sizes"].items():
348 |             # Filter by size if specified
349 |             if filter_size and size_name != filter_size:
350 |                 continue
351 |                 
352 |             # Check if the instance meets the minimum requirements
353 |             if size.get("vCPU, cores", 0) < filter_min_vcpu:
354 |                 continue
355 |                 
356 |             if size.get("Memory, GB", 0) < filter_min_ram:
357 |                 continue
358 |                 
359 |             if size.get("GPU, cores", 0) < filter_min_gpu:
360 |                 continue
361 |                 
362 |             if size.get("GPU Memory, GB", 0) < filter_min_gpu_memory:
363 |                 continue
364 |                 
365 |             if size.get("Network Performance, Mbps", 0) < filter_min_network_performance:
366 |                 continue
367 |                 
368 |             if size.get("Dedicated EBS Throughput, Mbps", 0) < filter_min_ebs_throughput:
369 |                 continue
370 |                 
371 |             if size.get("Ephemeral Storage, GB", 0) < filter_min_ephemeral_storage:
372 |                 continue
373 |         
374 |             if "operations" not in size:
375 |                 raise ValueError(f"Instance {family_name}.{size_name} does not have operations")
376 |             for op_code, regions in size["operations"].items():
377 |                 if op_code != filter_op_code:
378 |                     continue
379 | 
380 |                 for region, prices in regions.items():
381 |                     if region != filter_region:
382 |                         continue
383 | 
384 |                     price_arr = prices.split(",")
385 |                     if len(price_arr) < price_offset:
386 |                         continue
387 | 
388 |                     price = price_arr[price_offset]
389 |                     if price == "":
390 |                         continue
391 | 
392 |                     on_demand_price = price_arr[on_demand_price_offset]
393 | 
394 |                     instance = {
395 |                         "Instance Type": f"{family_name}.{size_name}",
396 |                         "Region": filter_region,
397 |                         "Platform": filter_platform,
398 |                         "Tenancy": filter_tenancy,
399 |                         "Pricing Model": filter_pricing_model,
400 |                         "Effective Price per hour, USD": float(price),
401 |                         "Effective Price per month, USD": round(float(price) * 24 * 365 / 12, 2),
402 |                         "Effective Price per year, USD": round(float(price) * 24 * 365, 2)
403 |                     }
404 |                     if filter_pricing_model == "1-yr Partial Upfront":
405 |                         instance["Upfront Payment, USD"] = round(float(price) * 24 * 365 / 2, 2)
406 |                     elif filter_pricing_model == "1-yr All Upfront":
407 |                         instance["Upfront Payment, USD"] = round(float(price) * 24 * 365, 2)
408 |                     elif filter_pricing_model == "3-yr Partial Upfront":
409 |                         instance["Upfront Payment, USD"] = round(float(price) * 24 * 365 * 3 / 2, 2)
410 |                     elif filter_pricing_model == "3-yr All Upfront":
411 |                         instance["Upfront Payment, USD"] = round(float(price) * 24 * 365 * 3, 2)
412 |                         
413 |                     if on_demand_price and on_demand_price != price:
414 |                         instance["On-Demand Price per hour, USD"] = float(on_demand_price)
415 |                         instance["Discount Percentage"] = (1 - (float(price) / float(on_demand_price))) * 100
416 | 
417 |                     if len(price_arr) > on_demand_price_offset+6:
418 |                         cloudfix_rightspend_price = price_arr[on_demand_price_offset+6]
419 |                         if cloudfix_rightspend_price != "":
420 |                             instance["CloudFix RightSpend Price per hour, USD"] = float(cloudfix_rightspend_price)
421 |                             instance["CloudFix RightSpend Price per month, USD"] = round(float(cloudfix_rightspend_price) * 24 * 365 / 12, 2)
422 |                             instance["CloudFix RightSpend Price per year, USD"] = round(float(cloudfix_rightspend_price) * 24 * 365, 2)
423 |                             instance["CloudFix RightSpend Upfront Payment, USD"] = round(float(cloudfix_rightspend_price) * 24 * 7, 2)
424 |                     instance.update({key: value for key, value in family.items() if key != "sizes"})
425 |                     instance.update({key: value for key, value in size.items() if key != "operations"})
426 |             
427 |                     matching_instances.append(instance)
428 |     
429 |     # Filter by max price if specified
430 |     if filter_max_price_per_hour is not None:
431 |         matching_instances = [i for i in matching_instances if i["Effective Price per hour, USD"] <= filter_max_price_per_hour]
432 | 
433 |     # Define sort key mapping
434 |     sort_key_map = {
435 |         "Price": "Effective Price per hour, USD",
436 |         "Clock Speed GHz": "Clock Speed, GHz",
437 |         "vCPU cores": "vCPU, cores",
438 |         "Memory GB": "Memory, GB",
439 |         "Ephemeral Storage GB": "Ephemeral Storage, GB",
440 |         "Network Performance Mbps": "Network Performance, Mbps",
441 |         "Dedicated EBS Throughput Mbps": "Dedicated EBS Throughput, Mbps",
442 |         "GPU cores": "GPU, cores",
443 |         "GPU Memory GB": "GPU Memory, GB"
444 |     }
445 | 
446 |     # Sort by selected field
447 |     if sort_by not in sort_key_map:
448 |         raise ValueError(f"Invalid sort by: {sort_by}; valid sort by: {list(sort_key_map.keys())}")
449 |     sort_key = sort_key_map.get(sort_by, "Effective Price per hour, USD")
450 |     
451 |     # Two-pass sorting approach:
452 |     # 1. First sort by price (ascending) - this will be our secondary sort
453 |     matching_instances.sort(key=lambda x: x.get("Effective Price per hour, USD", 0))
454 |     
455 |     # 2. Then sort by the primary field with the specified direction
456 |     # Since Python's sort is stable, when primary fields are equal, the price order is preserved
457 |     matching_instances.sort(
458 |         key=lambda x: x.get(sort_key, 0),
459 |         reverse=(sort_order == "Descending")
460 |     )
461 |     
462 |     # Calculate pagination
463 |     items_per_page = 5
464 |     start_idx = page_num * items_per_page
465 |     end_idx = start_idx + items_per_page
466 |     
467 |     # Return the requested page
468 |     return matching_instances[start_idx:end_idx]
469 | 
470 | def handle_tools_call(params: Dict) -> Dict:
471 |     """Handle MCP tools/call request"""
472 |     try:
473 |         tool_name = params.get("name")
474 |         arguments = params.get("arguments", {})
475 |         
476 |         if tool_name == "ec2_instances_pricing":
477 |             result = ec2_instances_pricing(**arguments)
478 |             return {
479 |                 "content": [
480 |                     {
481 |                         "type": "text",
482 |                         "text": json.dumps(result, indent=2)
483 |                     }
484 |                 ],
485 |                 "isError": False
486 |             }
487 |         else:
488 |             return {
489 |                 "content": [
490 |                     {
491 |                         "type": "text",
492 |                         "text": f"Unknown tool: {tool_name}"
493 |                     }
494 |                 ],
495 |                 "isError": True
496 |             }
497 |     except Exception as e:
498 |         return {
499 |             "content": [
500 |                 {
501 |                     "type": "text",
502 |                     "text": f"Error executing tool: {str(e)}"
503 |                 }
504 |             ],
505 |             "isError": True
506 |         }
507 | 
508 | def handle_resources_list(params: Dict) -> Dict:
509 |     """Handle MCP resources/list request"""
510 |     return {
511 |         "resources": []
512 |     }
513 | 
514 | def handle_resources_read(params: Dict) -> Dict:
515 |     """Handle MCP resources/read request"""
516 |     return {
517 |         "contents": []
518 |     }
519 | 
520 | def handle_prompts_list(params: Dict) -> Dict:
521 |     """Handle MCP prompts/list request"""
522 |     return {
523 |         "prompts": []
524 |     }
525 | 
526 | def handle_prompts_get(params: Dict) -> Dict:
527 |     """Handle MCP prompts/get request"""
528 |     return {
529 |         "description": "",
530 |         "messages": []
531 |     }
532 | 
533 | def process_mcp_request(request: Dict) -> Dict:
534 |     """
535 |     Process MCP protocol request and return response
536 |     
537 |     Args:
538 |         request: JSON-RPC request object
539 |     
540 |     Returns:
541 |         JSON-RPC response object
542 |     """
543 |     try:
544 |         # Extract request details
545 |         jsonrpc = request.get("jsonrpc")
546 |         request_id = request.get("id")
547 |         method = request.get("method")
548 |         params = request.get("params", {})
549 |         
550 |         # Validate JSON-RPC version
551 |         if jsonrpc != "2.0":
552 |             return create_error_response(
553 |                 request_id, 
554 |                 -32600, 
555 |                 "Invalid Request: jsonrpc must be '2.0'"
556 |             )
557 |         
558 |         # Handle different MCP methods
559 |         if method == "initialize":
560 |             result = handle_initialize(params)
561 |             return create_success_response(request_id, result)
562 |         
563 |         elif method == "ping":
564 |             result = handle_ping()
565 |             return create_success_response(request_id, result)
566 |         
567 |         elif method == "tools/list":
568 |             result = handle_tools_list(params)
569 |             return create_success_response(request_id, result)
570 |         
571 |         elif method == "tools/call":
572 |             result = handle_tools_call(params)
573 |             return create_success_response(request_id, result)
574 |         
575 |         elif method == "resources/list":
576 |             result = handle_resources_list(params)
577 |             return create_success_response(request_id, result)
578 |         
579 |         elif method == "resources/read":
580 |             result = handle_resources_read(params)
581 |             return create_success_response(request_id, result)
582 |         
583 |         elif method == "prompts/list":
584 |             result = handle_prompts_list(params)
585 |             return create_success_response(request_id, result)
586 |         
587 |         elif method == "prompts/get":
588 |             result = handle_prompts_get(params)
589 |             return create_success_response(request_id, result)
590 |         
591 |         elif method == "notifications/initialized":
592 |             # This is a notification, no response needed
593 |             return None
594 |         
595 |         else:
596 |             return create_error_response(
597 |                 request_id,
598 |                 -32601,
599 |                 f"Method not found: {method}"
600 |             )
601 |     
602 |     except json.JSONDecodeError as e:
603 |         return create_error_response(
604 |             None,
605 |             -32700,
606 |             f"Parse error: {str(e)}"
607 |         )
608 |     
609 |     except Exception as e:
610 |         log_error(f"Unexpected error: {str(e)}")
611 |         log_error(traceback.format_exc())
612 |         return create_error_response(
613 |             request_id if 'request_id' in locals() else None,
614 |             -32603,
615 |             f"Internal error: {str(e)}"
616 |         )
617 | 
618 | def lambda_handler(event: Dict, context: Any) -> Dict:
619 |     """
620 |     AWS Lambda handler for HTTP requests via Function URL
621 |     
622 |     Args:
623 |         event: Lambda event containing HTTP request details
624 |         context: Lambda context object
625 |     
626 |     Returns:
627 |         HTTP response with JSON-RPC response
628 |     """
629 |     try:
630 |         # Handle CORS preflight requests
631 |         if event.get("requestContext", {}).get("http", {}).get("method") == "OPTIONS":
632 |             return {
633 |                 "statusCode": 200,
634 |                 "headers": {
635 |                     "Content-Type": "application/json",
636 |                     "Access-Control-Allow-Origin": "*",
637 |                     "Access-Control-Allow-Headers": "*",
638 |                     "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
639 |                     "Access-Control-Allow-Credentials": "true"
640 |                 },
641 |                 "body": ""
642 |             }
643 |         
644 |         # Get the HTTP method and body
645 |         http_method = event.get("requestContext", {}).get("http", {}).get("method", "POST")
646 |         body = event.get("body", "{}")
647 |         
648 |         # Parse the request body
649 |         if isinstance(body, str):
650 |             try:
651 |                 request = json.loads(body)
652 |             except json.JSONDecodeError:
653 |                 return {
654 |                     "statusCode": 400,
655 |                     "headers": {
656 |                         "Content-Type": "application/json",
657 |                         "Access-Control-Allow-Origin": "*"
658 |                     },
659 |                     "body": json.dumps({
660 |                         "error": "Invalid JSON in request body"
661 |                     })
662 |                 }
663 |         else:
664 |             request = body
665 |         
666 |         # Process the MCP request
667 |         response = process_mcp_request(request)
668 |         
669 |         # Return HTTP response
670 |         if response is None:
671 |             # For notifications, return empty success
672 |             return {
673 |                 "statusCode": 200,
674 |                 "headers": {
675 |                     "Content-Type": "application/json",
676 |                     "Access-Control-Allow-Origin": "*"
677 |                 },
678 |                 "body": ""
679 |             }
680 |         else:
681 |             return {
682 |                 "statusCode": 200,
683 |                 "headers": {
684 |                     "Content-Type": "application/json",
685 |                     "Access-Control-Allow-Origin": "*"
686 |                 },
687 |                 "body": json.dumps(response)
688 |             }
689 |     
690 |     except Exception as e:
691 |         log_error(f"Unexpected error in lambda_handler: {str(e)}")
692 |         log_error(traceback.format_exc())
693 |         return {
694 |             "statusCode": 500,
695 |             "headers": {
696 |                 "Content-Type": "application/json",
697 |                 "Access-Control-Allow-Origin": "*"
698 |             },
699 |             "body": json.dumps({
700 |                 "error": "Internal server error",
701 |                 "message": str(e)
702 |             })
703 |         } 
```