> ## Documentation Index
> Fetch the complete documentation index at: https://docs.grainql.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Security & connections

> How Grain secures MCP access — scopes, tokens, revocation, and what each credential can actually do.

Handing an AI agent access to your analytics is a real trust decision. This page explains exactly what Grain does (and doesn't do) with the credentials an MCP client obtains, and how to stay in control.

## The three credential paths

Grain's MCP endpoint accepts three kinds of credentials, in priority order. Most users only ever use the first one.

<Tabs>
  <Tab title="OAuth (recommended)">
    `Authorization: Bearer mcpat_…`

    Issued by Grain's own OAuth 2.1 server after you click **Authorize** in the browser. Scopes are enforced — the client sees only the tools it was granted.

    **Used by:** Claude.ai, Claude Desktop, Claude Code, Cursor, ChatGPT, and any MCP‑compliant client with OAuth support.

    **Workspace:** pinned at consent time, immutable for the lifetime of the token.
  </Tab>

  <Tab title="Raw Auth0 bearer">
    `Authorization: Bearer eyJ…` (a signed JWT)

    Used by same‑origin code in the Grain dashboard itself. Forwarded as‑is to `grainsvc`, so the client sees the full tool surface.

    **Tenant source:** `X-Grain-Tenant` header or `?tenant=` query param.

    **Who should use this:** mostly nobody — it exists for the dashboard's own server‑side calls.
  </Tab>

  <Tab title="Tenant API key">
    `X-API-Key: <tenant secret>`

    Legacy path that predates MCP. Restricted to the three Query API tools: `grain.events.list`, `grain.query`, `grain.query.count`, `grain.dimensions.discover`. No scope filtering (the key already declares its permissions).

    **Who should use this:** custom server‑to‑server integrations that can't do OAuth.
  </Tab>
</Tabs>

## What scopes actually mean

Scopes are the control plane. When a client asks to authorize, it specifies the scopes it needs. You can **always approve less than it asks for** — the consent screen shows each scope with a plain‑English description.

| Scope             | Tools unlocked                                                                                    | Rough question it answers   |
| ----------------- | ------------------------------------------------------------------------------------------------- | --------------------------- |
| `mcp:read`        | `grain.events.list`, `grain.dimensions.discover`, `grain.integration.status`                      | "What's in this workspace?" |
| `mcp:query`       | `grain.query`, `grain.query.count`, `grain.query.compare`, `grain.query.digest`                   | "What happened?"            |
| `mcp:investigate` | `grain.correlate_event`, `grain.track.analyze`, `grain.segment.compare`, `grain.sessions.cluster` | "Why did it happen?"        |

<Tip>
  If you only want an agent to produce weekly digests, approve `mcp:read` + `mcp:query` and leave `mcp:investigate` off. You can always reconnect later and grant more.
</Tip>

There is no write scope in the current surface. No MCP tool creates, updates, or deletes anything — not segments, not tracks, not users. You can safely grant all three scopes to a trusted client without worrying about mutations.

## How tokens are stored

This is the part most people care about when they ask "is this safe?":

<AccordionGroup>
  <Accordion title="Grain‑issued tokens (`mcpat_…`, `mcprt_…`)">
    Stored only as **SHA‑256 hashes**. The plaintext values leave Grain's servers exactly once — in the `/oauth/token` response to the client. If someone dumps the database, there is no way to reconstruct a valid token from the rows.
  </Accordion>

  <Accordion title="Upstream Auth0 tokens">
    The access token and refresh token Grain captures from Auth0 during consent are **AES‑GCM encrypted** application‑side before they're inserted. The encryption key lives in `MCP_SESSION_ENC_KEY` (never in the database). Every write is ciphertext; no plaintext Auth0 token is ever persisted or logged.
  </Accordion>

  <Accordion title="Authorization codes">
    Single‑use, 60‑second TTL, atomic compare‑and‑swap on redemption. A leaked code can be spent exactly zero additional times.
  </Accordion>

  <Accordion title="PKCE">
    S256 only. No `plain` code\_challenge\_method is accepted, and no client secrets are ever issued. This is a hard requirement of RFC 8252 / OAuth 2.1 and we lean into it.
  </Accordion>

  <Accordion title="Row‑level security">
    The three OAuth tables (`mcp_oauth_clients`, `mcp_oauth_codes`, `mcp_oauth_sessions`) are RLS‑locked to `service_role`. The anon key cannot read a row under any circumstances.
  </Accordion>
</AccordionGroup>

## Managing your connections

Every active MCP client shows up in one place:

<Card title="Settings → Connectors → MCP clients" icon="plug" href="https://grainql.com/dashboard/settings?tab=connectors&category=mcp">
  See which clients are connected, what scopes they have, when they last called a tool, and revoke any of them with one click.
</Card>

For each connection you can see:

* **Client name** (e.g. `Claude Desktop`, `Cursor`) — captured during Dynamic Client Registration.
* **Workspace** the session is pinned to.
* **Scopes** granted.
* **Connected** — when you authorized it.
* **Last used** — when the client last successfully called a tool.
* **Session ID** — a short opaque handle for support.

### Revoking a connection

Click **Revoke**. A confirmation dialog appears; confirm and the session's `revoked_at` timestamp fills immediately. The client's next tool call returns `401` with a `WWW-Authenticate` header — compliant clients will either prompt you to re‑authorize or mark the server as disconnected.

<Warning>
  Revocation is instant on the Grain side but some clients cache tool lists in‑process. If a revoked client seems to still see tools, restart it; any actual tool call will fail.
</Warning>

## Workspace isolation

This is the invariant that matters most in a multi‑tenant product: **an MCP session is pinned to exactly one workspace, chosen at consent time.**

* You can't switch workspaces mid‑session. If you want the agent to look at a different workspace, authorize a second connection.
* Every tool call re‑checks the tenant against the session. A user who is removed from a workspace loses MCP access to it the next time the client calls a tool.
* The OAuth consent flow re‑verifies your membership on the chosen workspace before issuing an authorization code. Stale memberships can't sneak through.

## What gets logged

* **Request‑level:** method, tool name, caller's tenant, elapsed ms, budget consumed. No arguments, no result bodies.
* **Errors:** normalized error message + HTTP status. Stack traces only in dev.
* **Audit:** every successful tool call updates `last_used_at` on the session row, which is what you see in the Connections panel.

Plaintext tokens never appear in logs. If you ever see one — that's a bug; please report it.

## Hardening checklist for teams

<AccordionGroup>
  <Accordion title="Use a test workspace first">
    Pipe a low‑volume / staging workspace into Grain, connect your AI client to *that*, and watch a few sessions. Promote to production only once the blast radius feels comfortable.
  </Accordion>

  <Accordion title="Grant the minimum scope that works">
    If the agent only ever does briefings, `mcp:read + mcp:query` is enough. Add `mcp:investigate` once the agent demonstrably needs it.
  </Accordion>

  <Accordion title="Review the Connections panel weekly">
    Especially after turnover — revoke anything where "Last used" is "never" or older than your tolerance.
  </Accordion>

  <Accordion title="Rotate the per‑tenant API key yearly">
    Only relevant if you're using the legacy `X-API-Key` path. OAuth sessions rotate automatically on refresh.
  </Accordion>
</AccordionGroup>

## References

* [RFC 8414 — OAuth 2.0 Authorization Server Metadata](https://datatracker.ietf.org/doc/html/rfc8414)
* [RFC 9728 — OAuth 2.0 Protected Resource Metadata](https://datatracker.ietf.org/doc/html/rfc9728)
* [RFC 7591 — OAuth 2.0 Dynamic Client Registration](https://datatracker.ietf.org/doc/html/rfc7591)
* [Model Context Protocol — Authorization](https://modelcontextprotocol.io/specification/draft/basic/authorization)
