awslabs.cost-explorer-mcp-server 0.0.1__py3-none-any.whl → 0.0.2__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/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """
2
2
  AWS Labs Cost Explorer MCP Server package.
3
- """
3
+ """
@@ -1,6 +1,6 @@
1
- """
2
- Cost Explorer MCP Server module.
1
+ """Cost Explorer MCP Server module.
3
2
 
4
3
  This module provides MCP tools for analyzing AWS costs and usage data through the AWS Cost Explorer API.
5
4
  """
6
- __version__ = '0.0.0'
5
+
6
+ __version__ = "0.0.0"
@@ -1,221 +1,259 @@
1
1
  """Helper functions for the Cost Explorer MCP server."""
2
2
 
3
+ import boto3
3
4
  import logging
4
5
  import re
5
- from typing import Dict, Any, List, Tuple
6
- import boto3
7
6
  from datetime import datetime
7
+ from typing import Any, Dict, Tuple
8
+
8
9
 
9
10
  # Set up logging
10
11
  logger = logging.getLogger(__name__)
11
12
 
12
13
  # Initialize AWS Cost Explorer client
13
- ce = boto3.client('ce')
14
+ ce = boto3.client("ce")
15
+
14
16
 
15
17
  def validate_date_format(date_str: str) -> Tuple[bool, str]:
16
- """
17
- Validate that a date string is in YYYY-MM-DD format and is a valid date.
18
-
18
+ """Validate that a date string is in YYYY-MM-DD format and is a valid date.
19
+
19
20
  Args:
20
21
  date_str: The date string to validate
21
-
22
+
22
23
  Returns:
23
24
  Tuple of (is_valid, error_message)
24
25
  """
25
26
  # Check format with regex
26
- if not re.match(r'^\d{4}-\d{2}-\d{2}$', date_str):
27
+ if not re.match(r"^\d{4}-\d{2}-\d{2}$", date_str):
27
28
  return False, f"Date '{date_str}' is not in YYYY-MM-DD format"
28
-
29
+
29
30
  # Check if it's a valid date
30
31
  try:
31
- datetime.strptime(date_str, '%Y-%m-%d')
32
+ datetime.strptime(date_str, "%Y-%m-%d")
32
33
  return True, ""
33
34
  except ValueError as e:
34
35
  return False, f"Invalid date '{date_str}': {str(e)}"
35
36
 
36
- def get_dimension_values(key: str, billing_period_start: str, billing_period_end: str) -> Dict[str, Any]:
37
+
38
+ def get_dimension_values(
39
+ key: str, billing_period_start: str, billing_period_end: str
40
+ ) -> Dict[str, Any]:
37
41
  """Get available values for a specific dimension."""
38
42
  # Validate date formats
39
43
  is_valid_start, error_start = validate_date_format(billing_period_start)
40
44
  if not is_valid_start:
41
- return {'error': error_start}
42
-
45
+ return {"error": error_start}
46
+
43
47
  is_valid_end, error_end = validate_date_format(billing_period_end)
44
48
  if not is_valid_end:
45
- return {'error': error_end}
46
-
49
+ return {"error": error_end}
50
+
47
51
  # Validate date range
48
52
  if billing_period_start > billing_period_end:
49
- return {'error': f"Start date '{billing_period_start}' cannot be after end date '{billing_period_end}'"}
50
-
53
+ return {
54
+ "error": f"Start date '{billing_period_start}' cannot be after end date '{billing_period_end}'"
55
+ }
56
+
51
57
  try:
52
58
  response = ce.get_dimension_values(
53
- TimePeriod={
54
- 'Start': billing_period_start,
55
- 'End': billing_period_end
56
- },
57
- Dimension=key.upper()
59
+ TimePeriod={"Start": billing_period_start, "End": billing_period_end},
60
+ Dimension=key.upper(),
58
61
  )
59
- dimension_values = response['DimensionValues']
60
- values = [value['Value'] for value in dimension_values]
61
- return {'dimension': key.upper(), 'values': values}
62
+ dimension_values = response["DimensionValues"]
63
+ values = [value["Value"] for value in dimension_values]
64
+ return {"dimension": key.upper(), "values": values}
62
65
  except Exception as e:
63
66
  logger.error(f"Error getting dimension values: {e}")
64
- return {'error': str(e)}
67
+ return {"error": str(e)}
65
68
 
66
69
 
67
- def get_tag_values(tag_key: str, billing_period_start: str, billing_period_end: str) -> Dict[str, Any]:
70
+ def get_tag_values(
71
+ tag_key: str, billing_period_start: str, billing_period_end: str
72
+ ) -> Dict[str, Any]:
68
73
  """Get available values for a specific tag key."""
69
74
  # Validate date formats
70
75
  is_valid_start, error_start = validate_date_format(billing_period_start)
71
76
  if not is_valid_start:
72
- return {'error': error_start}
73
-
77
+ return {"error": error_start}
78
+
74
79
  is_valid_end, error_end = validate_date_format(billing_period_end)
75
80
  if not is_valid_end:
76
- return {'error': error_end}
77
-
81
+ return {"error": error_end}
82
+
78
83
  # Validate date range
79
84
  if billing_period_start > billing_period_end:
80
- return {'error': f"Start date '{billing_period_start}' cannot be after end date '{billing_period_end}'"}
81
-
85
+ return {
86
+ "error": f"Start date '{billing_period_start}' cannot be after end date '{billing_period_end}'"
87
+ }
88
+
82
89
  try:
83
90
  response = ce.get_tags(
84
- TimePeriod={'Start': billing_period_start,
85
- 'End': billing_period_end},
86
- TagKey=tag_key
91
+ TimePeriod={"Start": billing_period_start, "End": billing_period_end},
92
+ TagKey=tag_key,
87
93
  )
88
- tag_values = response['Tags']
89
- return {'tag_key': tag_key, 'values': tag_values}
94
+ tag_values = response["Tags"]
95
+ return {"tag_key": tag_key, "values": tag_values}
90
96
  except Exception as e:
91
97
  logger.error(f"Error getting tag values: {e}")
92
- return {'error': str(e)}
98
+ return {"error": str(e)}
93
99
 
94
100
 
101
+ def validate_expression(
102
+ expression: Dict[str, Any], billing_period_start: str, billing_period_end: str
103
+ ) -> Dict[str, Any]:
104
+ """Recursively validate the filter expression.
95
105
 
