Skip to content

Commit ee549ff

Browse files
authored
feat(billing-cost-management-mcp-server): add cpu_vendor_architectures to Compute Optimizer EC2, ASG, and RDS recommendations (#4742)
Add an optional cpu_vendor_architectures parameter to get_ec2_instance_recommendations, get_auto_scaling_group_recommendations, and get_rds_recommendations to request Graviton recommendations. Values (CURRENT, AWS_ARM64) are matched case- and underscore-insensitively and sent as recommendationPreferences.cpuVendorArchitectures; the response echoes them as applied_recommendation_preferences. Other operations and invalid values are rejected with a structured validation_error.
1 parent 8055d68 commit ee549ff

3 files changed

Lines changed: 329 additions & 9 deletions

File tree

‎src/billing-cost-management-mcp-server/CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99
### Added
10+
- Added an optional `cpu_vendor_architectures` parameter to the AWS Compute Optimizer `get_ec2_instance_recommendations`, `get_auto_scaling_group_recommendations`, and `get_rds_recommendations` operations to request Graviton recommendations. It takes a JSON array (or a single value) of `CURRENT` and/or `AWS_ARM64`, matched case- and underscore-insensitively, and is sent as `recommendationPreferences.cpuVendorArchitectures`; without it the API ranks options within the current CPU architecture. The response echoes the preference as `applied_recommendation_preferences`. Other operations reject the parameter with a structured `validation_error`, as do values outside `CURRENT`/`AWS_ARM64`.
1011
- Added an optional `resource_arns` parameter to the `compute-optimizer` tool to look up recommendations for specific resources. It is sent as each operation's ARN filter (`instanceArns`, `autoScalingGroupArns`, `volumeArns`, `functionArns`, `serviceArns`, or `resourceArns` for RDS and idle), so a single resource no longer requires paging the full recommendation list. A single bare ARN is accepted as well as a JSON array. The region is taken from the ARNs when `region` is omitted; ARNs spanning multiple regions, or in a different region than `region`, are rejected.
1112
- Added an optional `order_by` parameter to the AWS Compute Optimizer `get_idle_recommendations` operation (`{"dimension": "SavingsValue"|"SavingsValueAfterDiscount", "order": "Asc"|"Desc"}`, matched case-insensitively). Idle results now default to `SavingsValueAfterDiscount` descending, so the first page holds the largest savings. Other operations reject `order_by` rather than ignore it.
1213
- Added more fields to the AWS Compute Optimizer recommendation responses, including finding reason codes, current performance risk, inferred workload types, migration effort, GPU info, savings after discounts, utilization metrics, lookback period, and effective recommendation preferences, plus Auto Scaling group capacity, EBS volume configuration, and RDS cluster and storage details. The EC2, Auto Scaling group, EBS, RDS, and ECS responses now also carry the API's response-level `errors` list (Lambda's API has none).

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

