vTilt
Why vTiltHow It WorksFeaturesFAQDocs
Docs / OAuth
Quick startEvent forwarding
MCP server
Guides
OverviewAuthenticationOAuthAgent skills (prompts)AI intelligenceGoogle Ads
Client setup
CursorClaude DesktopVS CodeCodex
Realtime
Debug ViewRealtime Dashboard
Integration guides
Frontend frameworks
Next.jsNuxt.jsVue.jsReactReact RouterRemixGatsbySvelte / SvelteKitAstroAngularTanStack StartDocusaurus
Backend frameworks
NestJSHonoCloudflare WorkersDjangoFlaskLaravelPhoenixRuby on Rails
Backend languages
PythonPHPRubyElixirGoJava.NET / C#Rust
Stack guides
Vue + PHP
SDK
Browser SDK
InstallScript bundlesEvent trackingAutocaptureIdentify & aliasWeb VitalsSession recordingChat widgetFeature readinessRemote configurationReverse proxyDebug logging
Node SDK
Install & setupCapture, identify & aliasContext & shutdown

Documentation

vTilt
Quick startEvent forwarding

MCP server

OverviewAuthenticationOAuthAgent skills (prompts)AI intelligenceGoogle Ads

Realtime

Debug ViewRealtime Dashboard

Integration guides

SDK

OAuth

Connect MCP clients to vTilt with OAuth 2.1 + PKCE — no personal API key required. Covers dynamic client registration, discovery, scopes, and the consent flow.

Note

Note: Part of the MCP server docs — pair this with Authentication for vtu_ keys and headers.

vTilt's MCP server speaks OAuth 2.1 with PKCE, so modern MCP clients (Claude Desktop, ChatGPT Connector, Cursor, custom agents) can sign you in with a browser instead of asking for a vtu_ personal API key. The first time a client connects, vTilt walks you through the same sign-in flow you use for the dashboard and shows a one-screen consent prompt; after that the client refreshes its access token in the background.

Tip

Tip: Personal API keys still work. OAuth is additive — pick whichever fits your client. New clients should prefer OAuth; legacy stdio setups and shell scripts can stay on vtu_ keys.

#When to choose OAuth vs a personal API key

You're using…Use OAuthUse a personal API key
Claude Desktop, ChatGPT Connector, Cursor (≥ MCP DCR support)Yes — click "Connect" and sign inOnly if your client doesn't support OAuth yet
A shell script, CI job, or curlNo — there's no browser to consentYes — paste Authorization: Bearer vtu_…
A custom agent you wroteYes if you can wire OAuth 2.1 + PKCEYes if shipping OAuth is overkill
Sharing with a teammateEach teammate connects their own clientMint a new key per teammate (revoke independently)

OAuth tokens are short-lived JWTs (1 hour) refreshed automatically; vtu_ keys live until you revoke them. Both flows give the same access to the same projects you can see in the dashboard.

#1. Connect your client

Most clients that support OAuth detect vTilt automatically once you point them at the MCP URL:

https://www.vtilt.com/api/mcp
text

The client fetches /.well-known/oauth-protected-resource/api/mcp to discover that this resource is protected by vTilt's authorization server, then redirects you to https://www.vtilt.com/auth/signin in your browser. After you sign in, vTilt shows a consent screen listing the client's name, homepage, and the scopes it's requesting. Click Allow and the client receives an authorization code, exchanges it for an access token, and starts calling tools.

You won't see this flow again until the client is reset — refreshing tokens happens silently in the background.

#2. Discovery endpoints

These are public, unauthenticated metadata documents that let MCP clients self-configure:

EndpointWhat it advertises
https://www.vtilt.com/.well-known/oauth-authorization-serverOAuth 2.1 authorization server metadata (RFC 8414): authorize, token, registration URLs.
https://www.vtilt.com/.well-known/openid-configurationOpenID Connect Discovery document. Same data as above plus OIDC-specific fields.
https://www.vtilt.com/.well-known/oauth-protected-resource/api/mcpProtected Resource Metadata (RFC 9728): tells clients which authorization server protects /api/mcp.

Note

Note: Clients should fetch these documents at connection time and re-fetch periodically — never hard-code the endpoint URLs.

