Skip to content

fix(billing-cost-management-mcp-server): cost-explorer filter grammar + classify ParamValidationError - #4766

Merged
callnmm merged 4 commits into
awslabs:mainfrom
callnmm:fix/cost-explorer-filter-grammar-and-param-validation
Oct 16, 2026
Merged

callnmm merged 4 commits into
awslabs:mainfrom
callnmm:fix/cost-explorer-filter-grammar-and-param-validation

Conversation

@callnmm

@callnmm callnmm commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Fixes

Summary

Changes

Two fixes for the cost-explorer tool in billing-cost-management-mcp-server.

1. Classify ParamValidationError as validation_error (utilities/aws_service_base.py)

botocore.exceptions.ParamValidationError is raised client-side, before any network call, when request parameters are malformed (unknown keys such as Tags.MatchValue, missing required keys, type mismatches). It subclasses BotoCoreError, so handle_aws_error matched it in the BotoCoreError branch and returned error_type: aws_connection_error with the message AWS service connection error: ParamValidationError. That tells the caller the service is unreachable when the request shape is the problem, so callers tend to retry the same malformed request.

handle_aws_error now matches ParamValidationError before BotoCoreError and returns error_type: validation_error with botocore's message, keeping boto_error_type: ParamValidationError for diagnostics. Other BotoCoreError subclasses still return aws_connection_error.

2. Document filter grammar and time-period limits in the cost-explorer tool description (tools/cost_explorer_tools.py)

