OAuth

MCP authorization flow — client registration, frontend consent, code+verifier exchange for tokens.

The MCP server uses OAuth 2.1 with mandatory PKCE (S256). Consent happens on the uPubly frontend — the user authorizes with their logged-in account and receives the code on screen to hand to the agent.

Discovery

GET https://api.upubly.com/.well-known/oauth-authorization-server
GET https://api.upubly.com/.well-known/oauth-protected-resource

Flow

1. Register the client (DCR)

POST /oauth/register
Content-Type: application/json

{"client_name": "My Agent", "redirect_uris": ["http://localhost:9999/callback"]}

→ 201 {"client_id": "...", ...}. redirect_uris accepts https://, loopback (http://localhost|127.0.0.1) and custom app schemes; public http:// is rejected.

2. Generate PKCE and open the authorization

The agent generates code_verifier (random) and code_challenge = base64url(sha256(verifier)), then opens in the browser:

GET /oauth/authorize?response_type=code&client_id=<id>
    &redirect_uri=<uri>&code_challenge=<challenge>
    &code_challenge_method=S256&state=<state>

The API validates and redirects (302) to the front's consent screen:

https://upubly.com/oauth/consent?request=<request_id>

The front fetches the details and approves/denies with the user's session:

GET  /oauth/requests/<id>            → details (client name, redirect)
POST /oauth/requests/<id>/approve    → {"code": "...", "redirect_to": "..."}
POST /oauth/requests/<id>/deny       → {"redirect_to": "...?error=access_denied"}

redirect_to points at the client's redirect_uri with code + state — agents with a local listener receive it automatically. The screen also shows the code to copy — headless/SSH agents get the code pasted by the user. Showing the code is safe: without the code_verifier (which never leaves the agent) it cannot be exchanged for a token.

4. Exchange the code for tokens

POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=<code>&redirect_uri=<uri>
&client_id=<id>&code_verifier=<verifier>

→ {"access_token": "<JWT>", "refresh_token": "...", "expires_in": 28800}.

5. Refresh

POST /oauth/token
grant_type=refresh_token&refresh_token=<token>&client_id=<id>

Same-session rotation: the old refresh dies, a new pair comes back.

6. Revoke

POST /oauth/revoke
token=<refresh_token>&client_id=<id>

Security rules

  • PKCE is mandatory — only S256, plain is rejected.
  • The authorization code is single-use and expires in ~10 minutes.
  • redirect_uri must be an exact match of a registered URI.
  • Access token: 8h · Refresh token: 7 days.
  • Per-IP rate limit on public endpoints.