# auth.md: Threads for Claude

You are an agent that wants to call the Threads for Claude MCP server at `https://threads-mcp.app/mcp`. Every call acts on one person's Threads account, so access always starts with that person signing in with Threads in a browser and approving the connection. There is no API key and no way to register without the user.

This service uses **standard OAuth 2.1** (authorization code + PKCE, with dynamic client registration) as described by the MCP authorization spec. It does **not** implement the WorkOS `agent_auth` registration profile: there is no `identity_endpoint`, no `identity_assertion` / `id-jag` exchange, no `service_auth` (email-only) registration and no anonymous registration. Use the steps below instead.

## Step 1 — Discover

Call the MCP endpoint without a token. You get:

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="No authorization provided", resource_metadata="https://threads-mcp.app/.well-known/oauth-protected-resource/mcp"
```

Fetch the `resource_metadata` URL (RFC 9728 protected resource metadata):

```json
{ "resource": "https://threads-mcp.app/mcp", "authorization_servers": ["https://xcrbqlttioslgdggfdit.supabase.co/auth/v1"], "scopes_supported": ["openid","profile","email","offline_access"], "bearer_methods_supported": ["header"] }
```

Then fetch the authorization server metadata (RFC 8414) of `authorization_servers[0]`:

```http
GET https://xcrbqlttioslgdggfdit.supabase.co/.well-known/oauth-authorization-server/auth/v1
```

It lists `authorization_endpoint`, `token_endpoint`, `registration_endpoint`, `code_challenge_methods_supported` (use `S256`) and `grant_types_supported` (`authorization_code`, `refresh_token`).

### Scopes

Request `scope=openid profile email offline_access` (the `scopes_supported` above). These OAuth scopes only identify the user to this server; they do not widen what it can do. What the server may do on Threads is limited by the Threads permissions the user grants on Meta's consent screen when connecting: `threads_basic`, `threads_content_publish`, `threads_read_replies`, `threads_manage_replies`, `threads_manage_insights`, `threads_delete`, `threads_manage_mentions`, `threads_keyword_search`, `threads_profile_discovery`. Tools that post, reply or repost are marked `readOnlyHint: false` in their MCP annotations, and deleting a post is marked `destructiveHint: true`.

## Step 2 — Pick a method

There is one method: OAuth 2.1 authorization code with PKCE (S256), run by an MCP client on behalf of a user who can open a browser. Claude (web, desktop, mobile) and Claude Code do all of this automatically when the user adds `https://threads-mcp.app/mcp` as a connector; if you are one of those, just ask the user to add the connector and click Connect.

If you only have the user's email, or no user at all, stop: this service cannot be used without the user signing in with Threads.

## Step 3 — Register

Register your client with OAuth dynamic client registration (RFC 7591) at the `registration_endpoint` from the AS metadata:

```http
POST https://xcrbqlttioslgdggfdit.supabase.co/auth/v1/oauth/clients/register
Content-Type: application/json

{ "client_name": "Your agent", "redirect_uris": ["http://localhost:8765/callback"], "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none" }
```

Keep the returned `client_id`.

## Step 4 — Claim (user sign-in and consent)

Send the user to the `authorization_endpoint` with `response_type=code`, your `client_id` and `redirect_uri`, a PKCE `code_challenge` (`code_challenge_method=S256`), a random `state` and, optionally, `resource=https://threads-mcp.app/mcp`. The user signs in with Threads, sees which app is asking and approves. Their browser is redirected to your `redirect_uri` with `code` and `state`; check that `state` matches.

## Step 5 — Exchange

```http
POST https://xcrbqlttioslgdggfdit.supabase.co/auth/v1/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=<code>&redirect_uri=<redirect_uri>&client_id=<client_id>&code_verifier=<verifier>
```

The response carries an `access_token` (a short-lived JWT), `expires_in` and a `refresh_token`. When the access token expires, call the same endpoint with `grant_type=refresh_token`.

## Step 6 — Use the access_token

Send it as a bearer token on every MCP request (streamable HTTP):

```http
POST https://threads-mcp.app/mcp
Authorization: Bearer <access_token>
Content-Type: application/json
Accept: application/json, text/event-stream
```

Tools that publish, reply, repost or delete act publicly as the user. Show the exact text and get explicit confirmation first.

## Errors

| Response | Meaning | What to do |
| --- | --- | --- |
| `401` with `WWW-Authenticate: Bearer ... resource_metadata=...` | Missing, expired or invalid token | Refresh the token; if that fails, restart at Step 1 |
| `invalid_grant` at the token endpoint | Code or refresh token expired, reused or revoked | Restart at Step 4 |
| Tool result `isError: true` "Reconnect the Threads connector" | Threads revoked or expired the account's access | Ask the user to reconnect: https://threads-mcp.app/account |
| Tool result "no ... permission yet" | The connection predates a newer Threads permission | Ask the user to open https://threads-mcp.app/account and click "Update permissions" |
| Tool result "Threads API error: ..." | Threads rejected the request (limits, invalid media, etc.) | Read the message, fix the input; do not retry the same payload |
| `429` / `5xx` | Rate limited or temporary failure | Back off exponentially and retry |

## Revocation

- The user can disconnect at any time on https://threads-mcp.app/account (which also deletes their stored Threads token) or in Threads → Settings → Account → Website permissions.
- After revocation, tool calls fail with a reconnect message and refresh tokens stop working. There is no agent-callable RFC 7009 revocation endpoint; drop your tokens when the user asks you to disconnect.
