awslabs.cost-explorer-mcp-server 0.0.5__py3-none-any.whl → 0.0.7__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.
- awslabs/cost_explorer_mcp_server/comparison_handler.py +719 -0
- awslabs/cost_explorer_mcp_server/constants.py +106 -0
- awslabs/cost_explorer_mcp_server/cost_usage_handler.py +385 -0
- awslabs/cost_explorer_mcp_server/forecasting_handler.py +234 -0
- awslabs/cost_explorer_mcp_server/helpers.py +381 -59
- awslabs/cost_explorer_mcp_server/metadata_handler.py +88 -0
- awslabs/cost_explorer_mcp_server/models.py +70 -0
- awslabs/cost_explorer_mcp_server/server.py +51 -476
- awslabs/cost_explorer_mcp_server/utility_handler.py +50 -0
- {awslabs_cost_explorer_mcp_server-0.0.5.dist-info → awslabs_cost_explorer_mcp_server-0.0.7.dist-info}/METADATA +45 -16
- awslabs_cost_explorer_mcp_server-0.0.7.dist-info/RECORD +17 -0
- awslabs_cost_explorer_mcp_server-0.0.5.dist-info/RECORD +0 -10
- {awslabs_cost_explorer_mcp_server-0.0.5.dist-info → awslabs_cost_explorer_mcp_server-0.0.7.dist-info}/WHEEL +0 -0
- {awslabs_cost_explorer_mcp_server-0.0.5.dist-info → awslabs_cost_explorer_mcp_server-0.0.7.dist-info}/entry_points.txt +0 -0
- {awslabs_cost_explorer_mcp_server-0.0.5.dist-info → awslabs_cost_explorer_mcp_server-0.0.7.dist-info}/licenses/LICENSE +0 -0
- {awslabs_cost_explorer_mcp_server-0.0.5.dist-info → awslabs_cost_explorer_mcp_server-0.0.7.dist-info}/licenses/NOTICE +0 -0
|
@@ -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)}'}
|