96
- def validate_expression(expression: Dict[str, Any], billing_period_start: str, billing_period_end: str) -> Dict[str, Any]:
97
- """
98
- Recursively validate the filter expression.
99
-
100
106
  Args:
101
107
  expression: The filter expression to validate
102
108
  billing_period_start: Start date of the billing period
103
109
  billing_period_end: End date of the billing period
104
-
110
+
105
111
  Returns:
106
112
  Empty dictionary if valid, or an error dictionary
107
113
  """
108
114
  # Validate date formats
109
115
  is_valid_start, error_start = validate_date_format(billing_period_start)
110
116
  if not is_valid_start:
111
- return {'error': error_start}
112
-
117
+ return {"error": error_start}
118
+
113
119
  is_valid_end, error_end = validate_date_format(billing_period_end)
114
120
  if not is_valid_end:
115
- return {'error': error_end}
116
-
121
+ return {"error": error_end}
122
+
117
123
  # Validate date range
118
124
  if billing_period_start > billing_period_end:
119
- return {'error': f"Start date '{billing_period_start}' cannot be after end date '{billing_period_end}'"}
120
-
125
+ return {
126
+ "error": f"Start date '{billing_period_start}' cannot be after end date '{billing_period_end}'"
127
+ }
128
+
121
129
  try:
122
- if 'Dimensions' in expression:
123
- dimension = expression['Dimensions']
124
- if 'Key' not in dimension or 'Values' not in dimension or 'MatchOptions' not in dimension:
125
- return {'error': 'Dimensions filter must include "Key", "Values", and "MatchOptions".'}
130
+ if "Dimensions" in expression:
131
+ dimension = expression["Dimensions"]
132
+ if (
133
+ "Key" not in dimension
134
+ or "Values" not in dimension
135
+ or "MatchOptions" not in dimension
136
+ ):
137
+ return {
138
+ "error": 'Dimensions filter must include "Key", "Values", and "MatchOptions".'
139
+ }
126
140
 
127
- dimension_key = dimension['Key']
128
- dimension_values = dimension['Values']
141
+ dimension_key = dimension["Key"]
142
+ dimension_values = dimension["Values"]
129
143
  valid_values_response = get_dimension_values(
130
- dimension_key, billing_period_start, billing_period_end)
131
- if 'error' in valid_values_response:
132
- return {'error': valid_values_response['error']}
133
- valid_values = valid_values_response['values']
144
+ dimension_key, billing_period_start, billing_period_end
145
+ )
146
+ if "error" in valid_values_response:
147
+ return {"error": valid_values_response["error"]}
148
+ valid_values = valid_values_response["values"]
134
149
  for value in dimension_values:
135
150
  if value not in valid_values:
136
- return {'error': f"Invalid value '{value}' for dimension '{dimension_key}'. Valid values are: {valid_values}"}
151
+ return {
152
+ "error": f"Invalid value '{value}' for dimension '{dimension_key}'. Valid values are: {valid_values}"
153
+ }
137
154
 
138
- if 'Tags' in expression:
139
- tag = expression['Tags']
140
- if 'Key' not in tag or 'Values' not in tag or 'MatchOptions' not in tag:
141
- return {'error': 'Tags filter must include "Key", "Values", and "MatchOptions".'}
155
+ if "Tags" in expression:
156
+ tag = expression["Tags"]
157
+ if "Key" not in tag or "Values" not in tag or "MatchOptions" not in tag:
158
+ return {"error": 'Tags filter must include "Key", "Values", and "MatchOptions".'}
142
159
 
143
- tag_key = tag['Key']
144
- tag_values = tag['Values']
160
+ tag_key = tag["Key"]
161
+ tag_values = tag["Values"]
145
162
  valid_tag_values_response = get_tag_values(
146
- tag_key, billing_period_start, billing_period_end)
147
- if 'error' in valid_tag_values_response:
148
- return {'error': valid_tag_values_response['error']}
149
- valid_tag_values = valid_tag_values_response['values']
163
+ tag_key, billing_period_start, billing_period_end
164
+ )
165
+ if "error" in valid_tag_values_response:
166
+ return {"error": valid_tag_values_response["error"]}
167
+ valid_tag_values = valid_tag_values_response["values"]
150
168
  for value in tag_values:
151
169
  if value not in valid_tag_values:
152
- return {'error': f"Invalid value '{value}' for tag '{tag_key}'. Valid values are: {valid_tag_values}"}
170
+ return {
171
+ "error": f"Invalid value '{value}' for tag '{tag_key}'. Valid values are: {valid_tag_values}"
172
+ }
153
173
 
154
- if 'CostCategories' in expression:
155
- cost_category = expression['CostCategories']
156
- if 'Key' not in cost_category or 'Values' not in cost_category or 'MatchOptions' not in cost_category:
157
- return {'error': 'CostCategories filter must include "Key", "Values", and "MatchOptions".'}
174
+ if "CostCategories" in expression:
175
+ cost_category = expression["CostCategories"]
176
+ if (
177
+ "Key" not in cost_category
178
+ or "Values" not in cost_category
179
+ or "MatchOptions" not in cost_category
180
+ ):
181
+ return {
182
+ "error": 'CostCategories filter must include "Key", "Values", and "MatchOptions".'
183
+ }
158
184
 
