Skip to content

feat(billing-cost-management-mcp-server): add AWS Enterprise Support tool and operations - #4481

Open
frankolas wants to merge 1 commit into
awslabs:mainfrom
frankolas:feat/billing-enterprise-support-tools
Open

feat(billing-cost-management-mcp-server): add AWS Enterprise Support tool and operations#4481
frankolas wants to merge 1 commit into
awslabs:mainfrom
frankolas:feat/billing-enterprise-support-tools

Conversation

@frankolas

Copy link
Copy Markdown
Contributor

Notes

  • Adds an enterprise_support tool with three operations, get_charge_summary, get_contract_details and list_linked_account_charges, covering a billing period's Enterprise Support charge and the Support-eligible spend it was calculated from, the contract terms that govern how the charge is allocated, and the per-linked-account breakdown.

  • account_id on list_linked_account_charges is a linked-account filter rather than the caller, so it is never resolved from credentials and is omitted from the request when the caller omits it. Defaulting it would silently reduce an organization-wide breakdown to a single row.

  • list_linked_account_charges uses the shared paginate_aws_response helper rather than a local paginator, and large breakdowns offload to the session SQL tool through convert_response_if_needed.

  • Response field names pass through as returned, with only timestamps normalized.

  • AccessDeniedException and ResourceNotFoundException are classified with their own guidance because both are routinely misread in opposite directions; every other error falls through to the shared handler.

Testing

  • uv sync --frozen --all-extras --dev: clean
  • ruff check . and ruff format --check .: pass
  • pyright: 0 errors
  • pytest --cov --cov-branch --cov-report=term-missing: full suite passes, 52 new tests, 99% statement coverage on the new operations module and 100% on the new tool module

Kiro-cli Testing

Identifiers and monetary amounts are redacted below. Field names, response shapes and model behaviour are unmodified.

Running tool enterprise_support with the param (from mcp server: bcm-enterprise-support)
 ⋮  {
 ⋮    "operation": "get_charge_summary",
 ⋮    "billing_month": "2026-06"
 ⋮  }
 - Completed in 0.416s
> Here's your Enterprise Support charge summary for June 2026 (billing period 2026-06-01 to 2026-07-01). This bill is finalized (not estimated).
| Item | Amount |
|------|--------|
| Support Charge | $<redacted> |
| Support Discount | $<redacted> |
| Total Support-Eligible Spend | $<redacted> |
The eligible spend breaks down as:
- Usage spend: $<redacted>
- Reserved Instance spend: $<redacted>
- Savings Plans spend: $<redacted>
The charge was calculated at an effective rate of 1.0% of your total eligible spend, under the "Public Pricing Plan" (active 2025-11-01 through 2026-10-31). That plan has a 5% plan discount built into its pricing and uses a tiered structure — your $<redacted> in eligible spend falls into the second tier ($<redacted>–$<redacted>), which carries a $<redacted> base charge plus 7% on spend above $<redacted>.
A couple of notes:
- The planDiscountPercent of 5% is a discount embedded in the pricing plan itself (already reflected in the charge). The separate supportDiscount field — which would be a discount applied against the support charge — is $<redacted>.
- The spend breakdown (usage, RI, SP) decomposes the total eligible spend; it doesn't add to it.
Want me to pull the per-account breakdown or the contract details for this month?
Running tool enterprise_support with the param (from mcp server: bcm-enterprise-support)
 ⋮  {
 ⋮    "operation": "list_linked_account_charges",
 ⋮    "billing_month": "2026-06"
 ⋮  }
 - Completed in 0.391s
