Skip to content

Changelog & versioning

Component Version Notes
Server / package 0.4.0 Reported by GET /health and in the MCP server identity.
Tool surface v1 · wave 3 54 tools (40 read + 14 write). See the Tool Reference.
MCP SDK @modelcontextprotocol/sdk@1.29.0 Exact pin (the v1.x production line); not a caret range.
Transport Streamable HTTP, stateless JSON-buffered responses; no SSE, no session id, no resumability.
Protocol auth OAuth 2.1 + PKCE (S256) RFC 9728 / 8414 discovery + RFC 7591 DCR shim; RFC 8707 audience binding.

The tool surface follows MCP house conventions: readOnlyHint/destructiveHint annotations on every tool, a response_format: concise|detailed enum, limit + ~25k-token output truncation, account-id → name resolution, and onboarding-quality descriptions with example inputs.

⚠ Breaking for writes: MCP writes are now OFF by default, for every organization

Section titled “⚠ Breaking for writes: MCP writes are now OFF by default, for every organization”

This includes organizations that already have paid plans and working write tools. Nothing was grandfathered in. Until an admin turns writes on, every write tool refuses with:

writes_disabled_for_org: MCP writes are turned off for this organization.
An org admin can enable them in Settings → Organization → AI agent access.

Reads are completely unaffected — no reconnect, no re-consent, no added latency. If your agents only read, you will not notice this release.

How to turn writes back on: Settings → Organization → AI agent access, in the CloudQuell app, as an org admin or super_admin.

Until that toggle ships in the app, an org admin can set the preference directly through the existing preferences API:

Terminal window
# ORG_ID: from get_current_organization, or Settings → Organization.
# TOKEN: a Cognito bearer from a signed-in dashboard session.
curl -X PUT \
"https://x8ayoh3yui.execute-api.us-west-2.amazonaws.com/prod/api/organizations/$ORG_ID/preferences" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"preferences": {"mcp_writes_enabled": true}}'

The body is shallow-merged, so this leaves currency and every other preference untouched. Send false to turn writes back off. The route is admin-only server-side, and it is not reachable from an agent — update_organization_preferences refuses this specific key (see below).

admin:write never identified a user. Cognito’s Hosted UI has no per-scope consent toggle, so the DCR shim advertises the full scope set for every registration and every interactive connector token carries admin:write — including one signed in by a viewer. The scope was therefore checking something everybody had, and per-user authorization rested entirely on the downstream role checks. Those are a fine last line and a bad first one: the refusal arrives after the request has left, worded by whichever backend answered, and the organization that owns the data had no switch of its own.

Every write tool now checks four things, server-side, before any downstream call:

# Check Refusal
1 Token carries admin:write insufficient_scope
2 Organization has MCP writes enabled writes_disabled_for_org
3 Caller’s org role is admin/super_admin insufficient_role
4 Call passes confirm: true confirmation_required

If the organization switch or your role cannot be read, the write is refused with org_context_unavailable rather than attempted — the gate fails closed.

update_organization_preferences refuses to set mcp_writes_enabled: it strips the key, says so in the result, and applies the rest of your change. An agent must never be able to enable writes for itself, or the model would be circular. cak_ service keys are unaffected and remain read-only always — they are minted with the read scope alone and fail check 1 regardless.

The downstream org-role checks in CloudQuell are unchanged and remain the final authority; this gate narrows what reaches them.

get_finops_briefing — one call for “what should I look at today?” (1 new read tool, 54 total)

Section titled “get_finops_briefing — one call for “what should I look at today?” (1 new read tool, 54 total)”

The entry point for scheduled and headless agents, and for a Monday-morning review. It calls up to eight existing read tools in parallel and joins them into one briefing, in a fixed order: freshness, spend, movers, anomalies, budgets, commitments, savings, and — only when include_ai_spend: true — direct-provider AI spend.

  • Freshness is reported first, on purpose. When the verdict is not fresh the briefing opens with a warning, so every number below it carries its trust level.
  • Every section degrades independently. A section that fails or times out comes back as { status: "unavailable", reason } and is named in sectionsUnavailable; the briefing still succeeds. A section a provider cannot have (commitments for an LLM provider, savings outside AWS/Azure) reports not_applicable. Never read a missing section as zero.
  • Each section drills down with its named toolget_data_freshness, get_forecast, compare_periods, list_anomalies, get_budget_status, get_expiring_commitments, get_savings, get_ai_spend.
  • The text output is a compact markdown briefing; structuredContent carries one key per section plus generatedAt, window, costBasis and sectionsUnavailable.

