Skip to content

feat(billing-cost-management-mcp-server)!: move remaining tools to max_items and drop old helper - #4788

Open
finklen-amazon wants to merge 8 commits into
awslabs:mainfrom
finklen-amazon:bcm-pagination-3-special-and-cleanup
Open

finklen-amazon wants to merge 8 commits into
awslabs:mainfrom
finklen-amazon:bcm-pagination-3-special-and-cleanup

Conversation

@finklen-amazon

@finklen-amazon finklen-amazon commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Part 3 of 3; #4786 and #4787 have merged. Supersedes part of #4785:

  1. feat(billing-cost-management-mcp-server)!: add paginate_aws_items and move three tools to it #4786 - shared helper (paginate_aws_items, PageSpec, envelope, error contract, SQL offload) and three worked examples: cost-category, cost-allocation-tags, cost-anomaly.
  2. feat(billing-cost-management-mcp-server)!: move list tools to max_items #4787 - tools that only need a PageSpec, the response formatter and the service's items passed through: invoicing, invoice-units, billing views, get-billing-preferences, procurement-preferences, enterprise-support, sp-purchase-analyzer, sp-explorer, free-tier-usage, bcm-pricing-calc, Billing Conductor, compute-optimizer.
  3. feat(billing-cost-management-mcp-server)!: move remaining tools to max_items and drop old helper #4788 - tools that need merging, first-page, multi-region, ranking or nested-count logic: cost-explorer, cost-comparison, ri-performance, sp-performance, sp-recommendation, credits, Cost Optimization Hub, Compute Optimizer Automation, budgets, AWS Pricing; then removal of the old paginate_aws_response and the SQL offload's support for the old pagination shapes.

Fixes

Summary

Changes

Moves the remaining tools, those that need merging, first-page, multi-region, ranking or nested-count logic on top of paginate_aws_items from #4786, then removes the old paginate_aws_response and the SQL offload's support for the old pagination shapes. The contract, envelope and helper are described in #4786. After this PR the tree matches #4785, with the helper named paginate_aws_items, except that AWS Pricing keeps the service's order and the page-size changes below.

Tools changed:

Tool Files Notes
cost-explorer cost_explorer_operations.py, cost_explorer_tools.py getCostAndUsage, getCostAndUsageWithResources, getDimensionValues, getTagsOrValues and getCostCategories fetched only the first page unless max_pages or next_token was passed; they now fetch every page unless max_items is passed. max_items counts the groups nested in each period. GroupDefinitions/TotalSize come from page 1 and DimensionValueAttributes is concatenated. Paginated getCostCategories without cost_category_name read names from CostCategories instead of CostCategoryNames. Three false "automatic pagination" claims in the description are removed.
cost-comparison cost_comparison_tools.py Requested 10 comparisons per page although the API allows 2,000 (10 for drivers), multiplying paid Cost Explorer requests.
ri-performance ri_performance_tools.py Coverage and utilization made an extra request for Total that reused the last page's token (wrong window, extra cost). Total now comes from the first page. Grouped get_reservation_utilization takes max_pages (pages of 500 groups) instead of max_items, because its token only works with the same MaxResults on every page; max_items there, and max_pages on coverage, return validation_error.
sp-performance sp_performance_tools.py Coverage Total now comes from the first page, as above. getSavingsPlansUtilization sent a NextToken parameter the API does not have; it is not paginated and no longer takes paging parameters.
sp-recommendation sp_recommendation_tools.py The hand-written loop is replaced; the recommendation summary comes from page 1. next_page_token is renamed to next_token.
credits credits_operations.py, credits_tools.py The botocore paginator is replaced by paginate_aws_items.
cost-optimization (Cost Optimization Hub) cost_optimization_hub_helpers.py, cost_optimization_hub_tools.py List operations returned only the first page unless max_pages or next_token was passed. Summary aggregates come from page 1. The performance ranking reads complete_dataset to tell a partial result.
compute-optimizer-automation compute_optimizer_automation_operations.py, compute_optimizer_automation_tools.py The hand-written loops are replaced. Multi-region calls apply max_items per region; a failed region leaves the result partial with has_more: false.
budgets, budget-actions, budget-notifications budget_tools.py DescribeBudgetNotificationsForAccount counts budgets per page, each with up to 10 notifications, so its PageSpec uses items_per_page_unit and count_items. The budget_name filter ran after the result was cut to max_results, so a budget beyond the cut was reported as missing; a name now calls DescribeBudget.
pricing aws_pricing_operations.py, aws_pricing_tools.py get_service_codes sent max_results unchecked as MaxResults, so values above 100 failed with ValidationException. total_count (len() of the returned list, misleading on partial results) is removed; use pagination.items_returned. get_service_codes and get_attribute_values no longer sort each response: sorting a slice does not give one order across pages, and Python's sort differs from the service's order when one value is a prefix of another (the service returns us-east-1-x before us-east-1). They return values in the order the service returns them, which is the same across pages in practice, though the AWS documentation does not guarantee an order. get_service_attributes is not paginated and still sorts its attribute names.

