awslabs.cost-explorer-mcp-server 0.0.2__tar.gz → 0.0.4__tar.gz

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.
Files changed (29) hide show
  1. awslabs_cost_explorer_mcp_server-0.0.4/.pre-commit-config.yaml +14 -0
  2. {awslabs_cost_explorer_mcp_server-0.0.2 → awslabs_cost_explorer_mcp_server-0.0.4}/Dockerfile +9 -6
  3. {awslabs_cost_explorer_mcp_server-0.0.2 → awslabs_cost_explorer_mcp_server-0.0.4}/PKG-INFO +3 -2
  4. {awslabs_cost_explorer_mcp_server-0.0.2 → awslabs_cost_explorer_mcp_server-0.0.4}/README.md +1 -0
  5. awslabs_cost_explorer_mcp_server-0.0.4/awslabs/__init__.py +17 -0
  6. awslabs_cost_explorer_mcp_server-0.0.4/awslabs/cost_explorer_mcp_server/__init__.py +20 -0
  7. awslabs_cost_explorer_mcp_server-0.0.4/awslabs/cost_explorer_mcp_server/helpers.py +278 -0
  8. {awslabs_cost_explorer_mcp_server-0.0.2 → awslabs_cost_explorer_mcp_server-0.0.4}/awslabs/cost_explorer_mcp_server/server.py +120 -101
  9. awslabs_cost_explorer_mcp_server-0.0.4/docker-healthcheck.sh +26 -0
  10. {awslabs_cost_explorer_mcp_server-0.0.2 → awslabs_cost_explorer_mcp_server-0.0.4}/pyproject.toml +24 -40
  11. awslabs_cost_explorer_mcp_server-0.0.4/tests/__init__.py +15 -0
  12. awslabs_cost_explorer_mcp_server-0.0.4/tests/conftest.py +110 -0
  13. awslabs_cost_explorer_mcp_server-0.0.4/tests/test_helpers.py +666 -0
  14. awslabs_cost_explorer_mcp_server-0.0.4/tests/test_server.py +885 -0
  15. {awslabs_cost_explorer_mcp_server-0.0.2 → awslabs_cost_explorer_mcp_server-0.0.4}/uv.lock +368 -184
  16. awslabs_cost_explorer_mcp_server-0.0.2/.pre-commit-config.yaml +0 -19
  17. awslabs_cost_explorer_mcp_server-0.0.2/awslabs/__init__.py +0 -3
  18. awslabs_cost_explorer_mcp_server-0.0.2/awslabs/cost_explorer_mcp_server/__init__.py +0 -6
  19. awslabs_cost_explorer_mcp_server-0.0.2/awslabs/cost_explorer_mcp_server/helpers.py +0 -259
  20. awslabs_cost_explorer_mcp_server-0.0.2/docker-healthcheck.sh +0 -12
  21. awslabs_cost_explorer_mcp_server-0.0.2/tests/__init__.py +0 -1
  22. awslabs_cost_explorer_mcp_server-0.0.2/tests/conftest.py +0 -93
  23. awslabs_cost_explorer_mcp_server-0.0.2/tests/test_helpers.py +0 -282
  24. awslabs_cost_explorer_mcp_server-0.0.2/tests/test_server.py +0 -427
  25. {awslabs_cost_explorer_mcp_server-0.0.2 → awslabs_cost_explorer_mcp_server-0.0.4}/.gitignore +0 -0
  26. {awslabs_cost_explorer_mcp_server-0.0.2 → awslabs_cost_explorer_mcp_server-0.0.4}/.python-version +0 -0
  27. {awslabs_cost_explorer_mcp_server-0.0.2 → awslabs_cost_explorer_mcp_server-0.0.4}/CHANGELOG.md +0 -0
  28. {awslabs_cost_explorer_mcp_server-0.0.2 → awslabs_cost_explorer_mcp_server-0.0.4}/LICENSE +0 -0
  29. {awslabs_cost_explorer_mcp_server-0.0.2 → awslabs_cost_explorer_mcp_server-0.0.4}/NOTICE +0 -0
@@ -0,0 +1,14 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.9.6
4
+ hooks:
5
+ - id: ruff
6
+ args: [--fix]
7
+ - id: ruff-format
8
+
9
+ - repo: https://github.com/commitizen-tools/commitizen
10
+ rev: v3.13.0
11
+ hooks:
12
+ - id: commitizen
13
+ - id: commitizen-branch
14
+ stages: [pre-push]
@@ -1,13 +1,16 @@
1
1
  # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
2
  #
3
- # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
4
- # with the License. A copy of the License is located at
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
5
6
  #
6
- # http://www.apache.org/licenses/LICENSE-2.0
7
+ # http://www.apache.org/licenses/LICENSE-2.0
7
8
  #
8
- # or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
9
- # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
10
- # and limitations under the License.
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.
11
14
 