period is weekly (default; month-to-date vs the same days last month) or daily (last 7 days vs the 7 before). Cap every ranked list with items_per_section (default 5, max 10).

There is also a new cloudquell-weekly-briefing skill / MCP prompt that runs the briefing and drills into whatever crosses a threshold. It performs no writes.

run_cost_pivot now supports the resource dimension

Section titled “run_cost_pivot now supports the resource dimension”

Break any pivot down to individual resources — “which instances is this service’s spend actually on”. Four rules, all checked before the job is submitted so you get a sentence instead of an error code and a wasted query:

  • resource must be the last dimension;
  • the pivot must be scoped with integration_id — never provider or integration_ids, since resource ids are only meaningful within one connection;
  • at most 2 other dimensions before it;
  • a monthly window covers at most 3 months, not the usual 6.

Resource rows may carry _resource_id and _region — both are optional and independent, verified against prod: a row can have both, either or neither in the same response, and a missing _resource_id means the backend attributed none. The displayed resource value is sometimes the full ARN and sometimes a short name; it is the empty string "" for spend with no resource attribution (support, commitment fees, tax), and the literal "Others" for the collapse bucket — which can itself carry those keys, so detect it by the string.

For the single-service, single-day question, get_resource_breakdown is still the better tool: it takes a service and a date directly and runs no job.

months_back now narrows a pivot window (backend fix)

Section titled “months_back now narrows a pivot window (backend fix)”

Previously months_back was ignored on /cost-pivot-v2/jobs: the grain clamp overwrote an absent start_date with the grain floor, so months_back: 2 came back with seven monthly columns. Fixed in cost-reporting on 2026-09-06 and verified live — the same request now returns exactly three columns (two completed months plus the current MTD).

No action needed. The MCP server always translated months_back into an explicit start_date and continues to, so run_cost_pivot callers were never affected; the translation is now redundant and deliberately kept.

Pivots. run_cost_pivot + get_cost_pivot (2 new read tools, 53 total).

Arbitrary group-by breakdowns — by product, usage type, tag, region, cost centre, charge type or provider — at monthly / weekly / daily granularity. This closes the gap left in 0.2.0, where query_costs view="detailed" began returning pivotsAvailable: false and compare_periods could only group by service or account.

These supersede the deferred generate_report / get_report_job_status names from the original plan. Same underlying cost-pivot-v2 job API; the pivot-shaped names describe what the tools actually do, and neither generates a saved report (list_saved_reports / get_report remain the report tools).

  • Async by nature. Both drive an Athena job. run_cost_pivot waits up to wait_seconds (default 20, max 22) and returns rows if the job finishes, or a { jobId, status, pollAfterMs, expiresAt } handle if not. get_cost_pivot collects the handle. Jobs are org-scoped and expire after 24 hours; there is no cancel tool.
  • Both windows are always echoed. The backend clamps silently, so every response carries requestedWindow and returnedWindow (derived from columns, the only authority) plus a windowNote when they disagree.
  • limit is a client-side cap, applied after sorting by _total desc — the backend returns every matching row in one response and does not page. nextCursor is always null and cursor is inert.
  • RESPONSE_TOO_LARGE (413) is the documented main failure mode for a wide pivot, with the correct remedy: fewer dimensions or a shorter window.
  • Rows keep the backend’s key shape verbatim, so a tag pivot row carries a literal "tag:user_app" key. Tag keys are case- and hyphen-sensitive and the untagged bucket is lowercase untagged.
  • resource is not exposed in this wave — its scope and ordering restrictions warrant a separate tool.

Cross-links. query_costs view="detailed", compare_periods, get_tag_coverage and get_allocation_breakdown now point at run_cost_pivot with concrete examples, and the spend-review, allocation-hygiene and anomaly-triage skills use it.

Contract verified against prod. docs/cost-pivot-v2-contract.md is no longer marked unverified — see its §11 for what matched and the four things that differed, including that months_back cannot narrow a window (the tools translate it into an explicit start_date).

