Skip to main content
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): the same pp_* keys the REST API accepts, sent as a bearer. No browser, no consent screen — see 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.

At a glance

Protected resourcehttps://mcp.particle.pro
Resource metadatahttps://mcp.particle.pro/.well-known/oauth-protected-resource (and /mcp sub-path)
Authorization Serverhttps://api.particle.pro
AS metadatahttps://api.particle.pro/.well-known/oauth-authorization-server
JWKShttps://api.particle.pro/.well-known/jwks.json
Grant typesauthorization_code, refresh_token
PKCERequired, code_challenge_method=S256 only
Auth methodsnone (public clients), client_secret_basic, client_secret_post
Scopesmcp:read, mcp:write
Access token TTL15 minutes
Refresh token TTL30 days, rotated on every use
Agent onboardinghttps://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:
GET /.well-known/oauth-protected-resource HTTP/1.1
Host: mcp.particle.pro
{
  "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:
GET /.well-known/oauth-authorization-server HTTP/1.1
Host: api.particle.pro
{
  "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 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/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 agent-registration protocol, so autonomous agents (and agent-readiness scanners) can discover how to obtain a credential without reading human documentation:
"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_codedynamic client registration (or a Client ID Metadata Document) followed by the PKCE authorization-code flow, 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:
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):
{
  "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:
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:
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:
{
  "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.
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:
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 → 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:
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:
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 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 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:
ScopeGranted access
mcp:readRead your Particle Pro data via MCP tools.
mcp:writeTake 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 — install snippets for stock MCP clients.
  • Errors — how tool errors surface (different from REST application/problem+json).
  • auth_required — REST-side auth error code.