> ## Documentation Index
> Fetch the complete documentation index at: https://docs.attensira.com/llms.txt
> Use this file to discover all available pages before exploring further.

# OAuth for the MCP server

> Endpoints, scopes, PKCE, client registration, token lifetimes and revocation for the Attensira MCP authorization server.

Most people never need this page. If you are connecting Claude, Claude Code, Cursor or VS Code, the client does all of this for you — go to [Connect a client](/mcp/connect) and click through the browser prompt.

This page is for the other case: you are writing an MCP client, reviewing Attensira before listing or approving it, or debugging a flow that stops somewhere in the middle and you need to know exactly which document said what.

## The three hosts

| Host                 | Role                 | What it serves                                          |
| -------------------- | -------------------- | ------------------------------------------------------- |
| `mcp.attensira.com`  | Resource server      | The MCP endpoint and its protected-resource metadata    |
| `auth.attensira.com` | Authorization server | Discovery, authorize, token, JWKS, revoke, registration |
| `app.attensira.com`  | Consent              | The sign-in and approval screen a human actually sees   |

They are separate names on purpose. The resource server never sees a signing key, and the issuer string is one origin with no ambiguity about what it means.

## Discovery

Start with an unauthenticated request to the MCP endpoint. It answers `401` with a challenge that points at the metadata:

```bash Shell theme={null}
curl -i -X POST https://mcp.attensira.com/mcp
```

```http Response header theme={null}
WWW-Authenticate: Bearer realm="attensira", error="invalid_token", error_description="The access token is invalid or expired", resource_metadata="https://mcp.attensira.com/.well-known/oauth-protected-resource/mcp"
```

Two paths serve the protected-resource document (RFC 9728), with identical bodies — clients differ on which one they probe:

```text Protected-resource metadata theme={null}
GET https://mcp.attensira.com/.well-known/oauth-protected-resource
GET https://mcp.attensira.com/.well-known/oauth-protected-resource/mcp
```

```json Body theme={null}
{
  "resource": "https://mcp.attensira.com/mcp",
  "authorization_servers": ["https://auth.attensira.com"],
  "bearer_methods_supported": ["header"],
  "scopes_supported": ["attensira:read", "attensira:write"],
  "resource_documentation": "https://docs.attensira.com/mcp/overview"
}
```

The authorization server publishes its own document (RFC 8414), again on two paths with the same body — the OpenID path exists only because several clients probe it first and fail closed on a `404`. Attensira is not an OpenID Provider and never issues an `id_token`:

```text Authorization-server metadata theme={null}
GET https://auth.attensira.com/.well-known/oauth-authorization-server
GET https://auth.attensira.com/.well-known/openid-configuration
```

## Endpoints

| Endpoint                                     | Method | Purpose                                                        |
| -------------------------------------------- | ------ | -------------------------------------------------------------- |
| `https://auth.attensira.com/oauth/authorize` | `GET`  | Browser redirect. Signs the user in and asks for consent       |
| `https://auth.attensira.com/oauth/token`     | `POST` | Code and refresh exchange, `application/x-www-form-urlencoded` |
| `https://auth.attensira.com/oauth/jwks`      | `GET`  | Public keys for verifying access tokens                        |
| `https://auth.attensira.com/oauth/revoke`    | `POST` | RFC 7009 revocation. Always answers `200`                      |
| `https://auth.attensira.com/oauth/register`  | `POST` | RFC 7591 dynamic client registration                           |

Every one of these is on `auth.attensira.com` only. The same paths on `webapp.attensira.com` return `404` — one issuer, one origin.

## Scopes

There are exactly two, and there will not be a third in this version.

| Scope             | Grants                                                                                                                                                               |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `attensira:read`  | Metrics, prompts, answers, pages, sessions, automations, account — everything that only reads                                                                        |
| `attensira:write` | Everything read grants, plus every mutation: adding prompts and competitors, creating, running and deleting automations, resolving inbox tasks, and asking the agent |

`attensira:write` implies `attensira:read`; you do not need to request both, though clients commonly do.

An authorize request that names no scope at all gets `attensira:read` only. A client that needs to change your workspace has to ask for it, so the consent screen can tell you that is what it is asking for. See [the tool reference](/mcp/tools) for which scope each tool needs.

## Authorization code with PKCE

Only the authorization code grant is supported, and only with PKCE using `S256`. There is no implicit grant, no password grant, and `code_challenge_method=plain` is rejected.

```text Authorize request theme={null}
GET https://auth.attensira.com/oauth/authorize
  ?response_type=code
  &client_id=<your client id>
  &redirect_uri=<exact registered URI>
  &code_challenge=<base64url SHA-256 of the verifier>
  &code_challenge_method=S256
  &scope=attensira:read%20attensira:write
  &resource=https%3A%2F%2Fmcp.attensira.com%2Fmcp
  &state=<opaque>
```

The user signs in with Clerk, picks a workspace, and approves. The redirect back carries the code, your `state` echoed byte for byte, and the issuer:

```text Redirect theme={null}
<redirect_uri>?code=<opaque>&state=<echoed>&iss=https%3A%2F%2Fauth.attensira.com
```

Then exchange it:

```bash Shell theme={null}
curl -X POST https://auth.attensira.com/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=authorization_code \
  -d code=<code> \
  -d client_id=<your client id> \
  -d redirect_uri=<the same URI> \
  -d code_verifier=<the verifier> \
  -d resource=https://mcp.attensira.com/mcp
```

