Skip to main content
Glama

Azure Pricing MCP Server

by msftnadavbh
azure_pricing_server.py•60.2 kB
#!/usr/bin/env python3 """ Azure Pricing MCP Server A Model Context Protocol server that provides tools for querying Azure retail pricing. """ import asyncio import json import logging from typing import Any, Dict, List, Optional, Union from urllib.parse import urlencode, quote import aiohttp from mcp.server import NotificationOptions, Server from mcp.server.models import InitializationOptions from mcp.server.session import ServerSession from mcp.server.stdio import stdio_server from mcp.types import ( CallToolRequest, CallToolResult, ListToolsRequest, ListToolsResult, Tool, TextContent, ) from pydantic import BaseModel, Field # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Azure Retail Prices API configuration AZURE_PRICING_BASE_URL = "https://prices.azure.com/api/retail/prices" DEFAULT_API_VERSION = "2023-01-01-preview" MAX_RESULTS_PER_REQUEST = 1000 class AzurePricingServer: """Azure Pricing MCP Server implementation.""" def __init__(self): self.session: Optional[aiohttp.ClientSession] = None async def __aenter__(self): """Async context manager entry.""" self.session = aiohttp.ClientSession() return self async def __aexit__(self, exc_type, exc_val, exc_tb): """Async context manager exit.""" if self.session: await self.session.close() async def _make_request(self, url: str, params: Dict[str, Any] = None, max_retries: int = 3) -> Dict[str, Any]: """Make HTTP request to Azure Pricing API with retry logic for rate limiting.""" if not self.session: raise RuntimeError("HTTP session not initialized") last_exception = None for attempt in range(max_retries + 1): # 0, 1, 2, 3 (4 total attempts) try: async with self.session.get(url, params=params) as response: if response.status == 429: # Too Many Requests if attempt < max_retries: wait_time = 5 * (attempt + 1) # 5, 10, 15 seconds logger.warning(f"Rate limited (429). Retrying in {wait_time} seconds... (attempt {attempt + 1}/{max_retries + 1})") await asyncio.sleep(wait_time) continue else: # Last attempt failed, raise the error response.raise_for_status() response.raise_for_status() return await response.json() except aiohttp.ClientResponseError as e: if e.status == 429 and attempt < max_retries: wait_time = 5 * (attempt + 1) logger.warning(f"Rate limited (429). Retrying in {wait_time} seconds... (attempt {attempt + 1}/{max_retries + 1})") await asyncio.sleep(wait_time) last_exception = e continue else: logger.error(f"HTTP request failed: {e}") raise except aiohttp.ClientError as e: logger.error(f"HTTP request failed: {e}") raise except Exception as e: logger.error(f"Unexpected error during request: {e}") raise # If we get here, all retries failed if last_exception: raise last_exception async def search_azure_prices( self, service_name: Optional[str] = None, service_family: Optional[str] = None, region: Optional[str] = None, sku_name: Optional[str] = None, price_type: Optional[str] = None, currency_code: str = "USD", limit: int = 50, discount_percentage: Optional[float] = None, validate_sku: bool = True ) -> Dict[str, Any]: """Search Azure retail prices with various filters, SKU validation, and discount support.""" # Build filter conditions filter_conditions = [] if service_name: filter_conditions.append(f"serviceName eq '{service_name}'") if service_family: filter_conditions.append(f"serviceFamily eq '{service_family}'") if region: filter_conditions.append(f"armRegionName eq '{region}'") if sku_name: filter_conditions.append(f"contains(skuName, '{sku_name}')") if price_type: filter_conditions.append(f"priceType eq '{price_type}'") # Construct query parameters params = { "api-version": DEFAULT_API_VERSION, "currencyCode": currency_code } if filter_conditions: params["$filter"] = " and ".join(filter_conditions) # Limit results if limit < MAX_RESULTS_PER_REQUEST: params["$top"] = str(limit) # Make request data = await self._make_request(AZURE_PRICING_BASE_URL, params) # Process results items = data.get("Items", []) # If we have more results than requested, truncate if len(items) > limit: items = items[:limit] # SKU validation and clarification validation_info = {} if validate_sku and sku_name and not items: validation_info = await self._validate_and_suggest_skus(service_name, sku_name, currency_code) elif validate_sku and sku_name and isinstance(items, list) and len(items) > 10: # Too many results - provide clarification validation_info["clarification"] = { "message": f"Found {len(items)} SKUs matching '{sku_name}'. Consider being more specific.", "suggestions": [item.get("skuName") for item in items[:5] if item and item.get("skuName")] } # Apply discount if provided if discount_percentage is not None and discount_percentage > 0 and isinstance(items, list): items = self._apply_discount_to_items(items, discount_percentage) result = { "items": items, "count": len(items) if isinstance(items, list) else 0, "has_more": bool(data.get("NextPageLink")), "currency": currency_code, "filters_applied": filter_conditions } # Add discount info if applied if discount_percentage is not None and discount_percentage > 0: result["discount_applied"] = { "percentage": discount_percentage, "note": "Prices shown are after discount" } # Add validation info if available if validation_info: result.update(validation_info) return result async def _validate_and_suggest_skus( self, service_name: Optional[str], sku_name: str, currency_code: str = "USD" ) -> Dict[str, Any]: """Validate SKU name and suggest alternatives if not found.""" # Try to find similar SKUs suggestions = [] if service_name: # Search for SKUs within the service broad_search = await self.search_azure_prices( service_name=service_name, currency_code=currency_code, limit=100, validate_sku=False # Avoid recursion ) # Find SKUs that partially match sku_lower = sku_name.lower() items = broad_search.get("items", []) if items: # Only process if items exist for item in items: item_sku = item.get("skuName") if not item_sku: # Skip items without SKU names continue item_sku_lower = item_sku.lower() if (sku_lower in item_sku_lower or item_sku_lower in sku_lower or any(word in item_sku_lower for word in sku_lower.split() if word)): suggestions.append({ "sku_name": item_sku, "product_name": item.get("productName", "Unknown"), "price": item.get("retailPrice", 0), "unit": item.get("unitOfMeasure", "Unknown"), "region": item.get("armRegionName", "Unknown") }) # Remove duplicates and limit suggestions seen_skus = set() unique_suggestions = [] for suggestion in suggestions: sku = suggestion["sku_name"] if sku not in seen_skus: seen_skus.add(sku) unique_suggestions.append(suggestion) if len(unique_suggestions) >= 5: break return { "sku_validation": { "original_sku": sku_name, "found": False, "message": f"SKU '{sku_name}' not found" + (f" in service '{service_name}'" if service_name else ""), "suggestions": unique_suggestions } } def _apply_discount_to_items(self, items: List[Dict], discount_percentage: float) -> List[Dict]: """Apply discount percentage to pricing items.""" if not items: return [] discounted_items = [] for item in items: discounted_item = item.copy() # Apply discount to retail price if "retailPrice" in item and item["retailPrice"]: original_price = item["retailPrice"] discounted_price = original_price * (1 - discount_percentage / 100) discounted_item["retailPrice"] = round(discounted_price, 6) discounted_item["originalPrice"] = original_price # Apply discount to savings plans if present if "savingsPlan" in item and item["savingsPlan"] and isinstance(item["savingsPlan"], list): discounted_savings = [] for plan in item["savingsPlan"]: discounted_plan = plan.copy() if "retailPrice" in plan and plan["retailPrice"]: original_plan_price = plan["retailPrice"] discounted_plan_price = original_plan_price * (1 - discount_percentage / 100) discounted_plan["retailPrice"] = round(discounted_plan_price, 6) discounted_plan["originalPrice"] = original_plan_price discounted_savings.append(discounted_plan) discounted_item["savingsPlan"] = discounted_savings discounted_items.append(discounted_item) return discounted_items async def get_customer_discount(self, customer_id: Optional[str] = None) -> Dict[str, Any]: """Get customer discount information. Currently returns 10% default discount for all customers.""" # For now, return a default 10% discount for all customers # In the future, this could be enhanced to query a customer database return { "customer_id": customer_id or "default", "discount_percentage": 10.0, "discount_type": "standard", "description": "Standard customer discount", "valid_until": None, # No expiration for standard discount "applicable_services": "all", # Applies to all Azure services "note": "This is a default discount applied to all customers. Contact sales for enterprise discounts." } async def compare_prices( self, service_name: str, sku_name: Optional[str] = None, regions: Optional[List[str]] = None, currency_code: str = "USD", discount_percentage: Optional[float] = None ) -> Dict[str, Any]: """Compare prices across different regions or SKUs.""" comparisons = [] if regions and isinstance(regions, list): # Compare across regions for region in regions: try: result = await self.search_azure_prices( service_name=service_name, sku_name=sku_name, region=region, currency_code=currency_code, limit=10 ) if result["items"]: # Get the first item for comparison item = result["items"][0] comparisons.append({ "region": region, "sku_name": item.get("skuName"), "retail_price": item.get("retailPrice"), "unit_of_measure": item.get("unitOfMeasure"), "product_name": item.get("productName"), "meter_name": item.get("meterName") }) except Exception as e: logger.warning(f"Failed to get prices for region {region}: {e}") else: # Compare different SKUs within the same service result = await self.search_azure_prices( service_name=service_name, currency_code=currency_code, limit=20 ) # Group by SKU sku_prices = {} items = result.get("items", []) for item in items: sku = item.get("skuName") if sku and sku not in sku_prices: sku_prices[sku] = { "sku_name": sku, "retail_price": item.get("retailPrice"), "unit_of_measure": item.get("unitOfMeasure"), "product_name": item.get("productName"), "region": item.get("armRegionName"), "meter_name": item.get("meterName") } comparisons = list(sku_prices.values()) # Apply discount if provided if discount_percentage is not None and discount_percentage > 0: for comparison in comparisons: if "retail_price" in comparison and comparison["retail_price"]: original_price = comparison["retail_price"] discounted_price = original_price * (1 - discount_percentage / 100) comparison["retail_price"] = round(discounted_price, 6) comparison["original_price"] = original_price # Sort by price comparisons.sort(key=lambda x: x.get("retail_price", 0)) result = { "comparisons": comparisons, "service_name": service_name, "currency": currency_code, "comparison_type": "regions" if regions else "skus" } # Add discount info if applied if discount_percentage is not None and discount_percentage > 0: result["discount_applied"] = { "percentage": discount_percentage, "note": "Prices shown are after discount" } return result async def estimate_costs( self, service_name: str, sku_name: str, region: str, hours_per_month: float = 730, # Default to full month currency_code: str = "USD", discount_percentage: Optional[float] = None ) -> Dict[str, Any]: """Estimate monthly costs based on usage.""" # Get pricing information result = await self.search_azure_prices( service_name=service_name, sku_name=sku_name, region=region, currency_code=currency_code, limit=5 ) if not result["items"]: return { "error": f"No pricing found for {sku_name} in {region}", "service_name": service_name, "sku_name": sku_name, "region": region } item = result["items"][0] hourly_rate = item.get("retailPrice", 0) # Apply discount if provided if discount_percentage is not None and discount_percentage > 0: original_hourly_rate = hourly_rate hourly_rate = hourly_rate * (1 - discount_percentage / 100) # Calculate estimates monthly_cost = hourly_rate * hours_per_month daily_cost = hourly_rate * 24 yearly_cost = monthly_cost * 12 # Check for savings plans savings_plans = item.get("savingsPlan", []) savings_estimates = [] for plan in savings_plans: plan_hourly = plan.get("retailPrice", 0) # Apply discount to savings plan prices too if discount_percentage is not None and discount_percentage > 0: original_plan_hourly = plan_hourly plan_hourly = plan_hourly * (1 - discount_percentage / 100) plan_monthly = plan_hourly * hours_per_month plan_yearly = plan_monthly * 12 savings_percent = ((hourly_rate - plan_hourly) / hourly_rate) * 100 if hourly_rate > 0 else 0 plan_data = { "term": plan.get("term"), "hourly_rate": round(plan_hourly, 6), "monthly_cost": round(plan_monthly, 2), "yearly_cost": round(plan_yearly, 2), "savings_percent": round(savings_percent, 2), "annual_savings": round((yearly_cost - plan_yearly), 2) } # Add original prices if discount was applied if discount_percentage is not None and discount_percentage > 0: plan_data["original_hourly_rate"] = original_plan_hourly plan_data["original_monthly_cost"] = round(original_plan_hourly * hours_per_month, 2) plan_data["original_yearly_cost"] = round(original_plan_hourly * hours_per_month * 12, 2) savings_estimates.append(plan_data) result = { "service_name": service_name, "sku_name": item.get("skuName"), "region": region, "product_name": item.get("productName"), "unit_of_measure": item.get("unitOfMeasure"), "currency": currency_code, "on_demand_pricing": { "hourly_rate": round(hourly_rate, 6), "daily_cost": round(daily_cost, 2), "monthly_cost": round(monthly_cost, 2), "yearly_cost": round(yearly_cost, 2) }, "usage_assumptions": { "hours_per_month": hours_per_month, "hours_per_day": round(hours_per_month / 30.44, 2) # Average days per month }, "savings_plans": savings_estimates } # Add discount info and original prices if discount was applied if discount_percentage is not None and discount_percentage > 0: result["discount_applied"] = { "percentage": discount_percentage, "note": "All prices shown are after discount" } result["on_demand_pricing"]["original_hourly_rate"] = original_hourly_rate result["on_demand_pricing"]["original_daily_cost"] = round(original_hourly_rate * 24, 2) result["on_demand_pricing"]["original_monthly_cost"] = round(original_hourly_rate * hours_per_month, 2) result["on_demand_pricing"]["original_yearly_cost"] = round(original_hourly_rate * hours_per_month * 12, 2) return result async def discover_skus( self, service_name: str, region: Optional[str] = None, price_type: str = "Consumption", limit: int = 100 ) -> Dict[str, Any]: """Discover available SKUs for a specific Azure service.""" # Build filter conditions filter_conditions = [f"serviceName eq '{service_name}'"] if region: filter_conditions.append(f"armRegionName eq '{region}'") if price_type: filter_conditions.append(f"priceType eq '{price_type}'") # Construct query parameters params = { "api-version": DEFAULT_API_VERSION, "currencyCode": "USD" } if filter_conditions: params["$filter"] = " and ".join(filter_conditions) # Limit results if limit < MAX_RESULTS_PER_REQUEST: params["$top"] = str(limit) # Make request data = await self._make_request(AZURE_PRICING_BASE_URL, params) # Process and deduplicate SKUs skus = {} items = data.get("Items", []) for item in items: sku_name = item.get("skuName") arm_sku_name = item.get("armSkuName") product_name = item.get("productName") region = item.get("armRegionName") price = item.get("retailPrice", 0) unit = item.get("unitOfMeasure") meter_name = item.get("meterName") if sku_name and sku_name not in skus: skus[sku_name] = { "sku_name": sku_name, "arm_sku_name": arm_sku_name, "product_name": product_name, "sample_price": price, "unit_of_measure": unit, "meter_name": meter_name, "sample_region": region, "available_regions": [region] if region else [] } elif sku_name and region and region not in skus[sku_name]["available_regions"]: # Add region to existing SKU skus[sku_name]["available_regions"].append(region) # Convert to list and sort by SKU name sku_list = list(skus.values()) sku_list.sort(key=lambda x: x["sku_name"]) return { "service_name": service_name, "skus": sku_list, "total_skus": len(sku_list), "price_type": price_type, "region_filter": region } async def search_azure_prices_with_fuzzy_matching( self, service_name: Optional[str] = None, service_family: Optional[str] = None, region: Optional[str] = None, sku_name: Optional[str] = None, price_type: Optional[str] = None, currency_code: str = "USD", limit: int = 50, suggest_alternatives: bool = True ) -> Dict[str, Any]: """ Search Azure retail prices with fuzzy matching and suggestions. If exact matches aren't found, suggests similar services. """ # First try exact search exact_result = await self.search_azure_prices( service_name=service_name, service_family=service_family, region=region, sku_name=sku_name, price_type=price_type, currency_code=currency_code, limit=limit ) # If we got results, return them if exact_result["items"]: return exact_result # If no results and suggest_alternatives is True, try fuzzy matching if suggest_alternatives and (service_name or service_family): return await self._find_similar_services( service_name=service_name, service_family=service_family, currency_code=currency_code, limit=limit ) return exact_result async def _find_similar_services( self, service_name: Optional[str] = None, service_family: Optional[str] = None, currency_code: str = "USD", limit: int = 50 ) -> Dict[str, Any]: """Find services with similar names or suggest alternatives.""" # Common service name mappings service_mappings = { # User input -> Correct Azure service name "app service": "Azure App Service", "web app": "Azure App Service", "web apps": "Azure App Service", "app services": "Azure App Service", "websites": "Azure App Service", "web service": "Azure App Service", "virtual machine": "Virtual Machines", "vm": "Virtual Machines", "vms": "Virtual Machines", "compute": "Virtual Machines", "storage": "Storage", "blob": "Storage", "blob storage": "Storage", "file storage": "Storage", "disk": "Storage", "sql": "Azure SQL Database", "sql database": "Azure SQL Database", "database": "Azure SQL Database", "sql server": "Azure SQL Database", "cosmos": "Azure Cosmos DB", "cosmosdb": "Azure Cosmos DB", "cosmos db": "Azure Cosmos DB", "document db": "Azure Cosmos DB", "kubernetes": "Azure Kubernetes Service", "aks": "Azure Kubernetes Service", "k8s": "Azure Kubernetes Service", "container service": "Azure Kubernetes Service", "functions": "Azure Functions", "function app": "Azure Functions", "serverless": "Azure Functions", "redis": "Azure Cache for Redis", "cache": "Azure Cache for Redis", "ai": "Azure AI services", "cognitive": "Azure AI services", "cognitive services": "Azure AI services", "openai": "Azure OpenAI", "networking": "Virtual Network", "network": "Virtual Network", "vnet": "Virtual Network", "load balancer": "Load Balancer", "lb": "Load Balancer", "application gateway": "Application Gateway", "app gateway": "Application Gateway", } suggestions = [] search_term = service_name.lower() if service_name else "" # Try exact mapping first if search_term in service_mappings: correct_name = service_mappings[search_term] result = await self.search_azure_prices( service_name=correct_name, currency_code=currency_code, limit=limit ) if result["items"]: result["suggestion_used"] = correct_name result["original_search"] = service_name result["match_type"] = "exact_mapping" return result # Try partial matching for common terms partial_matches = [] for user_term, azure_service in service_mappings.items(): if search_term in user_term or user_term in search_term: partial_matches.append(azure_service) # Remove duplicates and try each match for azure_service in list(set(partial_matches)): result = await self.search_azure_prices( service_name=azure_service, currency_code=currency_code, limit=5 ) if result["items"]: suggestions.append({ "service_name": azure_service, "match_reason": f"Partial match for '{service_name}'", "sample_items": result["items"][:3] }) # If still no matches, do a broad search and look for similar services if not suggestions: broad_result = await self.search_azure_prices( service_family=service_family, currency_code=currency_code, limit=100 ) # Find services that contain the search term matching_services = set() for item in broad_result.get("items", []): service = item.get("serviceName", "") product = item.get("productName", "") if (search_term in service.lower() or search_term in product.lower() or any(word in service.lower() for word in search_term.split())): matching_services.add(service) # Create suggestions from found services for service in list(matching_services)[:5]: # Limit to top 5 service_result = await self.search_azure_prices( service_name=service, currency_code=currency_code, limit=3 ) if service_result["items"]: suggestions.append({ "service_name": service, "match_reason": f"Contains '{search_term}'", "sample_items": service_result["items"][:2] }) return { "items": [], "count": 0, "has_more": False, "currency": currency_code, "original_search": service_name or service_family, "suggestions": suggestions, "match_type": "suggestions_only" } async def discover_service_skus( self, service_hint: str, region: Optional[str] = None, currency_code: str = "USD", limit: int = 30 ) -> Dict[str, Any]: """ Discover SKUs for a service with intelligent service name matching. Args: service_hint: User's description of the service (e.g., "app service", "web app") region: Optional specific region to filter by currency_code: Currency for pricing limit: Maximum number of results """ # Use fuzzy matching to find the right service result = await self.search_azure_prices_with_fuzzy_matching( service_name=service_hint, region=region, currency_code=currency_code, limit=limit ) # If we found exact matches, process SKUs if result["items"]: skus = {} service_used = result.get("suggestion_used", service_hint) for item in result["items"]: sku_name = item.get("skuName", "Unknown") arm_sku = item.get("armSkuName", "Unknown") product = item.get("productName", "Unknown") price = item.get("retailPrice", 0) unit = item.get("unitOfMeasure", "Unknown") item_region = item.get("armRegionName", "Unknown") if sku_name not in skus: skus[sku_name] = { "sku_name": sku_name, "arm_sku_name": arm_sku, "product_name": product, "prices": [], "regions": set() } skus[sku_name]["prices"].append({ "price": price, "unit": unit, "region": item_region }) skus[sku_name]["regions"].add(item_region) # Convert sets to lists for JSON serialization for sku_data in skus.values(): sku_data["regions"] = list(sku_data["regions"]) # Keep only the cheapest price for summary - handle empty sequences valid_prices = [p["price"] for p in sku_data["prices"] if p["price"] > 0] if valid_prices: sku_data["min_price"] = min(valid_prices) else: # If no valid prices > 0, use the first price (even if 0) or default to 0 sku_data["min_price"] = sku_data["prices"][0]["price"] if sku_data["prices"] else 0 sku_data["sample_unit"] = sku_data["prices"][0]["unit"] if sku_data["prices"] else "Unknown" return { "service_found": service_used, "original_search": service_hint, "skus": skus, "total_skus": len(skus), "currency": currency_code, "match_type": result.get("match_type", "exact") } # If no exact matches, return suggestions return { "service_found": None, "original_search": service_hint, "skus": {}, "total_skus": 0, "currency": currency_code, "suggestions": result.get("suggestions", []), "match_type": "no_match" } # Create the MCP server server = Server("azure-pricing") # Global server instance pricing_server = AzurePricingServer() @server.list_tools() async def handle_list_tools() -> List[Tool]: """List available tools.""" return [ Tool( name="azure_price_search", description="Search Azure retail prices with various filters", inputSchema={ "type": "object", "properties": { "service_name": { "type": "string", "description": "Azure service name (e.g., 'Virtual Machines', 'Storage')" }, "service_family": { "type": "string", "description": "Service family (e.g., 'Compute', 'Storage', 'Networking')" }, "region": { "type": "string", "description": "Azure region (e.g., 'eastus', 'westeurope')" }, "sku_name": { "type": "string", "description": "SKU name to search for (partial matches supported)" }, "price_type": { "type": "string", "description": "Price type: 'Consumption', 'Reservation', or 'DevTestConsumption'" }, "currency_code": { "type": "string", "description": "Currency code (default: USD)", "default": "USD" }, "limit": { "type": "integer", "description": "Maximum number of results (default: 50)", "default": 50 }, "discount_percentage": { "type": "number", "description": "Discount percentage to apply to prices (e.g., 10 for 10% discount)" }, "validate_sku": { "type": "boolean", "description": "Whether to validate SKU names and provide suggestions (default: true)", "default": True } } } ), Tool( name="azure_price_compare", description="Compare Azure prices across regions or SKUs", inputSchema={ "type": "object", "properties": { "service_name": { "type": "string", "description": "Azure service name to compare" }, "sku_name": { "type": "string", "description": "Specific SKU to compare (optional)" }, "regions": { "type": "array", "items": {"type": "string"}, "description": "List of regions to compare (if not provided, compares SKUs)" }, "currency_code": { "type": "string", "description": "Currency code (default: USD)", "default": "USD" }, "discount_percentage": { "type": "number", "description": "Discount percentage to apply to prices (e.g., 10 for 10% discount)" } }, "required": ["service_name"] } ), Tool( name="azure_cost_estimate", description="Estimate Azure costs based on usage patterns", inputSchema={ "type": "object", "properties": { "service_name": { "type": "string", "description": "Azure service name" }, "sku_name": { "type": "string", "description": "SKU name" }, "region": { "type": "string", "description": "Azure region" }, "hours_per_month": { "type": "number", "description": "Expected hours of usage per month (default: 730 for full month)", "default": 730 }, "currency_code": { "type": "string", "description": "Currency code (default: USD)", "default": "USD" }, "discount_percentage": { "type": "number", "description": "Discount percentage to apply to prices (e.g., 10 for 10% discount)" } }, "required": ["service_name", "sku_name", "region"] } ), Tool( name="azure_discover_skus", description="Discover available SKUs for a specific Azure service", inputSchema={ "type": "object", "properties": { "service_name": { "type": "string", "description": "Azure service name" }, "region": { "type": "string", "description": "Azure region (optional)" }, "price_type": { "type": "string", "description": "Price type (default: 'Consumption')", "default": "Consumption" }, "limit": { "type": "integer", "description": "Maximum number of SKUs to return (default: 100)", "default": 100 } }, "required": ["service_name"] } ), Tool( name="azure_sku_discovery", description="Discover available SKUs for Azure services with intelligent name matching", inputSchema={ "type": "object", "properties": { "service_hint": { "type": "string", "description": "Service name or description (e.g., 'app service', 'web app', 'vm', 'storage'). Supports fuzzy matching." }, "region": { "type": "string", "description": "Optional Azure region to filter results" }, "currency_code": { "type": "string", "description": "Currency code (default: USD)", "default": "USD" }, "limit": { "type": "integer", "description": "Maximum number of results (default: 30)", "default": 30 } }, "required": ["service_hint"] } ), Tool( name="get_customer_discount", description="Get customer discount information. Returns default 10% discount for all customers.", inputSchema={ "type": "object", "properties": { "customer_id": { "type": "string", "description": "Customer ID (optional, defaults to 'default' customer)" } } } ) ] @server.call_tool() async def handle_call_tool(name: str, arguments: dict) -> list: """Handle tool calls.""" try: async with pricing_server: if name == "azure_price_search": # Always get customer discount and apply it customer_discount = await pricing_server.get_customer_discount() discount_percentage = customer_discount["discount_percentage"] # Add discount to arguments if not already specified if "discount_percentage" not in arguments: arguments["discount_percentage"] = discount_percentage result = await pricing_server.search_azure_prices(**arguments) # Format the response if result["items"]: formatted_items = [] for item in result["items"]: formatted_item = { "service": item.get("serviceName"), "product": item.get("productName"), "sku": item.get("skuName"), "region": item.get("armRegionName"), "location": item.get("location"), "discounted_price": item.get("retailPrice"), "unit": item.get("unitOfMeasure"), "type": item.get("type"), "savings_plans": item.get("savingsPlan", []) } # Add original price and savings if discount was applied if "originalPrice" in item: original_price = item["originalPrice"] discounted_price = item["retailPrice"] savings_amount = original_price - discounted_price formatted_item["original_price"] = original_price formatted_item["savings_amount"] = round(savings_amount, 6) formatted_item["savings_percentage"] = round((savings_amount / original_price * 100), 2) if original_price > 0 else 0 formatted_items.append(formatted_item) if result["count"] > 0: response_text = f"Found {result['count']} Azure pricing results:\n\n" # Add discount information if applied if "discount_applied" in result: response_text += f"šŸ’° **Customer Discount Applied: {result['discount_applied']['percentage']}%**\n" response_text += f" {result['discount_applied']['note']}\n\n" # Add SKU validation info if present if "sku_validation" in result: validation = result["sku_validation"] response_text += f"āš ļø SKU Validation: {validation['message']}\n" if validation["suggestions"]: response_text += "šŸ” Suggested SKUs:\n" for suggestion in validation["suggestions"][:3]: response_text += f" • {suggestion['sku_name']}: ${suggestion['price']} per {suggestion['unit']}\n" response_text += "\n" # Add clarification info if present if "clarification" in result: clarification = result["clarification"] response_text += f"ā„¹ļø {clarification['message']}\n" if clarification["suggestions"]: response_text += "Top matches:\n" for suggestion in clarification["suggestions"]: response_text += f" • {suggestion}\n" response_text += "\n" # Add summary of savings if discount was applied if "discount_applied" in result: total_original_cost = sum(item.get("original_price", 0) for item in formatted_items) total_discounted_cost = sum(item.get("discounted_price", 0) for item in formatted_items) total_savings = total_original_cost - total_discounted_cost if total_savings > 0: response_text += f"šŸ’° **Total Savings Summary:**\n" response_text += f" Original Total: ${total_original_cost:.6f}\n" response_text += f" Discounted Total: ${total_discounted_cost:.6f}\n" response_text += f" **You Save: ${total_savings:.6f}**\n\n" response_text += "**Detailed Pricing:**\n" response_text += json.dumps(formatted_items, indent=2) return [ TextContent( type="text", text=response_text ) ] else: # Handle case where items exist but count is 0 (shouldn't happen, but safety) response_text = "No valid pricing results found." return [ TextContent( type="text", text=response_text ) ] else: response_text = "No pricing results found for the specified criteria." # Show discount info even when no results if "discount_applied" in result: response_text += f"\n\nšŸ’° Note: Your {result['discount_applied']['percentage']}% customer discount would have been applied to any results." # Add SKU validation info if present if "sku_validation" in result: validation = result["sku_validation"] response_text += f"\n\nāš ļø {validation['message']}\n" if validation["suggestions"]: response_text += "\nšŸ” Did you mean one of these SKUs?\n" for suggestion in validation["suggestions"][:5]: response_text += f" • {suggestion['sku_name']}: ${suggestion['price']} per {suggestion['unit']}" if suggestion['region']: response_text += f" (in {suggestion['region']})" response_text += "\n" return [ TextContent( type="text", text=response_text ) ] elif name == "azure_price_compare": result = await pricing_server.compare_prices(**arguments) response_text = f"Price comparison for {result['service_name']}:\n\n" # Add discount information if applied if "discount_applied" in result: response_text += f"šŸ’° {result['discount_applied']['percentage']}% discount applied - {result['discount_applied']['note']}\n\n" response_text += json.dumps(result["comparisons"], indent=2) return [ TextContent( type="text", text=response_text ) ] elif name == "azure_cost_estimate": result = await pricing_server.estimate_costs(**arguments) if "error" in result: return [ TextContent( type="text", text=f"Error: {result['error']}" ) ] # Format cost estimate estimate_text = f""" Cost Estimate for {result['service_name']} - {result['sku_name']} Region: {result['region']} Product: {result['product_name']} Unit: {result['unit_of_measure']} Currency: {result['currency']} """ # Add discount information if applied if "discount_applied" in result: estimate_text += f"\nšŸ’° {result['discount_applied']['percentage']}% discount applied - {result['discount_applied']['note']}\n" estimate_text += f""" Usage Assumptions: - Hours per month: {result['usage_assumptions']['hours_per_month']} - Hours per day: {result['usage_assumptions']['hours_per_day']} On-Demand Pricing: - Hourly Rate: ${result['on_demand_pricing']['hourly_rate']} - Daily Cost: ${result['on_demand_pricing']['daily_cost']} - Monthly Cost: ${result['on_demand_pricing']['monthly_cost']} - Yearly Cost: ${result['on_demand_pricing']['yearly_cost']} """ # Add original pricing if discount was applied if "discount_applied" in result and "original_hourly_rate" in result['on_demand_pricing']: estimate_text += f""" Original Pricing (before discount): - Hourly Rate: ${result['on_demand_pricing']['original_hourly_rate']} - Daily Cost: ${result['on_demand_pricing']['original_daily_cost']} - Monthly Cost: ${result['on_demand_pricing']['original_monthly_cost']} - Yearly Cost: ${result['on_demand_pricing']['original_yearly_cost']} """ if result['savings_plans']: estimate_text += "\nSavings Plans Available:\n" for plan in result['savings_plans']: estimate_text += f""" {plan['term']} Term: - Hourly Rate: ${plan['hourly_rate']} - Monthly Cost: ${plan['monthly_cost']} - Yearly Cost: ${plan['yearly_cost']} - Savings: {plan['savings_percent']}% (${plan['annual_savings']} annually) """ # Add original pricing for savings plans if discount was applied if "original_hourly_rate" in plan: estimate_text += f"""- Original Hourly Rate: ${plan['original_hourly_rate']} - Original Monthly Cost: ${plan['original_monthly_cost']} - Original Yearly Cost: ${plan['original_yearly_cost']} """ return [ TextContent( type="text", text=estimate_text ) ] elif name == "azure_discover_skus": result = await pricing_server.discover_skus(**arguments) # Format the response skus = result.get("skus", []) if skus: return [ TextContent( type="text", text=f"Found {result['total_skus']} SKUs for {result['service_name']}:\n\n" + json.dumps(skus, indent=2) ) ] else: return [ TextContent( type="text", text="No SKUs found for the specified service." ) ] elif name == "azure_sku_discovery": result = await pricing_server.discover_service_skus(**arguments) if result["service_found"]: # Format successful SKU discovery service_name = result["service_found"] original_search = result["original_search"] skus = result["skus"] total_skus = result["total_skus"] match_type = result.get("match_type", "exact") response_text = f"SKU Discovery for '{original_search}'" if match_type == "exact_mapping": response_text += f" (mapped to: {service_name})" response_text += f"\n\nFound {total_skus} SKUs for {service_name}:\n\n" # Group SKUs by product products = {} for sku_name, sku_data in skus.items(): product = sku_data["product_name"] if product not in products: products[product] = [] products[product].append((sku_name, sku_data)) for product, product_skus in products.items(): response_text += f"šŸ“¦ {product}:\n" for sku_name, sku_data in sorted(product_skus)[:10]: # Limit to 10 per product min_price = sku_data.get("min_price", 0) unit = sku_data.get("sample_unit", "Unknown") region_count = len(sku_data.get("regions", [])) response_text += f" • {sku_name}\n" response_text += f" Price: ${min_price} per {unit}" if region_count > 1: response_text += f" (available in {region_count} regions)" response_text += "\n" response_text += "\n" return [ TextContent( type="text", text=response_text ) ] else: # Format suggestions when no exact match suggestions = result.get("suggestions", []) original_search = result["original_search"] if suggestions: response_text = f"No exact match found for '{original_search}'\n\n" response_text += "šŸ” Did you mean one of these services?\n\n" for i, suggestion in enumerate(suggestions[:5], 1): service_name = suggestion["service_name"] match_reason = suggestion["match_reason"] sample_items = suggestion["sample_items"] response_text += f"{i}. {service_name}\n" response_text += f" Reason: {match_reason}\n" if sample_items: response_text += " Sample SKUs:\n" for item in sample_items[:3]: sku = item.get("skuName", "Unknown") price = item.get("retailPrice", 0) unit = item.get("unitOfMeasure", "Unknown") response_text += f" • {sku}: ${price} per {unit}\n" response_text += "\n" response_text += "šŸ’” Try using one of the exact service names above." else: response_text = f"No matches found for '{original_search}'\n\n" response_text += "šŸ’” Try using terms like:\n" response_text += "• 'app service' or 'web app' for Azure App Service\n" response_text += "• 'vm' or 'virtual machine' for Virtual Machines\n" response_text += "• 'storage' or 'blob' for Storage services\n" response_text += "• 'sql' or 'database' for SQL Database\n" response_text += "• 'kubernetes' or 'aks' for Azure Kubernetes Service" return [ TextContent( type="text", text=response_text ) ] elif name == "get_customer_discount": result = await pricing_server.get_customer_discount(**arguments) response_text = f"""Customer Discount Information Customer ID: {result['customer_id']} Discount Type: {result['discount_type']} Discount Percentage: {result['discount_percentage']}% Description: {result['description']} Applicable Services: {result['applicable_services']} {result['note']} """ return [ TextContent( type="text", text=response_text ) ] else: return [ TextContent( type="text", text=f"Unknown tool: {name}" ) ] except Exception as e: logger.error(f"Error handling tool call {name}: {e}") return [ TextContent( type="text", text=f"Error: {str(e)}" ) ] async def main(): """Main entry point for the server.""" # Use stdio transport async with stdio_server() as (read_stream, write_stream): initialization_options = server.create_initialization_options( notification_options=NotificationOptions(tools_changed=True) ) await server.run( read_stream, write_stream, initialization_options ) if __name__ == "__main__": asyncio.run(main())

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/msftnadavbh/AzurePricingMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server