Changelog & versioning
Versioning
Section titled “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.
v1 wave 3 — server 0.4.0
Section titled “v1 wave 3 — server 0.4.0”⚠ 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:
# 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.
The gate, in full
Section titled “The gate, in full”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
freshthe 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 insectionsUnavailable; the briefing still succeeds. A section a provider cannot have (commitments for an LLM provider, savings outside AWS/Azure) reportsnot_applicable. Never read a missing section as zero. - Each section drills down with its named tool —
get_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;
structuredContentcarries one key per section plusgeneratedAt,window,costBasisandsectionsUnavailable.
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:
resourcemust be the last dimension;- the pivot must be scoped with
integration_id— neverproviderorintegration_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.
v1 wave 2 — server 0.3.0
Section titled “v1 wave 2 — server 0.3.0”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_statusnames 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_reportremain the report tools).
- Async by nature. Both drive an Athena job.
run_cost_pivotwaits up towait_seconds(default 20, max 22) and returns rows if the job finishes, or a{ jobId, status, pollAfterMs, expiresAt }handle if not.get_cost_pivotcollects 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
requestedWindowandreturnedWindow(derived fromcolumns, the only authority) plus awindowNotewhen they disagree. limitis a client-side cap, applied after sorting by_totaldesc — the backend returns every matching row in one response and does not page.nextCursoris alwaysnullandcursoris 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 lowercaseuntagged. resourceis 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).
Unreleased — backend-contract fixes
Section titled “Unreleased — backend-contract fixes”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.
v1 — initial release
Section titled “v1 — initial release”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_costsis one flexible cost query (view=+ filters);get_savingsreturns the whole savings picture in one call;investigate_anomalychains 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_budgetdoes 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.
Shipped after v1 (additive)
Section titled “Shipped after v1 (additive)”v1 wave 1 — server 0.2.0
Section titled “v1 wave 1 — server 0.2.0”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 amethodand a plain-Englishconfidencenote (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 withnew/goneflags. Explicit windows or thewow/mom/yoypresets.investigate_anomalyis the documented follow-up for why.get_resource_breakdown— the per-resource step insideinvestigate_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?”. Afresh/stale/degradedverdict 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 theinvite_idthatcancel_inviteneeds (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 ascostBasisso 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_idnow 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_forecastandget_resource_breakdownaccept 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.
Earlier
Section titled “Earlier”- Service-account keys (
cak_). Paid organizations mint, list, and revoke keys from Settings → API Keys; presented asAuthorization: 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.
Deferred (not in v1)
Section titled “Deferred (not in v1)”- 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.
Recent additions (v1, additive)
Section titled “Recent additions (v1, additive)”- 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_statusnow acceptsnotes/realized_savings/dismissed_reason. dismiss_anomalynow requireswindow(part of the dismissal key), so a dismissal can always be reversed byundismiss_anomaly.- Hardening: JSON-RPC batch requests are rejected (they could otherwise slip
past the tier gate); every downstream call has a timeout; the DCR
/registershim validatesredirect_uris.
Upgrade / stability notes
Section titled “Upgrade / stability notes”- 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.