#3. Dynamic Client Registration

vTilt supports Dynamic Client Registration (RFC 7591), so MCP clients can self-register without you provisioning anything in the dashboard. The flow is:

curl -X POST 'https://www.vtilt.com/api/auth/oauth2/register' \
  -H 'Content-Type: application/json' \
  -d '{
    "client_name": "My Custom Agent",
    "redirect_uris": ["http://localhost:8080/callback"],
    "scope": "mcp:read offline_access",
    "token_endpoint_auth_method": "none",
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"]
  }'
bash

A successful response returns a client_id you store in your agent's config. Public clients (those that can't keep a secret — desktop apps, CLIs, browser SPAs) should set token_endpoint_auth_method: "none" and use PKCE; confidential clients (backend services with a secure secret store) can use client_secret_basic or client_secret_post.

Important

Important: PKCE (code_challenge + code_verifier) is required for every OAuth 2.1 flow at vTilt, including confidential clients. Clients that don't send a challenge will receive invalid_request at the authorize endpoint.

#4. Scopes

ScopePurpose
mcp:readRead your dashboard data via MCP tools (projects, persons, events, recordings, KPIs, docs search, VQL). Included in default MCP client registration.
mcp:writeCall write-scoped MCP tools (project-update, google-ads-* mutations, …). Opt-in, not a default — your client must request it (it is advertised in scopes_supported) and you grant it on the consent screen. Required to see write tools in tools/list. Does not bypass project permissions — each tool still checks RBAC (e.g. google_ads:write on your project role).
offline_accessIssue a refresh token so the client can renew its access token without re-prompting you for consent.
openidStandard OIDC scope. Lets the client retrieve an ID token alongside the access token.
emailIncludes your email in the ID token so the client can show "signed in as you@example.com" in its UI.

Most agent workflows want mcp:read offline_access — the agent reads data, refreshes silently, and never has to ask again. Add mcp:write when you need write tools (for example project-update or google-ads-* mutations). The consent screen lists each scope in plain language; every write call lands a row in the MCP audit log (see MCP server overview → Writing data).

Warning

Warning: Granting mcp:write lets the agent change project state. Treat the refresh token like a database password and revoke it the moment a teammate or device no longer needs access.

#Write access (mcp:write) vs project permissions

OAuth scopes and project permissions are separate:

LayerWhat it controlsExample
OAuth mcp:writeWhether write-scoped tools appear in tools/list and may be invoked at the credential levelWithout it, google-ads-campaign-status-set is hidden
Project RBACWhether your user may perform the action on the pinned projectgoogle_ads:write on the project member record

Org owners and admins get full project permissions automatically. Members need the relevant actions on their project role even when the OAuth token includes mcp:write.

vTilt advertises mcp:write on the authorization server and on /.well-known/oauth-protected-resource/api/mcp. It is opt-in, not a default — following least-privilege, new client registrations default to mcp:read only. A spec-compliant client reads scopes_supported and requests mcp:write for you; if yours doesn't, add mcp:write to the scope= parameter it sends to /oauth2/authorize.

  1. Ensure your client requests mcp:write (most modern clients do this automatically from scopes_supported; otherwise add it to the scope= parameter).
  2. Disconnect the existing MCP OAuth connection in your client (or revoke the client’s refresh token), then reconnect and sign in again. On the consent screen you should see an extra line: Change project data via MCP…
  3. Call context-get — key_access should be "write" and write tools (including Google Ads writes) should appear in tools/list when your project role allows them.

If you connected before requesting mcp:write, your old token only carries mcp:read; reconnecting with the wider scope is required — refreshing the access token does not add scopes.

#5. The consent screen

After signing in you'll see a screen like this:

My Custom Agent
https://my-agent.example.com

My Custom Agent is requesting permission to access your vTilt account.

This will allow My Custom Agent to:
  • Read your dashboard data (projects, persons, events, recordings)
  • Change project data via MCP (write tools — audited)
  • Stay connected when you are not actively using the app

  [ Deny ]   [ Allow ]
text