cost_basis is now LAYERED, not all-or-nothing. resolve_cost_settings in the cost-reporting Lambda loads the organization’s saved per-integration cost settings first and overrides only the toggles a request actually sends; an omitted toggle keeps its saved value instead of snapping to the API default. The costBasis echo mirrors that: source is organization_defaults when nothing was pinned and organization_defaults+overrides otherwise, with a new overrides array naming the pinned toggles. A null flag means “saved, and not visible to this server” — it never means false.

get_data_freshness reads the ingestion endpoint’s declared scope. GET /api/integrations/{integration_id}/ingestion-status now returns provider and scope (aws_only | generic). For a non-AWS connection it nulls the AWS-only counter blocks instead of zeroing them, derives status from the integration’s last_sync_at, and explains itself with a note — the zeros used to read as “never ingested” for an Azure/GCP/Snowflake/LLM connection that had been syncing for weeks. The tool now reports ingestionDetail: null plus ingestionDetailNote on those, and drops its provider heuristic. lastSyncAt from GET /api/integrations remains the authoritative verdict either way. The gateway path parameter was renamed {provider_id}{integration_id} to match what it has always carried.

get_resource_breakdown / investigate_anomaly report the window they covered. granularity defaults to "monthly" server-side, so a bare date resolves to that whole calendar month — and the tool used to describe the window by re-deriving the backend’s arithmetic from the request. The backend now states it: ?resolved_window=true returns { resolvedWindow: {granularity, start, end}, resources } (both dates inclusive) instead of the bare row array. Tools surface it as resolvedWindow / resourceWindow and narrate from it; against an older backend the derived text is flagged as such rather than asserted.

query_costs no longer takes group_by, and the backend 400s on dimensions. dimensions was parsed into the cost-dashboard route’s QueryFilters and then never read by any view’s SQL — a caller passing it got a 200 and a payload that was not pivoted, with nothing saying so. The route now rejects it with a 400 naming the cost-pivot-v2 job API. group_by on query_costs existed only to feed that parameter and is removed. compare_periods keeps its own group_by (service / account), which is real.

query_costs { view: "detailed" } returns what the backend actually sends. byProduct, byUsageType, byTag and byAccountMonth were projected out of a payload.detailedViewData key that the cost-dashboard backend has never sent — the dashboard builds that object client-side — so all four came back null on every call, with no error. They are removed from the response, the output schema and the tool description. The view now returns exactly availableServices, availableTagKeys and availableLinkedAccounts, plus pivotsAvailable: false and a note saying that product / usage-type / tag / account pivots are served by the cost-pivot-v2 job API and are not yet exposed through MCP.

Read tools (29): query_costs, get_ai_spend, get_savings, get_recommendation_history, list_anomalies, investigate_anomaly, get_weekly_cost_slices, list_anomaly_dismissals, get_budgets, get_budget, get_budget_status, list_commitments, get_expiring_commitments, get_tag_coverage, get_allocation_breakdown, list_cost_centers, list_allocation_rules, list_saved_reports, get_report, list_alert_rules, get_alert_rule, list_triggered_alerts, list_integrations, get_integration, list_notification_channels, get_metadata, get_available_services, get_available_tag_keys, get_available_linked_accounts.

Write tools (14, require admin:write + confirm:true): create_budget, update_budget, delete_budget, update_recommendation_status, dismiss_anomaly, undismiss_anomaly, trigger_integration_ingest, create_notification_channel, delete_notification_channel, test_notification_channel, send_invite, cancel_invite, update_organization_name, update_organization_preferences.

Notable design points in v1:

  • Workflow-shaped tools, not 1:1 endpoint mirrors. query_costs is one flexible cost query (view= + filters); get_savings returns the whole savings picture in one call; investigate_anomaly chains three reads into a single investigation.
  • Anomalies are derived client-side from month-over-month / week-over-week movement — the backend exposes no precomputed anomaly list.
  • create_budget does not accept a currency — the backend stamps the org’s display currency.
  • Stable public endpoint. Clients connect at the friendly custom domain https://mcp.cloudquell.com/mcp (live; TLS cert + API Gateway mapping + DNS all in place). The OAuth resource/audience identifier matches it, so this is the canonical URL for every client.

