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

# Authentication

> OAuth 2.1 for interactive clients — discovery, dynamic client registration, PKCE, refresh, revocation — and API-key bearer auth for headless agents.

The Particle Pro MCP server accepts two credentials, matched to two kinds of callers:

* **OAuth 2.1** (interactive clients — Claude Code, Cursor, VS Code, ChatGPT, Claude Desktop): a user approves the connection in a browser once, and every request thereafter carries a short-lived JWT minted by the Particle Pro Authorization Server, bound to the MCP resource via its `aud` claim. Granular, per-project, user-revocable — prefer it whenever a human is present to consent.
* **Platform API keys** (headless agents — cron jobs, server-side harnesses, [OpenAI's Responses API](https://platform.openai.com/docs/guides/tools-remote-mcp#authentication)): the same `pp_*` keys the REST API accepts, sent as a bearer. No browser, no consent screen — see [API keys for headless agents](#api-keys-for-headless-agents).

Most of this page documents the OAuth flow for client implementers. If you are using a stock MCP client the flow is automatic — see the [Quickstart](/mcp/quickstart).

## At a glance

|                          |                                                                                                                                       |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| **Protected resource**   | `https://mcp.particle.pro`                                                                                                            |
| **Resource metadata**    | `https://mcp.particle.pro/.well-known/oauth-protected-resource` (and `/mcp` sub-path)                                                 |
| **Authorization Server** | `https://api.particle.pro`                                                                                                            |
| **AS metadata**          | `https://api.particle.pro/.well-known/oauth-authorization-server`                                                                     |
| **JWKS**                 | `https://api.particle.pro/.well-known/jwks.json`                                                                                      |
| **Grant types**          | `authorization_code`, `refresh_token`                                                                                                 |
| **PKCE**                 | Required, `code_challenge_method=S256` only                                                                                           |
| **Auth methods**         | `none` (public clients), `client_secret_basic`, `client_secret_post`                                                                  |
| **Scopes**               | `mcp:read`, `mcp:write`                                                                                                               |
| **Access token TTL**     | 15 minutes                                                                                                                            |
| **Refresh token TTL**    | 30 days, rotated on every use                                                                                                         |
| **Agent onboarding**     | [`https://api.particle.pro/auth.md`](https://api.particle.pro/auth.md) (also on the MCP host) + `agent_auth` block in the AS metadata |

## Discovery

Start from the resource. Fetch the protected-resource metadata document:

```http theme={"dark"}
GET /.well-known/oauth-protected-resource HTTP/1.1
Host: mcp.particle.pro
```

```json theme={"dark"}
{
  "resource": "https://mcp.particle.pro",
  "authorization_servers": ["https://api.particle.pro"],
  "bearer_methods_supported": ["header"],
  "scopes_supported": ["mcp:read", "mcp:write"],
  "resource_documentation": "https://docs.particle.pro/mcp"
}
```

Then fetch the Authorization Server metadata from the URL in `authorization_servers`:

```http theme={"dark"}
GET /.well-known/oauth-authorization-server HTTP/1.1
Host: api.particle.pro
```

```json theme={"dark"}
{
  "issuer": "https://api.particle.pro",
  "authorization_endpoint": "https://api.particle.pro/oauth/authorize",
  "token_endpoint": "https://api.particle.pro/oauth/token",
  "registration_endpoint": "https://api.particle.pro/oauth/register",
  "revocation_endpoint": "https://api.particle.pro/oauth/revoke",
  "jwks_uri": "https://api.particle.pro/.well-known/jwks.json",
  "scopes_supported": ["mcp:read", "mcp:write"],
  "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"],
  "subject_types_supported": ["public"],
  "client_id_metadata_document_supported": true,
  "service_documentation": "https://docs.particle.pro/mcp/oauth"
}
```

The live document also carries an `agent_auth` block for autonomous agents — see [Agent discovery](#agent-discovery) below.

If a request to the MCP endpoint comes in unauthenticated, the server returns `401` with a `WWW-Authenticate` header pointing at the resource-metadata document, so a client that doesn't pre-fetch metadata still discovers the AS:

```http theme={"dark"}
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="mcp", error="invalid_token", resource_metadata="https://mcp.particle.pro/.well-known/oauth-protected-resource"
```

The human-readable description (e.g. `bearer token required`, `grant revoked or unknown`) is in the response body, not the header.

## Agent discovery

Particle Pro publishes the [auth.md](https://github.com/workos/auth.md) agent-registration protocol, so autonomous agents (and agent-readiness scanners) can discover how to obtain a credential without reading human documentation:

* **`/auth.md`** — a human- and machine-readable onboarding recipe at [`https://api.particle.pro/auth.md`](https://api.particle.pro/auth.md) and [`https://mcp.particle.pro/auth.md`](https://mcp.particle.pro/auth.md): the discovery walkthrough, a credential decision tree, registration steps, errors, and revocation. A docs-flavored mirror lives at [Agent authentication](/auth).
* **`agent_auth`** — a machine-readable block inside the AS metadata document above:

```json theme={"dark"}
"agent_auth": {
  "skill": "https://api.particle.pro/auth.md",
  "register_uri": "https://api.particle.pro/oauth/register",
  "revocation_uri": "https://api.particle.pro/oauth/revoke",
  "authorization_endpoint": "https://api.particle.pro/oauth/authorize",
  "token_endpoint": "https://api.particle.pro/oauth/token",
  "identity_types_supported": ["user_consent_authorization_code"],
  "credential_types_supported": ["access_token", "api_key"],
  "user_consent_authorization_code": {
    "registration_endpoint": "https://api.particle.pro/oauth/register",
    "authorization_endpoint": "https://api.particle.pro/oauth/authorize",
    "token_endpoint": "https://api.particle.pro/oauth/token",
    "code_challenge_methods_supported": ["S256"],
    "resource": "https://mcp.particle.pro",
    "consent_uri": "https://platform.particle.pro/oauth/consent"
  },
  "unsupported_flows": {
    "anonymous": "No anonymous or self-service agent credential issuance. Every credential is tied to a human-owned platform account.",
    "identity_assertion": "No ID-JAG / identity-assertion exchange. Use authorization_code + PKCE (interactive) or a dashboard-issued pp_* API key (headless).",
    "device_authorization": "No device authorization grant. Headless agents use a pp_* API key minted at https://platform.particle.pro/tokens."
  },
  "events_supported": []
}
```

Every advertised URL is a shipping endpoint. The only identity type is `user_consent_authorization_code` — [dynamic client registration](#dynamic-client-registration-rfc-7591) (or a [Client ID Metadata Document](#client-id-metadata-document-cimd)) followed by the [PKCE authorization-code flow](#authorization-code-with-pkce), with a human approving on platform.particle.pro. Flows Particle Pro deliberately does not offer (anonymous registration, ID-JAG identity assertion, device authorization) are declared in `unsupported_flows`, so an agent fails fast instead of probing.

The REST API publishes its own RFC 9728 document at `https://api.particle.pro/.well-known/oauth-protected-resource`. Mind the asymmetry it implies: the REST surface authenticates with `pp_*` API keys only — AS-issued access tokens are audience-bound to the MCP resource and are not accepted by the REST API.

## Client registration

You have two ways to register a client.

### Dynamic Client Registration (RFC 7591)

The simplest path. POST to the registration endpoint with at least `client_name` and `redirect_uris`:

```http theme={"dark"}
POST /oauth/register HTTP/1.1
Host: api.particle.pro
Content-Type: application/json

{
  "client_name": "Acme Research Agent",
  "client_uri": "https://acme.example",
  "logo_uri": "https://acme.example/logo.png",
  "redirect_uris": ["http://localhost:43217/callback"],
  "token_endpoint_auth_method": "none"
}
```

Response (RFC 7591 § 3.2):

```json theme={"dark"}
{
  "client_id": "c_abc123…",
  "client_name": "Acme Research Agent",
  "redirect_uris": ["http://localhost:43217/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}
```

Defaults applied when omitted:

* `token_endpoint_auth_method`: `none` (i.e. public client, PKCE-only)
* `grant_types`: `["authorization_code", "refresh_token"]`
* `response_types`: `["code"]`

For confidential clients (any `token_endpoint_auth_method` other than `none`) the response includes `client_secret` exactly once — store it securely, the AS does not return it again.

### Client ID Metadata Document (CIMD)

Agents that already publish a metadata document at an HTTPS URL can skip DCR entirely and pass the URL itself as `client_id` to `/oauth/authorize`. The AS fetches the document, validates it, and caches it. The CIMD spec is `draft-ietf-oauth-client-id-metadata-document`.

Use this when your agent runs in many places (you don't want to register N clients) but has a stable identity (your domain).

## Authorization code with PKCE

Generate a high-entropy `code_verifier`, then derive its S256 challenge:

```bash theme={"dark"}
code_verifier=$(openssl rand -base64 64 | tr -d '\n' | tr '+/' '-_' | tr -d '=' | cut -c1-128)
code_challenge=$(printf "%s" "$code_verifier" | openssl dgst -sha256 -binary | base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=')
```

Open the authorization URL in a browser:

```
https://api.particle.pro/oauth/authorize?
  response_type=code&
  client_id=c_abc123…&
  redirect_uri=http%3A%2F%2Flocalhost%3A43217%2Fcallback&
  scope=mcp%3Aread%20mcp%3Awrite&
  state=<random>&
  code_challenge=<code_challenge>&
  code_challenge_method=S256&
  resource=https%3A%2F%2Fmcp.particle.pro
```

Required parameters:

* `response_type=code` (only flow supported)
* `client_id` — from registration or CIMD URL
* `redirect_uri` — must match a value the client registered
* `code_challenge` + `code_challenge_method=S256` — PKCE is mandatory
* `resource=https://mcp.particle.pro` — RFC 8707 audience binding; the AS rejects any other value

`scope` is optional. If omitted the AS issues `mcp:read mcp:write` by default. Naming an unknown scope is rejected (the AS will redirect with `error=invalid_scope` rather than silently up-scoping).

The user signs in (if not already), picks a project, and approves the requested scopes. The AS redirects back to your `redirect_uri`:

```
http://localhost:43217/callback?code=auth_xyz…&state=<your-state>
```

Exchange the code for tokens:

```http theme={"dark"}
POST /oauth/token HTTP/1.1
Host: api.particle.pro
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=auth_xyz…&
redirect_uri=http%3A%2F%2Flocalhost%3A43217%2Fcallback&
code_verifier=<your-code_verifier>&
client_id=c_abc123…&
resource=https%3A%2F%2Fmcp.particle.pro
```

For confidential clients, authenticate one of two ways:

* **`client_secret_basic`** — drop `client_id` from the form body and send `client_id:client_secret` URL-form-encoded in an HTTP Basic `Authorization` header.
* **`client_secret_post`** — keep `client_id` in the form body and add a `client_secret` field alongside it.

Public clients (`token_endpoint_auth_method=none`) keep `client_id` in the form body and send no secret.

Response:

```json theme={"dark"}
{
  "access_token": "eyJhbGciOiJSUzI1NiIs…",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "rt_…",
  "scope": "mcp:read mcp:write"
}
```

## Calling the MCP endpoint

Send the access token as a bearer in the `Authorization` header. The MCP server only accepts header-borne tokens (`bearer_methods_supported: ["header"]`) — no query-string tokens, no form-body tokens.

```http theme={"dark"}
POST /mcp HTTP/1.1
Host: mcp.particle.pro
Authorization: Bearer eyJhbGciOiJSUzI1NiIs…
Content-Type: application/json

{ "jsonrpc": "2.0", "method": "tools/list", "id": 1 }
```

The resource server validates the token in this order:

1. Verify the JWT signature against the rotating JWKS at `https://api.particle.pro/.well-known/jwks.json`.
2. Confirm `iss=https://api.particle.pro`, `aud=https://mcp.particle.pro`, and `exp` is in the future.
3. Look up the `grant_id` claim — the grant must still be active (not revoked, not user-disabled).
4. Confirm the token's `sub`, `client_id`, and `project_id` claims match the grant row (defense in depth against a buggy or compromised signer).

Any failure returns `401` with a `WWW-Authenticate` challenge.

## API keys for headless agents

Autonomous agents have no browser to complete an OAuth consent in. For them, the MCP endpoint accepts the same project-scoped `pp_*` API keys as the REST API — as an `Authorization: Bearer` header or an `X-API-Key` header:

```bash theme={"dark"}
curl https://mcp.particle.pro/ \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer $PARTICLE_API_KEY" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tools/call",
    "params": {
      "name": "particle_entity_resolve",
      "arguments": { "query": "sam altman" }
    },
    "id": 1
  }'
```

The key authenticates exactly as it does on the REST surface — same project and organization binding, same subscription gating, same rate limits, and tool calls meter identically to OAuth traffic. Create and rotate keys from [platform.particle.pro](https://platform.particle.pro) → your project's settings.

Credential hygiene for agents:

* **Keys are project-scoped** — give each agent its own key in a project provisioned for it, so a leaked key exposes one project's access, is attributable in request logs, and can be revoked without breaking anything else.
* **Never embed keys in code or prompts.** Provide them through a secrets vault or environment variable, as in the `$PARTICLE_API_KEY` example above.
* **Prefer OAuth when a human is present.** Consent-scoped, short-lived, per-client tokens beat a long-lived shared secret anywhere an interactive approval is possible.

An invalid or expired key gets a `401` whose body says `invalid or expired API key` — distinct from the JWT validation errors above, so a misconfigured agent can tell which credential path failed.

## Refresh and rotation

When the access token nears expiry, exchange the refresh token:

```http theme={"dark"}
POST /oauth/token HTTP/1.1
Host: api.particle.pro
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&
refresh_token=rt_…&
client_id=c_abc123…&
resource=https%3A%2F%2Fmcp.particle.pro
```

Refresh tokens **rotate** on every use — the response carries a *new* refresh token; the old one is consumed. If the AS sees the same refresh token presented twice (replay) or two refreshes race for the same token, it revokes the entire grant chain immediately. Store the latest refresh token atomically; never retry a refresh request with the same token.

## Revocation

Either side can revoke a grant:

```http theme={"dark"}
POST /oauth/revoke HTTP/1.1
Host: api.particle.pro
Content-Type: application/x-www-form-urlencoded

token=rt_…&
client_id=c_abc123…
```

Users can also revoke connections from the platform UI — see [Manage connections](#manage-connections) below. Once a grant is revoked, all access tokens issued from it stop validating immediately at the resource server (the grant lookup fails) and the refresh token can no longer be exchanged.

## Manage connections

Every OAuth approval creates a **connection** (a grant) binding one MCP client to one of your projects. You can review and revoke them at any time — no client involvement needed:

1. Sign in at [platform.particle.pro](https://platform.particle.pro) and open **Connected Applications**.
2. Each entry shows the client's registered name, the project it acts on, and when it was approved. Find the client you want to disconnect.
3. Choose **Revoke**. The grant — and every access and refresh token issued from it — stops working immediately.

To reconnect, just use the client again: the next tool call triggers a fresh OAuth flow, and the consent screen lets you pick a different project if the original grant was scoped to the wrong one (the most common reason for a `403` after a previously working connection).

Revoke a connection whenever a device is lost, an agent misbehaves, or you no longer use the client — reconnecting later is a one-click approval.

## Audience binding (RFC 8707)

Every authorization request and every token request must include `resource=https://mcp.particle.pro`. The AS rejects mismatches with `error=invalid_target`. The minted access token carries `aud=https://mcp.particle.pro`, and the MCP server rejects any token with a different `aud` — even a valid token issued for some hypothetical other Particle resource would not work here. This is the standard "confused deputy" safeguard.

## Scopes

Two scopes today; finer-grained scopes can be added as the surface grows:

| Scope       | Granted access                                                                                                                                 |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `mcp:read`  | Read your Particle Pro data via MCP tools.                                                                                                     |
| `mcp:write` | Take actions on your behalf in Particle Pro via MCP tools. (All current tools are read-only — `mcp:write` is reserved for future write tools.) |

Today every tool advertises `readOnlyHint: true`; `mcp:write` is requested by default but does not unlock additional surface yet.

## Key rotation

The AS uses RS256 with a rotating JWKS. The signer publishes the "next" key well before promoting it to "active" and keeps "retired" keys live long enough for outstanding tokens to expire (15-minute access-token TTL). Validators cache the JWKS for up to 5 minutes — a fresh `kid` will be picked up within that window.

## See also

* [Quickstart](/mcp/quickstart) — install snippets for stock MCP clients.
* [Errors](/mcp/errors) — how tool errors surface (different from REST `application/problem+json`).
* [`auth_required`](/errors/auth_required) — REST-side auth error code.
