awslabs.cost-explorer-mcp-server 0.0.1__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 +3 -0
- awslabs/cost_explorer_mcp_server/__init__.py +6 -0
- awslabs/cost_explorer_mcp_server/helpers.py +221 -0
- awslabs/cost_explorer_mcp_server/server.py +408 -0
- awslabs_cost_explorer_mcp_server-0.0.1.dist-info/METADATA +187 -0
- awslabs_cost_explorer_mcp_server-0.0.1.dist-info/RECORD +10 -0
- awslabs_cost_explorer_mcp_server-0.0.1.dist-info/WHEEL +4 -0
- awslabs_cost_explorer_mcp_server-0.0.1.dist-info/entry_points.txt +2 -0
- awslabs_cost_explorer_mcp_server-0.0.1.dist-info/licenses/LICENSE +175 -0
- awslabs_cost_explorer_mcp_server-0.0.1.dist-info/licenses/NOTICE +2 -0
awslabs/__init__.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Helper functions for the Cost Explorer MCP server."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
from typing import Dict, Any, List, Tuple
|
|
6
|
+
import boto3
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
# Set up logging
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
# Initialize AWS Cost Explorer client
|
|
13
|
+
ce = boto3.client('ce')
|
|
14
|
+
|
|
15
|
+
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
|
+
|
|
19
|
+
Args:
|
|
20
|
+
date_str: The date string to validate
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Tuple of (is_valid, error_message)
|
|
24
|
+
"""
|
|
25
|
+
# Check format with regex
|
|
26
|
+
if not re.match(r'^\d{4}-\d{2}-\d{2}$', date_str):
|
|
27
|
+
return False, f"Date '{date_str}' is not in YYYY-MM-DD format"
|
|
28
|
+
|
|
29
|
+
# Check if it's a valid date
|
|
30
|
+
try:
|
|
31
|
+
datetime.strptime(date_str, '%Y-%m-%d')
|
|
32
|
+
return True, ""
|
|
33
|
+
except ValueError as e:
|
|
34
|
+
return False, f"Invalid date '{date_str}': {str(e)}"
|
|
35
|
+
|
|
36
|
+
def get_dimension_values(key: str, billing_period_start: str, billing_period_end: str) -> Dict[str, Any]:
|
|
37
|
+
"""Get available values for a specific dimension."""
|
|
38
|
+
# Validate date formats
|
|
39
|
+
is_valid_start, error_start = validate_date_format(billing_period_start)
|
|
40
|
+
if not is_valid_start:
|
|
41
|
+
return {'error': error_start}
|
|
42
|
+
|
|
43
|
+
is_valid_end, error_end = validate_date_format(billing_period_end)
|
|
44
|
+
if not is_valid_end:
|
|
45
|
+
return {'error': error_end}
|
|
46
|
+
|
|
47
|
+
# Validate date range
|
|
48
|
+
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
|
+
|
|
51
|
+
try:
|
|
52
|
+
response = ce.get_dimension_values(
|
|
53
|
+
TimePeriod={
|
|
54
|
+
'Start': billing_period_start,
|
|
55
|
+
'End': billing_period_end
|
|
56
|
+
},
|
|
57
|
+
Dimension=key.upper()
|
|
58
|
+
)
|
|
59
|
+
dimension_values = response['DimensionValues']
|
|
60
|
+
values = [value['Value'] for value in dimension_values]
|
|
61
|
+
return {'dimension': key.upper(), 'values': values}
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.error(f"Error getting dimension values: {e}")
|
|
64
|
+
return {'error': str(e)}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_tag_values(tag_key: str, billing_period_start: str, billing_period_end: str) -> Dict[str, Any]:
|
|
68
|
+
"""Get available values for a specific tag key."""
|
|
69
|
+
# Validate date formats
|
|
70
|
+
is_valid_start, error_start = validate_date_format(billing_period_start)
|
|
71
|
+
if not is_valid_start:
|
|
72
|
+
return {'error': error_start}
|
|
73
|
+
|
|
74
|
+
is_valid_end, error_end = validate_date_format(billing_period_end)
|
|
75
|
+
if not is_valid_end:
|
|
76
|
+
return {'error': error_end}
|
|
77
|
+
|
|
78
|
+
# Validate date range
|
|
79
|
+
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
|
+
|
|
82
|
+
try:
|
|
83
|
+
response = ce.get_tags(
|
|
84
|
+
TimePeriod={'Start': billing_period_start,
|
|
85
|
+
'End': billing_period_end},
|
|
86
|
+
TagKey=tag_key
|
|
87
|
+
)
|
|
88
|
+
tag_values = response['Tags']
|
|
89
|
+
return {'tag_key': tag_key, 'values': tag_values}
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.error(f"Error getting tag values: {e}")
|
|
92
|
+
return {'error': str(e)}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
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
|
+
Args:
|
|
101
|
+
expression: The filter expression to validate
|
|
102
|
+
billing_period_start: Start date of the billing period
|
|
103
|
+
billing_period_end: End date of the billing period
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Empty dictionary if valid, or an error dictionary
|
|
107
|
+
"""
|
|
108
|
+
# Validate date formats
|
|
109
|
+
is_valid_start, error_start = validate_date_format(billing_period_start)
|
|
110
|
+
if not is_valid_start:
|
|
111
|
+
return {'error': error_start}
|
|
112
|
+
|
|
113
|
+
is_valid_end, error_end = validate_date_format(billing_period_end)
|
|
114
|
+
if not is_valid_end:
|
|
115
|
+
return {'error': error_end}
|
|
116
|
+
|
|
117
|
+
# Validate date range
|
|
118
|
+
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
|
+
|
|
121
|
+
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".'}
|
|
126
|
+
|
|
127
|
+
dimension_key = dimension['Key']
|
|
128
|
+
dimension_values = dimension['Values']
|
|
129
|
+
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']
|
|
134
|
+
for value in dimension_values:
|
|
135
|
+
if value not in valid_values:
|
|
136
|
+
return {'error': f"Invalid value '{value}' for dimension '{dimension_key}'. Valid values are: {valid_values}"}
|
|
137
|
+
|
|
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".'}
|
|
142
|
+
|
|
143
|
+
tag_key = tag['Key']
|
|
144
|
+
tag_values = tag['Values']
|
|
145
|
+
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']
|
|
150
|
+
for value in tag_values:
|
|
151
|
+
if value not in valid_tag_values:
|
|
152
|
+
return {'error': f"Invalid value '{value}' for tag '{tag_key}'. Valid values are: {valid_tag_values}"}
|
|
153
|
+
|
|
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".'}
|
|
158
|
+
|
|
159
|
+
logical_operators = ['And', 'Or', 'Not']
|
|
160
|
+
logical_count = sum(1 for op in logical_operators if op in expression)
|
|
161
|
+
|
|
162
|
+
if logical_count > 1:
|
|
163
|
+
return {'error': 'Only one logical operator (And, Or, Not) is allowed per expression in filter parameter.'}
|
|
164
|
+
|
|
165
|
+
if logical_count == 0 and len(expression) > 1:
|
|
166
|
+
return {'error': 'Filter parameter with multiple expressions require a logical operator (And, Or, Not).'}
|
|
167
|
+
|
|
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']:
|
|
172
|
+
result = validate_expression(
|
|
173
|
+
sub_expression, billing_period_start, billing_period_end)
|
|
174
|
+
if 'error' in result:
|
|
175
|
+
return result
|
|
176
|
+
|
|
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']:
|
|
181
|
+
result = validate_expression(
|
|
182
|
+
sub_expression, billing_period_start, billing_period_end)
|
|
183
|
+
if 'error' in result:
|
|
184
|
+
return result
|
|
185
|
+
|
|
186
|
+
if 'Not' in expression:
|
|
187
|
+
if not isinstance(expression['Not'], dict):
|
|
188
|
+
return {'error': 'Not expression must be a single expression.'}
|
|
189
|
+
result = validate_expression(
|
|
190
|
+
expression['Not'], billing_period_start, billing_period_end)
|
|
191
|
+
if 'error' in result:
|
|
192
|
+
return result
|
|
193
|
+
|
|
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".'}
|
|
196
|
+
|
|
197
|
+
return {}
|
|
198
|
+
except Exception as e:
|
|
199
|
+
return {'error': f'Error validating expression: {str(e)}'}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def validate_group_by(group_by: Dict[str, Any]) -> Dict[str, Any]:
|
|
203
|
+
"""
|
|
204
|
+
Validate the group_by parameter.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
group_by: The group_by dictionary to validate
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Empty dictionary if valid, or an error dictionary
|
|
211
|
+
"""
|
|
212
|
+
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
|
+
|
|
219
|
+
return {}
|
|
220
|
+
except Exception as e:
|
|
221
|
+
return {'error': f'Error validating group_by: {str(e)}'}
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"""Cost Explorer MCP server implementation.
|
|
2
|
+
|
|
3
|
+
This server provides tools for analyzing AWS costs and usage data through the AWS Cost Explorer API.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import boto3
|
|
7
|
+
import logging
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
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
|
+
from awslabs.cost_explorer_mcp_server.helpers import (
|
|
15
|
+
get_dimension_values,
|
|
16
|
+
get_tag_values,
|
|
17
|
+
validate_expression,
|
|
18
|
+
validate_group_by,
|
|
19
|
+
validate_date_format
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Set up logging
|
|
23
|
+
logging.basicConfig(level=logging.INFO)
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
# Initialize AWS Cost Explorer client
|
|
27
|
+
ce = boto3.client('ce')
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DateRange(BaseModel):
|
|
31
|
+
"""Date range model for cost queries."""
|
|
32
|
+
start_date: str = Field(
|
|
33
|
+
...,
|
|
34
|
+
description="The start date of the billing period in YYYY-MM-DD format. Defaults to last month, if not provided."
|
|
35
|
+
)
|
|
36
|
+
end_date: str = Field(
|
|
37
|
+
...,
|
|
38
|
+
description="The end date of the billing period in YYYY-MM-DD format."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@field_validator('start_date')
|
|
42
|
+
@classmethod
|
|
43
|
+
def validate_start_date(cls, v):
|
|
44
|
+
is_valid, error = validate_date_format(v)
|
|
45
|
+
if not is_valid:
|
|
46
|
+
raise ValueError(error)
|
|
47
|
+
return v
|
|
48
|
+
|
|
49
|
+
@field_validator('end_date')
|
|
50
|
+
@classmethod
|
|
51
|
+
def validate_end_date(cls, v, info):
|
|
52
|
+
is_valid, error = validate_date_format(v)
|
|
53
|
+
if not is_valid:
|
|
54
|
+
raise ValueError(error)
|
|
55
|
+
|
|
56
|
+
# Access the start_date from the data dictionary
|
|
57
|
+
start_date = info.data.get('start_date')
|
|
58
|
+
if start_date and v < start_date:
|
|
59
|
+
raise ValueError(f"End date '{v}' cannot be before start date '{start_date}'")
|
|
60
|
+
|
|
61
|
+
return v
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class GroupBy(BaseModel):
|
|
65
|
+
"""Group by model for cost queries."""
|
|
66
|
+
type: str = Field(
|
|
67
|
+
...,
|
|
68
|
+
description="Type of grouping. Valid values are DIMENSION, TAG, and COST_CATEGORY."
|
|
69
|
+
)
|
|
70
|
+
key: str = Field(
|
|
71
|
+
...,
|
|
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."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class FilterExpression(BaseModel):
|
|
77
|
+
"""Filter expression model for cost queries."""
|
|
78
|
+
filter_json: str = Field(
|
|
79
|
+
...,
|
|
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']}}]}."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class CostMetric(BaseModel):
|
|
85
|
+
"""Cost metric model."""
|
|
86
|
+
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."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class DimensionKey(BaseModel):
|
|
93
|
+
"""Dimension key model."""
|
|
94
|
+
dimension_key: str = Field(
|
|
95
|
+
...,
|
|
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."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# Create FastMCP server
|
|
101
|
+
app = FastMCP(title="Cost Explorer MCP Server")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@app.tool("get_today_date")
|
|
105
|
+
async def get_today_date(ctx: Context) -> Dict[str, str]:
|
|
106
|
+
"""Retrieve current date information.
|
|
107
|
+
|
|
108
|
+
This tool retrieves the current date in YYYY-MM-DD format and the current month in YYYY-MM format.
|
|
109
|
+
It's useful for comparing if the billing period requested by the user is not in the future.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Dictionary containing today's date and current month
|
|
113
|
+
"""
|
|
114
|
+
return {
|
|
115
|
+
'today_date': datetime.now().strftime('%Y-%m-%d'),
|
|
116
|
+
'current_month': datetime.now().strftime('%Y-%m')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@app.tool("get_dimension_values")
|
|
121
|
+
async def get_dimension_values_tool(
|
|
122
|
+
ctx: Context,
|
|
123
|
+
date_range: DateRange,
|
|
124
|
+
dimension: DimensionKey
|
|
125
|
+
) -> Dict[str, Any]:
|
|
126
|
+
"""Retrieve available dimension values for AWS Cost Explorer.
|
|
127
|
+
|
|
128
|
+
This tool retrieves all available and valid values for a specified dimension (e.g., SERVICE, REGION)
|
|
129
|
+
over a period of time. This is useful for validating filter values or exploring available options
|
|
130
|
+
for cost analysis.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
date_range: The billing period start and end dates in YYYY-MM-DD format
|
|
134
|
+
dimension: The dimension key to retrieve values for (e.g., SERVICE, REGION, LINKED_ACCOUNT)
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Dictionary containing the dimension name and list of available values
|
|
138
|
+
"""
|
|
139
|
+
try:
|
|
140
|
+
response = get_dimension_values(
|
|
141
|
+
dimension.dimension_key,
|
|
142
|
+
date_range.start_date,
|
|
143
|
+
date_range.end_date
|
|
144
|
+
)
|
|
145
|
+
return response
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(f"Error getting dimension values: {e}")
|
|
148
|
+
return {'error': f'Error getting dimension values: {str(e)}'}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@app.tool("get_tag_values")
|
|
152
|
+
async def get_tag_values_tool(
|
|
153
|
+
ctx: Context,
|
|
154
|
+
date_range: DateRange,
|
|
155
|
+
tag_key: str = Field(..., description="The tag key to retrieve values for")
|
|
156
|
+
) -> Dict[str, Any]:
|
|
157
|
+
"""Retrieve available tag values for AWS Cost Explorer.
|
|
158
|
+
|
|
159
|
+
This tool retrieves all available values for a specified tag key over a period of time.
|
|
160
|
+
This is useful for validating tag filter values or exploring available tag options for cost analysis.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
date_range: The billing period start and end dates in YYYY-MM-DD format
|
|
164
|
+
tag_key: The tag key to retrieve values for
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Dictionary containing the tag key and list of available values
|
|
168
|
+
"""
|
|
169
|
+
try:
|
|
170
|
+
response = get_tag_values(
|
|
171
|
+
tag_key,
|
|
172
|
+
date_range.start_date,
|
|
173
|
+
date_range.end_date
|
|
174
|
+
)
|
|
175
|
+
return response
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.error(f"Error getting tag values: {e}")
|
|
178
|
+
return {'error': f'Error getting tag values: {str(e)}'}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@app.tool("get_cost_and_usage")
|
|
182
|
+
async def get_cost_and_usage(
|
|
183
|
+
ctx: Context,
|
|
184
|
+
date_range: DateRange,
|
|
185
|
+
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."
|
|
188
|
+
),
|
|
189
|
+
group_by: Optional[Union[Dict[str, str], str]] = Field(
|
|
190
|
+
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'."
|
|
192
|
+
),
|
|
193
|
+
filter_expression: Optional[Dict[str, Any]] = Field(
|
|
194
|
+
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']}}]}."
|
|
196
|
+
),
|
|
197
|
+
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
|
+
)
|
|
201
|
+
) -> Dict[str, Any]:
|
|
202
|
+
"""Retrieve AWS cost and usage data.
|
|
203
|
+
|
|
204
|
+
This tool retrieves AWS cost and usage data for AWS services during a specified billing period,
|
|
205
|
+
with optional filtering and grouping. It dynamically generates cost reports tailored to specific needs
|
|
206
|
+
by specifying parameters such as granularity, billing period dates, and filter criteria.
|
|
207
|
+
|
|
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
|
|
210
|
+
API which treats end_date as exclusive.
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
date_range: The billing period start and end dates in YYYY-MM-DD format (end date is inclusive)
|
|
215
|
+
granularity: The granularity at which cost data is aggregated (DAILY, MONTHLY, HOURLY)
|
|
216
|
+
group_by: Either a dictionary with Type and Key, or simply a string key to group by
|
|
217
|
+
filter_expression: Filter criteria as a Python dictionary
|
|
218
|
+
metric: Cost metric to use (UnblendedCost, BlendedCost, etc.)
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Dictionary containing cost report data grouped according to the specified parameters
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
# 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
|
+
|
|
230
|
+
billing_period_start = date_range.start_date
|
|
231
|
+
billing_period_end = date_range.end_date
|
|
232
|
+
|
|
233
|
+
# Define valid metrics and their expected data structure
|
|
234
|
+
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}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if metric not in valid_metrics:
|
|
245
|
+
return {'error': f"Invalid metric: {metric}. Valid values are {', '.join(valid_metrics.keys())}."}
|
|
246
|
+
|
|
247
|
+
metric_config = valid_metrics[metric]
|
|
248
|
+
|
|
249
|
+
# Adjust end date for Cost Explorer API (exclusive)
|
|
250
|
+
# 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')
|
|
253
|
+
|
|
254
|
+
# Process filter
|
|
255
|
+
filter_criteria = filter_expression
|
|
256
|
+
|
|
257
|
+
# Validate filter expression if provided
|
|
258
|
+
if filter_criteria:
|
|
259
|
+
# This validates both structure and values against AWS Cost Explorer
|
|
260
|
+
validation_result = validate_expression(
|
|
261
|
+
filter_criteria, billing_period_start, billing_period_end_adj)
|
|
262
|
+
if 'error' in validation_result:
|
|
263
|
+
return validation_result
|
|
264
|
+
|
|
265
|
+
# Process group_by
|
|
266
|
+
if not group_by:
|
|
267
|
+
group_by = {"Type": "DIMENSION", "Key": "SERVICE"}
|
|
268
|
+
elif isinstance(group_by, str):
|
|
269
|
+
group_by = {"Type": "DIMENSION", "Key": group_by}
|
|
270
|
+
|
|
271
|
+
# Validate group_by using the existing validate_group_by function
|
|
272
|
+
validation_result = validate_group_by(group_by)
|
|
273
|
+
if 'error' in validation_result:
|
|
274
|
+
return validation_result
|
|
275
|
+
|
|
276
|
+
# Prepare API call parameters
|
|
277
|
+
common_params = {
|
|
278
|
+
'TimePeriod': {
|
|
279
|
+
'Start': billing_period_start,
|
|
280
|
+
'End': billing_period_end_adj
|
|
281
|
+
},
|
|
282
|
+
'Granularity': granularity,
|
|
283
|
+
'GroupBy': [{'Type': group_by['Type'].upper(), 'Key': group_by['Key']}],
|
|
284
|
+
'Metrics': [metric]
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if filter_criteria:
|
|
288
|
+
common_params['Filter'] = filter_criteria
|
|
289
|
+
|
|
290
|
+
# Get cost data
|
|
291
|
+
grouped_costs = {}
|
|
292
|
+
next_token = None
|
|
293
|
+
while True:
|
|
294
|
+
if next_token:
|
|
295
|
+
common_params['NextPageToken'] = next_token
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
response = ce.get_cost_and_usage(**common_params)
|
|
299
|
+
except Exception as e:
|
|
300
|
+
logger.error(f"Error calling Cost Explorer API: {e}")
|
|
301
|
+
return {'error': f'AWS Cost Explorer API error: {str(e)}'}
|
|
302
|
+
|
|
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:
|
|
307
|
+
logger.warning(f"Skipping group with no keys: {group}")
|
|
308
|
+
continue
|
|
309
|
+
|
|
310
|
+
group_key = group['Keys'][0]
|
|
311
|
+
|
|
312
|
+
# 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
|
+
|
|
319
|
+
# 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
|
+
|
|
324
|
+
try:
|
|
325
|
+
metric_data = group['Metrics'][metric]
|
|
326
|
+
|
|
327
|
+
# 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
|
+
|
|
332
|
+
# Process based on metric type
|
|
333
|
+
if metric_config["is_cost"]:
|
|
334
|
+
# Handle cost metrics
|
|
335
|
+
cost = float(metric_data['Amount'])
|
|
336
|
+
grouped_costs.setdefault(date, {}).update({group_key: cost})
|
|
337
|
+
else:
|
|
338
|
+
# 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'
|
|
342
|
+
else:
|
|
343
|
+
unit = metric_data.get('Unit', 'Count')
|
|
344
|
+
amount = float(metric_data['Amount'])
|
|
345
|
+
grouped_costs.setdefault(date, {}).update({group_key: (amount, unit)})
|
|
346
|
+
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)}"}
|
|
349
|
+
|
|
350
|
+
next_token = response.get('NextPageToken')
|
|
351
|
+
if not next_token:
|
|
352
|
+
break
|
|
353
|
+
|
|
354
|
+
# Process results
|
|
355
|
+
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
|
+
|
|
359
|
+
try:
|
|
360
|
+
if metric_config["is_cost"]:
|
|
361
|
+
# Process cost metrics
|
|
362
|
+
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)
|
|
366
|
+
else:
|
|
367
|
+
# 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()})
|
|
370
|
+
units_df = pd.DataFrame(
|
|
371
|
+
{(k, 'Unit'): {k1: v1[1] for k1, v1 in v.items()} for k, v in grouped_costs.items()})
|
|
372
|
+
df = pd.concat([usage_df, units_df], axis=1)
|
|
373
|
+
|
|
374
|
+
result = {'GroupedCosts': df.to_dict()}
|
|
375
|
+
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}
|
|
378
|
+
|
|
379
|
+
result = {'GroupedCosts': df.to_dict()}
|
|
380
|
+
|
|
381
|
+
# Convert all keys to strings for JSON serialization
|
|
382
|
+
def stringify_keys(d):
|
|
383
|
+
if isinstance(d, dict):
|
|
384
|
+
return {str(k): stringify_keys(v) for k, v in d.items()}
|
|
385
|
+
elif isinstance(d, list):
|
|
386
|
+
return [stringify_keys(i) for i in d]
|
|
387
|
+
else:
|
|
388
|
+
return d
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
result = stringify_keys(result)
|
|
392
|
+
return result
|
|
393
|
+
except Exception as e:
|
|
394
|
+
logger.error(f"Error serializing result: {e}")
|
|
395
|
+
return {'error': f"Error serializing result: {str(e)}"}
|
|
396
|
+
|
|
397
|
+
except Exception as e:
|
|
398
|
+
logger.error(f"Error generating cost report: {e}")
|
|
399
|
+
import traceback
|
|
400
|
+
logger.error(f"Traceback: {traceback.format_exc()}")
|
|
401
|
+
return {'error': f'Error generating cost report: {str(e)}'}
|
|
402
|
+
|
|
403
|
+
def main():
|
|
404
|
+
"""Run the MCP server with CLI argument support."""
|
|
405
|
+
app.run()
|
|
406
|
+
|
|
407
|
+
if __name__ == "__main__":
|
|
408
|
+
main()
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: awslabs.cost-explorer-mcp-server
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: MCP server for analyzing AWS costs and usage data through the AWS Cost Explorer API
|
|
5
|
+
Project-URL: Homepage, https://awslabs.github.io/mcp/
|
|
6
|
+
Project-URL: Documentation, https://awslabs.github.io/mcp/servers/cost-explorer-mcp-server/
|
|
7
|
+
Project-URL: Source, https://github.com/awslabs/mcp.git
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/awslabs/mcp/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/awslabs/mcp/blob/main/src/cost-explorer-mcp-server/CHANGELOG.md
|
|
10
|
+
Author: Amazon Web Services
|
|
11
|
+
Author-email: AWSLabs MCP <203918161+awslabs-mcp@users.noreply.github.com>
|
|
12
|
+
License: Apache-2.0
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
License-File: NOTICE
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: boto3>=1.36.20
|
|
25
|
+
Requires-Dist: fastmcp>=0.1.0
|
|
26
|
+
Requires-Dist: pandas>=2.2.3
|
|
27
|
+
Requires-Dist: pydantic>=2.10.6
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# Cost Explorer MCP Server
|
|
31
|
+
|
|
32
|
+
MCP server for analyzing AWS costs and usage data through the AWS Cost Explorer API.
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
### Analyze AWS costs and usage data
|
|
37
|
+
|
|
38
|
+
- Get detailed breakdown of your AWS costs by service, region, and other dimensions
|
|
39
|
+
- Understand how costs are distributed across various services
|
|
40
|
+
- Query historical cost data for specific time periods
|
|
41
|
+
- Filter costs by various dimensions, tags, and cost categories
|
|
42
|
+
|
|
43
|
+
### Query cost data with natural language
|
|
44
|
+
|
|
45
|
+
- Ask questions about your AWS costs in plain English
|
|
46
|
+
- Get instant answers about your AWS spending patterns
|
|
47
|
+
- Retrieve historical cost data with simple queries
|
|
48
|
+
|
|
49
|
+
### Generate cost reports and insights
|
|
50
|
+
|
|
51
|
+
- Generate comprehensive cost reports based on your AWS Cost Explorer data
|
|
52
|
+
- Get cost breakdowns by various dimensions (service, region, account, etc.)
|
|
53
|
+
- Analyze usage patterns and spending trends
|
|
54
|
+
|
|
55
|
+
## Prerequisites
|
|
56
|
+
|
|
57
|
+
1. Install `uv` from [Astral](https://docs.astral.sh/uv/getting-started/installation/) or the [GitHub README](https://github.com/astral-sh/uv#installation)
|
|
58
|
+
2. Install Python using `uv python install 3.10`
|
|
59
|
+
3. Set up AWS credentials with access to AWS Cost Explorer
|
|
60
|
+
- You need an AWS account with appropriate permissions
|
|
61
|
+
- Configure AWS credentials with `aws configure` or environment variables
|
|
62
|
+
- Ensure your IAM role/user has permissions to access AWS Cost Explorer API
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
|
|
66
|
+
Here are some ways you can work with MCP across AWS, and we'll be adding support to more products including Amazon Q Developer CLI soon: (e.g. for Amazon Q Developer CLI MCP, `~/.aws/amazonq/mcp.json`):
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"mcpServers": {
|
|
71
|
+
"awslabs.cost-explorer-mcp-server": {
|
|
72
|
+
"command": "uvx",
|
|
73
|
+
"args": ["awslabs.cost-explorer-mcp-server@latest"],
|
|
74
|
+
"env": {
|
|
75
|
+
"FASTMCP_LOG_LEVEL": "ERROR",
|
|
76
|
+
"AWS_PROFILE": "your-aws-profile"
|
|
77
|
+
},
|
|
78
|
+
"disabled": false,
|
|
79
|
+
"autoApprove": []
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
or docker after a successful `docker build -t awslabs/cost-explorer-mcp-server .`:
|
|
86
|
+
|
|
87
|
+
```file
|
|
88
|
+
# fictitious `.env` file with AWS temporary credentials
|
|
89
|
+
AWS_ACCESS_KEY_ID=
|
|
90
|
+
AWS_SECRET_ACCESS_KEY=
|
|
91
|
+
AWS_SESSION_TOKEN=
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"mcpServers": {
|
|
97
|
+
"awslabs.cost-explorer-mcp-server": {
|
|
98
|
+
"command": "docker",
|
|
99
|
+
"args": [
|
|
100
|
+
"run",
|
|
101
|
+
"--rm",
|
|
102
|
+
"--interactive",
|
|
103
|
+
"--env",
|
|
104
|
+
"FASTMCP_LOG_LEVEL=ERROR",
|
|
105
|
+
"--env-file",
|
|
106
|
+
"/full/path/to/file/above/.env",
|
|
107
|
+
"awslabs/cost-explorer-mcp-server:latest"
|
|
108
|
+
],
|
|
109
|
+
"env": {},
|
|
110
|
+
"disabled": false,
|
|
111
|
+
"autoApprove": []
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
NOTE: Your credentials will need to be kept refreshed from your host
|
|
118
|
+
|
|
119
|
+
### AWS Authentication
|
|
120
|
+
|
|
121
|
+
The MCP server uses the AWS profile specified in the `AWS_PROFILE` environment variable. If not provided, it defaults to the "default" profile in your AWS configuration file.
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
"env": {
|
|
125
|
+
"AWS_PROFILE": "your-aws-profile"
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
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
|
+
## Security Considerations
|
|
131
|
+
|
|
132
|
+
### Required IAM Permissions
|
|
133
|
+
The following IAM permissions are required for this MCP server:
|
|
134
|
+
- ce:GetCostAndUsage
|
|
135
|
+
- ce:GetDimensionValues
|
|
136
|
+
- ce:GetTags
|
|
137
|
+
|
|
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
|
+
}
|
|
154
|
+
|
|
155
|
+
## Available Tools
|
|
156
|
+
|
|
157
|
+
The Cost Explorer MCP Server provides the following tools:
|
|
158
|
+
|
|
159
|
+
1. `get_today_date` - Get the current date and month to determine relevent data when answering last month.
|
|
160
|
+
2. `get_dimension_values` - Get available values for a specific dimension (e.g., SERVICE, REGION)
|
|
161
|
+
3. `get_tag_values` - Get available values for a specific tag key
|
|
162
|
+
4. `get_cost_and_usage` - Retrieve AWS cost and usage data with filtering and grouping options
|
|
163
|
+
|
|
164
|
+
## Example Usage
|
|
165
|
+
|
|
166
|
+
Here are some examples of how to use the Cost Explorer MCP Server:
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
### Get dimension values
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
What AWS services did I use last month?
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Generate a cost report
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
Show me my AWS costs for the last 3 months grouped by service in us-east-1 region
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
What were my EC2 costs excluding us-east-2 for January 2025?
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
|
|
@@ -0,0 +1,10 @@
|
|
|
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,,
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|