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.
GET https://api.upubly.com/.well-known/oauth-authorization-server
GET https://api.upubly.com/.well-known/oauth-protected-resourcePOST /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.
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.
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}.
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.
POST /oauth/revoke
token=<refresh_token>&client_id=<id>S256, plain is rejected.redirect_uri must be an exact match of a registered URI.