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.
@@ -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.helpers import (
25
- format_date_for_api,
26
- get_cost_explorer_client,
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 datetime import datetime, timedelta
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 Context, FastMCP
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
- # Constants
46
- COST_EXPLORER_END_DATE_OFFSET = 1
47
-
48
-
49
- class DateRange(BaseModel):
50
- """Date range model for cost queries."""
51
-
52
- start_date: str = Field(
53
- ...,
54
- description='The start date of the billing period in YYYY-MM-DD format. Defaults to last month, if not provided.',
55
- )
56
- end_date: str = Field(
57
- ..., description='The end date of the billing period in YYYY-MM-DD format.'
58
- )
59
-
60
- @field_validator('start_date', 'end_date')
61
- @classmethod
62
- def validate_individual_dates(cls, v):
63
- """Validate that individual dates are in YYYY-MM-DD format and are valid dates."""
64
- is_valid, error = validate_date_format(v)
65
- if not is_valid:
66
- raise ValueError(error)
67
- return v
68
-
69
- def model_post_init(self, __context):
70
- """Validate the date range after both dates are set."""
71
- is_valid, error = validate_date_range(self.start_date, self.end_date)
72
- if not is_valid:
73
- raise ValueError(error)
74
-
75
- def validate_with_granularity(self, granularity: str):
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
- 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)
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
+ }