Lines changed: 157 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,19 @@
119119
default. To change it, pass `order_by` as a JSON object, e.g.
120120
'{"dimension": "SavingsValue", "order": "Asc"}' (dimension: SavingsValue or
121121
SavingsValueAfterDiscount; order: Asc or Desc). Only get_idle_recommendations accepts
122-
`order_by`.""",
122+
`order_by`.
123+
124+
EC2, Auto Scaling group, and RDS recommendations rank options within the current CPU
125+
architecture by default (cpuVendorArchitectures CURRENT). To get Graviton options, pass
126+
`cpu_vendor_architectures` as a JSON array, e.g. '["AWS_ARM64"]' (Graviton only). Values:
127+
CURRENT, AWS_ARM64. '["CURRENT", "AWS_ARM64"]' ranks both architectures in one pool and
128+
still returns only the top options (usually all Graviton); to compare x86 with Graviton,
129+
make one call without the parameter and one with '["AWS_ARM64"]'. This is a per-request
130+
preference, not an account setting; the result echoes it as
131+
`applied_recommendation_preferences`, and each recommendation's
132+
`effective_recommendation_preferences.cpuVendorArchitectures` reflects it. Only
133+
get_ec2_instance_recommendations, get_auto_scaling_group_recommendations, and
134+
get_rds_recommendations accept `cpu_vendor_architectures`.""",
123135
)
124136
async def compute_optimizer(
125137
ctx: Context,
@@ -131,6 +143,7 @@ async def compute_optimizer(
131143
next_token: Optional[str] = None,
132144
resource_arns: Optional[str] = None,
133145
order_by: Optional[str] = None,
146+
cpu_vendor_architectures: Optional[str] = None,
134147
) -> Dict[str, Any]:
135148
"""Retrieves recommendations from AWS Compute Optimizer.
136149
@@ -147,6 +160,9 @@ async def compute_optimizer(
147160
order_by: get_idle_recommendations only. Optional JSON object
148161
{"dimension": "SavingsValue"|"SavingsValueAfterDiscount", "order": "Asc"|"Desc"};
149162
defaults to SavingsValueAfterDiscount, Desc
163+
cpu_vendor_architectures: EC2, Auto Scaling group, and RDS only. Optional JSON
164+
array of CURRENT and/or AWS_ARM64, sent as
165+
recommendationPreferences.cpuVendorArchitectures; the API defaults to CURRENT
150166
151167
Returns:
152168
Dict containing the Compute Optimizer recommendations
@@ -172,6 +188,23 @@ async def compute_optimizer(
172188
{'provided_order_by': order_by},
173189
)
174190

191+
# Only EC2, ASG, and RDS take a CPU architecture preference; reject it elsewhere.
192+
cpu_architectures = None
193+
if cpu_vendor_architectures and operation in _FILTER_SPECS:
194+
if operation not in _CPU_VENDOR_ARCHITECTURE_OPERATIONS:
195+
return _error_response(
196+
operation,
197+
'validation_error',
198+
'cpu_vendor_architectures is supported only by '
199+
f'{", ".join(_CPU_VENDOR_ARCHITECTURE_OPERATIONS)}, not {operation}.',
200+
{'provided_cpu_vendor_architectures': cpu_vendor_architectures},
201+
)
202+
cpu_architectures, error = _resolve_cpu_vendor_architectures(
203+
operation, cpu_vendor_architectures
204+
)
205+
if error:
206+
return error
207+
175208
# Initialize Compute Optimizer client using shared utility
176209
co_client = create_aws_client('compute-optimizer', region_name=region)
177210

@@ -227,11 +260,25 @@ async def compute_optimizer(
227260
# Process the operation
228261
if operation == 'get_ec2_instance_recommendations':
229262
return await get_ec2_instance_recommendations(
230-
ctx, co_client, max_results, filters, account_ids, next_token, arn_list
263+
ctx,
264+
co_client,
265+
max_results,
266+
filters,
267+
account_ids,
268+
next_token,
269+
arn_list,
270+
cpu_vendor_architectures=cpu_architectures,
231271
)
232272
elif operation == 'get_auto_scaling_group_recommendations':
233273
return await get_auto_scaling_group_recommendations(
234-
ctx, co_client, max_results, filters, account_ids, next_token, arn_list
274+
ctx,
275+
co_client,
276+
max_results,
277+
filters,
278+
account_ids,
279+
next_token,
280+
arn_list,
281+
cpu_vendor_architectures=cpu_architectures,
235282
)
236283
elif operation == 'get_ebs_volume_recommendations':
237284
return await get_ebs_volume_recommendations(
@@ -243,7 +290,14 @@ async def compute_optimizer(
243290
)
244291
elif operation == 'get_rds_recommendations':
245292
return await get_rds_recommendations(
246-
ctx, co_client, max_results, filters, account_ids, next_token, arn_list
293+
ctx,
294+
co_client,
295+
max_results,
296+
filters,
297+
account_ids,
298+
next_token,
299+
arn_list,
300+
cpu_vendor_architectures=cpu_architectures,
247301
)
248302
elif operation == 'get_ecs_service_recommendations':
249303
return await get_ecs_service_recommendations(
@@ -495,6 +549,14 @@ def _resolve_region_for_arns(arns: List[str], region: Optional[str]) -> Optional
495549
_IDLE_ORDER_BY_ORDERS = ('Asc', 'Desc')
496550
_IDLE_DEFAULT_ORDER_BY = {'dimension': 'SavingsValueAfterDiscount', 'order': 'Desc'}
497551

552+
# Operations whose request takes recommendationPreferences.cpuVendorArchitectures.
553+
_CPU_VENDOR_ARCHITECTURE_OPERATIONS = (
554+
'get_ec2_instance_recommendations',
555+
'get_auto_scaling_group_recommendations',
556+
'get_rds_recommendations',
557+
)
558+
_CPU_VENDOR_ARCHITECTURES = ('CURRENT', 'AWS_ARM64')
559+
498560
_DEFAULT_MAX_RESULTS_LIMIT = 1000
499561
_MAX_RESULTS_LIMITS = {'get_idle_recommendations': 100}
500562

@@ -664,6 +726,7 @@ def _build_request_params(
664726
next_token: Optional[str],
665727
resource_arns: Optional[List[str]],
666728
arn_param: str,
729+
cpu_vendor_architectures: Optional[List[str]] = None,
667730
) -> Tuple[Dict[str, Any], Optional[Dict[str, Any]]]:
668731
"""Build the request for a get_*_recommendations call, validating locally first.
669732
@@ -710,9 +773,52 @@ def _build_request_params(
710773
if resource_arns:
711774
request_params[arn_param] = resource_arns
712775

776+
if cpu_vendor_architectures:
777+
request_params['recommendationPreferences'] = {
778+
'cpuVendorArchitectures': cpu_vendor_architectures
779+
}
780+
713781
return request_params, None
714782

715783

784+
def _resolve_cpu_vendor_architectures(
785+
operation: str, cpu_vendor_architectures: str
786+
) -> Tuple[Optional[List[str]], Optional[Dict[str, Any]]]:
787+
"""Parse cpu_vendor_architectures onto the API's enum spelling.
788+
789+
Accepts a JSON array or a single bare value. Values fold case and underscores (the API
790+
rejects e.g. `aws_arm64`); duplicates are dropped, order kept.
791+
792+
Returns:
793+
(architectures, None), or (None, error response) for an invalid value.
794+
"""
795+
valid = list(_CPU_VENDOR_ARCHITECTURES)
796+
text = cpu_vendor_architectures.strip()
797+
parsed: Any = parse_json(text, 'cpu_vendor_architectures') if text.startswith('[') else [text]
798+
if not isinstance(parsed, list) or not parsed:
799+
return None, _error_response(
800+
operation,
801+
'validation_error',
802+
'cpu_vendor_architectures must be a JSON array like ["AWS_ARM64"].',
803+
{'provided_cpu_vendor_architectures': parsed, 'valid_values': valid},
804+
)
805+
canonical = {_filter_value_key(v): v for v in valid}
806+
resolved: List[str] = []
807+
for value in parsed:
808+
match = canonical.get(_filter_value_key(value)) if isinstance(value, str) else None
809+
if match is None:
810+
return None, _error_response(
811+
operation,
812+
'validation_error',
813+
f'Invalid cpu_vendor_architectures value: {value}. Must be one of: '
814+
f'{", ".join(valid)}.',
815+
{'provided_cpu_vendor_architectures': parsed, 'valid_values': valid},
816+
)
817+
if match not in resolved:
818+
resolved.append(match)
819+
return resolved, None
820+
821+
716822
def _resolve_idle_order_by(
717823
order_by: Optional[str],
718824
) -> Tuple[Dict[str, str], Optional[Dict[str, Any]]]:
@@ -837,6 +943,10 @@ def _new_formatted_response(
837943
if 'filters' in request_params:
838944
# Echo what was actually queried (post-normalization), not what was typed.
839945
formatted_response['applied_filters'] = request_params['filters']
946+
if 'recommendationPreferences' in request_params:
947+
formatted_response['applied_recommendation_preferences'] = request_params[
948+
'recommendationPreferences'
949+
]
840950
return formatted_response
841951

842952

@@ -853,12 +963,26 @@ def _format_metrics(metrics: Optional[List[Dict[str, Any]]]) -> List[Dict[str, A
853963

854964

855965
async def get_ec2_instance_recommendations(
856-
ctx, co_client, max_results, filters, account_ids, next_token, resource_arns=None
966+
ctx,
967+
co_client,
968+
max_results,
969+
filters,
970+
account_ids,
971+
next_token,
972+
resource_arns=None,
973+
cpu_vendor_architectures=None,
857974
):
858975
"""Get EC2 instance recommendations."""
859976
operation = 'get_ec2_instance_recommendations'
860977
request_params, error = _build_request_params(
861-
operation, max_results, filters, account_ids, next_token, resource_arns, 'instanceArns'
978+
operation,
979+
max_results,
980+
filters,
981+
account_ids,
982+
next_token,
983+
resource_arns,
984+
'instanceArns',
985+
cpu_vendor_architectures,
862986
)
863987
if error:
864988
return error
@@ -942,7 +1066,14 @@ def _format_asg_configuration(config: Dict[str, Any]) -> Dict[str, Any]:
9421066

9431067

9441068
async def get_auto_scaling_group_recommendations(
945-
ctx, co_client, max_results, filters, account_ids, next_token, resource_arns=None
1069+
ctx,
1070+
co_client,
1071+
max_results,
1072+
filters,
1073+
account_ids,
1074+
next_token,
1075+
resource_arns=None,
1076+
cpu_vendor_architectures=None,
9461077
):
9471078
"""Get Auto Scaling group recommendations."""
9481079
operation = 'get_auto_scaling_group_recommendations'
@@ -954,6 +1085,7 @@ async def get_auto_scaling_group_recommendations(
9541085
next_token,
9551086
resource_arns,
9561087
'autoScalingGroupArns',
1088+
cpu_vendor_architectures,
9571089
)
9581090
if error:
9591091
return error
@@ -1162,7 +1294,14 @@ async def get_lambda_function_recommendations(
11621294

11631295

11641296
async def get_rds_recommendations(
1165-
ctx, co_client, max_results, filters, account_ids, next_token, resource_arns=None
1297+
ctx,
1298+
co_client,
1299+
max_results,
1300+
filters,
1301+
account_ids,
1302+
next_token,
1303+
resource_arns=None,
1304+
cpu_vendor_architectures=None,
11661305
):
11671306
"""Get Amazon RDS and Aurora database recommendations.
11681307
@@ -1174,6 +1313,8 @@ async def get_rds_recommendations(
11741313
account_ids: Optional list of account IDs as JSON string
11751314
next_token: Pagination token
11761315
resource_arns: Optional list of RDS DB instance or cluster ARNs
1316+
cpu_vendor_architectures: Optional resolved list for
1317+
recommendationPreferences.cpuVendorArchitectures
11771318
11781319
Returns:
11791320
Dict containing RDS instance recommendations
@@ -1183,7 +1324,14 @@ async def get_rds_recommendations(
11831324

11841325
operation = 'get_rds_recommendations'
11851326
request_params, error = _build_request_params(
1186-
operation, max_results, filters, account_ids, next_token, resource_arns, 'resourceArns'
1327+
operation,
1328+
max_results,
1329+
filters,
1330+
account_ids,
1331+
next_token,
1332+
resource_arns,
1333+
'resourceArns',
1334+
cpu_vendor_architectures,
11871335
)
11881336
if error:
11891337
return error

0 commit comments

Comments
 (0)