159
- logical_operators = ['And', 'Or', 'Not']
185
+ logical_operators = ["And", "Or", "Not"]
160
186
  logical_count = sum(1 for op in logical_operators if op in expression)
161
187
 
162
188
  if logical_count > 1:
163
- return {'error': 'Only one logical operator (And, Or, Not) is allowed per expression in filter parameter.'}
189
+ return {
190
+ "error": "Only one logical operator (And, Or, Not) is allowed per expression in filter parameter."
191
+ }
164
192
 
165
193
  if logical_count == 0 and len(expression) > 1:
166
- return {'error': 'Filter parameter with multiple expressions require a logical operator (And, Or, Not).'}
194
+ return {
195
+ "error": "Filter parameter with multiple expressions require a logical operator (And, Or, Not)."
196
+ }
167
197
 
168
- if 'And' in expression:
169
- if not isinstance(expression['And'], list):
170
- return {'error': 'And expression must be a list of expressions.'}
171
- for sub_expression in expression['And']:
198
+ if "And" in expression:
199
+ if not isinstance(expression["And"], list):
200
+ return {"error": "And expression must be a list of expressions."}
201
+ for sub_expression in expression["And"]:
172
202
  result = validate_expression(
173
- sub_expression, billing_period_start, billing_period_end)
174
- if 'error' in result:
203
+ sub_expression, billing_period_start, billing_period_end
204
+ )
205
+ if "error" in result:
175
206
  return result
176
207
 
177
- if 'Or' in expression:
178
- if not isinstance(expression['Or'], list):
179
- return {'error': 'Or expression must be a list of expressions.'}
180
- for sub_expression in expression['Or']:
208
+ if "Or" in expression:
209
+ if not isinstance(expression["Or"], list):
210
+ return {"error": "Or expression must be a list of expressions."}
211
+ for sub_expression in expression["Or"]:
181
212
  result = validate_expression(
182
- sub_expression, billing_period_start, billing_period_end)
183
- if 'error' in result:
213
+ sub_expression, billing_period_start, billing_period_end
214
+ )
215
+ if "error" in result:
184
216
  return result
185
217
 
186
- if 'Not' in expression:
187
- if not isinstance(expression['Not'], dict):
188
- return {'error': 'Not expression must be a single expression.'}
218
+ if "Not" in expression:
219
+ if not isinstance(expression["Not"], dict):
220
+ return {"error": "Not expression must be a single expression."}
189
221
  result = validate_expression(
190
- expression['Not'], billing_period_start, billing_period_end)
191
- if 'error' in result:
222
+ expression["Not"], billing_period_start, billing_period_end
223
+ )
224
+ if "error" in result:
192
225
  return result
193
226
 
194
- if not any(k in expression for k in ['Dimensions', 'Tags', 'CostCategories', 'And', 'Or', 'Not']):
195
- return {'error': 'Filter Expression must include at least one of the following keys: "Dimensions", "Tags", "CostCategories", "And", "Or", "Not".'}
227
+ if not any(
228
+ k in expression for k in ["Dimensions", "Tags", "CostCategories", "And", "Or", "Not"]
229
+ ):
230
+ return {
231
+ "error": 'Filter Expression must include at least one of the following keys: "Dimensions", "Tags", "CostCategories", "And", "Or", "Not".'
232
+ }
196
233
 
197
234
  return {}
198
235
  except Exception as e:
199
- return {'error': f'Error validating expression: {str(e)}'}
236
+ return {"error": f"Error validating expression: {str(e)}"}
200
237
 
201
238
 
202
239
  def validate_group_by(group_by: Dict[str, Any]) -> Dict[str, Any]:
203
- """
204
- Validate the group_by parameter.
205
-
240
+ """Validate the group_by parameter.
241
+
206
242
  Args:
207
243
  group_by: The group_by dictionary to validate
208
-
244
+
209
245
  Returns:
210
246
  Empty dictionary if valid, or an error dictionary
