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

# Authentication

> OAuth 2.1 with PKCE — connect via a hosted metadata document (CIMD, the preferred path for new clients) or Dynamic Client Registration. MCP-aware clients like Claude Desktop and Cursor handle both automatically.

## TL;DR

You don't need to manage anything by hand. MCP-aware clients walk the full OAuth flow themselves the first time they hit the Cometly MCP endpoint:

1. The client sees a `401 Unauthorized` with a `WWW-Authenticate` header pointing at our OAuth discovery URL.
2. The client identifies itself — either via a hosted metadata document (CIMD) or by calling the registration endpoint (DCR).
3. The client opens your browser to the Cometly authorization page.
4. You log in, pick which Space to grant access to, and click **Approve**.
5. The client exchanges the resulting code for an access token bound to that Space.

After that the client just sends `Authorization: Bearer <token>` on every tool call.

If you're **building a custom MCP client**, you have two registration paths: [CIMD](#client-id-metadata-document-cimd) (preferred — no registration call required) and [Dynamic Client Registration](#dynamic-client-registration) (fallback, e.g. for custom URI schemes). If you want the gritty detail, keep reading.

## Endpoints

Cometly is its own authorization server. There's no third-party identity provider in the middle.

| Endpoint                                 | URL                                                                     |
| ---------------------------------------- | ----------------------------------------------------------------------- |
| Protected resource metadata (RFC 9728)   | `https://app.cometly.com/.well-known/oauth-protected-resource/{path}`   |
| Authorization server metadata (RFC 8414) | `https://app.cometly.com/.well-known/oauth-authorization-server/{path}` |
| Dynamic client registration              | `POST https://app.cometly.com/mcp/oauth/register`                       |
| Authorization (browser)                  | `GET https://app.cometly.com/mcp/oauth/authorize`                       |
| Token exchange                           | `POST https://app.cometly.com/mcp/oauth/token`                          |
| MCP server (resource)                    | `https://app.cometly.com/mcp/{space_id}/public-api`                     |

We expose **both** the path-prefix (RFC 9728) and path-suffix (RFC 8414) `.well-known` URL variants because different clients implement different revisions of the spec. Claude Desktop uses the prefix form; Cursor uses the suffix form. Both resolve to the same metadata.

## Discovery metadata

Hitting the auth-server metadata endpoint returns:

```json theme={null}
{
  "issuer": "https://app.cometly.com",
  "authorization_endpoint": "https://app.cometly.com/mcp/oauth/authorize",
  "token_endpoint": "https://app.cometly.com/mcp/oauth/token",
  "registration_endpoint": "https://app.cometly.com/mcp/oauth/register",
  "response_types_supported": ["code"],
  "code_challenge_methods_supported": ["S256"],
  "scopes_supported": ["mcp:use"],
  "grant_types_supported": ["authorization_code"],
  "client_id_metadata_document_supported": true
}
```

PKCE with `S256` is required. There's no implicit grant, no password grant, no client-credentials grant — just authorization code with PKCE.

## Client ID Metadata Document (CIMD)

CIMD is the preferred registration path for new custom MCP clients. Instead of calling `/mcp/oauth/register` to get a client ID, you host a JSON document at an `https://` URL and use that URL itself as your `client_id` — no registration request needed.

