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