Skip to content

Commit 9a53397

Browse files
fix(billing-cost-management-mcp-server): name INSTANCE_TYPE_FAMILY for SP coverage, document SP filters, add sort_by (#4901)
* fix(billing-cost-management-mcp-server): name INSTANCE_TYPE_FAMILY for SP coverage, document SP filters, add sort_by The sp-performance description told callers to use INSTANCE_FAMILY for GetSavingsPlansCoverage group_by and filter. The service rejects that key; it accepts INSTANCE_TYPE_FAMILY, the same key the utilization operations use. Name INSTANCE_TYPE_FAMILY everywhere and drop the note contrasting the two. Also document what the description left out: - get_savings_plans_utilization filter dimensions, and that only Dimensions, And and Not are allowed (Or, Tags and CostCategories are rejected). The filter text no longer says "And/Or", since Or is not accepted. - SAVINGS_PLANS_TYPE is not a coverage dimension. - The exact PAYMENT_OPTION and SAVINGS_PLANS_TYPE value spellings; any other spelling returns a DataUnavailableException with no service message rather than a ValidationException, so check the spelling before moving the window. Add an optional sort_by (JSON string, passed through as SortBy) for coverage and utilization details, with each operation's sort keys. The API sorts before paging, so sort_by plus max_items gives a top N. Utilization ignores it: sorting there reorders the time periods out of date order. * fix(billing-cost-management-mcp-server): list all SP utilization details filters; note coverage sort needs group_by get_savings_plans_utilization_details also filters on SAVINGS_PLANS_TYPE and on the plan end-date bounds AGREEMENT_END_DATE_TIME_AFTER and AGREEMENT_END_DATE_TIME_BEFORE (YYYY-MM-DD). The description listed only five dimensions and scoped SAVINGS_PLANS_TYPE to utilization, steering callers away from a filter details accepts. Sorting coverage by InstanceFamily, Region or Service needs that dimension in group_by; otherwise the API returns "SortKey=[Region] should be in groupBy list to provide sorting". Say so next to the coverage sort keys. * docs(billing-cost-management-mcp-server): say sp-performance sorts before max_items State the order of operations for sort_by and max_items: the API sorts every matching item by the key first and then pages, and max_items=N keeps the first N of that order. sort_by plus max_items=N is therefore the top N by the key, not N arbitrary items sorted afterwards. Checked live: with group_by INSTANCE_TYPE_FAMILY and sort_by OnDemandCost DESCENDING, max_items=3 returned exactly the first 3 groups of the full sorted list, while the same request without sort_by returned a different 3.
1 parent 1524b38 commit 9a53397

2 files changed

Lines changed: 293 additions & 14 deletions

File tree

‎src/billing-cost-management-mcp-server/awslabs/billing_cost_management_mcp_server/tools/sp_performance_tools.py‎

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,12 @@
5151
3. get_savings_plans_utilization_details: Shows detailed per-Savings Plan utilization
5252
5353
COVERAGE grouping and filtering (get_savings_plans_coverage):
54-
- group_by accepts ONLY INSTANCE_FAMILY, REGION, or SERVICE. LINKED_ACCOUNT is
54+
- group_by accepts ONLY INSTANCE_TYPE_FAMILY, REGION, or SERVICE. LINKED_ACCOUNT is
5555
NOT a valid group_by attribute — grouping by it returns a ValidationException.
56-
- filter accepts LINKED_ACCOUNT, REGION, SERVICE, INSTANCE_FAMILY. To see coverage
56+
- filter accepts LINKED_ACCOUNT, REGION, SERVICE, INSTANCE_TYPE_FAMILY. To see coverage
5757
per member account, FILTER by one LINKED_ACCOUNT and call once per account; do
58-
not try to group_by LINKED_ACCOUNT.
58+
not try to group_by LINKED_ACCOUNT. SAVINGS_PLANS_TYPE is not a coverage dimension
59+
(ValidationException).
5960
- Each group_by entry is a GroupDefinition with Type and Key, e.g.
6061
`[{"Type": "DIMENSION", "Key": "REGION"}]`. There is no "Value" field in a
6162
group_by entry — a `{Key, Value}` shape is rejected (seen as a boto
@@ -77,14 +78,46 @@
7778
utilization or coverage) and do NOT resend the identical request. Correct an unknown
7879
filter value; query an earlier window only if the question allows it.
7980
81+
UTILIZATION filters (get_savings_plans_utilization):
82+
- filter accepts LINKED_ACCOUNT, SAVINGS_PLAN_ARN, SAVINGS_PLANS_TYPE, REGION,
83+
PAYMENT_OPTION, INSTANCE_TYPE_FAMILY. Only Dimensions, And and Not are allowed; an Or,
84+
Tags or CostCategories expression returns a ValidationException.
85+
8086
UTILIZATION DETAILS filters (get_savings_plans_utilization_details):
81-
- filter accepts LINKED_ACCOUNT, SAVINGS_PLAN_ARN, REGION, PAYMENT_OPTION,
82-
INSTANCE_TYPE_FAMILY (note INSTANCE_TYPE_FAMILY here, INSTANCE_FAMILY for coverage).
83-
- The filter is a Cost Explorer Expression. And/Or each require at least TWO
87+
- filter accepts LINKED_ACCOUNT, SAVINGS_PLAN_ARN, SAVINGS_PLANS_TYPE, REGION,
88+
PAYMENT_OPTION, INSTANCE_TYPE_FAMILY, AGREEMENT_END_DATE_TIME_AFTER and
89+
AGREEMENT_END_DATE_TIME_BEFORE (one YYYY-MM-DD value each, compared with each plan's
90+
end date).
91+
- The filter is a Cost Explorer Expression. And requires at least TWO
8492
operands ("And expression must have at least 2 operands"). For a single
8593
condition, pass the leaf object directly — e.g.
8694
`{"Dimensions": {"Key": "REGION", "Values": ["us-east-1"]}}` — not wrapped in
87-
a one-element `And`/`Or`.
95+
a one-element `And`.
96+
97+
FILTER VALUES (exact, case-sensitive spellings):
98+
- PAYMENT_OPTION: "No Upfront", "Partial Upfront", "All Upfront".
99+
- SAVINGS_PLANS_TYPE (get_savings_plans_utilization and
100+
get_savings_plans_utilization_details; not accepted by coverage): "ComputeSavingsPlans",
101+
"EC2InstanceSavingsPlans", "SageMakerSavingsPlans", "DatabaseSavingsPlans".
102+
- Any other spelling (NO_UPFRONT, Compute, COMPUTE_SP) matches nothing and returns a
103+
DataUnavailableException, which the service sends without a message, not a
104+
ValidationException. On that error with a filter set, check these spellings before
105+
changing the window.
106+
107+
SORTING (sort_by; get_savings_plans_coverage and get_savings_plans_utilization_details):
108+
- A JSON string `{"Key": "...", "SortOrder": "ASCENDING" or "DESCENDING"}`.
109+
- Order of operations: the API sorts every matching item by the key first, then pages;
110+
max_items=N keeps the first N of that sorted order. So sort_by + max_items=N returns the
111+
top N items by the key across everything that matches, not N arbitrary items sorted
112+
afterwards (see PAGINATION for what an item is).
113+
- Coverage keys: CoveragePercentage, SpendCoveredBySavingsPlan (singular, unlike the
114+
metric), OnDemandCost, TotalCost, InstanceFamily, Region, Service. Sort keys are not
115+
dimension names: sort by InstanceFamily, group by INSTANCE_TYPE_FAMILY. Sorting coverage
116+
by InstanceFamily, Region or Service needs that dimension in group_by
117+
(INSTANCE_TYPE_FAMILY, REGION or SERVICE); otherwise the API returns a ValidationException.
118+
- Utilization details keys: UtilizationPercentage, TotalCommitment, UsedCommitment,
119+
UnusedCommitment, NetSavings, AmortizedRecurringCommitment, AmortizedUpfrontCommitment.
120+
- get_savings_plans_utilization ignores sort_by and returns its periods in date order.
88121
89122
RESPONSE PERIODS (all three operations):
90123
- `requested_time_period` echoes the start_date/end_date that were sent.
@@ -99,7 +132,7 @@
99132
IMPORTANT: For get_savings_plans_coverage, `granularity` and `group_by` are mutually
100133
exclusive. If `group_by` is provided, `granularity` is ignored (the Cost Explorer
101134
GetSavingsPlansCoverage API does not allow both). Valid `group_by` dimensions for
102-
coverage are SERVICE, REGION, or INSTANCE_FAMILY.
135+
coverage are SERVICE, REGION, or INSTANCE_TYPE_FAMILY.
103136
104137
PAGINATION (get_savings_plans_coverage, where an item is one period x group entry, and
105138
get_savings_plans_utilization_details, where an item is one Savings Plan;
@@ -125,6 +158,7 @@ async def sp_performance(
125158
filter: Optional[str] = None,
126159
max_items: Optional[int] = None,
127160
next_token: Optional[str] = None,
161+
sort_by: Optional[str] = None,
128162
) -> Dict[str, Any]:
129163
"""Tool that retrieves AWS Savings Plans coverage and utilization data.
130164
@@ -135,12 +169,17 @@ async def sp_performance(
135169
end_date: End date in YYYY-MM-DD format (exclusive). Defaults to today if not provided. Savings Plans data for the last 2-3 days (counting today) is not processed yet: a start_date in those days returns a DataUnavailableException, and a window reaching into them leaves them out.
136170
granularity: Time granularity of the data (DAILY or MONTHLY). Defaults to DAILY. For coverage, ignored when group_by is provided (granularity and group_by are mutually exclusive).
137171
metrics: List of metrics to retrieve as a JSON string. For coverage, only 'SpendCoveredBySavingsPlans' is valid.
138-
group_by: Optional grouping of results as a JSON string. For coverage, supports SERVICE, REGION, or INSTANCE_FAMILY (NOT LINKED_ACCOUNT). Each entry is {"Type": ..., "Key": ...}; there is no "Value" field.
139-
filter: Optional filter to apply to the results as a JSON string. Coverage filter dimensions: LINKED_ACCOUNT, REGION, SERVICE, INSTANCE_FAMILY. And/Or need >=2 operands; for one condition pass the leaf object directly.
172+
group_by: Optional grouping of results as a JSON string. For coverage, supports SERVICE, REGION, or INSTANCE_TYPE_FAMILY (NOT LINKED_ACCOUNT). Each entry is {"Type": ..., "Key": ...}; there is no "Value" field.
173+
filter: Optional filter to apply to the results as a JSON string. Coverage filter dimensions: LINKED_ACCOUNT, REGION, SERVICE, INSTANCE_TYPE_FAMILY. And needs >=2 operands; for one condition pass the leaf object directly. Values are case-sensitive; see FILTER VALUES in the tool description.
140174
max_items: Total number of items to return across all pages. Omit it to fetch every
141-
page. Coverage and utilization details only.
175+
page. Coverage and utilization details only. With sort_by, these are the first N
176+
items in the requested SortOrder (the top N), not N items sorted afterwards.
142177
next_token: The pagination.next_token from a previous response; continue with otherwise
143178
identical parameters. Coverage and utilization details only.
179+
sort_by: Optional sort as a JSON string, e.g. {"Key": "CoveragePercentage", "SortOrder": "ASCENDING"}.
180+
Applied before max_items, so sort_by + max_items=N returns the top N.
181+
Coverage and utilization details only (ignored by get_savings_plans_utilization);
182+
the valid keys differ per operation (see SORTING).
144183
145184
Returns:
146185
Dict containing the savings plans coverage/utilization information
@@ -163,14 +202,15 @@ async def sp_performance(
163202
filter,
164203
max_items,
165204
next_token,
205+
sort_by,
166206
)
167207
elif operation == 'get_savings_plans_utilization':
168208
return await get_savings_plans_utilization(
169209
ctx, ce_client, start_date, end_date, granularity, filter
170210
)
171211
elif operation == 'get_savings_plans_utilization_details':
172212
return await get_savings_plans_utilization_details(
173-
ctx, ce_client, start_date, end_date, filter, max_items, next_token
213+
ctx, ce_client, start_date, end_date, filter, max_items, next_token, sort_by
174214
)
175215
else:
176216
return format_response(
@@ -205,6 +245,7 @@ async def get_savings_plans_coverage(
205245
filter_expr: Optional[str],
206246
max_items: Optional[int] = None,
207247
next_token: Optional[str] = None,
248+
sort_by: Optional[str] = None,
208249
) -> Dict[str, Any]:
209250
"""Retrieves Savings Plans coverage data.
210251
@@ -219,6 +260,7 @@ async def get_savings_plans_coverage(
219260
filter_expr: Filter expression as JSON string
220261
max_items: Total number of coverage entries to return. None fetches every page.
221262
next_token: The token from a previous response.
263+
sort_by: Sort definition as JSON string
222264
223265
Returns:
224266
Dict containing coverage data
@@ -256,6 +298,9 @@ async def get_savings_plans_coverage(
256298
if filter_expr:
257299
request_params['Filter'] = parse_json(filter_expr, 'filter')
258300

301+
if sort_by:
302+
request_params['SortBy'] = parse_json(sort_by, 'sort_by')
303+
259304
all_coverages, _, pagination = await paginate_aws_items(
260305
ctx,
261306
'GetSavingsPlansCoverage',
@@ -374,6 +419,7 @@ async def get_savings_plans_utilization_details(
374419
filter_expr: Optional[str],
375420
max_items: Optional[int] = None,
376421
next_token: Optional[str] = None,
422+
sort_by: Optional[str] = None,
377423
) -> Dict[str, Any]:
378424
"""Retrieves detailed Savings Plans utilization data.
379425
@@ -385,6 +431,7 @@ async def get_savings_plans_utilization_details(
385431
filter_expr: Filter expression as JSON string
386432
max_items: Total number of Savings Plans to return. None fetches every page.
387433
next_token: The token from a previous response.
434+
sort_by: Sort definition as JSON string
388435
389436
Returns:
390437
Dict containing detailed utilization data
@@ -408,6 +455,9 @@ async def get_savings_plans_utilization_details(
408455
if filter_expr:
409456
request_params['Filter'] = parse_json(filter_expr, 'filter')
410457

458+
if sort_by:
459+
request_params['SortBy'] = parse_json(sort_by, 'sort_by')
460+
411461
all_details, pages, pagination = await paginate_aws_items(
412462
ctx,
413463
'GetSavingsPlansUtilizationDetails',

0 commit comments

Comments
 (0)