Build an assistant client

Sign in with OAuth

How an assistant signs in without an API key: discovery, registration, consent and tokens, for building your own client.

An AI assistant signs in to Setherion with OAuth instead of an API key: the person presses Allow on a Setherion page, and the assistant receives a token. This page is the contract for building your own client. If you only want to connect Claude, ChatGPT, Cursor or VS Code, follow Connect an AI assistant.

The flow is OAuth 2.1 as the MCP authorization specification (2025-06-18) asks for it:

  • the authorization code flow, with PKCE (S256) always required;
  • public clients only, with no client secret;
  • dynamic client registration (RFC 7591), so no one pastes a client id anywhere;
  • tokens bound to one resource, the MCP endpoint (RFC 8707).

The tokens work on the MCP server and nowhere else. The REST endpoints of this reference take an API key.

ThingValue
Issuerhttps://www.setherion.com
Resourcehttps://www.setherion.com/api/mcp
Scopemcp
Access tokenStarts sto_, lasts 1 hour
Refresh tokenStarts str_, lasts 30 days, replaced every time it is used
Authorization codeStarts stc_, lasts 10 minutes, works once

1. Discover

Send a request to the MCP endpoint with no credential. The 401 points to the resource metadata (RFC 9728) in its WWW-Authenticate header:

bash
curl https://www.setherion.com/.well-known/oauth-protected-resource/api/mcp
json
{
  "resource": "https://www.setherion.com/api/mcp",
  "authorization_servers": ["https://www.setherion.com"],
  "bearer_methods_supported": ["header"],
  "scopes_supported": ["mcp"],
  "resource_name": "Setherion"
}

The same document is at /.well-known/oauth-protected-resource. Then read the authorization server metadata (RFC 8414):

bash
curl https://www.setherion.com/.well-known/oauth-authorization-server
json
{
  "issuer": "https://www.setherion.com",
  "authorization_endpoint": "https://www.setherion.com/oauth/authorize",
  "token_endpoint": "https://www.setherion.com/api/oauth/token",
  "registration_endpoint": "https://www.setherion.com/api/oauth/register",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none"],
  "scopes_supported": ["mcp"]
}

Both documents, the registration and the token endpoint answer any origin (Access-Control-Allow-Origin: *), so a client in a browser page can call them.

2. Register

POSThttps://www.setherion.com/api/oauth/register

Plan: Every plan

Register once per install. Registering grants nothing by itself: a person still has to press Allow, and the code is sent back only to the exact addresses registered here.

bash
curl -X POST https://www.setherion.com/api/oauth/register \
  -H "Content-Type: application/json" \
  -d '{ "client_name": "My assistant", "redirect_uris": ["http://localhost:8787/callback"] }'
FieldMeaning
redirect_urisRequired. Between 1 and 10 addresses, each at most 2,000 characters. Duplicates are dropped.
client_nameOptional. Shown to the person on the consent page. Cut to 100 characters; An AI assistant when left out.
grant_typesOptional. Only authorization_code and refresh_token are accepted.
response_typesOptional. Only code.

A redirect address must be https, or http back to this machine (localhost, 127.0.0.1 or [::1]). It may not have a fragment or a username. The one address that is not https or http is cursor://anysphere.cursor-mcp/oauth/callback, for older Cursor apps.

Whatever authentication method a client asks for, it is registered as a public client with no secret, and the answer says so:

