# CloudQuell MCP Server > Hosted, remote Model Context Protocol (MCP) server exposing CloudQuell's FinOps + > cloud-cost data to AI agents. Read-mostly: 54 tools (40 read, 14 write). Writes > are OFF BY DEFAULT for every organization: a write needs the `admin:write` OAuth > scope AND the org switch enabled AND an admin/super_admin org role AND > `confirm:true`. Org-scoped to the signed-in user. Streamable HTTP (stateless), > OAuth 2.1 + PKCE. ## Endpoint - MCP URL: https://mcp.cloudquell.com/mcp - Health (public): https://mcp.cloudquell.com/health - Transport: Streamable HTTP, stateless, JSON-buffered (no SSE, no session id) - Resource/audience (RFC 8707): https://mcp.cloudquell.com/mcp - Server version: 0.4.0 · tool surface v1 wave 3 · SDK @modelcontextprotocol/sdk 1.29.0 ## Connect (≈5 min) 1. Add the MCP URL to your client (Claude connector, Cursor mcp.json url, Goose remote extension, or `npx -y mcp-remote ` for stdio clients like Claude Desktop / Codex). 2. A browser opens the CloudQuell Hosted UI; sign in (email/password or Google). 3. Approve access. An interactive connector's token carries both `read` and `admin:write` (Cognito has no per-scope consent toggle, so every connector user gets both — including a viewer). The scope is NOT what permits a write: writes are off by default per organization and additionally need an admin role and `confirm:true`. 4. Ask: "Using CloudQuell, what's our MTD AWS spend vs last month?" Discovery (auto-configures compliant clients, no client id/secret needed): - GET /.well-known/oauth-protected-resource (RFC 9728) - GET /.well-known/oauth-authorization-server (RFC 8414) - POST /register (RFC 7591 Dynamic Client Registration shim) ## Auth model - OAuth 2.1 Authorization Code + PKCE (S256), public client (no secret). - Scopes: `read` (all read tools) and `admin:write` (lets write tools be ATTEMPTED). The DCR shim advertises both, so an interactive token always carries both; `cak_` service keys are minted read-only and can never hold `admin:write`. Standard OIDC scopes openid/email/profile identify the user. - WRITES ARE OFF BY DEFAULT, PER ORGANIZATION — including existing paid orgs. An org admin enables them at Settings → Organization → AI agent access. - Mutations gated four ways, server-side, before any downstream call, in order: (1) token carries `admin:write` -> else `insufficient_scope`; (2) org has `mcp_writes_enabled` -> else `writes_disabled_for_org`; (3) caller's org role is admin|super_admin -> else `insufficient_role`; (4) call passes `confirm:true` -> else `confirmation_required`. If the org switch/role cannot be read: `org_context_unavailable` (fails closed). Any refusal means NO downstream effect. - An agent can NEVER enable writes for itself: update_organization_preferences refuses to set `mcp_writes_enabled` (it strips the key and says so). Only a human admin, in the app. - Reads are unaffected: the org lookup behind checks 2-3 is made lazily on the first write of a request, never on a read. - Org is derived from the signed-in user (token `sub`); no cross-tenant access. CloudQuell role still governs admin-only actions downstream too. - Tokens are short-lived (~1h) with refresh. Headless/scheduled agents: paid orgs mint service-account `cak_` keys (Settings → API Keys; read-only, org-scoped, 1-year validity) and send `Authorization: Bearer cak_…` — no OAuth flow needed. ## Conventions - response_format: concise (default) | detailed. - limit on list tools; ~25k-token (~100KB) output cap — on overflow returns {truncated:true, message:...}, narrow the request. - Account ids resolved to names. Cost tools that take account_id accept EITHER a sub-account name OR a 12-digit account id (the id is resolved for you). - Cost data is ingested DAILY (not real-time) and provider billing exports lag 2-3 days; responses carry lastUpdated/currency where available. Call get_data_freshness before reporting numbers to a person. - Structured output: query_costs, get_forecast, compare_periods, get_weekly_cost_slices, get_ai_spend, run_cost_pivot, get_cost_pivot and get_finops_briefing declare an outputSchema and return structuredContent. For all but the briefing it mirrors the text exactly; the briefing's text is markdown by design, so both halves are built from one payload object. ## Tools — READ (40, scope: read) - get_finops_briefing: START HERE for "what should I look at today?" — the tool for SCHEDULED/HEADLESS agents and Monday-morning reviews. Calls up to EIGHT of the read tools below in parallel and joins them into one briefing, in order: freshness, spend, movers, anomalies, budgets, commitments, savings, ai_spend (only when include_ai_spend=true). FRESHNESS IS FIRST ON PURPOSE — when its verdict is not "fresh" the briefing opens with a warning and every number below carries that caveat. Each section degrades independently: a failed/timed-out one returns {status:"unavailable", reason} and is named in `sectionsUnavailable`; one a provider cannot have (commitments for an LLM provider, savings outside AWS/Azure) returns {status:"not_applicable"}. NEVER read a missing section as zero. Drill into each with its own tool: get_data_freshness, get_forecast, compare_periods, list_anomalies, get_budget_status, get_expiring_commitments, get_savings, get_ai_spend. Text output is compact markdown; structuredContent has one key per section plus generatedAt, window, costBasis, sectionsUnavailable. period(weekly|daily=weekly), provider, integration_id, cost_basis, include_ai_spend(false), items_per_section(5, max 10), anomaly_min_pct(25), anomaly_min_cost(100), response_format. Whole call is bounded at 24s. - query_costs: flexible cost query. view=overview (default: MTD vs prev month, by-account/by-service rollups, service comparison) | detailed (the pivot VOCABULARY for the scope: availableServices, availableTagKeys, availableLinkedAccounts — NOT pivot tables; there is no group_by on this tool). Filters: provider (aws|azure|gcp|snowflake|anthropic|openai — scope to one provider's integrations), start_date, end_date (YYYY-MM-DD), account_id (account NAME, or its 12-digit id), service. cost_basis{use_amortized, include_credits, include_discounts, include_refunds, include_taxes} pins the accounting basis and is echoed back as costBasis — LAYERED: each field you set overrides just that toggle, and every field you omit keeps the org's saved value. costBasis reports source=organization_defaults (nothing pinned) or organization_defaults+overrides with `overrides` naming the pinned toggles; a null flag means "saved, not visible from here", never false. (Other views handled by dedicated tools below.) - run_cost_pivot: arbitrary group-by pivot (THE tool for product/usage-type/tag/ region/cost-centre/charge-type/provider breakdowns; query_costs view=detailed has none and compare_periods is service|account only). dimensions[1-3] from service|account|region|provider|service_category|subcategory|usage_family| charge_type|charge_subtype|tagged|cost_center|resource|tag:. Scope: exactly one of provider|integration_id|integration_ids. granularity monthly(default)|weekly| daily; start_date/end_date or months_back(default 3). ASYNC Athena job: returns rows if quick, else {jobId,status,pollAfterMs} -> call get_cost_pivot. Windows are clamped SILENTLY (daily ~30d, weekly ~55d, monthly ~6mo) so the response echoes requestedWindow + returnedWindow + windowNote. Rows key each dimension VERBATIM (literal "tag:user_app"); tag keys are case/hyphen-sensitive (get_available_tag_keys first); untagged bucket is lowercase. limit is a CLIENT-side cap after sorting by _total desc. Main failure: RESPONSE_TOO_LARGE (413) -> fewer dimensions or shorter window, never a different limit. - run_cost_pivot `resource` dimension: breaks any pivot down to individual resources. FOUR rules, all enforced client-side BEFORE the job is submitted: (1) `resource` must be the LAST dimension; (2) scope with integration_id ONLY — never provider/integration_ids (resource ids are only meaningful within one connection); (3) at most 2 other dimensions before it; (4) a monthly window covers at most 3 months, not 6 (the backend clamps that silently, hence the local refusal). ROW SHAPE (verified on prod, not assumed): _resource_id and _region are OPTIONAL AND INDEPENDENT — a row may have both, either or neither in the SAME response; a missing _resource_id means the backend attributed none, not that the resource has none. The displayed `resource` value is sometimes the full ARN and sometimes a short name, so prefer _resource_id for console links. `resource` can be the EMPTY STRING "" = spend with no resource attribution (support/commitment fees/tax), not null. Rows valued "Others" are the collapse bucket — detect by that literal string, since such a row can still carry _resource_id/_region. For the single-service, single-day question use get_resource_breakdown instead (no job). - get_cost_pivot: collect a run_cost_pivot job by job_id. Running -> {status, pollAfterMs} (grandTotals may already be final while rows are not — not a result). Succeeded -> same shape as run_cost_pivot. No paging: nextCursor always null, cursor inert. Jobs org-scoped, expire 24h (JOB_NOT_FOUND after). - get_forecast: projected month-end spend, org-wide + per-service/per-account rankings. A PROJECTION computed from ingested daily data, not a backend forecast — same algorithm/anchors as the dashboard's "Forecasted Month-End" KPI. Returns asOfDay, daysInMonth, mtdActual, forecastMonthEnd, previousMonthTotal, deltaVsPreviousMonthPct, method(prev-month-shape|linear| flat-mtd|waiting) and a plain-English confidence note — ALWAYS report the note with the number. Per-entity rows pace independently and do NOT sum to the total. provider, integration_id, account_id, service, limit(10). - compare_periods: THE "what changed" tool. Two windowed queries in parallel, joined into movers ranked by absolute delta with new/gone flags. Deltas are period_a MINUS period_b. period_a/period_b{start_date,end_date} or preset(mom default|wow|yoy), group_by(service default|account — the only two pivots with windowed aggregation), provider, integration_id, cost_basis, limit(25). Follow up with investigate_anomaly for WHY. - get_ai_spend: AI/LLM spend. provider(anthropic|openai|bedrock, default anthropic), include_bedrock(default false), group_by(model|token_type|workspace|api_key|service_tier|cost_type| provider|day|none), filters model/token_type/workspace (string or array), start_date+end_date(req). Rows carry cost($, org currency) + tokens + requests + tokenMeteredCost (the $/1M numerator — divide by that, not cost). DIRECT providers only unless you opt in: Bedrock is AWS-billed and ALREADY in cloud spend. With include_bedrock/provider=bedrock the answer separates directTotal (not in cloud totals) from crossBilledTotal (already there) and allAiTotal; never add those, or totalCost, to a cloud figure. - get_savings: savings rollup (banked/open/potential) + by-action-type breakdown + effective-savings metrics + recommendations. Filters: minMonthlySavings, actionType, accountId, product, limit(50). - get_recommendation_history: status-change audit trail for one recommendation. recommendation_id(req). - list_anomalies: anomalies DERIVED client-side from MoM/WoW movement. provider(aws|azure|gcp|snowflake|anthropic|openai), min_pct_increase(25, >=0), min_cost(0), service, limit(50). For anthropic/openai "service"=model; for snowflake it's the warehouse/usage type. - investigate_anomaly: deep-dive one service (chains anomalies + resource_breakdown + ±7-day daily series + verdict). service(req), provider, date, account_id, top_n_resources(15). - get_weekly_cost_slices: weekly per-service slices + MoM comparison. provider, service, account_id, limit(500). - get_resource_breakdown: one service's spend broken down to INDIVIDUAL RESOURCES (the step investigate_anomaly runs internally, standalone). Returns resource id/type/account/region/operation/usage type, each one's share of the service's window spend, and its previous-period cost. service(req), date(defaults to the latest ingested day for that service), granularity(monthly default = the whole CALENDAR MONTH containing date | weekly | daily), provider(defaults to all cloud providers), account_id, integration_id, limit(25). - list_anomaly_dismissals: currently-suppressed anomalies + their (service,period, window) key (for undismiss_anomaly). - get_budgets: budget definitions. integration_id, limit. - get_budget: one budget by budget_id(req). - get_budget_status: actual vs forecast + status (on_track|warning|over). integration_id, limit. - list_commitments: RIs & Savings Plans + utilization/expiry summary. integration_id. - get_expiring_commitments: status filter, default expiring_soon (active|expiring_soon|expired|unknown). integration_id. - get_tag_coverage: tagged vs untagged split, by tag key, top untagged services. - get_allocation_breakdown: spend by cost center (own + rolled-up + %), Unallocated bucket. - list_cost_centers: cost-center definitions + parent/child hierarchy. - list_allocation_rules: rules mapping spend to cost centers. cost_center_id filter. - list_saved_reports: saved custom reports + pivot config. integration_id. - get_report: one saved report by report_id(req) (filtered client-side). - list_alert_rules: configured cost-alert rules. integration_id. - get_alert_rule: one rule by alert_id(req). - list_triggered_alerts: firing history (rule firings + budget crossings). since, severity(delivered|failed|suppressed), limit(1-500). - list_integrations: provider integrations/connections (id, name, provider, status) — the integration_id other tools accept. - get_integration: one integration by integration_id(req). - list_notification_channels: configured channels (channel_id, name, type) — the channel_id delete/test need. - get_metadata: services + tag keys + linked accounts in one call. integration_id. - get_available_services: valid `service` filter values. integration_id. - get_available_tag_keys: valid tag keys. integration_id. - get_available_linked_accounts: {accountId, accountName} pairs. Either value works as an account_id filter. integration_id. - get_data_freshness: "can I trust these numbers?" — verdict fresh|stale|degraded with a one-line reason, plus per-connection provider/status/lastSuccessfulIngest/ staleness hours/error. degraded = a connection is failing, was never set up, or the account is FROZEN (recent data hidden on purpose). Run it FIRST in any workflow that shows a number to a person. stale_after_hours(48), include_ingestion_detail(true), limit(50). - list_organizations: every org the signed-in user belongs to + their role, tier, member count and the default. All other tools act on the CURRENT org only. - get_current_organization: the org every other tool acts on — name, id, tier, status, counts, trial/cancellation state, and your_role (what actually decides whether a write is accepted server-side). - list_members: the human roster of the current org (user_id, email, name, role, joined_at) + a count per role. - list_invites: invites the org has SENT (pending + recently expired), each with the invite_id cancel_invite needs. The accept token is NEVER returned. Requires an admin role. status(pending|expired) filter. ## Tools — WRITE (14; need admin:write + org switch on + admin role + confirm:true) - create_budget: name(req), amount(req), start_date(req), period(monthly|quarterly| annual=monthly), integration_id, scope{platform|services|tagKey+tagValue| costCenterId}, threshold_pcts, notification_channels. Currency NOT settable. Additive. - update_budget: budget_id(req) + any of name, amount, start_date, period, integration_id, scope, threshold_pcts, notification_channels, enabled. Additive, idempotent. - delete_budget: budget_id(req). DESTRUCTIVE, idempotent. - update_recommendation_status: recommendation_id(req), status(req), notes, realized_savings, dismissed_reason. Additive, idempotent. - dismiss_anomaly: service(req), period(req e.g. "2026-06"), window(req e.g. "mtd"), reason. Additive, idempotent. - undismiss_anomaly: service(req), period(req), window(req) — reverse a dismissal. DESTRUCTIVE, idempotent. - trigger_integration_ingest: integration_id(req). Additive, idempotent. - create_notification_channel: channel_name(req), channel_type(req: slack|teams|email|webhook), destination(req). Additive. - delete_notification_channel: channel_id(req). DESTRUCTIVE, idempotent. - test_notification_channel: channel_id(req). Sends a REAL test message. Additive. - send_invite: email(req), role(viewer|contributor|admin|super_admin=contributor), message. Additive. - cancel_invite: invite_id(req) — get it from list_invites. DESTRUCTIVE, idempotent. - update_organization_name: org_id(req), organization_name(req). Additive, idempotent. - update_organization_preferences: org_id(req), preferences(req). Additive, idempotent. REFUSES the key `mcp_writes_enabled` (strips it, reports it) — an agent can never turn on MCP writes for its own organization. ## FinOps skills / MCP prompts - The same six FinOps workflows are available two ways: - **MCP prompts** — served by the server itself, so any client that lists prompts gets them with no install: cloudquell-weekly-briefing, cloudquell-spend-review, cloudquell-anomaly-triage, cloudquell-budget-guardrails, cloudquell-savings-actions, cloudquell-allocation-hygiene. Each takes optional `period` and `provider` arguments, appended as a focus note. - **Installable Agent Skills** — for Claude Code / Claude.ai / any skill-aware agent. Download + install: https://docs.cloudquell.com/mcp/get-started/skills/ (source: ../../skills/ in this repo). ## Coming soon / deferred - Async reports (generate_report write + its status poller, deferred as a unit). - Comparing a period change by product / usage type / tag: compare_periods supports service and account only — the cost-dashboard data source has no windowed aggregation for the other dimensions. ## Docs - Overview: ./overview.md - Supported clients: ./supported-clients.md - Connecting: ./connecting.md - Authentication & authorization: ./authentication.md - Access, limits & data scope: ./access-limits-and-data-scope.md - Tool reference: ./tool-reference.md - Example prompts & workflows: ./example-prompts-and-workflows.md - Prompting best practices: ./prompting-best-practices.md - Validating responses: ./validating-responses.md - Security & privacy: ./security-and-privacy.md - Troubleshooting & FAQ: ./troubleshooting-and-faq.md - Changelog & versioning: ./changelog.md