Skip to content

Authentication & authorization

The CloudQuell MCP server is an OAuth 2.1 resource server. Access is gated by standard, short-lived OAuth tokens minted by the CloudQuell identity service (Amazon Cognito). Writes are OFF by default for every organization: a write additionally needs an org-level switch that only a human admin can turn on, an admin role, and confirm: true — all checked server-side before anything moves.

  • Grant: Authorization Code with PKCE (S256) — mandated by the MCP authorization spec for remote HTTP servers.
  • Public client: the MCP app client has no secret; PKCE protects the code exchange. Your MCP client obtains the client id automatically via the /register (Dynamic Client Registration) shim — you never paste a client id or secret.
  • Identity provider: the CloudQuell Hosted UI (cloudquell-prod-auth.auth.us-west-2.amazoncognito.com), supporting email/password and Google sign-in — the same accounts as the dashboard.
  • Token lifetimes: access tokens are short-lived (~1 hour); a refresh token keeps the connection alive without re-prompting. Scheduled/agentic use is supported by a long refresh-token lifetime.

Clients configure themselves from these (all public, unauthenticated):

Route Spec Purpose
GET /.well-known/oauth-protected-resource RFC 9728 Declares the resource URI + supported scopes; points clients at the authorization server.
GET /.well-known/oauth-authorization-server RFC 8414 Maps OAuth endpoints onto the CloudQuell Hosted UI (/authorize, /token) and advertises /register.
POST /register RFC 7591 Dynamic Client Registration shim — returns the shared public PKCE client id.

The server’s canonical resource URI — and the OAuth audience — is:

https://mcp.cloudquell.com/mcp

Clients send resource=https://mcp.cloudquell.com/mcp on the authorize and token requests (RFC 8707). The server validates that tokens were minted for it and rejects tokens issued for anything else — the confused-deputy defense the spec requires. This URL is both the OAuth audience/resource identifier and the live endpoint you connect to — the same https://mcp.cloudquell.com/mcp used everywhere in these docs.

The server defines two custom scopes on its resource server:

Scope Fully-qualified name Grants
read https://mcp.cloudquell.com/mcp/read All 40 read tools. The only scope a cak_ service key can hold.
admin:write https://mcp.cloudquell.com/mcp/admin:write Lets the 14 write tools be attempted, in addition to read. It is not on its own permission to write — see below.

Plus the standard OIDC scopes (openid, email, profile) used to identify you.

  • read is the baseline. A token that carries only read can analyze everything and mutate nothing — every write tool refuses with insufficient_scope before any backend call.

  • An interactive connector’s token carries BOTH scopes, always. There is no per-scope consent toggle: the Dynamic Client Registration shim advertises the full scope set for every registration, so the token your client receives includes admin:write no matter who signed in — including a viewer. Cognito cannot express per-scope consent, so this is not something the server can fix by asking for less.

    That is exactly why admin:write is not what protects a write. The scope only says “this token may attempt a write tool”. What decides whether the write actually happens is the per-organization switch and your org role, both checked below.

Want a genuinely read-only connection? Use a cak_ service key (API keys) — those are minted read-only and cannot carry admin:write at all.

A new CloudQuell organization cannot be changed over MCP at all. Every organization starts with MCP writes disabled, and that includes existing paid organizations — nothing was grandfathered in. Until an admin turns the switch on, every write tool refuses and the connector is, in practice, a read-only analyst.

Turn it on in the CloudQuell app: Settings → Organization, in the AI agent access card, using the toggle “Allow AI agents to make changes via MCP”. Only an org admin or super_admin can — the toggle is visible but disabled for other roles — and only in the app: an agent cannot enable it for itself (see below).

Every write tool enforces these server-side, before any downstream call, in this order:

# Check Refusal when it fails
1 The access token carries admin:write insufficient_scope
2 The organization has MCP writes enabled writes_disabled_for_org
3 Your org role is admin or super_admin insufficient_role
4 The tool input includes confirm: true confirmation_required

If any check fails the tool returns that error and nothing changes — no budget is created, no invite is sent, no channel is touched. If the server cannot read your organization’s switch or your role, it refuses with org_context_unavailable rather than guessing: the gate fails closed.

The order is deliberate. The switch is checked before your role so that an organization which has not opted in gets told about the switch — the thing an admin can act on — rather than about the caller’s role. And confirm is checked last, so a caller who would be refused anyway is never told that all they need is confirm: true.

// Refused — the organization has not enabled MCP writes:
{ "name": "Prod AWS monthly", "amount": 50000, "start_date": "2026-07-01", "confirm": true }
// → "writes_disabled_for_org: MCP writes are turned off for this organization.
// An org admin can enable them in Settings → Organization → AI agent access."
// Refused — writes are on, but you are a viewer:
// → "insufficient_role: your role is viewer; MCP writes require admin or super_admin"
// Refused — everything else passes, but confirm is missing:
// → "confirmation_required: pass confirm:true to perform this write"
// Performed — switch on, admin role, confirm:true:
{ "name": "Prod AWS monthly", "amount": 50000, "start_date": "2026-07-01", "confirm": true }

This is stricter than relying on the MCP client’s allow/deny prompt: even a misbehaving client cannot mutate anything unless the organization has opted in and the signed-in human is an admin.

An agent can never enable writes for itself

Section titled “An agent can never enable writes for itself”

update_organization_preferences refuses to set mcp_writes_enabled. It strips the key, tells you it did, and applies the rest of your preference change. Otherwise the whole model would be circular — “MCP writes are off” would be fixable by an MCP write. Only a human admin, in the app, can flip it.

None of this touches reads. The organization lookup that backs checks 2 and 3 is made lazily, on the first write of a request, so a read-only session never performs it and pays nothing for it.

The MCP annotations (destructiveHint, etc.) shown in the Tool Reference are UI hints only; they never gate behavior. The four checks above are the real control.

Authorization (RBAC) is enforced by CloudQuell

Section titled “Authorization (RBAC) is enforced by CloudQuell”

Your role and organization membership apply at two layers, and both still hold:

  • At the MCP server (check 3 above): writes require admin or super_admin.
  • Downstream, in CloudQuell, as before. The server forwards a token that identifies you (by your Cognito sub); the backend resolves your organization and role from the database on every request, and admin-only operations still require the appropriate org role there. Holding admin:write does not elevate your CloudQuell role.

The MCP-layer role check does not replace the downstream one — it narrows what reaches it, so a refusal is explained in terms you can act on instead of arriving as a bare 403 from a backend you never called directly.

So a write needs: the admin:write scope + the organization switch + an admin org role + confirm: true + the backend permission for that action under your role.

The agent operates within your organization only. Identity is derived from your token’s sub, mapped to your org membership in CloudQuell — not from anything the client or agent can set. There is no parameter to “switch” to an org you don’t belong to. See Access, limits & data scope.

For headless / scheduled agents that can’t do an interactive browser login, paid organizations can mint service-account keys (prefix cak_) from Settings → API Keys in the dashboard. A key is presented as Authorization: Bearer cak_…, is always read-only — it is minted with the read scope alone and cannot carry admin:write, so it fails check 1 no matter what the organization switch says — org-scoped, and valid for one year (revocable immediately). No sign-in, refresh, or client registration is needed — the key is the credential. See API keys for the full lifecycle and usage guidance.