Page sizes:

  • utilities/aws_service_base.py: paginate_aws_items takes a keyword max_pages, mutually exclusive with max_items (ValueError naming both; max_pages < 1 is also a ValueError). It fetches at most that many pages at the spec's page size and returns every item with the service's token; a result it cuts short warns to call again with a larger max_pages. The time limit, repeated-token guard and failure handling are unchanged.
  • ri-performance (ri_performance_tools.py): grouped get_reservation_utilization is the only paginated API here whose token is bound to MaxResults (a continuation with a different, or omitted vs explicit, MaxResults fails with InvalidNextTokenException). It is requested at 500 groups per page (the API allows up to 7,000) and takes max_pages, so a result neither over- nor undershoots and the agent does not count items. Ungrouped utilization returns every period in one response and ignores both.
  • Billing Conductor get-billing-group-cost-report (migrated in feat(billing-cost-management-mcp-server)!: move list tools to max_items #4787; billing_conductor_operations.py, billing_conductor_tools.py): its token is bound to Arn, GroupBy and BillingPeriodRange, not to MaxResults, so it no longer sends a fixed 300. A new PageSpec.page_min (smallest page size the service accepts) raises a smaller request to 200, and the stop rule counts that size: max_items=500 requests 300 then 200, max_items=450 stops after 300, and a max_items below 200 requests, and can return, up to 200. The description and docstrings say so.
  • credits (credits_operations.py): GetCreditAllocationHistory's page_max is 500, the page size the service clamps to. A page ends only at a bill boundary, so it can hold more rows than maxResults; the helper keeps such a page.

Cleanup:

  • utilities/aws_service_base.py: paginate_aws_response is removed (and its exports from utilities and the package root); its tests in tests/utilities/test_aws_service_base.py go with it.
  • utilities/sql_utils.py: _derive_pagination_envelope reads only the lowercase pagination block, and convert_response_if_needed no longer takes pagination_token_key. The tests for the old shapes are replaced by one asserting they are ignored.
  • README "Pagination" and CHANGELOG now describe every paginated tool.

Breaking changes

  • Tool parameters: max_results, max_pages and page_size are removed from these tools (grouped ri-performance get_reservation_utilization takes max_pages instead of max_items), and sp-recommendation's next_page_token is renamed to next_token. MCP clients reject unknown parameters, so callers that pass the old names must switch to max_items / next_token.
  • Default behavior: omitting max_items now fetches every page (bounded by the 300 s time limit), where several of these tools previously returned one page.
  • Response shape: their paginated responses carry the lowercase pagination block. Removed: the uppercase Pagination block, top-level next_token, has_more_results, total_count, pages_fetched, total_results and duration_ms.
  • Python API: utilities.aws_service_base.paginate_aws_response is removed; use paginate_aws_items(ctx, operation, api_call, request_params, spec, *, max_items=None, next_token=None, max_pages=None), which takes a PageSpec and returns (items, pages, pagination). convert_response_if_needed no longer takes pagination_token_key.

Checklist

If your change doesn't seem to apply, please leave them unchecked.

  • I have reviewed the contributing guidelines
  • I have performed a self-review of this change
  • Changes have been tested
  • Changes are documented

Is this a breaking change? (Y/N) Y

RFC issue number:

Checklist:

  • Migration process documented
  • Implement warnings (if it can live side by side)

Testing: uv run --frozen pytest --cov --cov-branch (2233 passed, 98% branch coverage, every added line covered), ruff check, ruff format --check and pyright clean in src/billing-cost-management-mcp-server on this branch alone. Each migrated operation has tests for fetching every page without max_items, stopping at max_items (max_pages for grouped RI utilization), and continuing with the caller's token and identical parameters.

Acknowledgment

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of the project license.

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bandit found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@codecov

codecov Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.65%. Comparing base (73fb4d0) to head (64cb0b1).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4788      +/-   ##
==========================================
+ Coverage   93.61%   93.65%   +0.04%     
==========================================
  Files        1076     1059      -17     
  Lines       93116    92083    -1033     
  Branches    15124    14996     -128     
==========================================
- Hits        87168    86240     -928     
+ Misses       3549     3469      -80     
+ Partials     2399     2374      -25     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

- max_items is the total number of groups returned. Omit it to fetch every page (no limit). Pass
it, or narrow the filter, for queries that can be large. get_reservation_utilization pages in
fixed pages of 500 groups, and its first page is returned whole even when max_items is smaller.
- Top-N: sort_by + max_items=N.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will always return the first page of 500 groups when max_items is < 500, right?

I forgot why we don't limit on the first page?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right — it always asks for 500 at a time, so a smaller max_items still gets 500 back.

The reason: for this API (and Billing Conductor's cost report), next_token only works if every request asks for the same page size. So we can't shrink the last page to fit max_items. For max_items=700 with 500-item pages, the options are:

Option Returns Can continue? Downside
Stop after one page (today) 500 yes less than asked
Fetch 2 pages, return only 700, drop the token 700 no — ask again with a bigger max_items throws away 300 fetched items; can't continue
Fetch 2 pages, return 700 with the token 700 yes, but skips 701–1000 silently loses data
Same page size of 350 for both pages 700 only with the same max_items asking for more on a continuation fails
Fetch 2 pages, return all 1,000 1,000 yes returns more than asked

I'd go with the last one for these two APIs: round max_items up to whole pages and return them, with the token. Nothing is dropped or skipped, continuing always works, and the response says how many items it holds, so the caller knows. The cost is a bigger response than requested — up to one extra page, so max_items=10 gets 500 and max_items=700 gets 1,000. Every other API keeps returning exactly max_items.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update after checking every paginated API in this server: only GetReservationUtilization ties its next_token to MaxResults — a continuation with a different (or omitted) MaxResults is rejected. Billing Conductor's cost report turned out not to (any 200–300 works on each page), and every other API accepts a changed page size.

So, per the team discussion, instead of rounding up:

  • Grouped get_reservation_utilization now takes max_pages (pages of 500 groups) instead of max_items. Each response has items_returned and next_token, so nothing over- or under-shoots and the agent doesn't have to count. Passing max_items there returns a validation error pointing to max_pages.
  • get-billing-group-cost-report uses normal sizing within the API's 200–300 range, so it returns exactly max_items except that a request below 200 returns up to 200.
  • Every other API keeps max_items and returns exactly that many.

Pushed in the latest commits on this PR.

@finklen-amazon
finklen-amazon force-pushed the bcm-pagination-3-special-and-cleanup branch from 22c5e44 to 1bc4ce6 Compare October 20, 2026 14:02
Cost Explorer, cost-comparison, Reserved Instance and Savings Plans
performance, Savings Plans recommendations, credits, Cost Optimization
Hub, Compute Optimizer Automation, budgets and AWS Pricing now page
through paginate_aws_items. These are the tools that need more than a
pass-through: Cost Explorer counts the groups nested in each period
against max_items and takes GroupDefinitions and TotalSize from the
first page, coverage and utilization take Total from the first page, the
Savings Plans recommendation and Cost Optimization Hub keep their
first-page summaries, Cost Optimization Hub ranks efficiency metrics
over a fixed fetch, Compute Optimizer Automation applies max_items per
region in its multi-region calls, and budgets looks a budget_name up
with DescribeBudget. The remaining paginate_aws_response calls,
hand-written loops and the credits botocore paginator are replaced.

With no caller left, the old paginate_aws_response is removed, and the
SQL offload no longer reads the old Pagination wrapper or raw
single-page tokens.

Also fixes: Cost Explorer and Cost Optimization Hub list operations
returning only the first page unless max_pages or next_token was
passed, cost-comparison requesting 10 rows per page, a NextToken sent
to getSavingsPlansUtilization, the getCostCategories result key, an
extra Total request reusing the last page's token in Savings Plans and
Reserved Instance coverage and utilization, AWS Pricing MaxResults
above 100, and the budgets budget_name filter running after the cut.

BREAKING CHANGE: max_results, max_pages and page_size are removed from
these tools and sp-recommendation's next_page_token is renamed to
next_token. Their paginated responses use the lowercase pagination
block; Pagination, top-level next_token, has_more_results,
total_count, pages_fetched, total_results and duration_ms are removed.
utilities.aws_service_base.paginate_aws_response is removed in favor
of paginate_aws_items, and convert_response_if_needed no longer takes
pagination_token_key.
get_service_codes and get_attribute_values sorted the values in each
response. With max_items and next_token a list is read in slices, so
sorting each slice does not give one order across pages, and Python's
sort differs from the service's order when one value is a prefix of
another (the service returns us-east-1-x before us-east-1). Keep the
order the service returns, which is the same across pages in practice;
the API reference does not document an order. get_service_attributes
is not paginated and still sorts its attribute names.
GetReservationUtilization and GetBillingGroupCostReport bind their
continuation token to the page size, so every request sends the same
MaxResults and a page cannot shrink to fit max_items. Previously the
call stopped after the first page whenever the next full page could
pass max_items, so max_items=700 with pages of 500 returned only 500
items.

For PageSpec.fixed_page_size specs, paging now continues until at
least max_items items are collected (or the data, time limit or a
failure ends it) and returns every fetched item with the service's
token: max_items is rounded up to whole pages. Other specs keep exact
page sizing.
This reverts commit 1bc4ce6.

Rounding max_items up to whole pages made get_reservation_utilization
and get-billing-group-cost-report return more items than asked for.
Fixed-page specs go back to the general rule: stop before a page that
could take the result past max_items, and always return the first page.
The following commits replace the rounding with max_pages for
Reserved Instance utilization and a 200-300 page size for the
Billing Conductor cost report.
GetReservationUtilization grouped by SUBSCRIPTION_ID rejects a
continuation whose MaxResults differs from the first call's, so every
page is requested at 500 groups and a page cannot shrink to fit
max_items. A count-based limit therefore either undershoots (stop
before a page that could exceed it) or overshoots (round up to whole
pages), and the agent has to reason about page sizes to use it.

paginate_aws_items gains a keyword max_pages: it fetches at most that
many pages at the spec's page size and returns every item with the
service's token. It cannot be combined with max_items, must be at
least 1, and a result it cuts short warns to call again with a larger
max_pages. The time limit, repeated-token guard and failure handling
are unchanged.

ri-performance takes max_pages and passes it to grouped
get_reservation_utilization, which now returns validation_error for
max_items. get_reservation_coverage keeps max_items and returns
validation_error for max_pages. Ungrouped utilization returns every
period in one response and still ignores both.
GetBillingGroupCostReport's token is bound to Arn, GroupBy and
BillingPeriodRange, not to MaxResults: a continuation may ask for any
size the service accepts, 200 to 300. Requesting a fixed 300 on every
page made max_items=500 stop at 300 results.

PageSpec gains page_min, the smallest page size the service accepts;
a smaller requested size is raised to it, and the stop rule counts the
raised size as the worst case. The cost report drops fixed_page_size
and pages between 200 and 300, so max_items=500 returns 300 then 200,
and a max_items below 200 requests, and can return, up to 200.
GetCreditAllocationHistory accepts maxResults up to 1,000 but clamps a
page to 500 ledger rows. Set page_max to 500 so the page size the
server requests, and the bound it checks pages against, match what the
service returns.

The service ends a page only at a bill boundary, so a page can still
hold more rows than maxResults; the helper keeps such a page.
The second line of partial_data_warning had an f prefix but no
placeholders.
@finklen-amazon
finklen-amazon force-pushed the bcm-pagination-3-special-and-cleanup branch from dfde5a6 to 64cb0b1 Compare October 20, 2026 16:40

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: To triage

Development

Successfully merging this pull request may close these issues.

3 participants