12
15
  FROM public.ecr.aws/sam/build-python3.10@sha256:e78695db10ca8cb129e59e30f7dc9789b0dbd0181dba195d68419c72bac51ac1 AS uv
13
16
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: awslabs.cost-explorer-mcp-server
3
- Version: 0.0.2
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,7 @@ 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
+
130
131
  ## Cost Considerations
131
132
 
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.
@@ -98,6 +98,7 @@ The MCP server uses the AWS profile specified in the `AWS_PROFILE` environment v
98
98
  ```
99
99
 
100
100
  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.
101
+
101
102
  ## Cost Considerations
102
103
 
103
104
  **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.
@@ -0,0 +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
+
15
+ """
16
+ AWS Labs Cost Explorer MCP Server package.
17
+ """
@@ -0,0 +1,20 @@
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.
16
+
17
+ This module provides MCP tools for analyzing AWS costs and usage data through the AWS Cost Explorer API.
18
+ """
19
+
20
+ __version__ = '0.0.0'
@@ -0,0 +1,278 @@
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
+ """Helper functions for the Cost Explorer MCP server."""
16
+
17
+ import boto3
18
+ import logging
19
+ import re
20
+ from datetime import datetime
21
+ from typing import Any, Dict, Optional, Tuple
22
+
23
+
24
+ # Set up logging
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # Initialize AWS Cost Explorer client
28
+ ce = boto3.client('ce')
29
+
30
+
31
+ def validate_date_format(date_str: str) -> Tuple[bool, str]:
32
+ """Validate that a date string is in YYYY-MM-DD format and is a valid date.
33
+
34
+ Args:
35
+ date_str: The date string to validate
36
+
37
+ Returns:
38
+ Tuple of (is_valid, error_message)
39
+ """
40
+ # Check format with regex
41
+ if not re.match(r'^\d{4}-\d{2}-\d{2}$', date_str):
42
+ return False, f"Date '{date_str}' is not in YYYY-MM-DD format"
43
+
44
+ # Check if it's a valid date
45
+ try:
46
+ datetime.strptime(date_str, '%Y-%m-%d')
47
+ return True, ''
48
+ except ValueError as e:
49
+ return False, f"Invalid date '{date_str}': {str(e)}"
50
+
51
+
52
+ def get_dimension_values(
53
+ key: str, billing_period_start: str, billing_period_end: str
54
+ ) -> Dict[str, Any]:
55
+ """Get available values for a specific dimension."""
56
+ # Validate date formats
57
+ is_valid_start, error_start = validate_date_format(billing_period_start)
58
+ if not is_valid_start:
59
+ return {'error': error_start}
60
+
61
+ is_valid_end, error_end = validate_date_format(billing_period_end)
62
+ if not is_valid_end:
63
+ return {'error': error_end}
64
+
65
+ # Validate date range
66
+ if billing_period_start > billing_period_end:
67
+ return {
68
+ 'error': f"Start date '{billing_period_start}' cannot be after end date '{billing_period_end}'"
69
+ }
70
+
71
+ try:
72
+ response = ce.get_dimension_values(
73
+ TimePeriod={'Start': billing_period_start, 'End': billing_period_end},
74
+ Dimension=key.upper(),
75
+ )
76
+ dimension_values = response['DimensionValues']
77
+ values = [value['Value'] for value in dimension_values]
78
+ return {'dimension': key.upper(), 'values': values}
79
+ except Exception as e:
80
+ logger.error(f'Error getting dimension values: {e}')
81
+ return {'error': str(e)}
82
+
83
+
84
+ def get_tag_values(
85
+ tag_key: str, billing_period_start: str, billing_period_end: str
86
+ ) -> Dict[str, Any]:
87
+ """Get available values for a specific tag key."""
88
+ # Validate date formats
89
+ is_valid_start, error_start = validate_date_format(billing_period_start)
90
+ if not is_valid_start:
91
+ return {'error': error_start}
92
+
93
+ is_valid_end, error_end = validate_date_format(billing_period_end)
94
+ if not is_valid_end:
95
+ return {'error': error_end}
96
+
97
+ # Validate date range
98
+ if billing_period_start > billing_period_end:
99
+ return {
100
+ 'error': f"Start date '{billing_period_start}' cannot be after end date '{billing_period_end}'"
101
+ }
102
+
103
+ try:
104
+ response = ce.get_tags(
105
+ TimePeriod={'Start': billing_period_start, 'End': billing_period_end},
106
+ TagKey=tag_key,
107
+ )
108
+ tag_values = response['Tags']
109
+ return {'tag_key': tag_key, 'values': tag_values}
110
+ except Exception as e:
111
+ logger.error(f'Error getting tag values: {e}')
112
+ return {'error': str(e)}
113
+
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.
119
+
120
+ Args:
121
+ expression: The filter expression to validate
122
+ billing_period_start: Start date of the billing period
123
+ billing_period_end: End date of the billing period
124
+
125
+ Returns:
126
+ Empty dictionary if valid, or an error dictionary
127
+ """
128
+ # Validate date formats
129
+ is_valid_start, error_start = validate_date_format(billing_period_start)
130
+ if not is_valid_start:
131
+ return {'error': error_start}
132
+
133
+ is_valid_end, error_end = validate_date_format(billing_period_end)
134
+ if not is_valid_end:
135
+ return {'error': error_end}
136
+
137
+ # Validate date range
138
+ if billing_period_start > billing_period_end:
139
+ return {
140
+ 'error': f"Start date '{billing_period_start}' cannot be after end date '{billing_period_end}'"
141
+ }
142
+
143
+ try:
144
+ if 'Dimensions' in expression:
145
+ dimension = expression['Dimensions']
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
+ }
154
+
155
+ dimension_key = dimension['Key']
156
+ dimension_values = dimension['Values']
157
+ valid_values_response = get_dimension_values(
158
+ dimension_key, billing_period_start, billing_period_end
159
+ )
160
+ if 'error' in valid_values_response:
161
+ return {'error': valid_values_response['error']}
162
+ valid_values = valid_values_response['values']
163
+ for value in dimension_values:
164
+ if value not in valid_values:
165
+ return {
166
+ 'error': f"Invalid value '{value}' for dimension '{dimension_key}'. Valid values are: {valid_values}"
167
+ }
168
+
169
+ if 'Tags' in expression:
170
+ tag = expression['Tags']
171
+ if 'Key' not in tag or 'Values' not in tag or 'MatchOptions' not in tag:
172
+ return {'error': 'Tags filter must include "Key", "Values", and "MatchOptions".'}
173
+
174
+ tag_key = tag['Key']
175
+ tag_values = tag['Values']
176
+ valid_tag_values_response = get_tag_values(
177
+ tag_key, billing_period_start, billing_period_end
178
+ )
179
+ if 'error' in valid_tag_values_response:
180
+ return {'error': valid_tag_values_response['error']}
181
+ valid_tag_values = valid_tag_values_response['values']
182
+ for value in tag_values:
183
+ if value not in valid_tag_values:
184
+ return {
185
+ 'error': f"Invalid value '{value}' for tag '{tag_key}'. Valid values are: {valid_tag_values}"
186
+ }
187
+
188
+ if 'CostCategories' in expression:
189
+ cost_category = expression['CostCategories']
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
+ }
198
+
199
+ logical_operators = ['And', 'Or', 'Not']
200
+ logical_count = sum(1 for op in logical_operators if op in expression)
201
+
202
+ if logical_count > 1:
203
+ return {
204
+ 'error': 'Only one logical operator (And, Or, Not) is allowed per expression in filter parameter.'
205
+ }
206
+
207
+ if logical_count == 0 and len(expression) > 1:
208
+ return {
209
+ 'error': 'Filter parameter with multiple expressions require a logical operator (And, Or, Not).'
210
+ }
211
+
212
+ if 'And' in expression:
213
+ if not isinstance(expression['And'], list):
214
+ return {'error': 'And expression must be a list of expressions.'}
215
+ for sub_expression in expression['And']:
216
+ result = validate_expression(
217
+ sub_expression, billing_period_start, billing_period_end
218
+ )
219
+ if 'error' in result:
220
+ return result
221
+
222
+ if 'Or' in expression:
223
+ if not isinstance(expression['Or'], list):
224
+ return {'error': 'Or expression must be a list of expressions.'}
225
+ for sub_expression in expression['Or']:
226
+ result = validate_expression(
227
+ sub_expression, billing_period_start, billing_period_end
228
+ )
229
+ if 'error' in result:
230
+ return result
231
+
232
+ if 'Not' in expression:
233
+ if not isinstance(expression['Not'], dict):
234
+ return {'error': 'Not expression must be a single expression.'}
235
+ result = validate_expression(
236
+ expression['Not'], billing_period_start, billing_period_end
237
+ )
238
+ if 'error' in result:
239
+ return result
240
+
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
+ }
247
+
248
+ return {}
249
+ except Exception as e:
250
+ return {'error': f'Error validating expression: {str(e)}'}
251
+
252
+
253
+ def validate_group_by(group_by: Optional[Dict[str, Any]]) -> Dict[str, Any]:
254
+ """Validate the group_by parameter.
255
+
256
+ Args:
257
+ group_by: The group_by dictionary to validate
258
+
259
+ Returns:
260
+ Empty dictionary if valid, or an error dictionary
261
+ """
262
+ try:
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
+ ):
269
+ return {'error': 'group_by must be a dictionary with "Type" and "Key" keys.'}
270
+
271
+ if group_by['Type'].upper() not in ['DIMENSION', 'TAG', 'COST_CATEGORY']:
272
+ return {
273
+ 'error': 'Invalid group Type. Valid types are DIMENSION, TAG, and COST_CATEGORY.'
274
+ }
275
+
276
+ return {}
277
+ except Exception as e:
278
+ return {'error': f'Error validating group_by: {str(e)}'}