211
247
  """
212
248
  try:
213
- if not isinstance(group_by, dict) or 'Type' not in group_by or 'Key' not in group_by:
214
- return {'error': 'group_by must be a dictionary with "Type" and "Key" keys.'}
215
-
216
- if group_by['Type'].upper() not in ['DIMENSION', 'TAG', 'COST_CATEGORY']:
217
- return {'error': 'Invalid group Type. Valid types are DIMENSION, TAG, and COST_CATEGORY.'}
218
-
249
+ if not isinstance(group_by, dict) or "Type" not in group_by or "Key" not in group_by:
250
+ return {"error": 'group_by must be a dictionary with "Type" and "Key" keys.'}
251
+
252
+ if group_by["Type"].upper() not in ["DIMENSION", "TAG", "COST_CATEGORY"]:
253
+ return {
254
+ "error": "Invalid group Type. Valid types are DIMENSION, TAG, and COST_CATEGORY."
255
+ }
256
+
219
257
  return {}
220
258
  except Exception as e:
221
- return {'error': f'Error validating group_by: {str(e)}'}
259
+ return {"error": f"Error validating group_by: {str(e)}"}
@@ -5,95 +5,101 @@ This server provides tools for analyzing AWS costs and usage data through the AW
5
5
 
6
6
  import boto3
7
7
  import logging
8
- from datetime import datetime, timedelta
9
8
  import pandas as pd
10
- from mcp.server.fastmcp import Context, FastMCP
11
- from pydantic import BaseModel, Field, field_validator
12
- from typing import Any, Dict, Optional, Union
13
-
14
9
  from awslabs.cost_explorer_mcp_server.helpers import (
15
10
  get_dimension_values,
16
11
  get_tag_values,
12
+ validate_date_format,
17
13
  validate_expression,
18
14
  validate_group_by,
19
- validate_date_format
20
15
  )
16
+ from datetime import datetime, timedelta
17
+ from mcp.server.fastmcp import Context, FastMCP
18
+ from pydantic import BaseModel, Field, field_validator
19
+ from typing import Any, Dict, Optional, Union
20
+
21
21
 
22
22
  # Set up logging
23
23
  logging.basicConfig(level=logging.INFO)
24
24
  logger = logging.getLogger(__name__)
25
25
 
26
26
  # Initialize AWS Cost Explorer client
27
- ce = boto3.client('ce')
27
+ ce = boto3.client("ce")
28
28
 
29
29
 
30
30
  class DateRange(BaseModel):
31
31
  """Date range model for cost queries."""
32
+
32
33
  start_date: str = Field(
33
34
  ...,
34
- description="The start date of the billing period in YYYY-MM-DD format. Defaults to last month, if not provided."
35
+ description="The start date of the billing period in YYYY-MM-DD format. Defaults to last month, if not provided.",
35
36
  )
36
37
  end_date: str = Field(
37
- ...,
38
- description="The end date of the billing period in YYYY-MM-DD format."
38
+ ..., description="The end date of the billing period in YYYY-MM-DD format."
39
39
  )
40
-
41
- @field_validator('start_date')
40
+
41
+ @field_validator("start_date")
42
42
  @classmethod
43
43
  def validate_start_date(cls, v):
44
+ """Validate that start_date is in YYYY-MM-DD format and is a valid date."""
44
45
  is_valid, error = validate_date_format(v)
45
46
  if not is_valid:
46
47
  raise ValueError(error)
47
48
  return v
48
-
49
- @field_validator('end_date')
49
+
50
+ @field_validator("end_date")
50
51
  @classmethod
51
52
  def validate_end_date(cls, v, info):
53
+ """Validate that end_date is in YYYY-MM-DD format and is a valid date, and not before start_date."""
52
54
  is_valid, error = validate_date_format(v)
53
55
  if not is_valid:
54
56
  raise ValueError(error)
55
-
57
+
56
58
  # Access the start_date from the data dictionary
57
- start_date = info.data.get('start_date')
59
+ start_date = info.data.get("start_date")
58
60
  if start_date and v < start_date:
59
61
  raise ValueError(f"End date '{v}' cannot be before start date '{start_date}'")
60
-
62
+
61
63
  return v
62
64
 
63
65
 
64
66
  class GroupBy(BaseModel):
65
67
  """Group by model for cost queries."""
68
+
66
69
  type: str = Field(
67
70
  ...,
68
- description="Type of grouping. Valid values are DIMENSION, TAG, and COST_CATEGORY."
71
+ description="Type of grouping. Valid values are DIMENSION, TAG, and COST_CATEGORY.",
69
72
  )
70
73
  key: str = Field(
71
74
  ...,
72
- 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."
75
+ 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.",
73
76
  )
74
77
 
75
78
 
76
79
  class FilterExpression(BaseModel):
77
80
  """Filter expression model for cost queries."""
81
+
78
82
  filter_json: str = Field(
79
83
  ...,
80
- 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']}}]}."
84
+ 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']}}]}.",
81
85
  )
82
86
 
83
87
 
84
88
  class CostMetric(BaseModel):
85
89
  """Cost metric model."""
90
+
86
91
  metric: str = Field(
87
92
  "UnblendedCost",
88
- 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."
93
+ 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.",
89
94
  )
90
95
 
91
96
 
92
97
  class DimensionKey(BaseModel):
93
98
  """Dimension key model."""
99
+
94
100
  dimension_key: str = Field(
95
101
  ...,
96
- 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."
102
+ 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.",
97
103
  )
98
104
 
99
105
 
@@ -108,20 +114,21 @@ async def get_today_date(ctx: Context) -> Dict[str, str]:
108
114
  This tool retrieves the current date in YYYY-MM-DD format and the current month in YYYY-MM format.
109
115
  It's useful for comparing if the billing period requested by the user is not in the future.
110
116
 
117
+ Args:
118
+ ctx: MCP context
119
+
111
120
  Returns:
112
121
  Dictionary containing today's date and current month
113
122
  """
114
123
  return {
115
- 'today_date': datetime.now().strftime('%Y-%m-%d'),
116
- 'current_month': datetime.now().strftime('%Y-%m')
124
+ "today_date": datetime.now().strftime("%Y-%m-%d"),
125
+ "current_month": datetime.now().strftime("%Y-%m"),
117
126
  }
118
127
 
119
128
 
120
129
  @app.tool("get_dimension_values")
121
130
  async def get_dimension_values_tool(
122
- ctx: Context,
123
- date_range: DateRange,
124
- dimension: DimensionKey
131
+ ctx: Context, date_range: DateRange, dimension: DimensionKey
125
132
  ) -> Dict[str, Any]:
126
133
  """Retrieve available dimension values for AWS Cost Explorer.
127
134
 
@@ -130,6 +137,7 @@ async def get_dimension_values_tool(
130
137
  for cost analysis.
131
138
 
132
139
  Args:
140
+ ctx: MCP context
133
141
  date_range: The billing period start and end dates in YYYY-MM-DD format
134
142
  dimension: The dimension key to retrieve values for (e.g., SERVICE, REGION, LINKED_ACCOUNT)
135
143
 
@@ -138,21 +146,19 @@ async def get_dimension_values_tool(
138
146
  """
139
147
  try:
140
148
  response = get_dimension_values(
141
- dimension.dimension_key,
142
- date_range.start_date,
143
- date_range.end_date
149
+ dimension.dimension_key, date_range.start_date, date_range.end_date
144
150
  )
145
151
  return response
146
152
  except Exception as e:
147
153
  logger.error(f"Error getting dimension values: {e}")
148
- return {'error': f'Error getting dimension values: {str(e)}'}
154
+ return {"error": f"Error getting dimension values: {str(e)}"}
149
155
 
150
156
 
151
157
  @app.tool("get_tag_values")
