Skip to content

fix(mcp): require operator spend caps before real purchases and price Savings Plan previews - #52

Merged
cristim merged 4 commits into
mainfrom
fix/47-mcp-spend-ceiling-and-preview-price
Oct 9, 2026
Merged

cristim merged 4 commits into
mainfrom
fix/47-mcp-spend-ceiling-and-preview-price

Conversation

@cristim

@cristim cristim commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Real purchases are refused unless an operator-set cap is configured and respected: CUDLY_MCP_MAX_COUNT (RIs; vCPUs for GCP CUDs), CUDLY_MCP_MAX_HOURLY_COMMITMENT (Savings Plans), CUDLY_MCP_MAX_MEMORY_GB (GCP CUDs, whose memory_gb is not bounded by the count cap).
  • Caps are env vars, not tool arguments; fail closed (unset, empty, NaN, Inf, non-positive, non-integer all refuse, naming the variable and the Claude Desktop setting, without echoing the value). Routed by CommitmentType (a Savings Plan with Count 0 or 5 still hits the hourly cap), checked in authorizeRealPurchase after the opt-in and before credentials or any provider call. Previews need no caps and stay offline.
  • Savings Plan previews now report cost = hourly x 8760 x term, computed locally. RI and CUD previews remain unpriced.
  • PurchaseResponse.Cost is documented in the schema. It has two meanings today (Savings Plan preview: total commitment over the term; executed purchase: provider-reported upfront cost); feat(mcp): USD spend cap on the execute path and a cumulative purchase budget #51 will unify it.

Limits (documented in README)

Caps are per call and the count cap is not a USD cap (50 x u-24tb1 at about $218/h for 3 years is roughly $286M). The execute-path USD cap and a cumulative budget are tracked in #51.

Behavior changes

  • Breaking for operators: with real purchases enabled, set the caps or purchases are refused (CHANGELOG, Breaking).
  • Savings Plan audit lines now carry the total commitment as the estimated cost (NewAuditRecord reads rec.CommitmentCost). The idempotency key is unchanged (test).
  • MCPB: optional number settings (min 1) in all three env blocks. The MANIFEST spec does not say what the host substitutes for an unset optional setting; an empty value and a literal ${user_config...} placeholder both hit the refusal (tested), so either behavior fails closed. Not verified in a real Claude Desktop host.

Verification (mocks only, no cloud calls)

  • go test -short on ./tools, ./cmd/..., . pass; golangci-lint 0 issues.
  • Regression: the issue's shape (50000/h, 3y, all-upfront, cap 100) is refused with 0 provider resolutions and 0 purchase calls; boundary equal-to-cap succeeds; table over all six count-bearing builders.
  • Mutation probes run: > to >=, routing by Count == 0, dropping the >= 1 parse check each make the cap tests fail.

Follow-up: #51 execute-path USD cap + cumulative budget

Closes #47

🤖 Generated with Claude Code

cristim and others added 2 commits October 9, 2026 16:15
Real purchases are refused unless CUDLY_MCP_MAX_COUNT (RIs, GCP vCPUs),
CUDLY_MCP_MAX_HOURLY_COMMITMENT (Savings Plans) or CUDLY_MCP_MAX_MEMORY_GB
(GCP CUDs) is set to a valid value and the request is within it. Caps are
routed by CommitmentType, checked after the opt-in and before credentials.
They are per call and are not USD caps; a USD cap and cumulative budget
are tracked in #51.

Refs #47

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Savings Plan previews report cost as the total commitment over the term
(hourly x 8760 x years), computed without a provider call. README,
server.json, the MCPB manifest and the changelog document the caps.

Refs #47

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@cristim cristim added urgency/this-sprint Within the current sprint triaged Item has been triaged priority/p1 Next up; this sprint severity/high Significant harm impact/all-users Affects every user effort/m Days type/bug Defect labels Oct 9, 2026
@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

  • Run on-demand review

This review includes 17 billable files and costs up to $4.25.

  • Ask an admin to make reviews automatic

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Or wait 40 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available. Your 87 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: LeanerCloud/cloud-commitments-mcp/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 72799914-c325-477c-9d98-03d2902b2e20

📥 Commits

Reviewing files that changed from the base of the PR and between f1a23e2 and 1418f8d.


