Public tools need no authentication. Personal and host tools use OAuth 2.1 with PKCE, as described in the MCP authorization specification. If you use an MCP-capable assistant, you do not have to do anything here: it follows these steps for you. Read on if you are building your own client or want to understand what happens.
Eventa has two parts. The MCP server (mcp.eventa.live) is the protected resource. The Eventa API (api.eventa.live) is the authorization server: it signs users in, shows the consent page and issues tokens.
The flow at a glance
- The client calls a personal tool without a token and receives
401. - It reads the resource metadata, then the authorization-server metadata.
- It identifies itself with a client ID that is a URL, or registers dynamically.
- It sends the user to the authorization endpoint with a PKCE challenge and the permissions it needs.
- The user signs in to Eventa and approves on the consent page.
- The client receives a code on its redirect address and exchanges it for tokens.
- It calls the MCP server with the access token, and refreshes it when it expires.
1. Discovery
A call to a personal tool without a token answers:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="eventa", resource_metadata="https://mcp.eventa.live/.well-known/oauth-protected-resource/mcp", scope="profile:read"
Fetch the resource metadata named there (RFC 9728):
curl -s https://mcp.eventa.live/.well-known/oauth-protected-resource/mcp
{
"resource": "https://mcp.eventa.live/mcp",
"authorization_servers": ["https://api.eventa.live"],
"scopes_supported": ["profile:read", "events:read", "events:write", "clubs:read", "clubs:write", "venues:write", "host:read", "host:write", "offline_access"],
"bearer_methods_supported": ["header"],
"resource_name": "Eventa"
}
Then the authorization-server metadata (RFC 8414):
curl -s https://api.eventa.live/.well-known/oauth-authorization-server
It lists the endpoints and capabilities you need:
| Field | Value |
|---|---|
authorization_endpoint | https://api.eventa.live/oauth/authorize |
token_endpoint | https://api.eventa.live/oauth/token |
registration_endpoint | https://api.eventa.live/oauth/register |
revocation_endpoint | https://api.eventa.live/oauth/revoke |
jwks_uri | https://api.eventa.live/.well-known/jwks.json |
code_challenge_methods_supported | S256 only |
token_endpoint_auth_methods_supported | none (public clients) |
client_id_metadata_document_supported | true |
authorization_response_iss_parameter_supported | true |
2. Identify your client
There are two ways. The first is preferred.
Client ID Metadata Document. Your client_id is an HTTPS URL, and that URL serves a small JSON document (under 5 KB) describing your app. Eventa fetches it, and the consent page shows your domain as the publisher.
{
"client_id": "https://assistant.example/oauth/client.json",
"client_name": "Example Assistant",
"client_uri": "https://assistant.example",
"logo_uri": "https://assistant.example/logo.png",
"redirect_uris": ["https://assistant.example/oauth/callback"]
}
The URL needs a real path, no fragment, no credentials and no dot segments. The client_id field inside must equal the URL it is served from.
Dynamic registration is also available for older clients, at the registration_endpoint. Apps registered this way are shown as unverified on the consent page.
curl -s https://api.eventa.live/oauth/register \
-H "content-type: application/json" \
-d '{"client_name":"Example Assistant","redirect_uris":["https://assistant.example/oauth/callback"],"grant_types":["authorization_code","refresh_token"]}'
Redirect addresses: one to ten per client, matched exactly. Each must be HTTPS, or http on a loopback address (such as 127.0.0.1) for tools that run on the user's own machine. A loopback address may use any port. The consent page warns the user when the app returns to their own machine.
3. PKCE
PKCE with S256 is required. Create a random verifier and its challenge:
VERIFIER=$(openssl rand -base64 48 | tr -d '=+/' | cut -c1-64)
CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')
The challenge is 43 characters of base64url. Keep the verifier until you exchange the code.
4. Send the user to Eventa
Open this address in the user's browser:
https://api.eventa.live/oauth/authorize
?response_type=code
&client_id=https%3A%2F%2Fassistant.example%2Foauth%2Fclient.json
&redirect_uri=https%3A%2F%2Fassistant.example%2Foauth%2Fcallback
&code_challenge=CHALLENGE
&code_challenge_method=S256
&scope=profile%3Aread%20events%3Aread
&state=RANDOM_STATE
&resource=https%3A%2F%2Fmcp.eventa.live%2Fmcp
| Parameter | Notes |
|---|---|
response_type | Always code |
client_id, redirect_uri | As registered. A mismatch is shown on an Eventa error page and never redirected |
code_challenge, code_challenge_method | PKCE, S256 only |
scope | Space-separated permissions. Unknown values are ignored. If none are valid, profile:read events:read is used |
state | Your own random value; returned unchanged. Check it |
resource | https://mcp.eventa.live/mcp. Tokens are issued only for this audience |
Eventa shows the user the consent page: who is asking, who publishes the app, where the browser goes next, and each permission in plain words. When the user chooses Allow or Cancel, the browser returns to your redirect address:
https://assistant.example/oauth/callback?code=...&state=RANDOM_STATE&iss=https%3A%2F%2Fapi.eventa.live
Check that state matches and that iss equals https://api.eventa.live (RFC 9207). A refusal returns error=access_denied. A code is valid for 60 seconds and works once.
5. Exchange the code
curl -s https://api.eventa.live/oauth/token \
-d grant_type=authorization_code \
-d code="$CODE" \
-d redirect_uri="https://assistant.example/oauth/callback" \
-d client_id="https://assistant.example/oauth/client.json" \
-d code_verifier="$VERIFIER" \
-d resource="https://mcp.eventa.live/mcp"
The response is not cacheable and contains access_token, token_type (Bearer), expires_in (1800), refresh_token and scope.
6. Call the server
Send the token on every request:
curl -s https://mcp.eventa.live/mcp \
-H "authorization: Bearer $ACCESS_TOKEN" \
-H "content-type: application/json" \
-H "accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_my_profile","arguments":{}}}'
The server accepts only tokens issued for https://mcp.eventa.live/mcp. It never forwards your token to another service. For its own calls to the Eventa API it exchanges the token for a separate, short-lived one that names the server as the acting party.
7. Refresh
When the access token expires, send the refresh token:
curl -s https://api.eventa.live/oauth/token \
-d grant_type=refresh_token \
-d refresh_token="$REFRESH_TOKEN" \
-d client_id="https://assistant.example/oauth/client.json"
Refresh tokens rotate: each use returns a new one, and you must store it. A just-used token is refused for about a minute without penalty, to survive a retry. Reusing an old token after that ends the whole connection, and the user must approve again.
8. Revoke
curl -s https://api.eventa.live/oauth/revoke -d token="$REFRESH_TOKEN" -d client_id="https://assistant.example/oauth/client.json"
The endpoint answers 200 whatever the token was. Users can also end a connection themselves in Settings → Connected apps; the server stops accepting that connection's requests within about a minute.
Permissions
A write permission also covers the matching read.
| Permission | In plain words | Used by |
|---|---|---|
profile:read | See your name, profile picture and city | get_my_profile |
events:read | See your tickets, RSVPs and waitlist spots | list_my_tickets, get_my_rsvp |
events:write | RSVP and join or leave waitlists for you | rsvp_to_event, cancel_rsvp, join_event_waitlist |
clubs:read | See the clubs you belong to | list_my_clubs |
clubs:write | Join and leave clubs for you | join_club, leave_club |
venues:write | Send venue inquiries for you, with no payments | send_venue_inquiry |
host:read | See events you host, pending RSVPs and attendee names | my_hosted_events, event_dashboard, list_pending_rsvps, list_attendees |
host:write | Approve RSVPs, create promo codes and invite guests, for events you host | decide_rsvps, create_promo_code, invite_guests |
offline_access | Stay connected without signing in again | Longer-lived connection |
Ask for the smallest set you need. You can ask for more later (below).
Asking for more later (step-up)
If a tool needs a permission the token lacks, the server answers 403 with the full set to request:
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer realm="eventa", resource_metadata="https://mcp.eventa.live/.well-known/oauth-protected-resource/mcp", error="insufficient_scope", error_description="This needs: events:write", scope="profile:read events:write"
Repeat the authorization step with that scope. The user approves the additional permission, and you receive a new token.
Lifetimes
| Item | Lifetime |
|---|---|
| Authorization code | 60 seconds, single use |
| Consent request | 10 minutes |
| Access token | 30 minutes |
| Refresh token | Rotates on use. Ends after 30 days unused |
| Whole connection | Ends 90 days after approval, or when the user disconnects |
Errors
OAuth endpoints return the standard JSON form: {"error": "...", "error_description": "..."}.
| Error | Meaning |
|---|---|
invalid_request | A parameter is missing or malformed, such as no PKCE challenge |
invalid_client | Unknown or unverifiable client |
invalid_redirect_uri | The redirect address is not allowed |
invalid_client_metadata | The registration or metadata document is invalid |
invalid_grant | The code, verifier or refresh token is wrong, used, expired or revoked |
invalid_target | The resource is not this server |
access_denied | The user chose Cancel |
Checklist for client authors
- Use PKCE
S256, a randomstate, and verifyiss. - Send
resource=https://mcp.eventa.live/mcpand accept tokens only for it. - Store refresh tokens securely and replace them after every use.
- Never log tokens, and never pass a token on to another service.
- Handle
401by signing in again and403 insufficient_scopeby asking for more. - Offer the user a way to disconnect.