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
|
@@ -17,495 +17,70 @@
|
|
|
17
17
|
This server provides tools for analyzing AWS costs and usage data through the AWS Cost Explorer API.
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
-
import json
|
|
21
20
|
import os
|
|
22
|
-
import pandas as pd
|
|
23
21
|
import sys
|
|
24
|
-
from awslabs.cost_explorer_mcp_server.
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
from awslabs.cost_explorer_mcp_server.comparison_handler import (
|
|
23
|
+
get_cost_and_usage_comparisons,
|
|
24
|
+
get_cost_comparison_drivers,
|
|
25
|
+
)
|
|
26
|
+
from awslabs.cost_explorer_mcp_server.cost_usage_handler import get_cost_and_usage
|
|
27
|
+
from awslabs.cost_explorer_mcp_server.forecasting_handler import get_cost_forecast
|
|
28
|
+
from awslabs.cost_explorer_mcp_server.metadata_handler import (
|
|
27
29
|
get_dimension_values,
|
|
28
30
|
get_tag_values,
|
|
29
|
-
validate_date_format,
|
|
30
|
-
validate_date_range,
|
|
31
|
-
validate_expression,
|
|
32
|
-
validate_group_by,
|
|
33
31
|
)
|
|
34
|
-
from
|
|
32
|
+
from awslabs.cost_explorer_mcp_server.utility_handler import get_today_date
|
|
35
33
|
from loguru import logger
|
|
36
|
-
from mcp.server.fastmcp import
|
|
37
|
-
from pydantic import BaseModel, Field, field_validator
|
|
38
|
-
from typing import Any, Dict, Optional, Union
|
|
34
|
+
from mcp.server.fastmcp import FastMCP
|
|
39
35
|
|
|
40
36
|
|
|
41
37
|
# Configure Loguru logging
|
|
42
38
|
logger.remove()
|
|
43
39
|
logger.add(sys.stderr, level=os.getenv('FASTMCP_LOG_LEVEL', 'WARNING'))
|
|
44
40
|
|
|
45
|
-
#
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"""Validate the date range with granularity-specific constraints."""
|
|
77
|
-
is_valid, error = validate_date_range(self.start_date, self.end_date, granularity)
|
|
78
|
-
if not is_valid:
|
|
79
|
-
raise ValueError(error)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
class DimensionKey(BaseModel):
|
|
83
|
-
"""Dimension key model."""
|
|
84
|
-
|
|
85
|
-
dimension_key: str = Field(
|
|
86
|
-
...,
|
|
87
|
-
description='The name of the dimension to retrieve values for. Valid values are AZ, INSTANCE_TYPE, LINKED_ACCOUNT, OPERATION, PURCHASE_TYPE, SERVICE, USAGE_TYPE, PLATFORM, TENANCY, RECORD_TYPE, LEGAL_ENTITY_NAME, INVOICING_ENTITY, DEPLOYMENT_OPTION, DATABASE_ENGINE, CACHE_ENGINE, INSTANCE_TYPE_FAMILY, REGION, BILLING_ENTITY, RESERVATION_ID, SAVINGS_PLANS_TYPE, SAVINGS_PLAN_ARN, OPERATING_SYSTEM.',
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
# Create FastMCP server
|
|
92
|
-
app = FastMCP(title='Cost Explorer MCP Server')
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
@app.tool('get_today_date')
|
|
96
|
-
async def get_today_date(ctx: Context) -> Dict[str, str]:
|
|
97
|
-
"""Retrieve current date information.
|
|
98
|
-
|
|
99
|
-
This tool retrieves the current date in YYYY-MM-DD format and the current month in YYYY-MM format.
|
|
100
|
-
It's useful for calculating relevent date when user ask last N months/days.
|
|
101
|
-
|
|
102
|
-
Args:
|
|
103
|
-
ctx: MCP context
|
|
104
|
-
|
|
105
|
-
Returns:
|
|
106
|
-
Dictionary containing today's date and current month
|
|
107
|
-
"""
|
|
108
|
-
return {
|
|
109
|
-
'today_date': datetime.now().strftime('%Y-%m-%d'),
|
|
110
|
-
'current_month': datetime.now().strftime('%Y-%m'),
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
@app.tool('get_dimension_values')
|
|
115
|
-
async def get_dimension_values_tool(
|
|
116
|
-
ctx: Context, date_range: DateRange, dimension: DimensionKey
|
|
117
|
-
) -> Dict[str, Any]:
|
|
118
|
-
"""Retrieve available dimension values for AWS Cost Explorer.
|
|
119
|
-
|
|
120
|
-
This tool retrieves all available and valid values for a specified dimension (e.g., SERVICE, REGION)
|
|
121
|
-
over a period of time. This is useful for validating filter values or exploring available options
|
|
122
|
-
for cost analysis.
|
|
123
|
-
|
|
124
|
-
Args:
|
|
125
|
-
ctx: MCP context
|
|
126
|
-
date_range: The billing period start and end dates in YYYY-MM-DD format
|
|
127
|
-
dimension: The dimension key to retrieve values for (e.g., SERVICE, REGION, LINKED_ACCOUNT)
|
|
128
|
-
|
|
129
|
-
Returns:
|
|
130
|
-
Dictionary containing the dimension name and list of available values
|
|
131
|
-
"""
|
|
132
|
-
try:
|
|
133
|
-
response = get_dimension_values(
|
|
134
|
-
dimension.dimension_key, date_range.start_date, date_range.end_date
|
|
135
|
-
)
|
|
136
|
-
return response
|
|
137
|
-
except Exception as e:
|
|
138
|
-
logger.error(f'Error getting dimension values for {dimension.dimension_key}: {e}')
|
|
139
|
-
return {'error': f'Error getting dimension values: {str(e)}'}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
@app.tool('get_tag_values')
|
|
143
|
-
async def get_tag_values_tool(
|
|
144
|
-
ctx: Context,
|
|
145
|
-
date_range: DateRange,
|
|
146
|
-
tag_key: str = Field(..., description='The tag key to retrieve values for'),
|
|
147
|
-
) -> Dict[str, Any]:
|
|
148
|
-
"""Retrieve available tag values for AWS Cost Explorer.
|
|
149
|
-
|
|
150
|
-
This tool retrieves all available values for a specified tag key over a period of time.
|
|
151
|
-
This is useful for validating tag filter values or exploring available tag options for cost analysis.
|
|
152
|
-
|
|
153
|
-
Args:
|
|
154
|
-
ctx: MCP context
|
|
155
|
-
date_range: The billing period start and end dates in YYYY-MM-DD format
|
|
156
|
-
tag_key: The tag key to retrieve values for
|
|
157
|
-
|
|
158
|
-
Returns:
|
|
159
|
-
Dictionary containing the tag key and list of available values
|
|
160
|
-
"""
|
|
161
|
-
try:
|
|
162
|
-
response = get_tag_values(tag_key, date_range.start_date, date_range.end_date)
|
|
163
|
-
return response
|
|
164
|
-
except Exception as e:
|
|
165
|
-
logger.error(f'Error getting tag values for {tag_key}: {e}')
|
|
166
|
-
return {'error': f'Error getting tag values: {str(e)}'}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
@app.tool('get_cost_and_usage')
|
|
170
|
-
async def get_cost_and_usage(
|
|
171
|
-
ctx: Context,
|
|
172
|
-
date_range: DateRange,
|
|
173
|
-
granularity: str = Field(
|
|
174
|
-
'MONTHLY',
|
|
175
|
-
description='The granularity at which cost data is aggregated. Valid values are DAILY, MONTHLY, and HOURLY. If not provided, defaults to MONTHLY.',
|
|
176
|
-
),
|
|
177
|
-
group_by: Optional[Union[Dict[str, str], str]] = Field(
|
|
178
|
-
'SERVICE',
|
|
179
|
-
description="Either a dictionary with Type and Key for grouping costs, or simply a string key to group by (which will default to DIMENSION type). Example dictionary: {'Type': 'DIMENSION', 'Key': 'SERVICE'}. Example string: 'SERVICE'.",
|
|
180
|
-
),
|
|
181
|
-
filter_expression: Optional[Dict[str, Any]] = Field(
|
|
182
|
-
None,
|
|
183
|
-
description="Filter criteria as a Python dictionary to narrow down AWS costs. Supports filtering by Dimensions (SERVICE, REGION, etc.), Tags, or CostCategories. You can use logical operators (And, Or, Not) for complex filters. MatchOptions validation: For Dimensions, valid values are EQUALS and CASE_SENSITIVE. For Tags and CostCategories, valid values are EQUALS, ABSENT, and CASE_SENSITIVE (defaults to EQUALS and CASE_SENSITIVE). Examples: 1) Simple service filter: {'Dimensions': {'Key': 'SERVICE', 'Values': ['Amazon Elastic Compute Cloud - Compute', 'Amazon Simple Storage Service'], 'MatchOptions': ['EQUALS']}}. 2) Region filter: {'Dimensions': {'Key': 'REGION', 'Values': ['us-east-1'], 'MatchOptions': ['EQUALS']}}. 3) Combined filter: {'And': [{'Dimensions': {'Key': 'SERVICE', 'Values': ['Amazon Elastic Compute Cloud - Compute'], 'MatchOptions': ['EQUALS']}}, {'Dimensions': {'Key': 'REGION', 'Values': ['us-east-1'], 'MatchOptions': ['EQUALS']}}]}.",
|
|
184
|
-
),
|
|
185
|
-
metric: str = Field(
|
|
186
|
-
'UnblendedCost',
|
|
187
|
-
description='The metric to return in the query. Valid values are AmortizedCost, BlendedCost, NetAmortizedCost, NetUnblendedCost, NormalizedUsageAmount, UnblendedCost, and UsageQuantity. IMPORTANT: For UsageQuantity, the service aggregates usage numbers without considering units, making results meaningless when mixing different unit types (e.g., compute hours + data transfer GB). To get meaningful UsageQuantity metrics, you MUST filter by USAGE_TYPE or group by USAGE_TYPE/USAGE_TYPE_GROUP to ensure consistent units.',
|
|
188
|
-
),
|
|
189
|
-
) -> Dict[str, Any]:
|
|
190
|
-
"""Retrieve AWS cost and usage data.
|
|
191
|
-
|
|
192
|
-
This tool retrieves AWS cost and usage data for AWS services during a specified billing period,
|
|
193
|
-
with optional filtering and grouping. It dynamically generates cost reports tailored to specific needs
|
|
194
|
-
by specifying parameters such as granularity, billing period dates, and filter criteria.
|
|
195
|
-
|
|
196
|
-
Note: The end_date is treated as inclusive in this tool, meaning if you specify an end_date of
|
|
197
|
-
"2025-01-31", the results will include data for January 31st. This differs from the AWS Cost Explorer
|
|
198
|
-
API which treats end_date as exclusive.
|
|
199
|
-
|
|
200
|
-
IMPORTANT: When using UsageQuantity metric, AWS aggregates usage numbers without considering units.
|
|
201
|
-
This makes results meaningless when different usage types have different units (e.g., EC2 compute hours
|
|
202
|
-
vs data transfer GB). For meaningful UsageQuantity results, you MUST be very specific with filtering, including USAGE_TYPE or USAGE_TYPE_GROUP.
|
|
203
|
-
|
|
204
|
-
Example: Get monthly costs for EC2 and S3 services in us-east-1 for May 2025
|
|
205
|
-
await get_cost_and_usage(
|
|
206
|
-
ctx=context,
|
|
207
|
-
date_range={
|
|
208
|
-
"start_date": "2025-05-01",
|
|
209
|
-
"end_date": "2025-05-31"
|
|
210
|
-
},
|
|
211
|
-
granularity="MONTHLY",
|
|
212
|
-
group_by={"Type": "DIMENSION", "Key": "SERVICE"},
|
|
213
|
-
filter_expression={
|
|
214
|
-
"And": [
|
|
215
|
-
{
|
|
216
|
-
"Dimensions": {
|
|
217
|
-
"Key": "SERVICE",
|
|
218
|
-
"Values": ["Amazon Elastic Compute Cloud - Compute", "Amazon Simple Storage Service"],
|
|
219
|
-
"MatchOptions": ["EQUALS"]
|
|
220
|
-
}
|
|
221
|
-
},
|
|
222
|
-
{
|
|
223
|
-
"Dimensions": {
|
|
224
|
-
"Key": "REGION",
|
|
225
|
-
"Values": ["us-east-1"],
|
|
226
|
-
"MatchOptions": ["EQUALS"]
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
]
|
|
230
|
-
},
|
|
231
|
-
metric="UnblendedCost"
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
Example: Get meaningful UsageQuantity for specific EC2 instance usage
|
|
235
|
-
await get_cost_and_usage(
|
|
236
|
-
ctx=context,
|
|
237
|
-
{
|
|
238
|
-
"date_range": {
|
|
239
|
-
"end_date": "2025-05-01",
|
|
240
|
-
"start_date": "2025-05-31"
|
|
241
|
-
},
|
|
242
|
-
"filter_expression": {
|
|
243
|
-
"And": [
|
|
244
|
-
{
|
|
245
|
-
"Dimensions": {
|
|
246
|
-
"Values": [
|
|
247
|
-
"Amazon Elastic Compute Cloud - Compute"
|
|
248
|
-
],
|
|
249
|
-
"Key": "SERVICE",
|
|
250
|
-
"MatchOptions": [
|
|
251
|
-
"EQUALS"
|
|
252
|
-
]
|
|
253
|
-
}
|
|
254
|
-
},
|
|
255
|
-
{
|
|
256
|
-
"Dimensions": {
|
|
257
|
-
"Values": [
|
|
258
|
-
"EC2: Running Hours"
|
|
259
|
-
],
|
|
260
|
-
"Key": "USAGE_TYPE_GROUP",
|
|
261
|
-
"MatchOptions": [
|
|
262
|
-
"EQUALS"
|
|
263
|
-
]
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
]
|
|
267
|
-
},
|
|
268
|
-
"metric": "UsageQuantity",
|
|
269
|
-
"group_by": "USAGE_TYPE",
|
|
270
|
-
"granularity": "MONTHLY"
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
Args:
|
|
274
|
-
ctx: MCP context
|
|
275
|
-
date_range: The billing period start and end dates in YYYY-MM-DD format (end date is inclusive)
|
|
276
|
-
granularity: The granularity at which cost data is aggregated (DAILY, MONTHLY, HOURLY)
|
|
277
|
-
group_by: Either a dictionary with Type and Key, or simply a string key to group by
|
|
278
|
-
filter_expression: Filter criteria as a Python dictionary
|
|
279
|
-
metric: Cost metric to use (UnblendedCost, BlendedCost, etc.)
|
|
280
|
-
|
|
281
|
-
Returns:
|
|
282
|
-
Dictionary containing cost report data grouped according to the specified parameters
|
|
283
|
-
"""
|
|
284
|
-
# Initialize variables at function scope to avoid unbound variable issues
|
|
285
|
-
billing_period_start = date_range.start_date
|
|
286
|
-
billing_period_end = date_range.end_date
|
|
287
|
-
|
|
288
|
-
try:
|
|
289
|
-
# Process inputs - simplified granularity validation
|
|
290
|
-
granularity = str(granularity).upper()
|
|
291
|
-
|
|
292
|
-
if granularity not in ['DAILY', 'MONTHLY', 'HOURLY']:
|
|
293
|
-
return {
|
|
294
|
-
'error': f'Invalid granularity: {granularity}. Valid values are DAILY, MONTHLY, and HOURLY.'
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
# Validate date range with granularity-specific constraints
|
|
298
|
-
try:
|
|
299
|
-
date_range.validate_with_granularity(granularity)
|
|
300
|
-
except ValueError as e:
|
|
301
|
-
return {'error': str(e)}
|
|
302
|
-
|
|
303
|
-
# Define valid metrics and their expected data structure
|
|
304
|
-
valid_metrics = {
|
|
305
|
-
'AmortizedCost': {'has_unit': True, 'is_cost': True},
|
|
306
|
-
'BlendedCost': {'has_unit': True, 'is_cost': True},
|
|
307
|
-
'NetAmortizedCost': {'has_unit': True, 'is_cost': True},
|
|
308
|
-
'NetUnblendedCost': {'has_unit': True, 'is_cost': True},
|
|
309
|
-
'UnblendedCost': {'has_unit': True, 'is_cost': True},
|
|
310
|
-
'UsageQuantity': {'has_unit': True, 'is_cost': False},
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if metric not in valid_metrics:
|
|
314
|
-
return {
|
|
315
|
-
'error': f'Invalid metric: {metric}. Valid values are {", ".join(valid_metrics.keys())}.'
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
metric_config = valid_metrics[metric]
|
|
319
|
-
|
|
320
|
-
# Adjust end date for Cost Explorer API (exclusive)
|
|
321
|
-
# Add one day to make the end date inclusive for the user
|
|
322
|
-
billing_period_end_adj = (
|
|
323
|
-
datetime.strptime(billing_period_end, '%Y-%m-%d')
|
|
324
|
-
+ timedelta(days=COST_EXPLORER_END_DATE_OFFSET)
|
|
325
|
-
).strftime('%Y-%m-%d')
|
|
326
|
-
|
|
327
|
-
# Process filter
|
|
328
|
-
filter_criteria = filter_expression
|
|
329
|
-
|
|
330
|
-
# Validate filter expression if provided
|
|
331
|
-
if filter_criteria:
|
|
332
|
-
# This validates both structure and values against AWS Cost Explorer
|
|
333
|
-
validation_result = validate_expression(
|
|
334
|
-
filter_criteria, billing_period_start, billing_period_end_adj
|
|
335
|
-
)
|
|
336
|
-
if 'error' in validation_result:
|
|
337
|
-
return validation_result
|
|
338
|
-
|
|
339
|
-
# Process group_by
|
|
340
|
-
if group_by is None:
|
|
341
|
-
group_by = {'Type': 'DIMENSION', 'Key': 'SERVICE'}
|
|
342
|
-
elif isinstance(group_by, str):
|
|
343
|
-
group_by = {'Type': 'DIMENSION', 'Key': group_by}
|
|
344
|
-
|
|
345
|
-
# Validate group_by using the existing validate_group_by function
|
|
346
|
-
validation_result = validate_group_by(group_by)
|
|
347
|
-
if 'error' in validation_result:
|
|
348
|
-
return validation_result
|
|
349
|
-
|
|
350
|
-
# Prepare API call parameters
|
|
351
|
-
common_params = {
|
|
352
|
-
'TimePeriod': {
|
|
353
|
-
'Start': format_date_for_api(billing_period_start, granularity),
|
|
354
|
-
'End': format_date_for_api(billing_period_end_adj, granularity),
|
|
355
|
-
},
|
|
356
|
-
'Granularity': granularity,
|
|
357
|
-
'GroupBy': [{'Type': group_by['Type'].upper(), 'Key': group_by['Key']}],
|
|
358
|
-
'Metrics': [metric],
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
if filter_criteria:
|
|
362
|
-
common_params['Filter'] = filter_criteria
|
|
363
|
-
|
|
364
|
-
# Get cost data
|
|
365
|
-
grouped_costs = {}
|
|
366
|
-
next_token = None
|
|
367
|
-
ce = get_cost_explorer_client()
|
|
368
|
-
|
|
369
|
-
while True:
|
|
370
|
-
if next_token:
|
|
371
|
-
common_params['NextPageToken'] = next_token
|
|
372
|
-
|
|
373
|
-
try:
|
|
374
|
-
response = ce.get_cost_and_usage(**common_params)
|
|
375
|
-
except Exception as e:
|
|
376
|
-
logger.error(f'Error calling Cost Explorer API: {e}')
|
|
377
|
-
return {'error': f'AWS Cost Explorer API error: {str(e)}'}
|
|
378
|
-
|
|
379
|
-
for result_by_time in response['ResultsByTime']:
|
|
380
|
-
date = result_by_time['TimePeriod']['Start']
|
|
381
|
-
for group in result_by_time.get('Groups', []):
|
|
382
|
-
if not group.get('Keys') or len(group['Keys']) == 0:
|
|
383
|
-
logger.warning(f'Skipping group with no keys: {group}')
|
|
384
|
-
continue
|
|
385
|
-
|
|
386
|
-
group_key = group['Keys'][0]
|
|
387
|
-
|
|
388
|
-
# Validate that the metric exists in the response
|
|
389
|
-
if metric not in group.get('Metrics', {}):
|
|
390
|
-
logger.error(
|
|
391
|
-
f"Metric '{metric}' not found in response for group {group_key}"
|
|
392
|
-
)
|
|
393
|
-
return {
|
|
394
|
-
'error': f"Metric '{metric}' not found in response for group {group_key}"
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
try:
|
|
398
|
-
metric_data = group['Metrics'][metric]
|
|
399
|
-
|
|
400
|
-
# Validate metric data structure
|
|
401
|
-
if 'Amount' not in metric_data:
|
|
402
|
-
logger.error(
|
|
403
|
-
f'Amount not found in metric data for {group_key}: {metric_data}'
|
|
404
|
-
)
|
|
405
|
-
return {
|
|
406
|
-
'error': "Invalid response format: 'Amount' not found in metric data"
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
# Process based on metric type
|
|
410
|
-
if metric_config['is_cost']:
|
|
411
|
-
# Handle cost metrics
|
|
412
|
-
cost = float(metric_data['Amount'])
|
|
413
|
-
grouped_costs.setdefault(date, {}).update({group_key: cost})
|
|
414
|
-
else:
|
|
415
|
-
# Handle usage metrics (UsageQuantity, NormalizedUsageAmount)
|
|
416
|
-
if 'Unit' not in metric_data and metric_config['has_unit']:
|
|
417
|
-
logger.warning(
|
|
418
|
-
f"Unit not found in {metric} data for {group_key}, using 'Unknown' as unit"
|
|
419
|
-
)
|
|
420
|
-
unit = 'Unknown'
|
|
421
|
-
else:
|
|
422
|
-
unit = metric_data.get('Unit', 'Count')
|
|
423
|
-
amount = float(metric_data['Amount'])
|
|
424
|
-
grouped_costs.setdefault(date, {}).update({group_key: (amount, unit)})
|
|
425
|
-
except (ValueError, TypeError) as e:
|
|
426
|
-
logger.error(f'Error processing metric data: {e}, group: {group_key}')
|
|
427
|
-
return {'error': f'Error processing metric data: {str(e)}'}
|
|
428
|
-
|
|
429
|
-
next_token = response.get('NextPageToken')
|
|
430
|
-
if not next_token:
|
|
431
|
-
break
|
|
432
|
-
|
|
433
|
-
# Process results
|
|
434
|
-
if not grouped_costs:
|
|
435
|
-
logger.info(
|
|
436
|
-
f'No cost data found for the specified parameters: {billing_period_start} to {billing_period_end}'
|
|
437
|
-
)
|
|
438
|
-
return {
|
|
439
|
-
'message': 'No cost data found for the specified parameters',
|
|
440
|
-
'GroupedCosts': {},
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
try:
|
|
444
|
-
if metric_config['is_cost']:
|
|
445
|
-
# Process cost metrics
|
|
446
|
-
df = pd.DataFrame.from_dict(grouped_costs).round(2)
|
|
447
|
-
|
|
448
|
-
# Dynamic labeling based on group dimension
|
|
449
|
-
group_dimension = group_by['Key'].lower().replace('_', ' ')
|
|
450
|
-
df[f'{group_dimension.title()} Total'] = df.sum(axis=1).round(2)
|
|
451
|
-
df.loc[f'Total {metric}'] = df.sum().round(2)
|
|
452
|
-
df = df.sort_values(by=f'{group_dimension.title()} Total', ascending=False)
|
|
453
|
-
|
|
454
|
-
result = {'GroupedCosts': df.to_dict()}
|
|
455
|
-
else:
|
|
456
|
-
# Process usage metrics with cleaner structure
|
|
457
|
-
result_data = {}
|
|
458
|
-
for date, groups in grouped_costs.items():
|
|
459
|
-
result_data[date] = {}
|
|
460
|
-
for group_key, (amount, unit) in groups.items():
|
|
461
|
-
result_data[date][group_key] = {
|
|
462
|
-
'amount': round(float(amount), 2),
|
|
463
|
-
'unit': unit,
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
# Add metadata for usage metrics
|
|
467
|
-
result = {
|
|
468
|
-
'metadata': {
|
|
469
|
-
'grouped_by': group_by['Key'],
|
|
470
|
-
'metric': metric,
|
|
471
|
-
'period': f'{billing_period_start} to {billing_period_end}',
|
|
472
|
-
},
|
|
473
|
-
'GroupedUsage': result_data,
|
|
474
|
-
}
|
|
475
|
-
except Exception as e:
|
|
476
|
-
logger.error(f'Error processing cost data into DataFrame: {e}')
|
|
477
|
-
return {
|
|
478
|
-
'error': f'Error processing cost data: {str(e)}',
|
|
479
|
-
'raw_data': grouped_costs,
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
# Test JSON serialization first, only stringify if needed
|
|
483
|
-
try:
|
|
484
|
-
json.dumps(result)
|
|
485
|
-
return result
|
|
486
|
-
except (TypeError, ValueError):
|
|
487
|
-
# Only stringify if JSON serialization fails
|
|
488
|
-
def stringify_keys(d: Any) -> Any:
|
|
489
|
-
if isinstance(d, dict):
|
|
490
|
-
return {str(k): stringify_keys(v) for k, v in d.items()}
|
|
491
|
-
elif isinstance(d, list):
|
|
492
|
-
return [stringify_keys(i) if i is not None else None for i in d]
|
|
493
|
-
else:
|
|
494
|
-
return d
|
|
495
|
-
|
|
496
|
-
try:
|
|
497
|
-
result = stringify_keys(result)
|
|
498
|
-
return result
|
|
499
|
-
except Exception as e:
|
|
500
|
-
logger.error(f'Error serializing result: {e}')
|
|
501
|
-
return {'error': f'Error serializing result: {str(e)}'}
|
|
502
|
-
|
|
503
|
-
except Exception as e:
|
|
504
|
-
logger.error(
|
|
505
|
-
f'Error generating cost report for period {billing_period_start} to {billing_period_end}: {e}'
|
|
506
|
-
)
|
|
41
|
+
# Define server instructions
|
|
42
|
+
SERVER_INSTRUCTIONS = """
|
|
43
|
+
# AWS Cost Explorer MCP Server
|
|
44
|
+
|
|
45
|
+
## IMPORTANT: Each API call costs $0.01 - use filters and specific date ranges to minimize charges.
|
|
46
|
+
|
|
47
|
+
## Critical Rules
|
|
48
|
+
- Comparison periods: exactly 1 month, start on day 1 (e.g., "2025-04-01" to "2025-05-01")
|
|
49
|
+
- UsageQuantity: Recommended to filter by USAGE_TYPE, USAGE_TYPE_GROUP or results are meaningless
|
|
50
|
+
- When user says "last X months": Use complete calendar months, not partial periods
|
|
51
|
+
- get_cost_comparison_drivers: returns only top 10 most significant drivers
|
|
52
|
+
|
|
53
|
+
## Query Pattern Mapping
|
|
54
|
+
|
|
55
|
+
| User Query Pattern | Recommended Tool | Notes |
|
|
56
|
+
|-------------------|-----------------|-------|
|
|
57
|
+
| "What were my costs for..." | get_cost_and_usage | Use for historical cost analysis |
|
|
58
|
+
| "How much did I spend on..." | get_cost_and_usage | Filter by service/region as needed |
|
|
59
|
+
| "Show me costs by..." | get_cost_and_usage | Set group_by parameter accordingly |
|
|
60
|
+
| "Compare costs between..." | get_cost_and_usage_comparisons | Ensure exactly 1 month periods |
|
|
61
|
+
| "Why did my costs change..." | get_cost_comparison_drivers | Returns top 10 drivers only |
|
|
62
|
+
| "What caused my bill to..." | get_cost_comparison_drivers | Good for root cause analysis |
|
|
63
|
+
| "Predict/forecast my costs..." | get_cost_forecast | Works best with specific services |
|
|
64
|
+
| "What will I spend on..." | get_cost_forecast | Can filter by dimension |
|
|
65
|
+
|
|
66
|
+
## Cost Optimization Tips
|
|
67
|
+
- Always use specific date ranges rather than broad periods
|
|
68
|
+
- Filter by specific services when possible to reduce data processed
|
|
69
|
+
- For usage metrics, always filter by USAGE_TYPE or USAGE_TYPE_GROUP to get meaningful results
|
|
70
|
+
- Combine related questions into a single query where possible
|
|
71
|
+
"""
|
|
507
72
|
|
|
508
|
-
|
|
73
|
+
# Create FastMCP server with instructions
|
|
74
|
+
app = FastMCP(title='Cost Explorer MCP Server', instructions=SERVER_INSTRUCTIONS)
|
|
75
|
+
|
|
76
|
+
# Register all tools with the app
|
|
77
|
+
app.tool('get_today_date')(get_today_date)
|
|
78
|
+
app.tool('get_dimension_values')(get_dimension_values)
|
|
79
|
+
app.tool('get_tag_values')(get_tag_values)
|
|
80
|
+
app.tool('get_cost_forecast')(get_cost_forecast)
|
|
81
|
+
app.tool('get_cost_and_usage_comparisons')(get_cost_and_usage_comparisons)
|
|
82
|
+
app.tool('get_cost_comparison_drivers')(get_cost_comparison_drivers)
|
|
83
|
+
app.tool('get_cost_and_usage')(get_cost_and_usage)
|
|
509
84
|
|
|
510
85
|
|
|
511
86
|
def main():
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
Utility tools for Cost Explorer MCP Server.
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
|
+
from loguru import logger
|
|
25
|
+
from mcp.server.fastmcp import Context
|
|
26
|
+
from typing import Dict
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Configure Loguru logging
|
|
30
|
+
logger.remove()
|
|
31
|
+
logger.add(sys.stderr, level=os.getenv('FASTMCP_LOG_LEVEL', 'WARNING'))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def get_today_date(ctx: Context) -> Dict[str, str]:
|
|
35
|
+
"""Retrieve current date information in UTC time zone.
|
|
36
|
+
|
|
37
|
+
This tool retrieves the current date in YYYY-MM-DD format and the current month in YYYY-MM format.
|
|
38
|
+
It's useful for calculating relevent date when user ask last N months/days.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
ctx: MCP context
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Dictionary containing today's date and current month
|
|
45
|
+
"""
|
|
46
|
+
now_utc = datetime.now(timezone.utc)
|
|
47
|
+
return {
|
|
48
|
+
'today_date_UTC': now_utc.strftime('%Y-%m-%d'),
|
|
49
|
+
'current_month': now_utc.strftime('%Y-%m'),
|
|
50
|
+
}
|