OAuth & Scopes
The Vivreal MCP server is its own OAuth 2.1 authorization server (it brokers to AWS Cognito under the hood). Every MCP client authenticates once via PKCE, then issues tool calls with a short-lived bearer token.
Discovery
Three well-known endpoints are public:
GET https://mcp.vivreal.io/.well-known/oauth-protected-resource
Returns:
{
"resource": "https://mcp.vivreal.io",
"authorization_servers": ["https://mcp.vivreal.io"],
"bearer_methods_supported": ["header"],
"scopes_supported": [
"openid",
"profile",
"email",
"vivreal/cms.read",
"vivreal/cms.write",
"vivreal/cms.admin"
],
"resource_name": "Vivreal MCP API"
}
GET https://mcp.vivreal.io/.well-known/oauth-authorization-server
Returns the RFC 8414 authorization server metadata — same authorization_endpoint, token_endpoint, registration_endpoint, and code_challenge_methods_supported: ["S256"] you'd expect, plus the supported grant types and response types.
GET https://mcp.vivreal.io/.well-known/openid-configuration
Returns the standard OIDC discovery document — equivalent payload to the OAuth authorization server metadata above, exposed under the OpenID Connect well-known path for clients that probe there first.
Endpoints
| Endpoint | Purpose |
|---|---|
GET /oauth/authorize | Begin the auth flow — redirects to the Cognito Hosted UI at https://auth.vivreal.io |
POST /oauth/token | Exchange code or refresh token |
POST /oauth/register | Dynamic Client Registration (RFC 7591) — call once at install time |
POST /mcp | The MCP JSON-RPC endpoint itself (requires bearer token) |
PKCE is required — code_challenge_method=S256. Plain method is rejected.
The Cognito Hosted UI lives at the canonical custom domain https://auth.vivreal.io — same login screen users see at vivreal.io/app/login. The MCP server has no rendered login page of its own; /oauth/authorize 302-redirects to whichever Cognito domain is configured at deploy time.
Scopes
The Vivreal MCP server uses a 3-tier scope hierarchy:
| Scope | Read | Write content | Manage members / billing |
|---|---|---|---|
vivreal/cms.read | ✓ | ✗ | ✗ |
vivreal/cms.write | ✓ | ✓ | ✗ |
vivreal/cms.admin | ✓ | ✓ | ✓ |
Standard OIDC scopes (openid, profile, email) are also accepted and tell the server to load the user's email + profile info into the session — which the MCP tools then use to scope group membership.
Best practice: request the minimum scope set you need. For a read-only assistant, openid profile email vivreal/cms.read is enough.
The broker design
A traditional OAuth flow couldn't accept Claude.ai's random per-session callback URLs because Cognito requires every callback to be pre-registered. We solve this with a broker:
- Client (Claude.ai or Claude Code) calls
GET https://mcp.vivreal.io/oauth/authorize?redirect_uri=https://claude.ai/api/mcp/auth_callback&... - The broker stashes the client's
redirect_uri,state, and PKCE challenge in DynamoDB, generates ITS OWN PKCE pair, and redirects to Cognito withredirect_uri=https://mcp.vivreal.io/oauth/callback(a stable URL Cognito knows about). - Cognito runs the user through the hosted UI.
- Cognito redirects to
https://mcp.vivreal.io/oauth/callbackwith its authorization code. - The broker exchanges Cognito's code for a token (server-side), generates a one-time broker code (60-second TTL), and redirects the browser to the client's ORIGINAL
redirect_uri(e.g.claude.ai/api/mcp/auth_callback?code=BROKER_CODE). - The client POSTs
mcp.vivreal.io/oauth/tokenwith the broker code + its own PKCE verifier. The broker verifies PKCE against the challenge from step 1, then returns the Cognito token response.
The net effect: clients see a standards-compliant OAuth 2.1 + PKCE flow against mcp.vivreal.io, with full Dynamic Client Registration support. Cognito only ever sees one redirect URL it pre-registered. Random loopback ports, Claude.ai's /api/mcp/auth_callback, and any other compliant URI all work.
Accepted callback URLs
The broker accepts any valid HTTP/HTTPS URL as a redirect URI — there's no allowlist on the MCP server side. The URI is bound to the issued code via PKCE; tampering with the URI at token-exchange time fails verification.
Anthropic clients in particular use one of:
https://claude.ai/api/mcp/auth_callbackhttps://claude.com/api/mcp/auth_callback
Both are accepted as-is. No pre-registration step required.
Token TTLs
| Token | Lifetime |
|---|---|
| Access token | 1 hour |
| ID token | 1 hour |
| Refresh token | 30 days |
After 1 hour the client should refresh using the refresh_token grant against /oauth/token. After 30 days of no refresh, the user re-authenticates.
Session state
Each authenticated request gets a session row in DynamoDB keyed by a session header your MCP client sends with its requests. The session caches:
- User ID, email, username
- Stripe customer ID (used for billing-touching tools)
- List of groups the user belongs to (loaded once via Secure API at session bootstrap)
- The active group ID (set via
set-active-group) - The latest cached ID token (refreshed lazily)
Sessions expire after 24 hours of inactivity. Tools refresh ID-token claims (email, stripeCustomerId) on every call from the cached ID token — there's no per-call Cognito round-trip.
Programmatic example
# 1. Dynamic Client Registration
curl -X POST https://mcp.vivreal.io/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My MCP Client",
"redirect_uris": ["http://localhost:7654/cb"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"]
}'
# → returns { "client_id": "...", "redirect_uris": [...] }
# 2. Open the user's browser to:
# https://mcp.vivreal.io/oauth/authorize?
# response_type=code&
# client_id=...&
# redirect_uri=http://localhost:7654/cb&
# state=...&
# code_challenge=...&
# code_challenge_method=S256&
# scope=openid+profile+email+vivreal/cms.read+vivreal/cms.write
# 3. After the user authenticates, your loopback receives:
# http://localhost:7654/cb?code=BROKER_CODE&state=...
# 4. Exchange for the access token:
curl -X POST https://mcp.vivreal.io/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code&code=BROKER_CODE&code_verifier=...&redirect_uri=http://localhost:7654/cb&client_id=..."
# 5. Call MCP with the bearer token:
curl -X POST https://mcp.vivreal.io/mcp \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Don't share refresh tokens
A refresh token is good for 30 days. If you log it, store it in plaintext, or commit it to a repo, anyone with that string can act as the user for a month. Encrypt at rest. Rotate aggressively if exposure is suspected.