awslabs.cost-explorer-mcp-server 0.0.4__py3-none-any.whl → 0.0.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,234 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Cost Explorer MCP server implementation.
16
+
17
+ Forecasting tools for Cost Explorer MCP Server.
18
+ """
19
+
20
+ import os
21
+ import sys
22
+ from awslabs.cost_explorer_mcp_server.constants import (
23
+ VALID_FORECAST_GRANULARITIES,
24
+ VALID_FORECAST_METRICS,
25
+ VALID_PREDICTION_INTERVALS,
26
+ )
27
+ from awslabs.cost_explorer_mcp_server.helpers import (
28
+ get_cost_explorer_client,
29
+ validate_expression,
30
+ validate_forecast_date_range,
31
+ )
32
+ from awslabs.cost_explorer_mcp_server.models import DateRange
33
+ from datetime import datetime, timedelta, timezone
34
+ from loguru import logger
35
+ from mcp.server.fastmcp import Context
36
+ from pydantic import Field
37
+ from typing import Any, Dict, Optional
38
+
39
+
40
+ # Configure Loguru logging
41
+ logger.remove()
42
+ logger.add(sys.stderr, level=os.getenv('FASTMCP_LOG_LEVEL', 'WARNING'))
43
+
44
+
45
+ async def get_cost_forecast(
46
+ ctx: Context,
47
+ date_range: DateRange,
48
+ granularity: str = Field(
49
+ 'MONTHLY',
50
+ description=f'The granularity at which forecast data is aggregated. Valid values are {" and ".join(VALID_FORECAST_GRANULARITIES)}. DAILY forecasts support up to 3 months, MONTHLY forecasts support up to 12 months. If not provided, defaults to MONTHLY.',
51
+ ),
52
+ filter_expression: Optional[Dict[str, Any]] = Field(
53
+ None,
54
+ description='Filter criteria as a Python dictionary to narrow down AWS cost forecasts. Supports filtering by Dimensions (SERVICE, REGION, etc.), Tags, or CostCategories. You can use logical operators (And, Or, Not) for complex filters. Same format as get_cost_and_usage filter_expression.',
55
+ ),
56
+ metric: str = Field(
57
+ 'UNBLENDED_COST',
58
+ description=f'The metric to forecast. Valid values are {",".join(VALID_FORECAST_METRICS)}. Note: UsageQuantity forecasting is not supported by AWS Cost Explorer.',
59
+ ),
60
+ prediction_interval_level: int = Field(
61
+ 80,
62
+ description=f'The confidence level for the forecast prediction interval. Valid values are {" and ".join(map(str, VALID_PREDICTION_INTERVALS))}. Higher values provide wider confidence ranges.',
63
+ ),
64
+ ) -> Dict[str, Any]:
65
+ """Retrieve AWS cost forecasts based on historical usage patterns.
66
+
67
+ This tool generates cost forecasts for future periods using AWS Cost Explorer's machine learning models.
68
+ Forecasts are based on your historical usage patterns and can help with budget planning and cost optimization.
69
+
70
+ Important granularity limits:
71
+ - DAILY forecasts: Maximum 3 months into the future
72
+ - MONTHLY forecasts: Maximum 12 months into the future
73
+
74
+ Note: The forecast start date must be equal to or no later than the current date, while the end date
75
+ must be in the future. AWS automatically uses available historical data to generate forecasts.
76
+ Forecasts return total costs and cannot be grouped by dimensions like services or regions.
77
+
78
+ Example: Get monthly cost forecast for EC2 services for next quarter
79
+ await get_cost_forecast(
80
+ ctx=context,
81
+ date_range={
82
+ "start_date": "2025-06-19", # Today or earlier
83
+ "end_date": "2025-09-30" # Future date
84
+ },
85
+ granularity="MONTHLY",
86
+ filter_expression={
87
+ "Dimensions": {
88
+ "Key": "SERVICE",
89
+ "Values": ["Amazon Elastic Compute Cloud - Compute"],
90
+ "MatchOptions": ["EQUALS"]
91
+ }
92
+ },
93
+ metric="UNBLENDED_COST",
94
+ prediction_interval_level=80
95
+ )
96
+
97
+ Args:
98
+ ctx: MCP context
99
+ date_range: The forecast period dates in YYYY-MM-DD format (start_date <= today, end_date > today)
100
+ granularity: The granularity at which forecast data is aggregated (DAILY, MONTHLY)
101
+ filter_expression: Filter criteria as a Python dictionary
102
+ metric: Cost metric to forecast (UNBLENDED_COST, AMORTIZED_COST, etc.)
103
+ prediction_interval_level: Confidence level for prediction intervals (80 or 95)
104
+
105
+ Returns:
106
+ Dictionary containing forecast data with confidence intervals and metadata
107
+ """
108
+ # Initialize variables at function scope
109
+ forecast_start = date_range.start_date
110
+ forecast_end = date_range.end_date
111
+
112
+ try:
113
+ # Process inputs - simplified granularity validation
114
+ granularity = str(granularity).upper()
115
+
116
+ if granularity not in VALID_FORECAST_GRANULARITIES:
117
+ return {
118
+ 'error': f'Invalid granularity: {granularity}. Valid values for forecasting are {" and ".join(VALID_FORECAST_GRANULARITIES)}.'
119
+ }
120
+
121
+ # Validate forecast date range with granularity-specific limits
122
+ is_valid, error = validate_forecast_date_range(forecast_start, forecast_end, granularity)
123
+ if not is_valid:
124
+ return {'error': error}
125
+
126
+ # Validate prediction interval level
127
+ if prediction_interval_level not in VALID_PREDICTION_INTERVALS:
128
+ return {
129
+ 'error': f'Invalid prediction_interval_level: {prediction_interval_level}. Valid values are {" and ".join(map(str, VALID_PREDICTION_INTERVALS))}.'
130
+ }
131
+
132
+ if metric not in VALID_FORECAST_METRICS:
133
+ return {
134
+ 'error': f'Invalid metric: {metric}. Valid values for forecasting are {", ".join(VALID_FORECAST_METRICS)}.'
135
+ }
136
+
137
+ # Process filter - reuse existing validation
138
+ filter_criteria = filter_expression
139
+
140
+ # Validate filter expression if provided (using historical data for validation)
141
+ if filter_criteria:
142
+ # Use a recent historical period for filter validation
143
+ validation_end = datetime.now(timezone.utc).strftime('%Y-%m-%d')
144
+ validation_start = (datetime.now(timezone.utc) - timedelta(days=30)).strftime(
145
+ '%Y-%m-%d'
146
+ )
147
+
148
+ validation_result = validate_expression(
149
+ filter_criteria, validation_start, validation_end
150
+ )
151
+ if 'error' in validation_result:
152
+ return validation_result
153
+
154
+ # Prepare API call parameters
155
+ forecast_params = {
156
+ 'TimePeriod': {
157
+ 'Start': forecast_start,
158
+ 'End': forecast_end,
159
+ },
160
+ 'Metric': metric,
161
+ 'Granularity': granularity,
162
+ 'PredictionIntervalLevel': prediction_interval_level,
163
+ }
164
+
165
+ # Add filter if provided
166
+ if filter_criteria:
167
+ forecast_params['Filter'] = filter_criteria
168
+
169
+ # Get forecast data
170
+ ce = get_cost_explorer_client()
171
+
172
+ try:
173
+ response = ce.get_cost_forecast(**forecast_params)
174
+ except Exception as e:
175
+ logger.error(f'Error calling Cost Explorer forecast API: {e}')
176
+ return {
177
+ 'error': f'AWS Cost Explorer forecast API error: {str(e)},{str(forecast_start)}'
178
+ }
179
+
180
+ # Process forecast results
181
+ forecast_data = {}
182
+ total_forecast = 0.0
183
+ total_lower_bound = 0.0
184
+ total_upper_bound = 0.0
185
+
186
+ for forecast_result in response.get('ForecastResultsByTime', []):
187
+ period_start = forecast_result['TimePeriod']['Start']
188
+
189
+ # Extract forecast values
190
+ mean_value = float(forecast_result['MeanValue'])
191
+ prediction_interval = (
192
+ forecast_result.get('PredictionIntervalLowerBound', '0'),
193
+ forecast_result.get('PredictionIntervalUpperBound', '0'),
194
+ )
195
+ lower_bound = float(prediction_interval[0])
196
+ upper_bound = float(prediction_interval[1])
197
+
198
+ forecast_data[period_start] = {
199
+ 'predicted_cost': round(mean_value, 2),
200
+ 'confidence_range': {
201
+ 'lower_bound': round(lower_bound, 2),
202
+ 'upper_bound': round(upper_bound, 2),
203
+ },
204
+ }
205
+
206
+ # Accumulate totals
207
+ total_forecast += mean_value
208
+ total_lower_bound += lower_bound
209
+ total_upper_bound += upper_bound
210
+
211
+ # Build response
212
+ result = {
213
+ 'forecast_period': f'{forecast_start} to {forecast_end}',
214
+ 'granularity': granularity,
215
+ 'metric': metric,
216
+ 'confidence_level': f'{prediction_interval_level}%',
217
+ 'predictions': forecast_data,
218
+ 'total_forecast': {
219
+ 'predicted_cost': round(total_forecast, 2),
220
+ 'confidence_range': {
221
+ 'lower_bound': round(total_lower_bound, 2),
222
+ 'upper_bound': round(total_upper_bound, 2),
223
+ },
224
+ },
225
+ 'metadata': {'currency': 'USD'},
226
+ }
227
+
228
+ return result
229
+
230
+ except Exception as e:
231
+ logger.error(
232
+ f'Error generating cost forecast for period {forecast_start} to {forecast_end}: {e}'
233
+ )
234
+ return {'error': f'Error generating cost forecast: {str(e)}'}