Documentation only; no runtime change. The statements are based on the Cost Explorer API reference (the ce service model shipped with botocore) or the Cost Explorer user guide.

  • New FILTER EXPRESSION GRAMMAR block:
    • the leaf shape {<Dimensions|Tags|CostCategories>: {Key, Values, MatchOptions}} and the one-root-key rule
    • the MatchOptions that getCostAndUsage and getCostAndUsageWithResources accept (Dimensions: EQUALS, CASE_SENSITIVE; Tags and CostCategories: EQUALS, ABSENT, CASE_SENSITIVE) and the default (EQUALS, CASE_SENSITIVE)
    • a note that the other MatchOption values (CASE_INSENSITIVE, STARTS_WITH, ENDS_WITH, CONTAINS, GREATER_THAN_OR_EQUAL) apply to Cost Category rules and anomaly subscriptions, not to these filters
    • that for Tags, ABSENT with a Key and no Values matches costs without that tag key, plus correct examples
  • New WINDOW LIMITS block with the documented time-period limits:
    • getCostAndUsage: the current month and the previous 13 months; older months only at MONTHLY granularity with multi-year data enabled
    • getCostAndUsageWithResources: within the last 14 days
    • getSavingsPlansUtilization: start within the last 13 months, end before the current date
    • forecasts: start today or later, horizon up to 3 months DAILY or 18 months MONTHLY, and no forecast for accounts with too little history
  • Updated notes on getCostAndUsageWithResources, getDimensionValues, getCostForecast (its five accepted cost metrics), and getSavingsPlansUtilization.
  • The TAG entry in the dimension reference now says TAG is valid as a GroupBy type and through filter Tags leaves, but not as the dimension argument of getDimensionValues (it is not in the API's Dimension enum). Tag discovery goes through getTagsOrValues with tag_key.

Tests: added test_handle_param_validation_error. The existing test_handle_boto_core_error is unchanged and still passes, so plain BotoCoreError handling has not regressed. CHANGELOG.md has the classifier fix under Unreleased → Fixed and the description change under Unreleased → Changed.

User experience

Before:

  • A malformed parameter such as {"Tags": {"Key": "env", "MatchValue": "prod"}} returns error_type: aws_connection_error, message: AWS service connection error: ParamValidationError. The caller cannot tell from this that its request was malformed.
  • The tool description does not list the MatchOptions the filter accepts. Callers send values from the Cost Category rule grammar, such as CASE_INSENSITIVE, or send ABSENT together with Values, and Cost Explorer rejects the request with ValidationException.
  • TAG appears in the dimension reference without qualification, so callers pass it as the getDimensionValues dimension, which the API does not accept.

After:

  • The same malformed parameter returns error_type: validation_error, boto_error_type: ParamValidationError, and message: Parameter validation failed: Unknown parameter in Filter.Tags: "MatchValue", must be one of: Key, Values, MatchOptions, so the caller can correct the request.
  • The tool description lists the accepted MatchOptions and each operation's time-period limits, so callers can build valid requests on the first attempt.

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

Testing:

  • Reproduced the error response before and after the change by calling get_cost_and_usage with the malformed filter against a local closed endpoint (no AWS call is made: botocore rejects the parameters first). On main it returns aws_connection_error; on this branch it returns validation_error with the message above.
  • ruff check and ruff format --check pass for the package; pyright reports 0 errors on the changed source files
  • pre-commit hooks pass on the changed files, apart from check-license-header, which could not be installed in my environment (no license headers changed)
  • Full package test suite: 1797 passed

Is this a breaking change? (Y/N)

N. The tool's arguments and success responses are unchanged. The only behavior change is that ParamValidationError now returns error_type: validation_error instead of aws_connection_error. validation_error is an existing error type, already returned for invalid JSON input.

RFC issue number:

Checklist:

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

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 Oct 13, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.55%. Comparing base (20c5c13) to head (a5bee5d).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4766   +/-   ##
=======================================
  Coverage   93.55%   93.55%           
=======================================
  Files        1068     1068           
  Lines       92344    92355   +11     
  Branches    15025    15027    +2     
=======================================
+ Hits        86389    86400   +11     
  Misses       3562     3562           
  Partials     2393     2393           

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

finklen-amazon
finklen-amazon previously approved these changes Oct 13, 2026
wangyuhere
wangyuhere previously approved these changes Oct 14, 2026
@finklen-amazon

Copy link
Copy Markdown
Contributor

Thanks for this. The ParamValidationError fix also matters for retry_strategy (#4771): today it comes out as aws_connection_error, so the agent is told retry_same_request and resends the same invalid request.

Some other BotoCoreError subclasses have the same problem. They aren't connection failures either, but they map to retry_same_request:

  • NoCredentialsError, PartialCredentialsError, NoRegionError, ProfileNotFound, UnknownServiceError: credential or configuration problems that can't be fixed during the run, so they should be do_not_retry.

Would you be open to covering those here too, since this PR already adds a branch before the BotoCoreError one? To keep error_type stable, they could keep aws_connection_error and set retry_strategy='do_not_retry' explicitly in the response. ErrorSignalingMiddleware keeps a retry_strategy that is already set. Only the real connection classes (EndpointConnectionError, ConnectTimeoutError, ReadTimeoutError, ConnectionClosedError) would then still be retried.

Heads up: #4781 also touches aws_service_base.py, in format_response and the ClientError message fallback in handle_aws_error. Those are different hunks from yours, so any conflict should be trivial.

🤖 AI-assisted comment 🤖

…ationError as validation_error

ParamValidationError is a client-side botocore failure (unknown kwargs, bad
parameter shape, missing required keys, type mismatches) that subclasses
BotoCoreError. The previous order in handle_aws_error matched BotoCoreError
first, so every ParamValidationError was labeled aws_connection_error, telling
callers 'AWS is unreachable' when the real problem was their own request
shape.

Match ParamValidationError before BotoCoreError and return it as
validation_error, preserving boto_error_type=ParamValidationError for
diagnostics. Plain BotoCoreError still classifies as aws_connection_error.

Add a test asserting the new path is reached and the response carries
error_type=validation_error with boto_error_type=ParamValidationError.
…r grammar and window limits

Agent-side failures on the cost-explorer tool fall into three recurring
patterns that the current tool description does not warn against:

1. Filter MatchOptions grammar. Agents guess shape from training memory
   or carry over Cost Category RULE grammar (CASE_INSENSITIVE, STARTS_WITH,
   ENDS_WITH, CONTAINS), none of which are valid in a Cost Explorer filter.
   Add a FILTER EXPRESSION GRAMMAR block listing the per-leaf
   MatchOptions allow-list (Dimensions: EQUALS, CASE_SENSITIVE; Tags and
   CostCategories: EQUALS, ABSENT, CASE_SENSITIVE), the explicit NOT-valid
   list, and the ABSENT-takes-no-Values rule, with correct examples.

2. Window limits. The 14-day cap on getCostAndUsageWithResources, the
   38-month cap on standard CE operations, and the 7-month-history +
   future-start requirement for forecasts were absent or buried inline.
   Add a WINDOW LIMITS block and tighten the per-operation Notes.

3. getDimensionValues TAG. The DIMENSION REFERENCE listed TAG without
   qualification, so agents pass it as the dimension argument and hit
   ValidationException. TAG is valid only as a GroupBy Type and inside
   filter Tags leaves; clarify this in the DIMENSION REFERENCE and in the
   getDimensionValues Notes, and point agents to getTagsOrValues for tag
   discovery.

Documentation only, no runtime change.
… against the API reference

Corrects statements added in the previous commit so that every limit in
the cost-explorer tool description matches the Cost Explorer API reference
and user guide:

- Forecast horizon is 3 months at DAILY and 18 months at MONTHLY (was 12).
- Remove the claim that forecasts need 7 months of history. The user guide
  says only that no forecast is produced without enough history, commonly
  for accounts with less than one full billing cycle.
- getCostForecast accepts only the five cost metrics; USAGE_QUANTITY and
  NORMALIZED_USAGE_AMOUNT belong to getUsageForecast.
- getSavingsPlansUtilization: start must be within the last 13 months and
  end before the current date (was "up to 38 months").
- getCostAndUsage: current month plus the previous 13 months; older months
  only at MONTHLY with multi-year data enabled. Drop the history limits
  stated for getDimensionValues, getTagsOrValues and getCostCategories,
  which the reference does not give.
- Scope the MatchOptions list to getCostAndUsage and
  getCostAndUsageWithResources, the operations whose reference documents
  it, and note the default (EQUALS, CASE_SENSITIVE).
- Remove error names and error messages the reference does not document,
  and the unsupported reading of DataUnavailableException for Savings
  Plans utilization.

Also move the ParamValidationError entry in CHANGELOG.md from Changed to
Fixed. Documentation only.
… configuration errors

NoCredentialsError, PartialCredentialsError, NoRegionError, ProfileNotFound
and UnknownServiceError subclass BotoCoreError, so handle_aws_error reported
them as aws_connection_error and retry_strategy resolved to
retry_same_request. Retrying cannot fix them during the run.

Keep error_type=aws_connection_error so the classification stays stable, and
set retry_strategy=do_not_retry plus a hint explicitly on the response;
ErrorSignalingMiddleware keeps a retry_strategy that is already set. The real
connection failures (EndpointConnectionError, ConnectTimeoutError,
ReadTimeoutError, ConnectionClosedError) are still retry_same_request.

Also assert that ParamValidationError resolves to modify_request, and document
both in the README error-response section and CHANGELOG.

Requested in review by @finklen-amazon.
@callnmm
callnmm force-pushed the fix/cost-explorer-filter-grammar-and-param-validation branch from bcfdf0d to a5bee5d Compare October 16, 2026 14:59
@callnmm
callnmm enabled auto-merge October 16, 2026 17:48
@callnmm
callnmm added this pull request to the merge queue Oct 16, 2026
Merged via the queue into awslabs:main with commit d534d0f Oct 16, 2026
265 checks passed
@callnmm
callnmm deleted the fix/cost-explorer-filter-grammar-and-param-validation branch October 16, 2026 17:52
@github-project-automation github-project-automation Bot moved this from To triage to Done in awslabs/mcp Project Oct 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants