Skip to main content
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.

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.
ScopeTools unlockedRough question it answers
mcp:readgrain.events.list, grain.dimensions.discover, grain.integration.status”What’s in this workspace?”
mcp:querygrain.query, grain.query.count, grain.query.compare, grain.query.digest”What happened?”
mcp:investigategrain.correlate_event, grain.track.analyze, grain.segment.compare, grain.sessions.cluster”Why did it happen?”
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.
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?”:
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.
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.
Single‑use, 60‑second TTL, atomic compare‑and‑swap on redemption. A leaked code can be spent exactly zero additional times.
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.
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.

Managing your connections

Every active MCP client shows up in one place:

Settings → Connectors → MCP clients

See which clients are connected, what scopes they have, when they last called a tool, and revoke any of them with one click.
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.
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.

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

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.
If the agent only ever does briefings, mcp:read + mcp:query is enough. Add mcp:investigate once the agent demonstrably needs it.
Especially after turnover — revoke anything where “Last used” is “never” or older than your tolerance.
Only relevant if you’re using the legacy X-API-Key path. OAuth sessions rotate automatically on refresh.

References