json
{
  "client_id": "5f0c2a9d-4e30-4d7f-b8a5-6f1c2a9d4e30",
  "client_id_issued_at": 1790000000,
  "client_name": "My assistant",
  "redirect_uris": ["http://localhost:8787/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}

The status is 201. Registrations are limited to 20 an hour per network address.

Registration errors

Every error is { "error": "...", "error_description": "..." }.

Statuserrorerror_description
400invalid_redirect_uriSend between one and ten redirect_uris.
400invalid_redirect_uriRedirect URIs must be HTTPS, or HTTP back to localhost.
400invalid_client_metadataOnly authorization_code and refresh_token are supported.
400invalid_client_metadataOnly the code response type is supported.
429slow_downToo many registrations. Try again later.
503temporarily_unavailableNot configured. or Connecting assistants is not switched on yet.

3. Ask the person

Open the authorization endpoint in the person's browser. It is a page, not an API: the person signs in to Setherion if they are not already, sees the name you registered and where they will be sent back to, and presses Allow or Cancel.

text
https://www.setherion.com/oauth/authorize
  ?response_type=code
  &client_id=5f0c2a9d-4e30-4d7f-b8a5-6f1c2a9d4e30
  &redirect_uri=http%3A%2F%2Flocalhost%3A8787%2Fcallback
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
  &state=af0ifjsldkj
  &resource=https%3A%2F%2Fwww.setherion.com%2Fapi%2Fmcp
ParameterMeaning
response_typeRequired. code.
client_idRequired. From registration.
redirect_uriOne of the registered addresses, matched exactly. May be left out only when you registered exactly one.
code_challengeRequired. The S256 hash of your verifier, in base64url, 43 to 128 characters.
code_challenge_methodRequired. S256. plain is not accepted.
stateRecommended. Sent back unchanged. Up to 1,000 characters.
resourceOptional. If sent, it must be https://www.setherion.com/api/mcp (a trailing slash and the case of the scheme and host do not matter).

Make the verifier 43 to 128 characters from A-Z a-z 0-9 - . _ ~, and keep it until step 4.

The person is sent back to your redirect_uri with:

  • on Allow: code, state and iss (the issuer);
  • on Cancel: error=access_denied, error_description=The request was declined., state and iss;
  • on a problem with the request, once the client and address are known to be genuine: error, error_description, state and iss. The error is unsupported_response_type, invalid_request (PKCE missing, or the method is not S256) or invalid_target (the resource is not the MCP endpoint).

If the client is unknown, or the address is not one it registered, the page says so to the person and sends nobody anywhere, so this page can never be used to redirect someone to an address you did not register.

4. Exchange the code

POSThttps://www.setherion.com/api/oauth/token

Plan: Every plan

Send form-encoded fields, as OAuth specifies. JSON with the same names is accepted too.

bash
curl -X POST https://www.setherion.com/api/oauth/token \
  -d grant_type=authorization_code \
  -d code=stc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
  -d client_id=5f0c2a9d-4e30-4d7f-b8a5-6f1c2a9d4e30 \
  -d redirect_uri=http://localhost:8787/callback \
  -d code_verifier=YOUR_VERIFIER
FieldMeaning
grant_typeauthorization_code.
codeThe code from the redirect.
client_idThe client the code was issued to.
redirect_uriThe same address as in step 3.
code_verifierThe verifier whose S256 hash you sent as code_challenge.
resourceOptional. If sent, it must match the code's resource.
json
{
  "access_token": "sto_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "str_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "scope": "mcp"
}

Send the access token to the MCP server as Authorization: Bearer sto_.... Tokens are never cached: the answer carries Cache-Control: no-store.

5. Refresh

An access token lasts an hour. Before it ends, or when the MCP server answers 401, exchange the refresh token:

bash
curl -X POST https://www.setherion.com/api/oauth/token \
  -d grant_type=refresh_token \
  -d refresh_token=str_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
  -d client_id=5f0c2a9d-4e30-4d7f-b8a5-6f1c2a9d4e30

The answer is the same as in step 4, with a new refresh token. The old one stops working, so store the new one every time.

A reused refresh token ends the connection

Using a refresh token a second time is read as a stolen copy, and Setherion ends the whole connection: every token from that consent stops working and the person must press Allow again. The one exception is the same client presenting the same token within 60 seconds while the connection is still open, which is how two processes of one assistant, such as two windows, can refresh at once. A code used twice ends its connection in the same way.

Errors

Every error from the token endpoint has the status 400, apart from server_error, and the body { "error": "...", "error_description": "..." }.

Token errors
errorerror_description
unsupported_grant_typeUse authorization_code or refresh_token.
invalid_grantUnknown code.
invalid_grantThis code was already used.
invalid_grantThis code has expired.
invalid_grantThis code was issued to another application.
invalid_grantredirect_uri does not match the one the code was issued for.
invalid_grantPKCE verification failed.
invalid_grantUnknown refresh token.
invalid_grantThis refresh token has expired.
invalid_grantThis token was issued to another application.
invalid_grantThis refresh token was already used. The connection has been ended for safety.
invalid_targetThis code was issued for another resource. or This token was issued for another resource.
server_error (status 500)Not configured. or Could not issue tokens.

Ending a connection

The person can disconnect an assistant in Settings, then AI assistants and MCP. Every token of that connection, access and refresh, stops working on its very next request, not when the hour runs out. Listing and ending connections is only possible from a signed-in Setherion session, so nothing an assistant holds can do it.

Using an API key instead

A client that can only send a fixed header can skip all of this and send an API key to the MCP server. See Authentication.

Was this page helpful?

Join the community

Questions, ideas and what others are building.