Skip to content

OAuth for MCP clients

Memoturn is an OAuth 2.1 authorization server. An MCP client — Claude Code, Cursor, VS Code, Codex, Zed, anything that speaks the MCP authorization spec — can connect without a pasted key.

mt_live_ keys are not deprecated. They remain the right choice for CI, scripts, and anything without a browser. See Authentication.

Nothing Memoturn-specific. The whole flow is discovery:

1. POST /v1/projects/<slug>/mcp → 401 + WWW-Authenticate
2. GET /.well-known/oauth-protected-resource (named by that header)
3. GET /.well-known/oauth-authorization-server
4. POST /v1/oauth/register → client_id, no human involved
5. GET /v1/oauth/authorize?… → browser, sign in, consent
6. POST /v1/oauth/token → access + refresh token
7. POST /v1/projects/<slug>/mcp → Authorization: Bearer …

Steps 2–4 are why no configuration is needed: the 401 names the resource metadata, the resource metadata names the authorization server, and dynamic client registration means the client mints its own credentials.

Most hosts drive this for you once the server is added without an Authorization header.

Terminal window
# Claude Code — add, then authenticate. Adding alone does not start the flow;
# Claude Code marks the server as needing auth after its first 401.
claude mcp add memoturn --transport http \
https://api.memoturn.ai/v1/projects/<slug>/mcp
claude mcp login memoturn
# Codex CLI
codex mcp add memoturn --url https://api.memoturn.ai/v1/projects/<slug>/mcp
codex mcp login memoturn

Cursor, VS Code, and Zed prompt from their MCP settings panel instead. Zed runs the flow automatically for any remote server with no Authorization header configured.

The OpenCode plugin is not an MCP client — it drives the SDK directly — but it runs the same flow, registering itself per machine and storing the resulting token rather than an API key:

Terminal window
npx opencode-memoturn@latest login --project <slug>
EndpointPurpose
/.well-known/oauth-authorization-serverRFC 8414 metadata
/.well-known/oauth-protected-resourceRFC 9728 — pointed to from a 401
/.well-known/jwks.jsonVerification keys
POST /v1/oauth/registerRFC 7591 dynamic client registration
GET /v1/oauth/authorizeAuthorization request
POST /v1/oauth/tokenauthorization_code, refresh_token, client_credentials
POST /v1/oauth/parRFC 9126 pushed authorization requests
GET /v1/oauth/userinfoScope-gated claims
POST /v1/oauth/introspectRFC 7662
POST /v1/oauth/revokeRFC 7009

Constraints worth knowing before you integrate

Section titled “Constraints worth knowing before you integrate”

PKCE with S256 is required. Not merely supported — an authorization request without it is refused, and plain is not accepted at all.

response_type=code only. No implicit, no hybrid: both put tokens in URLs, where they end up in browser history, referer headers, and proxy logs.

Redirect URIs are matched exactly. No prefix or wildcard matching. https anywhere, http only on loopback, and custom app schemes.

Refresh tokens rotate, and reuse kills the family. Every redemption issues a replacement and revokes the one presented. A client that loses the response to a refresh must re-authenticate rather than retry with the old token.

Access tokens are JWTs, signed with EdDSA (Ed25519). Verify them against /.well-known/jwks.json; there is no need to call introspection per request.

ScopeGrants
openidConfirm your identity
profile, emailName, email address
offline_accessA refresh token
memory:readRead memories, turns, and search results
memory:writeRecord turns, create and forget memories
project:readProject settings, skills, usage
project:adminManage settings, API keys, members
skills:read / skills:writeRead / install skills

offline_access, memory:write, project:admin, and skills:write are flagged on the consent screen. Ask for the narrowest set that works: a client requesting project:admin to read memories will be declined by people who read the screen.

Scopes are enforced per tool — search_memory needs memory:read, record_turn needs memory:write. A higher level implies a lower one, so memory:write alone is enough to read. A tool the server does not recognise requires project:admin, so a newly added tool is never reachable by accident.

Pass the MCP endpoint as a resource parameter (RFC 8707) on the authorization request:

resource=https://api.memoturn.ai/v1/projects/<slug>/mcp

The issued token carries that value in aud and the resolved project in project_id, and is refused against any other project. Without it the token is scoped to whatever the user can reach, which is broader than most clients need.

Send a DPoP proof on the token request and the access token comes back sender-constrained — token_type: DPoP and a cnf.jkt claim. It then has to be presented with a matching proof on every call, so a stolen token is useless without the private key. A cnf.jkt token presented as a plain Bearer is refused (RFC 9449 §7.1). The binding survives refresh rotation.

Every project’s Connect → Clients tab lists the clients holding a live grant, with what each was granted and when. Revoking there revokes the grants and refresh tokens, and denylists the outstanding access token by jti — so a disconnect takes effect on the next call rather than at the end of the token’s hour.

Handle a 401 by starting the flow again rather than retrying: from your side a revocation is indistinguishable from an expiry, and both recover the same way.