vTilt remembers your decision per (user, client_id) — you only see the screen once per client. To revisit or revoke your decisions, manage clients from your account settings (revoke flow lands alongside future Phase 2 increments; for now, revoking a refresh token via Better Auth's API immediately kills the session).

#6. Token lifecycle

TokenFormatLifetimeWhere it lives
Authorization codeRandom string60 sReturned to the client's redirect_uri once. Single-use.
Access tokenJWT1 hourSent as Authorization: Bearer … on every MCP request.
Refresh tokenRandom string30 daysStored by the client; used to mint a new access token when the old one expires.
ID tokenJWT1 hourOIDC token returned alongside the access token if openid was requested.

Access tokens are verified locally against vTilt's JWKS (no DB round-trip), so the per-request cost is the same as vtu_ keys.

#7. Project pinning

OAuth tokens give the same access as vtu_ keys — every project you can see in the dashboard. Pin a project the same three ways:

# Header (most clients let you set headers per connection)
x-vtilt-project-id: 7c5c1a3a-9d62-4e09-8a4f-9a3ce1b1f4a8

# Query parameter on the MCP URL
https://www.vtilt.com/api/mcp?project_id=7c5c1a3a-9d62-4e09-8a4f-9a3ce1b1f4a8

# Session pin via tool call
> "Switch me to the Marketing site project."
text

Single-project accounts don't need to pin — the server picks your only project automatically.

#8. Common errors

Error responseMeaningFix
{"code":-32001,"message":"Unauthorized","data":{"reason":"oauth_signature_invalid"}}JWT signature can't be verified against vTilt's JWKSReconnect the client — its access token was minted against a different host
{"code":-32001,"message":"Unauthorized","data":{"reason":"oauth_jwks_not_found"}}JWKS document returned HTTP 404 (wrong path or routing)Ensure GET https://www.vtilt.com/api/auth/jwks returns 200; deploy must pin NEXT_PUBLIC_APP_URL to that same origin
{"code":-32001,"message":"Unauthorized","data":{"reason":"oauth_audience_mismatch"}}aud claim doesn't equal https://www.vtilt.com/api/mcpMake sure the client requested a token for the MCP resource (see the warning below)
{"code":-32001,"message":"Unauthorized","data":{"reason":"oauth_expired"}}Access token's exp claim is in the pastClient should refresh the token — most do this automatically
{"code":-32001,"message":"Unauthorized","data":{"reason":"oauth_unknown_user"}}JWT sub doesn't resolve to a vTilt user (user was deleted)Reconnect the client and sign in again
{"code":-32003,"message":"Forbidden","data":{"reason":"project_not_accessible"}}Header/query pin points at a project this user can't accessRemove the pin or pick a project from your dashboard
HTTP 400 at /api/auth/oauth2/authorize with error=invalid_requestMissing PKCE code_challenge (or invalid redirect_uri)Send code_challenge + code_challenge_method=S256; register redirect_uri

Important

Important: vTilt issues a JWT access token only when the client passes the RFC 8707 resource parameter in its token request — set resource=https://www.vtilt.com/api/mcp (URL-encoded) on both the authorize URL and the POST /api/auth/oauth2/token form body. Without it the issued token is opaque, has no aud claim, and /api/mcp rejects it as oauth_audience_mismatch. Clients surface this as the opaque message "Authorization with the MCP server failed" with no further detail. Most modern MCP SDKs (@modelcontextprotocol/sdk ≥ 1.12, Claude Desktop builds released after the 2025-06-18 spec) read scopes_supported from /.well-known/oauth-protected-resource/api/mcp and add the parameter automatically; older clients need a config change or upgrade.

#Next steps

  • Authentication — overview of both auth paths and the vtu_ key flow.
  • Claude Desktop — connect via OAuth (recommended) or the mcp-remote bridge.
  • MCP server overview — the full tool catalogue and example prompts.
PreviousAuthenticationNextAgent skills (prompts)

On this page

  • When to choose OAuth vs a personal API key
  • 1. Connect your client
  • 2. Discovery endpoints
  • 3. Dynamic Client Registration
  • 4. Scopes
  • Write access (mcp:write) vs project permissions
  • 5. The consent screen
  • 6. Token lifecycle
  • 7. Project pinning
  • 8. Common errors
  • Next steps