> Here's the breakdown for billing month 2026-06:
## Summary
- **Total accounts returned:** 19
- **Payer account:** 111122223333
- **Dataset complete:** ✅ Yes (all accounts fetched in a single page)
## Per-Account Breakdown
| Account ID | Prorated Support-Eligible Spend | Billable Seconds | Total Seconds | Partial Period? |
|---|---|---|---|---|
| 222233334444 | $<redacted> | 2,592,000 | 2,592,000 | No |
| 333344445555 | $<redacted> | 2,592,000 | 2,592,000 | No |
| 111122223333 | $<redacted> | 2,592,000 | 2,592,000 | No |
| 444455556666 | $<redacted> | 2,592,000 | 2,592,000 | No |
| 555566667777 | $<redacted> | 2,592,000 | 2,592,000 | No |
| 666677778888 | $<redacted> | 2,592,000 | 2,592,000 | No |
| 777788889999 | $<redacted> | 2,592,000 | 2,592,000 | No |
| 888899990000 | $<redacted> | 2,592,000 | 2,592,000 | No |
| 999900001111 | $<redacted> | 2,592,000 | 2,592,000 | No |
| 000011112222 | $<redacted> | 2,592,000 | 2,592,000 | No |
| 123456789012 | $<redacted> | 2,592,000 | 2,592,000 | No |
| 210987654321 | $<redacted> | 2,592,000 | 2,592,000 | No |
| 112233445566 | $<redacted> | 2,592,000 | 2,592,000 | No |
| 665544332211 | $<redacted> | 2,592,000 | 2,592,000 | No |
| 121212121212 | $<redacted> | 2,592,000 | 2,592,000 | No |
| 343434343434 | $<redacted> | 2,592,000 | 2,592,000 | No |
| 565656565656 | $<redacted> | 2,592,000 | 2,592,000 | No |
| 787878787878 | $<redacted> | 2,592,000 | 2,592,000 | No |
| 909090909090 | $<redacted> | 2,592,000 | 2,592,000 | No |
## Partial-Period Subscriptions
None. Every account has billableSeconds equal to totalSeconds (2,592,000 seconds = 30 days), meaning all 19 accounts were subscribed to Enterprise Support for the entire June 2026 billing period. No proration was applied to any account.

An account or period with no Enterprise Support data, showing the not-found classification rather than a denial:

Running tool enterprise_support with the param (from mcp server: bcm-enterprise-support)
 ⋮  {
 ⋮    "operation": "get_charge_summary",
 ⋮    "billing_month": "2026-07"
 ⋮  }
 - Completed in 0.399s
> Here's exactly what the tool returned:
Status: Error (HTTP 404 — Not Found)
Details:
- **Service:** Billing
- **Operation:** GetEnterpriseSupportChargeSummary
- **Error type:** not_found
- **Message:** "Billing GetEnterpriseSupportChargeSummary found no Enterprise Support data for the requested account and billing period. The call was authorized, so this is not a permission problem."
- **AWS Message:** "No data found for account 111122223333 in 2026-07"
- **Request ID:** xxxxxxxxxx
Resolution guidance from the tool: The usual causes are that the account is not an Enterprise Support customer, was not subscribed during that period, or is not the payer account for the Support profile.
So no Enterprise Support charge summary data exists for account 111122223333 for billing month 2026-07. The call was authorized (no permission issue), but there's simply no data for that account/period combination.

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.

@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.15%. Comparing base (cdd87a8) to head (cc78456).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff            @@
##             main    #4481    +/-   ##
========================================
  Coverage   93.14%   93.15%            
========================================
  Files        1038     1040     +2     
  Lines       87536    87656   +120     
  Branches    14123    14140    +17     
========================================
+ Hits        81535    81655   +120     
  Misses       3638     3638            
  Partials     2363     2363            

☔ 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.

@frankolas frankolas changed the title Add AWS Enterprise Support tool and operations for the Billing and Cost Management MCP server feat(billing-cost-management-mcp-server): add AWS Enterprise Support tool and operations Aug 12, 2026
@frankolas
frankolas force-pushed the feat/billing-enterprise-support-tools branch from b0c058f to 4bb9484 Compare August 12, 2026 18:39
@frankolas
frankolas force-pushed the feat/billing-enterprise-support-tools branch from 4bb9484 to cc78456 Compare August 12, 2026 19:12
Comment on lines +42 to +44
# Oldest billing month with Enterprise Support data. Requests earlier than this
# are rejected locally rather than spending a call that cannot return data.
EARLIEST_BILLING_MONTH = '2025-01'

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.

Will this always be true?

try:
client = _create_billing_client()
request_params = _build_linked_account_charges_request(
str(billing_month), account_id, max_results, next_token

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.

Do we need to cast to str if we run _validate_billing_month(billing_month)

Comment on lines +71 to +73
Both are routinely misread. ``AccessDeniedException`` is raised when the IAM
policy omits the action AND when the caller is not the payer account
authorized for the Support profile, so it must not be reported as an absence

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.

AND? IAM Policy of caller's role could contain the correct actions, but client could still throw AccessDeniedError if caller is not the payer account authorized for Support profile.

)


def _validate_billing_month(billing_month: Optional[str]) -> Optional[Dict[str, Any]]:

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.

We should validate billing_month passed is in YYYY-MM format because LLM can pass in a string without the zero padding for month, which might be dangerous for comparison operations in this python file. They assume YYYY-MM for comparison operation and these happen before any server side validation. We can do it in this file.. which will be easier and safer lift. (Another unsafe alternative could be to add client-side validation which can be overriden).

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