<Warning>
  The authorization code is single use. A second exchange of the same code does not merely fail — it revokes the grant it came from, on the assumption that a replayed code means the code was stolen. You then have to reconnect. Make sure your client is not retrying the exchange on a network error without checking whether the first attempt succeeded.
</Warning>

`redirect_uri` is matched exactly against what the client registered. The single exception is RFC 8252 loopback — `localhost`, `127.0.0.1` and `::1` compare scheme, host and path and ignore the port, so a CLI that listens on an ephemeral port works. No other host gets that carve-out and there are no wildcards.

The `resource` parameter (RFC 8707) must be exactly `https://mcp.attensira.com/mcp`. Anything else is `invalid_target`.

## Client identification

Three ways to be a client, in the order Attensira prefers them:

* **Verified clients.** Claude, Claude Code, `mcp-remote`, Cursor and VS Code are known to the authorization server, so the consent screen shows a real product name rather than a string the client supplied about itself.
* **Client ID Metadata Documents.** Host a metadata document at an HTTPS URL and use that URL as your `client_id`. Nothing to register, nothing to expire.
* **Dynamic client registration.** `POST https://auth.attensira.com/oauth/register` per RFC 7591, for clients that cannot do CIMD. Registration is rate limited by IP, `redirect_uris` is required and must be `https://` or loopback, and a registration expires after 90 days of the client not being used.

No client secrets are issued, to anyone. The token endpoint's only supported authentication method is `none`; a request that presents a secret is rejected with `invalid_client`. Every MCP client is a public client, and a secret shipped inside one is not a secret.

<Warning>
  Anyone can register a client and choose its display name. The consent screen marks a self-registered client as unverified, and that badge is the only thing distinguishing a real integration from someone who registered "Attensira Official" an hour ago. Read the name, the badge and the requested scopes before you approve, and treat an unverified client asking for `attensira:write` as a thing to be sure about.
</Warning>

## Tokens

| Token         | Form                                                       | Lifetime                                    |
| ------------- | ---------------------------------------------------------- | ------------------------------------------- |
| Access token  | ES256-signed JWT, audience `https://mcp.attensira.com/mcp` | 60 minutes, fixed                           |
| Refresh token | Opaque string prefixed `atn_rt_`, stored hashed            | 60-day sliding window, 180-day absolute cap |

Refresh tokens rotate: every use returns a new one and consumes the old. Replaying a consumed refresh token revokes the whole chain it belongs to, so a client that keeps a stale copy around and retries with it will log itself out. A well-behaved client stores only the newest.

The access token names you by reference — the user, the workspace and the granted scopes — and carries no email, no name and no other personal data.

<Note>
  Sixty minutes is also the worst case after you revoke a connection. Revocation stops the next refresh immediately, but an access token already in a client's hands stays valid until it expires. Plan on up to an hour, not instantly, and rotate anything else that was exposed alongside it.
</Note>

## Revoking

Two places, for two different situations.

Revoke a whole connection from **Settings → Connections** in the app — this is what you want when a laptop is lost, a teammate leaves, or you no longer recognise something in the list. See [Connected applications](/account/connections).

A client can also revoke its own tokens at the endpoint, which is what a well-behaved client does when you sign out of it:

```bash Shell theme={null}
curl -X POST https://auth.attensira.com/oauth/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d token=<refresh or access token> \
  -d client_id=<your client id>
```

Per RFC 7009 this always answers `200`, including for a token that was already revoked or never existed. A revocation endpoint that distinguished them would be a way to test whether a stolen token is live.

## Errors

Errors use the RFC 6749 envelope, never a `200` with an error inside it:

```json Error body theme={null}
{
  "error": "invalid_grant",
  "error_description": "The authorization grant is invalid, expired, or has already been used"
}
```

| `error`                  | HTTP     | Usually means                                                                                                                                               |
| ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_request`        | 400      | A parameter is missing, duplicated or malformed; PKCE absent; `code_challenge_method` is not `S256`                                                         |
| `invalid_client`         | 401      | Unknown `client_id`, or the client presented a secret                                                                                                       |
| `invalid_grant`          | 400      | Code or refresh token unknown, expired, already used, or issued to another client; `code_verifier` mismatch; `redirect_uri` differs from the authorize step |
| `unsupported_grant_type` | 400      | Anything other than `authorization_code` or `refresh_token`                                                                                                 |
| `invalid_scope`          | 400      | Requested scope is outside `attensira:read` and `attensira:write`                                                                                           |
| `invalid_target`         | 400      | `resource` is not exactly `https://mcp.attensira.com/mcp`                                                                                                   |
| `access_denied`          | redirect | You declined at the consent screen                                                                                                                          |
| `invalid_token`          | 401      | At `mcp.attensira.com/mcp`: the access token is missing, malformed, expired or not for this resource                                                        |
| `insufficient_scope`     | 403      | At `mcp.attensira.com/mcp`: a valid token that lacks `attensira:write` called a write tool                                                                  |

The `401` from the MCP endpoint is deliberately the same string for every cause — missing, malformed, expired, wrong audience, bad signature. Telling a caller *which* one is true tells an attacker how close they are.

Insufficient scope is a `403` and not a `401` on purpose: the token is fine, so a client that discards it and restarts discovery is doing the wrong thing. Reconnect with the wider scope instead.

## Static keys still work here

None of this replaces `atn_live_` keys. The MCP endpoint accepts an OAuth token or a static key, permanently, and headless callers such as n8n and CI should keep using keys — see [API keys](/account/api-keys). The [REST API](/api/authentication) does not use OAuth at all.
