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

# OAuth for client developers

> Discovery, registration, PKCE, scopes, tokens, revocation, limits and errors of the Workchats MCP server

This page is for people who build an MCP client. To connect an existing
client, see [Connect an AI assistant](/mcp/connect).

The Workchats MCP server follows the MCP authorization spec with OAuth 2.1.
A client needs only the MCP URL. It finds everything else through
discovery. The OAuth endpoints, clients and tokens on this page are
separate from the [public API's OAuth install](/guides/oauth-install): an
MCP token can't call `/v1`, and a bot token can't call `/mcp`.

## Hosts

Each environment has one host. It is the OAuth issuer, the authorization
server and the resource server.

| Environment | Issuer | MCP URLs |
| - | - | - |
| Production | `https://mcp.workchats.com` | `/mcp`, `/mcp/readonly` |
| Staging | `https://mcp.staging.workchats.com` | `/mcp`, `/mcp/readonly` |
| Dev | `https://mcp.dev.workchats.com` | `/mcp`, `/mcp/readonly` |

The examples below use Production.

| Method | Path | Purpose |
| - | - | - |
| `POST`, `GET`, `DELETE` | `/mcp` | Streamable HTTP endpoint |
| `POST`, `GET`, `DELETE` | `/mcp/readonly` | The same server with read tools only |
| `GET` | `/.well-known/oauth-protected-resource`, `/.well-known/oauth-protected-resource/mcp`, `/.well-known/oauth-protected-resource/mcp/readonly` | Protected resource metadata (RFC 9728) |
| `GET` | `/.well-known/oauth-authorization-server` | Authorization server metadata (RFC 8414) |
| `GET` | `/authorize` | Starts sign-in in the browser |
| `POST` | `/token` | `authorization_code` and `refresh_token` grants |
| `POST` | `/register` | Dynamic client registration (RFC 7591) |
| `POST` | `/revoke` | Token revocation (RFC 7009) |

## Discovery

A request to `/mcp` without a token gets `401` and a challenge that names
the resource metadata:

```http theme={null}
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.workchats.com/.well-known/oauth-protected-resource/mcp", scope="workchats:read workchats:write"
```

`GET /.well-known/oauth-protected-resource/mcp`:

```json theme={null}
{
  "resource": "https://mcp.workchats.com/mcp",
  "authorization_servers": ["https://mcp.workchats.com"],
  "scopes_supported": ["workchats:read", "workchats:write"],
  "bearer_methods_supported": ["header"],
  "resource_name": "Workchats",
  "resource_documentation": "https://developers.workchats.com/mcp"
}
```

For `/mcp/readonly`, `resource` is `https://mcp.workchats.com/mcp/readonly`,
`scopes_supported` is `["workchats:read"]`, and the `401` challenge names
that metadata URL and `scope="workchats:read"`.

`GET /.well-known/oauth-authorization-server`:

```json theme={null}
{
  "issuer": "https://mcp.workchats.com",
  "authorization_endpoint": "https://mcp.workchats.com/authorize",
  "token_endpoint": "https://mcp.workchats.com/token",
  "registration_endpoint": "https://mcp.workchats.com/register",
  "revocation_endpoint": "https://mcp.workchats.com/revoke",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none", "client_secret_basic", "client_secret_post"],
  "revocation_endpoint_auth_methods_supported": ["none", "client_secret_basic", "client_secret_post"],
  "scopes_supported": ["workchats:read", "workchats:write"],
  "client_id_metadata_document_supported": true,
  "authorization_response_iss_parameter_supported": true,
  "service_documentation": "https://developers.workchats.com/mcp"
}
```

## Register a client

A client identifies itself in one of two ways. Both are open: any client
may register, and a company admin can block a client by its block key.

### Client ID Metadata Document (preferred)

Use an `https://` URL as your `client_id`. At `/authorize`, Workchats
fetches the document at that URL. The fetch:

* uses HTTPS only, follows no redirects, waits at most 3 seconds and reads
  at most 10 KB
* refuses private and reserved addresses
* needs the document's `client_id` to equal its URL, and its
  `redirect_uris` to contain the requested `redirect_uri`
* needs a URL with a path (not just the host) of at most 2048 bytes
* needs a public client: `token_endpoint_auth_method` absent or `none`.
  Any other value gets `invalid_client` at `/authorize`.

A good document is cached for its `Cache-Control` `max-age`, kept between 5
minutes and 24 hours. With no `max-age`, or with `no-store`, it is cached
for 5 minutes. A failed fetch is not cached. One document URL is fetched at
most 5 times in 10 minutes.

### Dynamic client registration

`POST /register`, with no authentication, as JSON or a form:

```json theme={null}
{
  "client_name": "Example Assistant",
  "redirect_uris": ["https://assistant.example.com/oauth/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}
```

Rules:

* `client_name` is 1 to 100 printable characters. It can't contain the
  words Workchats, Octogle, admin, system or support.
* `redirect_uris` lists 1 to 5 URIs. Each is one of:
  * `https://` on any host
  * loopback `http://` on `127.0.0.1`, `localhost` or `[::1]`, any port
  * a private-use scheme that contains a period, for example
    `com.example.app:/callback`, or one of the editor schemes `cursor`,
    `vscode`, `vscode-insiders`, `windsurf` and `zed`
* No URI may have a fragment or a user name.
* The URIs of one registration are all `https` on one host, or all on one
  private-use scheme, or loopback with or without `https` on any hosts.
  Any other mix is refused. VS Code's fallback registration lists
  `https://insiders.vscode.dev/redirect`, `https://vscode.dev/redirect`,
  `http://127.0.0.1/` and `http://127.0.0.1:33418/` in one request. That
  mix is accepted. Its block key is `local:`, the lower-case
  `client_name` and `@web`, so it never shares a key or a fixed display name with an
  all-loopback app. Only an `https`-only registration gets a host key,
  because anyone can list an `https` host they do not own.
* `grant_types` may hold only `authorization_code` and `refresh_token`.
  `response_types` must be `["code"]`.
* `token_endpoint_auth_method` is `none`, `client_secret_basic` or
  `client_secret_post`. If you leave it out, it is `client_secret_basic`.
  The two secret methods get a `client_secret`, shown once.
* Extra metadata (`logo_uri`, `client_uri`, `tos_uri`, `policy_uri`,
  `contacts`, `scope`, `software_id`, `software_version`) is stored, at
  most 4 KB, but never fetched or shown.

The answer is `201` with a `client_id` that starts with `mcpc_`, and the
registered metadata. A registered client that never connects anyone is
deleted after 30 days.

### Private-use schemes

The MCP authorization spec allows only `https` and localhost redirect URIs.
Workchats also accepts private-use schemes (RFC 8252 section 7.1), because
desktop apps and editors such as Cursor and VS Code use them. The consent
screen warns the employee that the code goes to an app on their computer,
as it does for a loopback redirect.

### Block key and display name

Every client gets a **block key**, the part of its identity Workchats can
check. A company admin blocks a client by this key, so a block survives a
new registration.

| Client | Block key |
| - | - |
| Metadata document, `https` redirect | Host of the `client_id` URL, for example `chatgpt.com` |
| Metadata document, loopback or private-use redirect | `local:` and the host of the `client_id` URL, for example `local:claude.ai` |
| Registered, `https` redirects | Host of the redirect URIs |
| Registered, loopback redirects | `local:` and the lower-case `client_name` |
| Registered, loopback and HTTPS redirects | `local:`, the lower-case `client_name` and `@web`, for example `local:my tool@web` |
| Registered, private-use scheme | The scheme, for example `cursor` |

Well-known keys get a fixed display name: `claude.ai` is "Claude",
`chatgpt.com` and `chat.openai.com` are "ChatGPT", `local:claude code` is
"Claude Code", `cursor` is "Cursor" and `vscode` is "VS Code". A `local:`
key of a well-known host gets that host's name. Any other client shows its
`client_name`, cut to 40 characters. The display name is what the consent
screen shows and what the ["via" label](/mcp/drafts#the-label-on-sent-messages) of a sent
message says.

## Authorize

Send the employee's browser to `/authorize` with PKCE:

```
https://mcp.workchats.com/authorize
  ?response_type=code
  &client_id=<client_id>
  &redirect_uri=<a registered redirect URI>
  &code_challenge=<BASE64URL(SHA-256(code_verifier))>
  &code_challenge_method=S256
  &state=<opaque value>
  &scope=workchats:read workchats:write
  &resource=https://mcp.workchats.com/mcp
```

* `code_challenge_method` must be `S256`. `plain` is refused. The challenge
  is 43 characters of base64url.
* `redirect_uri` must match a registered URI exactly. A loopback URI may
  differ only in its port.
* `resource` (RFC 8707) is `https://mcp.workchats.com/mcp` or
  `https://mcp.workchats.com/mcp/readonly`. Left out, it is `/mcp`.
* `scope` is a subset of the resource's scopes. Left out, it is all of
  them.

If the client or the redirect URI is wrong, Workchats shows an error page
in the browser and never redirects. Any other error redirects to your
`redirect_uri` with `error` and your `state`: `unsupported_response_type`,
`invalid_request`, `invalid_target` or `invalid_scope`.

On success, the browser goes to the Workchats consent screen. The employee
signs in, picks one company and approves. Workchats then redirects to:

```
<redirect_uri>?code=<code>&state=<state>&iss=https://mcp.workchats.com
```

Check that `iss` is the issuer you started with (RFC 9207). If the employee
denies, the redirect carries `error=access_denied` instead of `code`.

The request is valid for 10 minutes, and the code for 10 minutes after
approval. Only employees with an active account can approve, and only in a
company that has AI assistants on and has not blocked your client.

## Exchange the code

`POST /token`, as a form or JSON:

```http theme={null}
POST https://mcp.workchats.com/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=<code>
&code_verifier=<code_verifier>
&redirect_uri=<the same redirect_uri>
&client_id=<client_id>
&resource=https://mcp.workchats.com/mcp
```

A client with a secret authenticates with HTTP Basic or with `client_id`
and `client_secret` in the body. HTTP Basic wins if both are sent.

```json theme={null}
{
  "access_token": "wc_mcp_at_live_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "wc_mcp_rt_live_…",
  "scope": "workchats:read workchats:write"
}
```

Token responses carry `Cache-Control: no-store`. Outside Production the
tokens contain `_test_` in place of `_live_`.

A code works once. Presenting a used code again returns `invalid_grant`
**and revokes the connection that code created**, because a replayed code
is more likely theft than a retry.

A new connection replaces an older one of the same employee, with the same
`client_id` and the same redirect URI. Loopback redirect URIs count as the
same whatever their port. So Claude on the web and a local app that uses
Claude's `client_id` keep separate connections.

## Call the MCP endpoint

Send the access token in the `Authorization` header on every request:

```http theme={null}
POST https://mcp.workchats.com/mcp
Authorization: Bearer wc_mcp_at_live_…
Content-Type: application/json
```

* A token in the query string is refused with `400`.
* A token for `/mcp` also works on `/mcp/readonly`, where it lists only
  read tools. A token for `/mcp/readonly` works only there.
* A token without `workchats:write` lists only read tools. Calling a write
  tool with it gets `403` with
  `WWW-Authenticate: Bearer error="insufficient_scope"`, so you can ask the
  employee for more.
* The server speaks MCP protocol versions `2026-07-28`, `2025-11-25` and
  `2025-06-18` over Streamable HTTP.
* `initialize` returns an `Mcp-Session-Id`. Send it on every later request
  of the session. A request without it gets `400`, and an id from another
  connection or an ended session gets `404`. A session ends on `DELETE`
  or after 10 idle minutes. A stateless `2026-07-28` request needs no
  session.
* A browser request must send an `Origin` that is one `https` origin or a
  loopback `http` origin. Any other `Origin` gets `403`.
* A request body is at most 1 MB, and must be JSON or a form. A larger body
  gets `413` with `{"error": "request_too_large"}`.

Every request checks the connection again: the employee, the company's AI
assistants setting and the block list. A failure is `401` with
`error="invalid_token"` in the challenge.

## Scopes

| Scope | Allows |
| - | - |
| `workchats:read` | Every read tool |
| `workchats:write` | Every write tool, including `draft_message` and `send_draft` |

There is no `offline_access` scope. Every grant gets a refresh token.

## Tokens and lifetimes

| Token | Lifetime |
| - | - |
| Authorization code | 10 minutes, single use |
| Access token, `wc_mcp_at_` | 1 hour |
| Refresh token, `wc_mcp_rt_` | 30 days from issue |

Tokens are opaque, not JWTs. Workchats looks each one up on every request,
so a revocation takes effect at once.

**Refresh.** `POST /token` with `grant_type=refresh_token` and
`refresh_token`, authenticated as for the code grant. The answer has a new
access token and a **new refresh token**. The old refresh token is spent,
and the old access token stops working.

* A refresh token works only for the client it was issued to.
* `scope`, if sent, can't be wider than the grant: `invalid_scope`.
  `resource`, if sent, must be the grant's: `invalid_target`.
* **Reuse ends the connection.** A spent refresh token presented again
  revokes the whole connection and returns `invalid_grant`. There is no
  grace period. A client that loses a refresh response must connect again.
* A connection ends when no refresh happens for 30 days, or when the
  company's maximum session lifetime passes.

## Revocation

`POST /revoke` (RFC 7009) with `token`, an access or a refresh token,
authenticated as for `/token`, revokes the connection of that token. It
answers `200` with `{}`, also for an unknown token, a revoked token or a
token of another client. Those it leaves alone.

Workchats also ends a connection when:

| Trigger | Ends |
| - | - |
| The employee disconnects it in Workchats | That connection |
| A company admin revokes it | That connection |
| A company admin blocks the client's block key | Every connection in the company with that key |
| A company admin turns AI assistants off | Every connection in the company |
| A company admin deactivates, removes or force-logs-out the employee | All the employee's connections |
| The employee stops being an employee | All the employee's connections, at their next request |
| A used code or a spent refresh token is presented | That connection |
| The same client connects again through the same redirect URI | The older connection |
| No refresh for 30 days, or the company's session lifetime passes | That connection |

After revocation, the next MCP request gets `401` with
`error="invalid_token"`. Show your own "reconnect" prompt. Turning AI
assistants on again or unblocking a client brings no connection back.

## Rate limits

OAuth endpoints, per IP address and per client:

| Endpoint | Limit |
| - | - |
| `/register` | 20 an hour per IP |
| `/authorize` | 30 a minute per IP |
| `/token`, `/revoke` | 60 a minute per IP, and 20 a minute per `client_id` |
| Metadata document fetches | 5 in 10 minutes per document URL |

Over a limit, `/token`, `/revoke` and `/register` answer `429` with
`{"error": "slow_down"}` and `Retry-After`. `/authorize` answers `429` with
a plain text page.

The MCP endpoint has per-employee limits, shared by all of an employee's
connections. See [Limits](/mcp/tools#limits).

## Errors

OAuth endpoints answer in the RFC 6749 shape, not the public API's error
envelope:

```json theme={null}
{ "error": "invalid_grant", "error_description": "The code or refresh token is invalid, expired, used or not issued to you." }
```

| Status | `error` | When |
| - | - | - |
| 400 | `invalid_request` | A required parameter is missing or wrong |
| 400 | `invalid_grant` | The code or refresh token is invalid, expired, used or not yours, or the employee can no longer connect |
| 400 | `invalid_target` | `resource` doesn't match the grant |
| 400 | `invalid_scope` | `scope` is wider than allowed |
| 400 | `unsupported_grant_type` | Not `authorization_code` or `refresh_token` |
| 400 | `invalid_redirect_uri` | `/register`: a redirect URI is not allowed, or the URIs mix kinds other than loopback with HTTPS |
| 400 | `invalid_client_metadata` | `/register`: other registration metadata is wrong |
| 401 | `invalid_client` | Client authentication failed. With HTTP Basic, the answer also carries `WWW-Authenticate: Basic` |
| 429 | `slow_down` | Over a rate limit |

Tool errors inside a successful MCP response are described in
[Tools](/mcp/tools#conventions).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.