152
158
  async def get_tag_values_tool(
153
159
  ctx: Context,
154
160
  date_range: DateRange,
155
- tag_key: str = Field(..., description="The tag key to retrieve values for")
161
+ tag_key: str = Field(..., description="The tag key to retrieve values for"),
156
162
  ) -> Dict[str, Any]:
157
163
  """Retrieve available tag values for AWS Cost Explorer.
158
164
 
@@ -160,6 +166,7 @@ async def get_tag_values_tool(
160
166
  This is useful for validating tag filter values or exploring available tag options for cost analysis.
161
167
 
162
168
  Args:
169
+ ctx: MCP context
163
170
  date_range: The billing period start and end dates in YYYY-MM-DD format
164
171
  tag_key: The tag key to retrieve values for
165
172
 
@@ -167,15 +174,11 @@ async def get_tag_values_tool(
167
174
  Dictionary containing the tag key and list of available values
168
175
  """
169
176
  try:
170
- response = get_tag_values(
171
- tag_key,
172
- date_range.start_date,
173
- date_range.end_date
174
- )
177
+ response = get_tag_values(tag_key, date_range.start_date, date_range.end_date)
175
178
  return response
176
179
  except Exception as e:
177
180
  logger.error(f"Error getting tag values: {e}")
178
- return {'error': f'Error getting tag values: {str(e)}'}
181
+ return {"error": f"Error getting tag values: {str(e)}"}
179
182
 
180
183
 
181
184
  @app.tool("get_cost_and_usage")
@@ -184,20 +187,20 @@ async def get_cost_and_usage(
184
187
  date_range: DateRange,
185
188
  granularity: str = Field(
186
189
  "MONTHLY",
187
- description="The granularity at which cost data is aggregated. Valid values are DAILY, MONTHLY, and HOURLY. If not provided, defaults to MONTHLY."
190
+ description="The granularity at which cost data is aggregated. Valid values are DAILY, MONTHLY, and HOURLY. If not provided, defaults to MONTHLY.",
188
191
  ),
189
192
  group_by: Optional[Union[Dict[str, str], str]] = Field(
190
193
  None,
191
- 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'."
194
+ 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'.",
192
195
  ),
193
196
  filter_expression: Optional[Dict[str, Any]] = Field(
194
197
  None,
195
- 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']}}]}."
198
+ 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']}}]}.",
196
199
  ),
197
200
  metric: str = Field(
198
201
  "UnblendedCost",
199
- description="The metric to return in the query. Valid values are AmortizedCost, BlendedCost, NetAmortizedCost, NetUnblendedCost, NormalizedUsageAmount, UnblendedCost, and UsageQuantity."
200
- )
202
+ description="The metric to return in the query. Valid values are AmortizedCost, BlendedCost, NetAmortizedCost, NetUnblendedCost, NormalizedUsageAmount, UnblendedCost, and UsageQuantity.",
203
+ ),
201
204
  ) -> Dict[str, Any]:
202
205
  """Retrieve AWS cost and usage data.
203
206
 
@@ -205,12 +208,42 @@ async def get_cost_and_usage(
205
208
  with optional filtering and grouping. It dynamically generates cost reports tailored to specific needs
206
209
  by specifying parameters such as granularity, billing period dates, and filter criteria.
207
210
 
208
- Note: The end_date is treated as inclusive in this tool, meaning if you specify an end_date of
209
- "2025-01-31", the results will include data for January 31st. This differs from the AWS Cost Explorer
211
+ Note: The end_date is treated as inclusive in this tool, meaning if you specify an end_date of
212
+ "2025-01-31", the results will include data for January 31st. This differs from the AWS Cost Explorer
210
213
  API which treats end_date as exclusive.
211
214
 
215
+ Example: Get monthly costs for EC2 and S3 services in us-east-1 for May 2025
216
+ await get_cost_and_usage(
217
+ ctx=context,
218
+ date_range={
219
+ "start_date": "2025-05-01",
220
+ "end_date": "2025-05-31"
221
+ },
222
+ granularity="MONTHLY",
223
+ group_by={"Type": "DIMENSION", "Key": "SERVICE"},
224
+ filter_expression={
225
+ "And": [
226
+ {
227
+ "Dimensions": {
228
+ "Key": "SERVICE",
229
+ "Values": ["Amazon Elastic Compute Cloud - Compute", "Amazon Simple Storage Service"],
230
+ "MatchOptions": ["EQUALS"]
231
+ }
232
+ },
233
+ {
234
+ "Dimensions": {
235
+ "Key": "REGION",
236
+ "Values": ["us-east-1"],
237
+ "MatchOptions": ["EQUALS"]
238
+ }
239
+ }
240
+ ]
241
+ },
242
+ metric="UnblendedCost"
243
+ )
212
244
 
213
245
  Args:
246
+ ctx: MCP context
214
247
  date_range: The billing period start and end dates in YYYY-MM-DD format (end date is inclusive)
215
248
  granularity: The granularity at which cost data is aggregated (DAILY, MONTHLY, HOURLY)
216
249
  group_by: Either a dictionary with Type and Key, or simply a string key to group by
@@ -220,36 +253,39 @@ async def get_cost_and_usage(
220
253
  Returns:
221
254
  Dictionary containing cost report data grouped according to the specified parameters
222
255
  """
223
-
224
256
  try:
225
257
  # Process inputs
226
258
  granularity = granularity.upper()
227
259
  if granularity not in ["DAILY", "MONTHLY", "HOURLY"]:
228
- return {'error': f"Invalid granularity: {granularity}. Valid values are DAILY, MONTHLY, and HOURLY."}
229
-
260
+ return {
261
+ "error": f"Invalid granularity: {granularity}. Valid values are DAILY, MONTHLY, and HOURLY."
262
+ }
263
+
230
264
  billing_period_start = date_range.start_date
