# 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 | } ```