MCP

OAuth 2.1 for app builders

Discovery, dynamic client registration, the PKCE authorization code flow, token lifetimes and revocation -- everything needed to build a client against the Reblog MCP server.


You probably do not need this page

If you are connecting Claude, Claude Code or Cursor, the client does all of this for you. Read connect a client instead. This page is for people writing the client.

Reblog is an OAuth 2.1 authorization server and a protected resource, implemented to the standards MCP clients already speak: RFC 8414 for discovery, RFC 9728 for resource metadata, RFC 7591 for dynamic registration, RFC 7636 for PKCE, RFC 7009 for revocation. A conforming client needs no configuration beyond the server URL.

Discovery#

Start from the protected resource. An unauthenticated call to /api/mcp answers 401 with a WWW-Authenticate header naming this document; fetching it points at the authorization server.

GET/.well-known/oauth-protected-resource

RFC 9728 resource metadata. Public, CORS-open, cacheable for an hour.

Response
{
  "resource": "https://reblog.so/api/mcp",
  "authorization_servers": ["https://reblog.so"],
  "scopes_supported": ["articles.read", "articles.create", "..."],
  "bearer_methods_supported": ["header"]
}
GET/.well-known/oauth-authorization-server

RFC 8414 authorization server metadata. Every endpoint below is derived from this document -- never hardcode them.

Response
{
  "issuer": "https://reblog.so",
  "authorization_endpoint": "https://reblog.so/app/oauth/authorize",
  "token_endpoint": "https://reblog.so/api/oauth/token",
  "registration_endpoint": "https://reblog.so/api/oauth/register",
  "revocation_endpoint": "https://reblog.so/api/oauth/revoke",
  "scopes_supported": ["articles.read", "..."],
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none", "client_secret_post"],
  "client_id_metadata_document_supported": true
}

The authorize endpoint is a page, not an API

It is the only endpoint under /app, because it is the consent screen a human looks at. Everything else sits at the domain root so integrations that do not follow redirects keep working.

Registration#

POST/api/oauth/register

RFC 7591 dynamic client registration. Public. Returns a client_id, and a secret only if you asked to be a confidential client.

Parameters

redirect_uris*string[]At least one, and each must be an acceptable redirect target. This is the only required field.
client_namestringShown to the user on the consent screen and stored on the grant, so the activity log can still name your app after a revoke. Defaults to Unknown MCP client.
token_endpoint_auth_methodstringnone (default, public client with PKCE), client_secret_post, or client_secret_basic.
grant_typesstring[]Filtered to authorization_code and refresh_token; both are assumed when omitted.
logo_uristringOptional icon for the consent screen.
Request
curl -X POST https://reblog.so/api/oauth/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "Editorial Bot",
    "redirect_uris": ["https://editorial.example.com/oauth/callback"],
    "token_endpoint_auth_method": "none"
  }'

Or skip registration entirely

The server advertises client_id_metadata_document_supported. A client may present an https URL as its client_id and have its metadata read from there, instead of registering first.

The authorization code flow#

  1. 1

    Generate a PKCE pair

    S256 is the only challenge method accepted. Plain is not supported.

    bash
    code_verifier=$(openssl rand -base64 60 | tr -d '=+/' | cut -c1-64)
    code_challenge=$(printf %s "$code_verifier" | openssl dgst -binary -sha256 | openssl base64 | tr '+/' '-_' | tr -d '=')
  2. 2

    Send the user to the consent screen

    They choose the access level and the projects, then approve.

    text
    https://reblog.so/app/oauth/authorize
      ?response_type=code
      &client_id=<client_id>
      &redirect_uri=https://editorial.example.com/oauth/callback
      &code_challenge=<code_challenge>
      &code_challenge_method=S256
      &state=<opaque>
      &scope=articles.read%20articles.create%20account.projects.read
  3. 3

    Exchange the code

    The code is single use and lives 60 seconds. Send form-encoded or JSON; both are accepted.

    bash
    curl -X POST https://reblog.so/api/oauth/token \
      -H "Content-Type: application/x-www-form-urlencoded" \
      -d grant_type=authorization_code \
      -d code=<code> \
      -d redirect_uri=https://editorial.example.com/oauth/callback \
      -d client_id=<client_id> \
      -d code_verifier=$code_verifier
  4. 4

    Call the MCP endpoint

    The access token is a bearer credential with the prefix rba_.

    bash
    curl -X POST https://reblog.so/api/mcp \
      -H "Authorization: Bearer rba_..." \
      -H "Content-Type: application/json" \
      -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Token response
{
  "access_token": "rba_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "rbr_...",
  "scope": "articles.read articles.create account.projects.read"
}

The scope parameter is optional. Omit it and the user picks a preset on the consent screen; send one and the screen shows exactly what you asked for. Unknown scope strings are dropped rather than rejected. The vocabulary is on permissions and scopes.

Refreshing#

bash
curl -X POST https://reblog.so/api/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=refresh_token \
  -d refresh_token=<refresh_token> \
  -d client_id=<client_id>

Refresh tokens rotate, and replay is detected

Every refresh returns a new pair and revokes the one you used. Presenting a consumed refresh token is treated as a stolen-token signal, not a retry: store the new token before you use it, and never refresh the same token from two processes.

Lifetimes#

CredentialPrefixLifetimeNotes
Authorization code-60 secondsSingle use, PKCE S256 required.
Access tokenrba_60 minutesBearer, header only.
Refresh tokenrbr_60 daysRotates on every use, with replay detection.

Revocation#

POST/api/oauth/revoke

RFC 7009. Revokes one access or refresh token. Always answers 200, whether or not the token existed.

Request
curl -X POST https://reblog.so/api/oauth/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d token=<access_or_refresh_token> \
  -d client_id=<client_id>

The user can also revoke the whole grant from Settings -> Connected apps, which kills every token it issued at once. Build for that: a revoked connection returns -32001 on the next call, and the right response is to start a fresh authorization, not to retry.

A grant is not a blank cheque#

  • The scopes you receive are capped, per call, by the user's own role on the target project. Ask for articles.delete all you like: on a project where they are a contributor, the tool is not offered.
  • Which projects a grant covers is chosen by the user at consent time, not requested by you. list_projects returns the resolved set.
  • A capability that ships after a grant was issued is not acquired by it unless the grant used a preset. Handle the case where a documented tool is absent: get_my_capabilities explains why.