231
265
  billing_period_end = date_range.end_date
232
-
266
+
233
267
  # Define valid metrics and their expected data structure
234
268
  valid_metrics = {
235
269
  "AmortizedCost": {"has_unit": True, "is_cost": True},
236
270
  "BlendedCost": {"has_unit": True, "is_cost": True},
237
271
  "NetAmortizedCost": {"has_unit": True, "is_cost": True},
238
272
  "NetUnblendedCost": {"has_unit": True, "is_cost": True},
239
- "NormalizedUsageAmount": {"has_unit": True, "is_cost": False},
240
273
  "UnblendedCost": {"has_unit": True, "is_cost": True},
241
- "UsageQuantity": {"has_unit": True, "is_cost": False}
274
+ "UsageQuantity": {"has_unit": True, "is_cost": False},
242
275
  }
243
-
276
+
244
277
  if metric not in valid_metrics:
245
- return {'error': f"Invalid metric: {metric}. Valid values are {', '.join(valid_metrics.keys())}."}
246
-
278
+ return {
279
+ "error": f"Invalid metric: {metric}. Valid values are {', '.join(valid_metrics.keys())}."
280
+ }
281
+
247
282
  metric_config = valid_metrics[metric]
248
283
 
249
284
  # Adjust end date for Cost Explorer API (exclusive)
250
285
  # Add one day to make the end date inclusive for the user
251
- billing_period_end_adj = (datetime.strptime(
252
- billing_period_end, '%Y-%m-%d') + timedelta(days=1)).strftime('%Y-%m-%d')
286
+ billing_period_end_adj = (
287
+ datetime.strptime(billing_period_end, "%Y-%m-%d") + timedelta(days=1)
288
+ ).strftime("%Y-%m-%d")
253
289
 
254
290
  # Process filter
255
291
  filter_criteria = filter_expression
@@ -258,8 +294,9 @@ async def get_cost_and_usage(
258
294
  if filter_criteria:
259
295
  # This validates both structure and values against AWS Cost Explorer
260
296
  validation_result = validate_expression(
261
- filter_criteria, billing_period_start, billing_period_end_adj)
262
- if 'error' in validation_result:
297
+ filter_criteria, billing_period_start, billing_period_end_adj
298
+ )
299
+ if "error" in validation_result:
263
300
  return validation_result
264
301
 
265
302
  # Process group_by