📒 Files selected for processing (17)
  • CHANGELOG.md
  • README.md
  • cmd/cudly-mcp/gcp_purchase_protocol_test.go
  • cmd/cudly-mcp/main_test.go
  • mcpb/manifest.json
  • server.json
  • tools/aws_ec2_ri.go
  • tools/aws_elasticache_ri.go
  • tools/aws_rds_ri.go
  • tools/aws_savingsplans.go
  • tools/aws_simple_ri.go
  • tools/azure_compute_ri.go
  • tools/gcp_computeengine_cud.go
  • tools/purchase.go
  • tools/purchase_test.go
  • tools/spend_caps.go
  • tools/spend_caps_test.go


  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@cristim

cristim commented Oct 9, 2026

Copy link
Copy Markdown
Member Author

Gate review (mcp gate-1): CHANGES REQUESTED at a701408

Independent adversarial review of the full diff (money path). The safety logic is correct; one real source defect blocks the merge.

Blocking

  1. tools/aws_savingsplans.go:31-40: savingsPlanHoursPerYear was inserted between the savingsPlansAccountLevelRegion doc comment and its const. The region doc ("savingsPlansAccountLevelRegion is the region used ...") now heads the hours constant's doc, and savingsPlansAccountLevelRegion has no doc. Fix: move the new const and its two-line comment above the region comment (or below the region const).

Non-blocking (author's call)
2. README.md Safety model: "A USD cap and a cumulative budget are tracked as a follow-up." Link #51 there; operators read the README, not the PR body.
3. PurchaseResponse.Cost overloads two meanings (SP preview = total over the term; executed = provider-reported upfront, which is 0 for a no-upfront SP, see cloud-commitments-go savingsplans/client.go:265 and calculatePaymentBreakdown). The schema description states both explicitly, so it is honest; a separate total_commitment field would be clearer. Could go into #51.
4. MCPB: if a host renders a number setting as 5.0, Atoi refuses a valid count cap (fails closed, so safe; usability only). Not verified in a real Claude Desktop host, as the PR body says.

Verified locally (fresh clone at the exact SHA, GOTOOLCHAIN=go1.26.9, mocks only, no cloud calls)

  • go build ./... && go vet ./... && go test ./...: 493 passed; golangci-lint run ./...: 0 issues.
  • Bypass: every purchase handler (EC2, RDS, ElastiCache, simple_ri for OpenSearch/Redshift/MemoryDB, SP, Azure, GCP) goes through ExecutePurchase; client.PurchaseCommitment has one caller (purchase.go:647), after authorizeRealPurchase (purchase.go:630). Order: opt-in, caps, scope, nil ResolveClient, all before ResolveClient.
  • Fails-before: removing the enforceSpendCaps call and the SP CommitmentCost (tests kept) fails 8 tests (all TestSpendCap* plus TestSavingsPlanPreviewShowsTotalCommitmentOffline). A full source revert fails to compile (tests reference the new env constants).
  • Mutations, each killed: count > to >=; hourly > to >=; memory > to >=; routing by Count == 0; dropping n < 1 in the parse; dropping the Count < 1 check; GCP builder CommitmentType CUD to RI (skips memory cap); dropping NaN/Inf rejection; SP cost without the term multiplier.
  • Boundaries covered by tests: count at cap / cap+1 across all six count-bearing builders, Count 0, cap values "", " ", "abc", "0", "-1", "1e3", "1.5", placeholder, NaN, Inf, -Inf, " 5 "; hourly at 100 and 100.01; GCP memory at 64 and far above; SP without details; unexpected Details shape fails closed.
  • SP preview total hourly x 8760 x years matches cloud-commitments-go calculateHoursInTerm (365 x 24 per year).
  • History: commit 1's tests include the SP preview check that passes only with commit 2, so the branch is not bisectable; acceptable only because the merge is a squash.
  • PR body links feat(mcp): USD spend cap on the execute path and a cumulative purchase budget #51 (open). CHANGELOG has a Breaking entry; README Safety model documents per-call and not-USD limits.

CI at this SHA: all completed checks green, Build MCP (ubuntu-latest) still pending, mergeStateStatus UNSTABLE. Not merged. Re-gate after the fix commit.

cristim and others added 2 commits October 9, 2026 16:28
Move savingsPlanHoursPerYear out of savingsPlansAccountLevelRegion's doc
comment and link #51 from the README.

Refs #47

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@cristim

cristim commented Oct 9, 2026

Copy link
Copy Markdown
Member Author

Gate re-review (mcp gate-1): APPROVED at 1418f8d

@cristim
cristim merged commit aa5c0ae into main Oct 9, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/all-users Affects every user priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/bug Defect urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(mcp): no spend ceiling on purchase tools and the preview never shows a price

1 participant