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.
@@ -17,473 +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 boto3
21
- import logging
22
- import pandas as pd
23
- from awslabs.cost_explorer_mcp_server.helpers import (
20
+ import os
21
+ import sys
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 (
24
29
  get_dimension_values,
25
30
  get_tag_values,
26
- validate_date_format,
27
- validate_expression,
28
- validate_group_by,
29
31
  )
30
- from datetime import datetime, timedelta
31
- from mcp.server.fastmcp import Context, FastMCP
32
- from pydantic import BaseModel, Field, field_validator
33
- from typing import Any, Dict, Optional, Union
34
-
35
-
36
- # Set up logging
37
- logging.basicConfig(level=logging.INFO)
38
- logger = logging.getLogger(__name__)
39
-
40
- # Initialize AWS Cost Explorer client
41
- ce = boto3.client('ce')
42
-
43
-
44
- class DateRange(BaseModel):
45
- """Date range model for cost queries."""
46
-
47
- start_date: str = Field(
48
- ...,
49
- description='The start date of the billing period in YYYY-MM-DD format. Defaults to last month, if not provided.',
50
- )
51
- end_date: str = Field(
52
- ..., description='The end date of the billing period in YYYY-MM-DD format.'
53
- )
54
-
55
- @field_validator('start_date')
56
- @classmethod
57
- def validate_start_date(cls, v):
58
- """Validate that start_date is in YYYY-MM-DD format and is a valid date."""
59
- is_valid, error = validate_date_format(v)
60
- if not is_valid:
61
- raise ValueError(error)
62
- return v
63
-
64
- @field_validator('end_date')
65
- @classmethod
66
- def validate_end_date(cls, v, info):
67
- """Validate that end_date is in YYYY-MM-DD format and is a valid date, and not before start_date."""
68
- is_valid, error = validate_date_format(v)
69
- if not is_valid:
70
- raise ValueError(error)
71
-
72
- # Access the start_date from the data dictionary
73
- start_date = info.data.get('start_date')
74
- if start_date and v < start_date:
75
- raise ValueError(f"End date '{v}' cannot be before start date '{start_date}'")
76
-
77
- return v
78
-
79
-
80
- class GroupBy(BaseModel):
81
- """Group by model for cost queries."""
82
-
83
- type: str = Field(
84
- ...,
85
- description='Type of grouping. Valid values are DIMENSION, TAG, and COST_CATEGORY.',
86
- )
87
- key: str = Field(
88
- ...,
89
- description='Key to group by. For DIMENSION type, valid values include AZ, INSTANCE_TYPE, LEGAL_ENTITY_NAME, INVOICING_ENTITY, LINKED_ACCOUNT, OPERATION, PLATFORM, PURCHASE_TYPE, SERVICE, TENANCY, RECORD_TYPE, and USAGE_TYPE.',
90
- )
91
-
92
-
93
- class FilterExpression(BaseModel):
94
- """Filter expression model for cost queries."""
95
-
96
- filter_json: str = Field(
97
- ...,
98
- 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. 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']}}]}.",
99
- )
100
-
101
-
102
- class CostMetric(BaseModel):
103
- """Cost metric model."""
104
-
105
- metric: str = Field(
106
- 'UnblendedCost',
107
- description='The metric to return in the query. Valid values are AmortizedCost, BlendedCost, NetAmortizedCost, NetUnblendedCost, NormalizedUsageAmount, UnblendedCost, and UsageQuantity. Note: For UsageQuantity, the service aggregates usage numbers without considering units. To get meaningful UsageQuantity metrics, filter by UsageType or UsageTypeGroups.',
108
- )
109
-
110
-
111
- class DimensionKey(BaseModel):
112
- """Dimension key model."""
113
-
114
- dimension_key: str = Field(
115
- ...,
116
- 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.',
117
- )
118
-
119
-
120
- # Create FastMCP server
121
- app = FastMCP(title='Cost Explorer MCP Server')
122
-
123
-
124
- @app.tool('get_today_date')
125
- async def get_today_date(ctx: Context) -> Dict[str, str]:
126
- """Retrieve current date information.
127
-
128
- This tool retrieves the current date in YYYY-MM-DD format and the current month in YYYY-MM format.
129
- It's useful for comparing if the billing period requested by the user is not in the future.
130
-
131
- Args:
132
- ctx: MCP context
133
-
134
- Returns:
135
- Dictionary containing today's date and current month
136
- """
137
- return {
138
- 'today_date': datetime.now().strftime('%Y-%m-%d'),
139
- 'current_month': datetime.now().strftime('%Y-%m'),
140
- }
141
-
142
-
143
- @app.tool('get_dimension_values')
144
- async def get_dimension_values_tool(
145
- ctx: Context, date_range: DateRange, dimension: DimensionKey
146
- ) -> Dict[str, Any]:
147
- """Retrieve available dimension values for AWS Cost Explorer.
148
-
149
- This tool retrieves all available and valid values for a specified dimension (e.g., SERVICE, REGION)
150
- over a period of time. This is useful for validating filter values or exploring available options
151
- 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
- dimension: The dimension key to retrieve values for (e.g., SERVICE, REGION, LINKED_ACCOUNT)
157
-
158
- Returns:
159
- Dictionary containing the dimension name and list of available values
160
- """
161
- try:
162
- response = get_dimension_values(
163
- dimension.dimension_key, date_range.start_date, date_range.end_date
164
- )
165
- return response
166
- except Exception as e:
167
- logger.error(f'Error getting dimension values: {e}')
168
- return {'error': f'Error getting dimension values: {str(e)}'}
169
-
170
-
171
- @app.tool('get_tag_values')
172
- async def get_tag_values_tool(
173
- ctx: Context,
174
- date_range: DateRange,
175
- tag_key: str = Field(..., description='The tag key to retrieve values for'),
176
- ) -> Dict[str, Any]:
177
- """Retrieve available tag values for AWS Cost Explorer.
178
-
179
- This tool retrieves all available values for a specified tag key over a period of time.
180
- This is useful for validating tag filter values or exploring available tag options for cost analysis.
181
-
182
- Args:
183
- ctx: MCP context
184
- date_range: The billing period start and end dates in YYYY-MM-DD format
185
- tag_key: The tag key to retrieve values for
186
-
187
- Returns:
188
- Dictionary containing the tag key and list of available values
189
- """
190
- try:
191
- response = get_tag_values(tag_key, date_range.start_date, date_range.end_date)
192
- return response
193
- except Exception as e:
194
- logger.error(f'Error getting tag values: {e}')
195
- return {'error': f'Error getting tag values: {str(e)}'}
196
-
197
-
198
- @app.tool('get_cost_and_usage')
199
- async def get_cost_and_usage(
200
- ctx: Context,
201
- date_range: DateRange,
202
- granularity: str = Field(
203
- 'MONTHLY',
204
- description='The granularity at which cost data is aggregated. Valid values are DAILY, MONTHLY, and HOURLY. If not provided, defaults to MONTHLY.',
205
- ),
206
- group_by: Optional[Union[Dict[str, str], str]] = Field(
207
- None,
208
- 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'.",
209
- ),
210
- filter_expression: Optional[Dict[str, Any]] = Field(
211
- None,
212
- 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. 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']}}]}.",
213
- ),
214
- metric: str = Field(
215
- 'UnblendedCost',
216
- description='The metric to return in the query. Valid values are AmortizedCost, BlendedCost, NetAmortizedCost, NetUnblendedCost, NormalizedUsageAmount, UnblendedCost, and UsageQuantity.',
217
- ),
218
- ) -> Dict[str, Any]:
219
- """Retrieve AWS cost and usage data.
220
-
221
- This tool retrieves AWS cost and usage data for AWS services during a specified billing period,
222
- with optional filtering and grouping. It dynamically generates cost reports tailored to specific needs
223
- by specifying parameters such as granularity, billing period dates, and filter criteria.
224
-
225
- Note: The end_date is treated as inclusive in this tool, meaning if you specify an end_date of
226
- "2025-01-31", the results will include data for January 31st. This differs from the AWS Cost Explorer
227
- API which treats end_date as exclusive.
228
-
229
- Example: Get monthly costs for EC2 and S3 services in us-east-1 for May 2025
230
- await get_cost_and_usage(
231
- ctx=context,
232
- date_range={
233
- "start_date": "2025-05-01",
234
- "end_date": "2025-05-31"
235
- },
236
- granularity="MONTHLY",
237
- group_by={"Type": "DIMENSION", "Key": "SERVICE"},
238
- filter_expression={
239
- "And": [
240
- {
241
- "Dimensions": {
242
- "Key": "SERVICE",
243
- "Values": ["Amazon Elastic Compute Cloud - Compute", "Amazon Simple Storage Service"],
244
- "MatchOptions": ["EQUALS"]
245
- }
246
- },
247
- {
248
- "Dimensions": {
249
- "Key": "REGION",
250
- "Values": ["us-east-1"],
251
- "MatchOptions": ["EQUALS"]
252
- }
253
- }
254
- ]
255
- },
256
- metric="UnblendedCost"
257
- )
258
-
259
- Args:
260
- ctx: MCP context
261
- date_range: The billing period start and end dates in YYYY-MM-DD format (end date is inclusive)
262
- granularity: The granularity at which cost data is aggregated (DAILY, MONTHLY, HOURLY)
263
- group_by: Either a dictionary with Type and Key, or simply a string key to group by
264
- filter_expression: Filter criteria as a Python dictionary
265
- metric: Cost metric to use (UnblendedCost, BlendedCost, etc.)
266
-
267
- Returns:
268
- Dictionary containing cost report data grouped according to the specified parameters
269
- """
270
- try:
271
- # Process inputs
272
- if isinstance(granularity, str):
273
- granularity = granularity.upper()
274
- else:
275
- # Handle case where granularity is a Pydantic FieldInfo object
276
- granularity = str(granularity).upper()
277
-
278
- if granularity not in ['DAILY', 'MONTHLY', 'HOURLY']:
279
- return {
280
- 'error': f'Invalid granularity: {granularity}. Valid values are DAILY, MONTHLY, and HOURLY.'
281
- }
282
-
283
- billing_period_start = date_range.start_date
284
- billing_period_end = date_range.end_date
285
-
286
- # Define valid metrics and their expected data structure
287
- valid_metrics = {
288
- 'AmortizedCost': {'has_unit': True, 'is_cost': True},
289
- 'BlendedCost': {'has_unit': True, 'is_cost': True},
290
- 'NetAmortizedCost': {'has_unit': True, 'is_cost': True},
291
- 'NetUnblendedCost': {'has_unit': True, 'is_cost': True},
292
- 'UnblendedCost': {'has_unit': True, 'is_cost': True},
293
- 'UsageQuantity': {'has_unit': True, 'is_cost': False},
294
- }
295
-
296
- if metric not in valid_metrics:
297
- return {
298
- 'error': f'Invalid metric: {metric}. Valid values are {", ".join(valid_metrics.keys())}.'
299
- }
300
-
301
- metric_config = valid_metrics[metric]
302
-
303
- # Adjust end date for Cost Explorer API (exclusive)
304
- # Add one day to make the end date inclusive for the user
305
- billing_period_end_adj = (
306
- datetime.strptime(billing_period_end, '%Y-%m-%d') + timedelta(days=1)
307
- ).strftime('%Y-%m-%d')
308
-
309
- # Process filter
310
- filter_criteria = filter_expression
311
-
312
- # Validate filter expression if provided
313
- if filter_criteria:
314
- # This validates both structure and values against AWS Cost Explorer
315
- validation_result = validate_expression(
316
- filter_criteria, billing_period_start, billing_period_end_adj
317
- )
318
- if 'error' in validation_result:
319
- return validation_result
320
-
321
- # Process group_by
322
- if not group_by:
323
- group_by = {'Type': 'DIMENSION', 'Key': 'SERVICE'}
324
- elif isinstance(group_by, str):
325
- group_by = {'Type': 'DIMENSION', 'Key': group_by}
326
-
327
- # Validate group_by using the existing validate_group_by function
328
- validation_result = validate_group_by(group_by)
329
- if 'error' in validation_result:
330
- return validation_result
331
-
332
- # Prepare API call parameters
333
- common_params = {
334
- 'TimePeriod': {
335
- 'Start': billing_period_start,
336
- 'End': billing_period_end_adj,
337
- },
338
- 'Granularity': granularity,
339
- 'GroupBy': [{'Type': group_by['Type'].upper(), 'Key': group_by['Key']}],
340
- 'Metrics': [metric],
341
- }
342
-
343
- if filter_criteria:
344
- common_params['Filter'] = filter_criteria
345
-
346
- # Get cost data
347
- grouped_costs = {}
348
- next_token = None
349
- while True:
350
- if next_token:
351
- common_params['NextPageToken'] = next_token
352
-
353
- try:
354
- response = ce.get_cost_and_usage(**common_params)
355
- except Exception as e:
356
- logger.error(f'Error calling Cost Explorer API: {e}')
357
- return {'error': f'AWS Cost Explorer API error: {str(e)}'}
358
-
359
- for result_by_time in response['ResultsByTime']:
360
- date = result_by_time['TimePeriod']['Start']
361
- for group in result_by_time.get('Groups', []):
362
- if not group.get('Keys') or len(group['Keys']) == 0:
363
- logger.warning(f'Skipping group with no keys: {group}')
364
- continue
365
-
366
- group_key = group['Keys'][0]
367
-
368
- # Validate that the metric exists in the response
369
- if metric not in group.get('Metrics', {}):
370
- logger.error(
371
- f"Metric '{metric}' not found in response for group {group_key}"
372
- )
373
- return {
374
- 'error': f"Metric '{metric}' not found in response for group {group_key}"
375
- }
376
-
377
- metric_data = group['Metrics'][metric]
378
-
379
- # Validate metric data structure
380
- if 'Amount' not in metric_data:
381
- logger.error(
382
- f'Amount not found in metric data for {group_key}: {metric_data}'
383
- )
384
- return {
385
- 'error': "Invalid response format: 'Amount' not found in metric data"
386
- }
387
-
388
- try:
389
- metric_data = group['Metrics'][metric]
390
-
391
- # Validate metric data structure
392
- if 'Amount' not in metric_data:
393
- logger.error(
394
- f'Amount not found in metric data for {group_key}: {metric_data}'
395
- )
396
- return {
397
- 'error': "Invalid response format: 'Amount' not found in metric data"
398
- }
399
-
400
- # Process based on metric type
401
- if metric_config['is_cost']:
402
- # Handle cost metrics
403
- cost = float(metric_data['Amount'])
404
- grouped_costs.setdefault(date, {}).update({group_key: cost})
405
- else:
406
- # Handle usage metrics (UsageQuantity, NormalizedUsageAmount)
407
- if 'Unit' not in metric_data and metric_config['has_unit']:
408
- logger.warning(
409
- f"Unit not found in {metric} data for {group_key}, using 'Unknown' as unit"
410
- )
411
- unit = 'Unknown'
412
- else:
413
- unit = metric_data.get('Unit', 'Count')
414
- amount = float(metric_data['Amount'])
415
- grouped_costs.setdefault(date, {}).update({group_key: (amount, unit)})
416
- except (ValueError, TypeError) as e:
417
- logger.error(f'Error processing metric data: {e}, data: {metric_data}')
418
- return {'error': f'Error processing metric data: {str(e)}'}
419
-
420
- next_token = response.get('NextPageToken')
421
- if not next_token:
422
- break
423
-
424
- # Process results
425
- if not grouped_costs:
426
- logger.info('No cost data found for the specified parameters')
427
- return {
428
- 'message': 'No cost data found for the specified parameters',
429
- 'GroupedCosts': {},
430
- }
431
-
432
- try:
433
- if metric_config['is_cost']:
434
- # Process cost metrics
435
- df = pd.DataFrame.from_dict(grouped_costs).round(2)
436
- df['Service total'] = df.sum(axis=1).round(2)
437
- df.loc['Total Costs'] = df.sum().round(2)
438
- df = df.sort_values(by='Service total', ascending=False)
439
- else:
440
- # Process usage metrics (UsageQuantity, NormalizedUsageAmount)
441
- usage_df = pd.DataFrame(
442
- {
443
- (k, 'Amount'): {k1: v1[0] for k1, v1 in v.items()}
444
- for k, v in grouped_costs.items()
445
- }
446
- )
447
- units_df = pd.DataFrame(
448
- {
449
- (k, 'Unit'): {k1: v1[1] for k1, v1 in v.items()}
450
- for k, v in grouped_costs.items()
451
- }
452
- )
453
- df = pd.concat([usage_df, units_df], axis=1)
454
-
455
- result = {'GroupedCosts': df.to_dict()}
456
- except Exception as e:
457
- logger.error(f'Error processing cost data into DataFrame: {e}')
458
- return {
459
- 'error': f'Error processing cost data: {str(e)}',
460
- 'raw_data': grouped_costs,
461
- }
462
-
463
- result = {'GroupedCosts': df.to_dict()}
464
-
465
- # Convert all keys to strings for JSON serialization
466
- def stringify_keys(d: Any) -> Any:
467
- if isinstance(d, dict):
468
- return {str(k): stringify_keys(v) for k, v in d.items()}
469
- elif isinstance(d, list):
470
- return [{} if i is None else stringify_keys(i) for i in d] # Handle None values
471
- else:
472
- return d
473
-
474
- try:
475
- result = stringify_keys(result)
476
- return result
477
- except Exception as e:
478
- logger.error(f'Error serializing result: {e}')
479
- return {'error': f'Error serializing result: {str(e)}'}
480
-
481
- except Exception as e:
482
- logger.error(f'Error generating cost report: {e}')
483
- import traceback
32
+ from awslabs.cost_explorer_mcp_server.utility_handler import get_today_date
33
+ from loguru import logger
34
+ from mcp.server.fastmcp import FastMCP
35
+
36
+
37
+ # Configure Loguru logging
38
+ logger.remove()
39
+ logger.add(sys.stderr, level=os.getenv('FASTMCP_LOG_LEVEL', 'WARNING'))
40
+
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
+ """
484
72
 
485
- logger.error(f'Traceback: {traceback.format_exc()}')
486
- return {'error': f'Error generating cost report: {str(e)}'}
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)
487
84
 
488
85
 
489
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
+ }