@@ -270,113 +307,141 @@ async def get_cost_and_usage(
270
307
 
271
308
  # Validate group_by using the existing validate_group_by function
272
309
  validation_result = validate_group_by(group_by)
273
- if 'error' in validation_result:
310
+ if "error" in validation_result:
274
311
  return validation_result
275
312
 
276
313
  # Prepare API call parameters
277
314
  common_params = {
278
- 'TimePeriod': {
279
- 'Start': billing_period_start,
280
- 'End': billing_period_end_adj
315
+ "TimePeriod": {
316
+ "Start": billing_period_start,
317
+ "End": billing_period_end_adj,
281
318
  },
282
- 'Granularity': granularity,
283
- 'GroupBy': [{'Type': group_by['Type'].upper(), 'Key': group_by['Key']}],
284
- 'Metrics': [metric]
319
+ "Granularity": granularity,
320
+ "GroupBy": [{"Type": group_by["Type"].upper(), "Key": group_by["Key"]}],
321
+ "Metrics": [metric],
285
322
  }
286
323
 
287
324
  if filter_criteria:
288
- common_params['Filter'] = filter_criteria
325
+ common_params["Filter"] = filter_criteria
289
326
 
290
327
  # Get cost data
291
328
  grouped_costs = {}
292
329
  next_token = None
293
330
  while True:
294
331
  if next_token:
295
- common_params['NextPageToken'] = next_token
332
+ common_params["NextPageToken"] = next_token
296
333
 
297
334
  try:
298
335
  response = ce.get_cost_and_usage(**common_params)
299
336
  except Exception as e:
300
337
  logger.error(f"Error calling Cost Explorer API: {e}")
301
- return {'error': f'AWS Cost Explorer API error: {str(e)}'}
338
+ return {"error": f"AWS Cost Explorer API error: {str(e)}"}
302
339
 
303
- for result_by_time in response['ResultsByTime']:
304
- date = result_by_time['TimePeriod']['Start']
305
- for group in result_by_time.get('Groups', []):
306
- if not group.get('Keys') or len(group['Keys']) == 0:
340
+ for result_by_time in response["ResultsByTime"]:
341
+ date = result_by_time["TimePeriod"]["Start"]
342
+ for group in result_by_time.get("Groups", []):
343
+ if not group.get("Keys") or len(group["Keys"]) == 0:
307
344
  logger.warning(f"Skipping group with no keys: {group}")
308
345
  continue
309
-
310
- group_key = group['Keys'][0]
311
-
346
+
347
+ group_key = group["Keys"][0]
348
+
312
349
  # Validate that the metric exists in the response
313
- if metric not in group.get('Metrics', {}):
314
- logger.error(f"Metric '{metric}' not found in response for group {group_key}")
315
- return {'error': f"Metric '{metric}' not found in response for group {group_key}"}
316
-
317
- metric_data = group['Metrics'][metric]
318
-
350
+ if metric not in group.get("Metrics", {}):
351
+ logger.error(
352
+ f"Metric '{metric}' not found in response for group {group_key}"
353
+ )
354
+ return {
355
+ "error": f"Metric '{metric}' not found in response for group {group_key}"
356
+ }
357
+
358
+ metric_data = group["Metrics"][metric]
359
+
319
360
  # Validate metric data structure
320
- if 'Amount' not in metric_data:
321
- logger.error(f"Amount not found in metric data for {group_key}: {metric_data}")
322
- return {'error': f"Invalid response format: 'Amount' not found in metric data"}
323
-
361
+ if "Amount" not in metric_data:
362
+ logger.error(
363
+ f"Amount not found in metric data for {group_key}: {metric_data}"
364
+ )
365
+ return {
366
+ "error": "Invalid response format: 'Amount' not found in metric data"
367
+ }
368
+
324
369
  try:
325
- metric_data = group['Metrics'][metric]
326
-
370
+ metric_data = group["Metrics"][metric]
371
+
327
372
  # Validate metric data structure
328
- if 'Amount' not in metric_data:
329
- logger.error(f"Amount not found in metric data for {group_key}: {metric_data}")
330
- return {'error': f"Invalid response format: 'Amount' not found in metric data"}
331
-
373
+ if "Amount" not in metric_data:
374
+ logger.error(
375
+ f"Amount not found in metric data for {group_key}: {metric_data}"
376
+ )
377
+ return {
378
+ "error": "Invalid response format: 'Amount' not found in metric data"
379
+ }
380
+
332
381
  # Process based on metric type
333
382
  if metric_config["is_cost"]:
334
383
  # Handle cost metrics
335
- cost = float(metric_data['Amount'])
384
+ cost = float(metric_data["Amount"])
336
385
  grouped_costs.setdefault(date, {}).update({group_key: cost})
337
386
  else:
338
387
  # Handle usage metrics (UsageQuantity, NormalizedUsageAmount)
339
- if 'Unit' not in metric_data and metric_config["has_unit"]:
340
- logger.warning(f"Unit not found in {metric} data for {group_key}, using 'Unknown' as unit")
341
- unit = 'Unknown'
388
+ if "Unit" not in metric_data and metric_config["has_unit"]:
389
+ logger.warning(
390
+ f"Unit not found in {metric} data for {group_key}, using 'Unknown' as unit"
391
+ )
392
+ unit = "Unknown"
342
393
  else:
343
- unit = metric_data.get('Unit', 'Count')
344
- amount = float(metric_data['Amount'])
394
+ unit = metric_data.get("Unit", "Count")
395
+ amount = float(metric_data["Amount"])
345
396
  grouped_costs.setdefault(date, {}).update({group_key: (amount, unit)})
346
397
  except (ValueError, TypeError) as e:
347
398
  logger.error(f"Error processing metric data: {e}, data: {metric_data}")
348
- return {'error': f"Error processing metric data: {str(e)}"}
399
+ return {"error": f"Error processing metric data: {str(e)}"}
349
400
 
350
- next_token = response.get('NextPageToken')
401
+ next_token = response.get("NextPageToken")
351
402
  if not next_token:
352
403
  break
353
404
 
354
405
  # Process results
355
406
  if not grouped_costs:
356
407
  logger.info("No cost data found for the specified parameters")
357
- return {'message': 'No cost data found for the specified parameters', 'GroupedCosts': {}}
358
-
408
+ return {
409
+ "message": "No cost data found for the specified parameters",
410
+ "GroupedCosts": {},
411
+ }
412
+
359
413
  try:
360
414
  if metric_config["is_cost"]:
361
415
  # Process cost metrics
362
416
  df = pd.DataFrame.from_dict(grouped_costs).round(2)
363
- df['Service total'] = df.sum(axis=1).round(2)
364
- df.loc['Total Costs'] = df.sum().round(2)
365
- df = df.sort_values(by='Service total', ascending=False)
417
+ df["Service total"] = df.sum(axis=1).round(2)
418
+ df.loc["Total Costs"] = df.sum().round(2)
419
+ df = df.sort_values(by="Service total", ascending=False)
366
420
  else:
367
421
  # Process usage metrics (UsageQuantity, NormalizedUsageAmount)
368
- usage_df = pd.DataFrame({(k, 'Amount'): {
369
- k1: v1[0] for k1, v1 in v.items()} for k, v in grouped_costs.items()})
422
+ usage_df = pd.DataFrame(
423
+ {
424
+ (k, "Amount"): {k1: v1[0] for k1, v1 in v.items()}
425
+ for k, v in grouped_costs.items()
426
+ }
427
+ )
370
428
  units_df = pd.DataFrame(
371
- {(k, 'Unit'): {k1: v1[1] for k1, v1 in v.items()} for k, v in grouped_costs.items()})
429
+ {
430
+ (k, "Unit"): {k1: v1[1] for k1, v1 in v.items()}
431
+ for k, v in grouped_costs.items()
432
+ }
433
+ )
372
434
  df = pd.concat([usage_df, units_df], axis=1)
373
435
 
374
- result = {'GroupedCosts': df.to_dict()}
436
+ result = {"GroupedCosts": df.to_dict()}
375
437
  except Exception as e:
376
438
  logger.error(f"Error processing cost data into DataFrame: {e}")
377
- return {'error': f"Error processing cost data: {str(e)}", 'raw_data': grouped_costs}
439
+ return {
440
+ "error": f"Error processing cost data: {str(e)}",
441
+ "raw_data": grouped_costs,
442
+ }
378
443
 
379
- result = {'GroupedCosts': df.to_dict()}
444
+ result = {"GroupedCosts": df.to_dict()}
380
445
 
381
446
  # Convert all keys to strings for JSON serialization
382
447
  def stringify_keys(d):
@@ -392,17 +457,20 @@ async def get_cost_and_usage(
392
457
  return result
393
458
  except Exception as e:
394
459
  logger.error(f"Error serializing result: {e}")
395
- return {'error': f"Error serializing result: {str(e)}"}
460
+ return {"error": f"Error serializing result: {str(e)}"}
396
461
 
397
462
  except Exception as e:
398
463
  logger.error(f"Error generating cost report: {e}")
399
464
  import traceback
465
+
400
466
  logger.error(f"Traceback: {traceback.format_exc()}")
401
- return {'error': f'Error generating cost report: {str(e)}'}
467
+ return {"error": f"Error generating cost report: {str(e)}"}
468
+
402
469
 
403
470
  def main():
404
471
  """Run the MCP server with CLI argument support."""
405
472
  app.run()
406
473
 
474
+
407
475
  if __name__ == "__main__":
408
476
  main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: awslabs.cost-explorer-mcp-server
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: MCP server for analyzing AWS costs and usage data through the AWS Cost Explorer API
5
5
  Project-URL: Homepage, https://awslabs.github.io/mcp/
6
6
  Project-URL: Documentation, https://awslabs.github.io/mcp/servers/cost-explorer-mcp-server/
@@ -127,6 +127,17 @@ The MCP server uses the AWS profile specified in the `AWS_PROFILE` environment v
127
127
  ```
128
128
 
129
129
  Make sure the AWS profile has permissions to access the AWS Cost Explorer API. The MCP server creates a boto3 session using the specified profile to authenticate with AWS services. Your AWS IAM credentials remain on your local machine and are strictly used for accessing AWS services.
130
+ ## Cost Considerations
131
+
132
+ **Important:** AWS Cost Explorer API incurs charges on a per-request basis. Each API call made by this MCP server will result in charges to your AWS account.
133
+
134
+ - **Cost Explorer API Pricing:** The AWS Cost Explorer API lets you directly access the interactive, ad-hoc query engine that powers AWS Cost Explorer. Each request will incur a cost of $0.01.
135
+ - Each tool invocation that queries Cost Explorer (get_dimension_values, get_tag_values, get_cost_and_usage) will generate at least one billable API request
136
+ - Complex queries with multiple filters or large date ranges may result in multiple API calls
137
+
138
+ For current pricing information, please refer to the [AWS Cost Explorer Pricing page](https://aws.amazon.com/aws-cost-management/aws-cost-explorer/pricing/).
139
+
140
+
130
141
  ## Security Considerations
131
142
 
132
143
  ### Required IAM Permissions
@@ -135,22 +146,7 @@ The following IAM permissions are required for this MCP server:
135
146
  - ce:GetDimensionValues
136
147
  - ce:GetTags
137
148
 
138
- Example IAM policy:
139
- json
140
- {
141
- "Version": "2012-10-17",
142
- "Statement": [
143
- {
144
- "Effect": "Allow",
145
- "Action": [
146
- "ce:GetCostAndUsage",
147
- "ce:GetDimensionValues",
148
- "ce:GetTags"
149
- ],
150
- "Resource": "*"
151
- }
152
- ]
153
- }
149
+
154
150
 
155
151
  ## Available Tools
156
152
 
@@ -0,0 +1,10 @@
1
+ awslabs/__init__.py,sha256=vi9O_PzTkEpojELcg0v9esQUWLbseZTynQB4YpNYzA8,51
2
+ awslabs/cost_explorer_mcp_server/__init__.py,sha256=DkDBVzhBu0iM3Y6ZrDi6zTydT3mtgKtwjavEhmLBnl0,169
3
+ awslabs/cost_explorer_mcp_server/helpers.py,sha256=Snc2rhsNFbl7WmLBbrIYR21jmKU7QAjshLiv26RyilU,9470
4
+ awslabs/cost_explorer_mcp_server/server.py,sha256=MTHU04Wm0izmrk52fWAQw-y9TpG2O-ACKzfcA_-2Rp0,20150
5
+ awslabs_cost_explorer_mcp_server-0.0.2.dist-info/METADATA,sha256=kUuzyv89t_LOq1UNDfcinZc97ZcAfR9mnEc162kK7iM,6340
6
+ awslabs_cost_explorer_mcp_server-0.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
+ awslabs_cost_explorer_mcp_server-0.0.2.dist-info/entry_points.txt,sha256=nkewGFi8GZCCtHhFofUmYii3OCeK_5qqgLXE4eUSFZg,98
8
+ awslabs_cost_explorer_mcp_server-0.0.2.dist-info/licenses/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142
9
+ awslabs_cost_explorer_mcp_server-0.0.2.dist-info/licenses/NOTICE,sha256=VL_gWrK0xFaHGFxxYj6BcZI30EkRxUH4Dv1u2Qsh3ao,92
10
+ awslabs_cost_explorer_mcp_server-0.0.2.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- awslabs/__init__.py,sha256=4CPNxIm93Eam4qERUKxlLe0tSv0h8nd0ldA40H6lRtI,50
2
- awslabs/cost_explorer_mcp_server/__init__.py,sha256=hzMRqVmpxqoFbtkSW8Xl6RNRwS_sVKktrHyZM2o9KJ4,168
3
- awslabs/cost_explorer_mcp_server/helpers.py,sha256=Rt7oVmU3TEVOF8wq4CWb8yJFLjeoSlwKLwSrHb4YA6s,9054
4
- awslabs/cost_explorer_mcp_server/server.py,sha256=OztURecVW8JOtJZLu6WX97YYOK_GPH-CUgImodI25qE,18388
5
- awslabs_cost_explorer_mcp_server-0.0.1.dist-info/METADATA,sha256=SUGpDEksg1cJj60shYmdiHtkgVZXAF1NJzPxFPknqGY,5848
6
- awslabs_cost_explorer_mcp_server-0.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
- awslabs_cost_explorer_mcp_server-0.0.1.dist-info/entry_points.txt,sha256=nkewGFi8GZCCtHhFofUmYii3OCeK_5qqgLXE4eUSFZg,98
8
- awslabs_cost_explorer_mcp_server-0.0.1.dist-info/licenses/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142
9
- awslabs_cost_explorer_mcp_server-0.0.1.dist-info/licenses/NOTICE,sha256=VL_gWrK0xFaHGFxxYj6BcZI30EkRxUH4Dv1u2Qsh3ao,92
10
- awslabs_cost_explorer_mcp_server-0.0.1.dist-info/RECORD,,