Eight new read tools, one new query_costs parameter, MCP prompts, and structured output. Entirely additive: no tool was renamed, no parameter changed meaning, and no write tool was added — the surface is still 14 writes.

New read tools (8):

  • get_forecast — projected month-end spend, org-wide plus per-service and per-account. A projection computed here from ingested daily data, not a backend forecast; it uses the same algorithm and anchors as the dashboard’s “Forecasted Month-End” KPI, so the two agree. Every answer carries a method and a plain-English confidence note (e.g. “only 4 days of data”) so a three-day extrapolation is never presented as a fact.
  • compare_periods — the “what changed?” tool. Two windowed queries in parallel, joined into a movers table ranked by the size of the change with new/gone flags. Explicit windows or the wow / mom / yoy presets. investigate_anomaly is the documented follow-up for why.
  • get_resource_breakdown — the per-resource step inside investigate_anomaly, exposed standalone: which individual resources drove a service’s spend, with each one’s share and its prior-period cost.
  • get_data_freshness — “can I trust these numbers?”. A fresh / stale / degraded verdict with a one-line reason, plus per-connection last-ingest and staleness. Run it first in any workflow that shows a number to a person.
  • list_organizations, get_current_organization, list_members, list_invites — organization context, the human roster, and the invite_id that cancel_invite needs (it was previously unguessable).

query_costs changes (additive):

  • cost_basis — pin the accounting basis for a call (use_amortized, include_credits, include_discounts, include_refunds, include_taxes). The applied basis comes back as costBasis so an agent can state it. ⚠ It is all-or-nothing: setting any field replaces the organization’s saved settings for that call and every omitted field takes the API default.
  • account_id now accepts a 12-digit account id as well as a name. The backend filters on the sub-account name, so an id used to return silently empty results; ids are now resolved automatically. get_forecast and get_resource_breakdown accept both too.

MCP prompts. The five FinOps skills are now served as MCP prompts (cloudquell-spend-review, -anomaly-triage, -budget-guardrails, -savings-actions, -allocation-hygiene), each taking optional period and provider arguments. Any client that lists prompts gets the workflows with no install step; the skill zips remain available for skill-aware clients.

Structured output. query_costs, get_forecast, compare_periods, get_weekly_cost_slices and get_ai_spend declare an outputSchema and return structuredContent alongside the text. The text is byte-identical to before — existing clients see no change.

Known limitation. compare_periods groups by service or account only. Product / usage-type / tag pivots have no windowed aggregation on the cost-dashboard data source, so they are deliberately not offered rather than returning silent zeros.

  • Service-account keys (cak_). Paid organizations mint, list, and revoke keys from Settings → API Keys; presented as Authorization: Bearer cak_… (read-only, org-scoped, 1-year validity). See API keys.
  • FinOps skills pack. Five installable Agent Skills (spend review, anomaly triage, budget guardrails, savings actions, allocation hygiene) — packaged workflow runbooks validated against the v1 tool surface. Source in skills/; customer downloads at docs.cloudquell.com.
  • Async report generation — both creating a report job (generate_report, a write) and polling its status (get_report_job_status) are deferred; they’ll ship together so an agent can start a job and track it end-to-end.
  • Unit economics (get_unit_cost) and resource-level waste listing — under consideration for a later wave.
  • Companion reads that unblock the writes: list_notification_channels, list_integrations, get_integration, list_anomaly_dismissals, get_recommendation_history — so an agent can discover the ids the write tools need rather than guessing.
  • New writes: update_budget, delete_budget, undismiss_anomaly; update_recommendation_status now accepts notes / realized_savings / dismissed_reason.
  • dismiss_anomaly now requires window (part of the dismissal key), so a dismissal can always be reversed by undismiss_anomaly.
  • Hardening: JSON-RPC batch requests are rejected (they could otherwise slip past the tier gate); every downstream call has a timeout; the DCR /register shim validates redirect_uris.
  • The SDK pin is exact (1.29.0) to keep the Lambda bundle reproducible and insulate the stateless transport from patch-level regressions. Any bump is validated against MCP Inspector before release.
  • Tool names and scopes are stable within v1. New tools are additive; if a tool’s behavior changes materially it will be noted here.