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.
What a client does
Section titled “What a client does”Nothing Memoturn-specific. The whole flow is discovery:
1. POST /v1/projects/<slug>/mcp → 401 + WWW-Authenticate2. GET /.well-known/oauth-protected-resource (named by that header)3. GET /.well-known/oauth-authorization-server4. POST /v1/oauth/register → client_id, no human involved5. GET /v1/oauth/authorize?… → browser, sign in, consent6. POST /v1/oauth/token → access + refresh token7. 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.
Doing it from a host you already use
Section titled “Doing it from a host you already use”Most hosts drive this for you once the server is added without an
Authorization header.
# 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>/mcpclaude mcp login memoturn
# Codex CLIcodex mcp add memoturn --url https://api.memoturn.ai/v1/projects/<slug>/mcpcodex mcp login memoturnCursor, 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:
npx opencode-memoturn@latest login --project <slug>Endpoints
Section titled “Endpoints”| Endpoint | Purpose |
|---|---|
/.well-known/oauth-authorization-server | RFC 8414 metadata |
/.well-known/oauth-protected-resource | RFC 9728 — pointed to from a 401 |
/.well-known/jwks.json | Verification keys |
POST /v1/oauth/register | RFC 7591 dynamic client registration |
GET /v1/oauth/authorize | Authorization request |
POST /v1/oauth/token | authorization_code, refresh_token, client_credentials |
POST /v1/oauth/par | RFC 9126 pushed authorization requests |
GET /v1/oauth/userinfo | Scope-gated claims |
POST /v1/oauth/introspect | RFC 7662 |
POST /v1/oauth/revoke | RFC 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.
Scopes
Section titled “Scopes”| Scope | Grants |
|---|---|
openid | Confirm your identity |
profile, email | Name, email address |
offline_access | A refresh token |
memory:read | Read memories, turns, and search results |
memory:write | Record turns, create and forget memories |
project:read | Project settings, skills, usage |
project:admin | Manage settings, API keys, members |
skills:read / skills:write | Read / 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.
Binding a token to one project
Section titled “Binding a token to one project”Pass the MCP endpoint as a resource parameter (RFC 8707) on the authorization
request:
resource=https://api.memoturn.ai/v1/projects/<slug>/mcpThe 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.
DPoP (optional)
Section titled “DPoP (optional)”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.
The user can cut you off
Section titled “The user can cut you off”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.