Skip to content

fix(billing-cost-management-mcp-server): document the cost-explorer Tags filter format and hint on tag keys that cannot match - #4941

Merged
finklen-amazon merged 2 commits into
awslabs:mainfrom
finklen-amazon:fix/bcm-ce-dimension-tag-docs
Oct 30, 2026
Merged

finklen-amazon merged 2 commits into
awslabs:mainfrom
finklen-amazon:fix/bcm-ce-dimension-tag-docs

Conversation

@finklen-amazon

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

Copy link
Copy Markdown
Contributor

Fixes

Summary

Changes

The cost-explorer description showed a Tags filter example but never said what a tag key or value must look like. Three common mistakes are accepted with no error and return a wrong answer:

  • A user: key prefix. Billing exports name user-defined tags user:<key>, but Cost Explorer adds that prefix to a filter key itself. So {"Tags": {"Key": "user:Environment", "Values": ["Dev"]}} matches nothing and returns $0. With ABSENT the same key matches every cost and returns the whole bill.
  • A TAG group_by label used as a value. A TAG group_by labels groups Environment$Dev (and Environment$ for costs without the key). Filtering on Environment$Dev matches nothing.
  • Wrong case. Tag keys and values are case-sensitive, and Tags filters take no case-insensitive option.

Description: a new "Tags leaves" section, for getCostAndUsage and getDimensionValues filters, states:

  • no user: prefix; tags AWS applies itself keep aws:;
  • keys and values are case-sensitive;
  • the group_by label is not a filter value, and ABSENT covers the <key>$ group;
  • "Values": [""] matches the same costs as ABSENT (an empty list is rejected);
  • a wrong key is silent: $0 with Values, every cost with ABSENT; copy the key from getTagsOrValues.

Response hint: docs alone do not stop a model that copied user:Environment from an export, so getCostAndUsage and getDimensionValues add a hint to a successful response when a Tags key starts with user:, or a key or value is a group_by label. The hint names the fix (Use 'Environment', Filter on 'Dev' instead, or Use ABSENT for the Environment$ group). It does not reject the request: a resource can carry a tag key that really starts with user:, and the hint says so.

Scope and safety of the hint:

  • Successful responses only. Error responses keep the retry guidance the error mapping adds.
  • Bad filters still reach the API. A filter the check cannot parse (malformed or very deep) goes on to the API's own validation, exactly as before.
  • Forecasts and getCostAndUsageWithResources get no hint. A live getCostForecast filtered on user:Environment still returned a forecast, so "matches nothing" would be wrong there. The resource-level operation was not checked live.

User experience

Before: a model filters on user:Environment with ABSENT, gets the whole bill back as "untagged spend", and reports it. Or it filters on user:Environment = Dev, or on Environment$Dev, gets $0, and reports no spend.

After: the description gives the key and value rules. If the model still sends one of those filters, the response says why it matched nothing (or everything) and what to send instead.

Testing

Run from src/billing-cost-management-mcp-server:

Live, read-only, against a real account. The description was read as served over stdio (list_tools), and calls went through the server (getCostAndUsage, one month, MONTHLY):

  • getTagsOrValues lists user tag keys with no user: prefix.
  • A TAG group_by returns Environment$<value> and Environment$ labels.
  • Environment = [Dev] returns non-zero cost, equal to the group line.
  • user:Environment = [Dev] returns $0.
  • Environment with ABSENT equals the Environment$ group line.
  • user:Environment with ABSENT equals the unfiltered total.
  • Environment = [""] equals Environment with ABSENT.
  • Environment = [Environment$Dev] returns $0.
  • A value in the other case, or the key in lower case, returns $0.
  • CASE_INSENSITIVE is rejected with ValidationException ("Allowed MatchOptions:CASE_SENSITIVE, EQUALS, ABSENT").
  • Not(user:Environment ABSENT) returns $0.
  • An aws:createdBy filter with its prefix returns cost.
  • A getDimensionValues call filtered on a user: key returns no values, and the response carries the hint.
  • The harness passed every check on the branch: 27/27 on the first revision, which made three fresh hint calls, and 25/25 after review changes, which made one. It failed 9 on main: the 7 description checks and the 2 hint checks. The final rewording of the hint passes the 24 checks that replay recorded responses; the unit tests cover the new wording.

Merge conflict

Second update: merged main into the branch (merge commit, no force-push) after #4962 (reject an empty filter) and #4964 (reject group_by where Cost Explorer has no GroupBy) landed.

The fix bytes are unchanged: every line this PR adds or removes relative to main is identical to the previous head. On the merged head:

  • tests/tools/ and the full suite pass, except one recommendation_details test that fails the same way on main.
  • ruff and pyright are clean.
  • Every added line and branch in cost_explorer_tools.py is covered.

Earlier update: rebased onto main after #4936 (refuse billing_view_arn where AWS ignores it). #4936 added a check just before the getDimensionValues call this PR wraps, and appended tests at the same spot. Both were kept, and the fix bytes were unchanged.

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) N

RFC issue number: N/A

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 29, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.76%. Comparing base (99c7f0e) to head (73d6797).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4941   +/-   ##
=======================================
  Coverage   93.76%   93.76%           
=======================================
  Files        1080     1080           
  Lines       94176    94230   +54     
  Branches    15312    15327   +15     
=======================================
+ Hits        88303    88357   +54     
  Misses       3498     3498           
  Partials     2375     2375           

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

anniewn
anniewn previously approved these changes Oct 29, 2026
…ags filter format and hint on tag keys that cannot match

getCostAndUsage and getDimensionValues add the user: prefix to
user-defined tag keys themselves, so a Tags filter on "user:Environment"
matches no cost: with Values it returns $0, and with ABSENT it returns
every cost, both with no error. A TAG
group_by label such as "Environment$Dev" copied into Values also matches
nothing.

The description gains a "Tags leaves" section for getCostAndUsage and
getDimensionValues filters: no user: prefix (tags AWS applies keep aws:),
keys and values are case-sensitive, the group_by label is not a filter
value, Values [""] matches the same costs as ABSENT, and a wrong key is
silent.

getCostAndUsage and getDimensionValues add a hint to a successful response
when a Tags key starts with user:, a key or value is a group_by label, or
both. The hint allows for a key that really starts with user:. Error
responses keep the recovery hint the error mapping adds, and a filter the
hint check cannot read still goes to the API's own validation. The
forecast and resource-level operations get no hint: a forecast filtered on
a user: key still returned a forecast, so the hint would be wrong there,
and the resource-level operation was not checked.
…-tag-docs

# Conflicts:
#	src/billing-cost-management-mcp-server/tests/tools/test_cost_explorer_tools.py
@finklen-amazon
finklen-amazon added this pull request to the merge queue Oct 30, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 30, 2026
@finklen-amazon
finklen-amazon added this pull request to the merge queue Oct 30, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 30, 2026
@finklen-amazon
finklen-amazon added this pull request to the merge queue Oct 30, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 30, 2026
@finklen-amazon
finklen-amazon added this pull request to the merge queue Oct 30, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 30, 2026
@finklen-amazon
finklen-amazon added this pull request to the merge queue Oct 30, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 30, 2026
@finklen-amazon
finklen-amazon added this pull request to the merge queue Oct 30, 2026
Merged via the queue into awslabs:main with commit 75d8cef Oct 30, 2026
142 of 143 checks passed
@finklen-amazon
finklen-amazon deleted the fix/bcm-ce-dimension-tag-docs branch October 30, 2026 21:21
@github-project-automation github-project-automation Bot moved this from To triage to Done in awslabs/mcp Project Oct 30, 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.

3 participants