Support is advertised via `client_id_metadata_document_supported: true` in the [discovery metadata](#discovery-metadata). If that field is absent or `false`, fall back to [Dynamic Client Registration](#dynamic-client-registration).

### Metadata document

Host a JSON file at any stable `https://` URL with a non-root path (for example, `https://app.example.com/oauth/client-metadata.json`). The document must include:

| Field                        | Required | Description                                                                    |
| ---------------------------- | -------- | ------------------------------------------------------------------------------ |
| `client_id`                  | Yes      | Must exactly equal the URL the document is hosted at.                          |
| `client_name`                | Yes      | Shown verbatim on the consent screen.                                          |
| `redirect_uris`              | Yes      | Array of allowed redirect URIs — see rules below.                              |
| `token_endpoint_auth_method` | Yes      | Must be `"none"`. Public clients only; `private_key_jwt` is not yet supported. |

All other fields (`client_uri`, `grant_types`, `response_types`) are accepted and recorded but not enforced.

```json theme={null}
{
  "client_id": "https://app.example.com/oauth/client-metadata.json",
  "client_name": "Example MCP Client",
  "client_uri": "https://app.example.com",
  "redirect_uris": ["https://app.example.com/callback", "http://127.0.0.1:3000/callback"],
  "grant_types": ["authorization_code"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}
```

On the consent screen, Cometly shows the verified client domain alongside `client_name`. Clients whose only redirect URIs are loopback addresses receive an advisory notice on the consent screen.

### Accepted `redirect_uris` for CIMD clients

Each URI in the document must satisfy one of two rules:

| Category          | Examples                                                 | Rule                                                                                                                                       |
| ----------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Loopback          | `http://127.0.0.1:33333/callback`, `http://localhost/cb` | Any port, any path. RFC 8252 loopback normalization applies.                                                                               |
| Same-origin https | `https://app.example.com/callback`                       | The redirect URI origin must exactly match the `client_id` URL origin (same scheme, host, and port). Cross-origin https URIs are rejected. |

**Custom URI schemes** (e.g. `cursor://`, `vscode://`) are **not** accepted for CIMD clients. If your client uses a custom scheme, use [Dynamic Client Registration](#dynamic-client-registration) instead — the custom-scheme allowlist applies only to DCR.

The `redirect_uri` you send on `/authorize` and `/token` must exactly match one of the URIs in the document (loopback port normalization per RFC 8252 applies).

### Document fetching and caching

On every `/authorize` request with a URL-shaped `client_id`, Cometly fetches and validates the metadata document. The fetch is:

* **SSRF-guarded** — private, loopback, link-local, and cloud-metadata IPs are refused.
* **TLS-verified** — the certificate must be valid; redirects are not followed by default.
* **Rate-limited and size-capped** — oversized or excessively frequent responses are rejected.

Cometly caches the validated document per `client_id` URL, honoring `Cache-Control: max-age` up to a 24-hour ceiling (default window 15 minutes). Deploy an updated document and wait up to 24 hours for all caches to clear, or set a shorter `max-age` on your server's response.

## Dynamic client registration

Dynamic Client Registration (RFC 7591) is the fallback path when CIMD isn't suitable — for example when your client uses a custom URI scheme (`cursor://`, `vscode://`) that CIMD doesn't accept, or when you can't host a metadata document at a stable https URL.

`POST /mcp/oauth/register` with the client's metadata. Minimal example:

```json theme={null}
{
  "client_name": "My MCP Client",
  "redirect_uris": ["http://127.0.0.1:33333/callback"],
  "token_endpoint_auth_method": "none"
}
```

Returns a `client_id` you'll use for the authorization request. There is no `client_secret` — Cometly's MCP server treats every client as a public client and relies on PKCE for security.

### Accepted `redirect_uris`

The redirect URI is validated at registration. Each URI must fall into one of three accepted categories; redirect URIs that don't match are rejected. This allowlist applies to DCR clients only — [CIMD clients](#accepted-redirect_uris-for-cimd-clients) use a same-host rule instead.

| Category                      | Examples                                                                                  | Use case                                                                                   |
| ----------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Loopback                      | `http://127.0.0.1:33333/callback`, `http://localhost/cb`                                  | Native desktop clients                                                                     |
| `https://` to an allowed host | `https://claude.ai/api/mcp/auth_callback`                                                 | Hosted web clients — see [Default allowed https hosts](#default-allowed-https-hosts) below |
| Well-known custom scheme      | `cursor://...`, `vscode://...`, `claude-desktop://...`, `com.example.app:/oauth2redirect` | Desktop and native apps                                                                    |

### Default allowed https hosts

These hosts are accepted for DCR clients out of the box. If you need a host that isn't listed, contact support: [support@cometly.com](mailto:support@cometly.com). (CIMD clients don't need to be on this list — they use the same-host rule described above.)

| Host(s)                              | Client                                                                         |
| ------------------------------------ | ------------------------------------------------------------------------------ |
| `claude.ai`, `claude.com`            | Anthropic Claude (web, desktop, mobile)                                        |
| `chatgpt.com`, `chat.openai.com`     | OpenAI ChatGPT                                                                 |
| `cursor.com`, `www.cursor.com`       | Cursor web agents                                                              |
| `vscode.dev`, `insiders.vscode.dev`  | VS Code for the Web / github.dev (desktop VS Code uses loopback + `vscode://`) |
| `grok.com`                           | xAI Grok                                                                       |
| `gemini.google.com`                  | Google Gemini                                                                  |
| `perplexity.ai`, `www.perplexity.ai` | Perplexity                                                                     |

### Accepted `client_name`

`client_name` is shown verbatim on the consent screen, so it's validated to prevent impersonation and UI hijacking:

* Maximum 120 characters.
* No control characters (`\x00–\x1f`, `\x7f`).
* Names that contain `cometly` as a standalone word (case-insensitive, word/whitespace/hyphen/underscore boundaries) are rejected to keep malicious clients from passing themselves off as Cometly itself.

### Rate limits

`POST /mcp/oauth/register` is throttled to **10 registrations per hour per IP**. Legitimate clients register once per install, so this leaves plenty of headroom for retries while making it impractical to flood the registration table. Exceeded requests return `429 temporarily_unavailable` with an RFC 7591 error body.

## Authorization flow

This flow is the same whether your `client_id` is an opaque string from DCR or an https URL from CIMD.

1. Generate a PKCE `code_verifier` and the matching `code_challenge` (`SHA256(verifier)`, base64url, no padding).

2. Redirect the user (in their browser) to:

   ```
   https://app.cometly.com/mcp/oauth/authorize
     ?response_type=code
     &client_id={client_id}
     &redirect_uri={redirect_uri}
     &code_challenge={code_challenge}
     &code_challenge_method=S256
     &state={random}
     &scope=mcp:use
   ```

3. The user logs in (if not already), then picks the Space and clicks **Approve**.

4. Cometly redirects back to your `redirect_uri` with `?code={code}&state={state}`.

5. Exchange the code:

   ```bash theme={null}
   curl -X POST https://app.cometly.com/mcp/oauth/token \
     -H "Content-Type: application/x-www-form-urlencoded" \
     -d "grant_type=authorization_code" \
     -d "code={code}" \
     -d "redirect_uri={redirect_uri}" \
     -d "client_id={client_id}" \
     -d "code_verifier={code_verifier}"
   ```

   Response:

   ```json theme={null}
   {
     "access_token": "1|abc...",
     "token_type": "Bearer",
     "expires_in": null,
     "scope": "mcp_space:42"
   }
   ```

## Calling the MCP server

Once you have an access token, every JSON-RPC call to the MCP endpoint carries it:

```bash theme={null}
curl -X POST https://app.cometly.com/mcp/42/public-api \
  -H "Authorization: Bearer 1|abc..." \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

The Space ID in the URL **must** match the Space the token was approved for — Cometly checks the token's `mcp_space:{id}` ability against the route param on every request and returns `403` if they disagree.

## Scopes

There's a single MCP scope today:

| Scope            | What it grants                                                                                                                                  |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `mcp_space:{id}` | Call any read-only tool against Space `{id}`. The Space ID is bound into the scope string — a token approved for Space 42 cannot read Space 43. |

When you click **Approve** in the consent UI, the token's ability is set to exactly one `mcp_space:{id}` value. There's no broader "all spaces" scope, by design.

## Revoking access

You can revoke a connected MCP client at any time:

1. Open **Settings → API Tokens** in your Cometly dashboard.
2. Find the token whose name matches the MCP client (e.g. `MCP — Claude Desktop`).
3. Click **Revoke**.

The next tool call from that client gets `401 Unauthorized` and the client will walk the auth flow again on next launch (or you can just remove it from the client's MCP settings).

Removing a user from a Space also immediately revokes any MCP tokens they have for that Space — there's no separate cleanup step.

## Common errors

| Status               | Meaning                                                                                                            | Recovery                                                                             |
| -------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
| `401`                | No / invalid / expired token.                                                                                      | Walk the OAuth flow again. The `WWW-Authenticate` header includes the discovery URL. |
| `403`                | Token's `mcp_space:{id}` doesn't match the URL's Space ID, or the user no longer has API permission in that Space. | Re-authorize with the correct Space.                                                 |
| `403` (subscription) | The Space's team doesn't have an active Public API subscription.                                                   | Reach out to your Cometly admin or contact sales.                                    |
| `429`                | Rate limit exceeded.                                                                                               | Back off. See [Rate Limits & Errors](/mcp/rate-limits-and-errors).                   |
