@thenavidm/threads-mcp-cli 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1019 -0
- package/SKILL.md +203 -0
- package/dist/api/client.d.ts +105 -0
- package/dist/api/client.js +305 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/errors.d.ts +92 -0
- package/dist/api/errors.js +195 -0
- package/dist/api/errors.js.map +1 -0
- package/dist/api/identity.d.ts +33 -0
- package/dist/api/identity.js +52 -0
- package/dist/api/identity.js.map +1 -0
- package/dist/auth/login.d.ts +32 -0
- package/dist/auth/login.js +204 -0
- package/dist/auth/login.js.map +1 -0
- package/dist/auth/store.d.ts +37 -0
- package/dist/auth/store.js +88 -0
- package/dist/auth/store.js.map +1 -0
- package/dist/auth/tokens.d.ts +54 -0
- package/dist/auth/tokens.js +96 -0
- package/dist/auth/tokens.js.map +1 -0
- package/dist/cli.d.ts +59 -0
- package/dist/cli.js +444 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +98 -0
- package/dist/config.js +185 -0
- package/dist/config.js.map +1 -0
- package/dist/content/containers.d.ts +89 -0
- package/dist/content/containers.js +210 -0
- package/dist/content/containers.js.map +1 -0
- package/dist/content/media.d.ts +61 -0
- package/dist/content/media.js +125 -0
- package/dist/content/media.js.map +1 -0
- package/dist/content/text.d.ts +68 -0
- package/dist/content/text.js +106 -0
- package/dist/content/text.js.map +1 -0
- package/dist/doctor.d.ts +14 -0
- package/dist/doctor.js +218 -0
- package/dist/doctor.js.map +1 -0
- package/dist/format/posts.d.ts +41 -0
- package/dist/format/posts.js +153 -0
- package/dist/format/posts.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +167 -0
- package/dist/index.js.map +1 -0
- package/dist/safety.d.ts +52 -0
- package/dist/safety.js +85 -0
- package/dist/safety.js.map +1 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.js +232 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/accounts.d.ts +27 -0
- package/dist/tools/accounts.js +162 -0
- package/dist/tools/accounts.js.map +1 -0
- package/dist/tools/discover.d.ts +56 -0
- package/dist/tools/discover.js +146 -0
- package/dist/tools/discover.js.map +1 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.js +16 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/insights.d.ts +55 -0
- package/dist/tools/insights.js +223 -0
- package/dist/tools/insights.js.map +1 -0
- package/dist/tools/kit.d.ts +90 -0
- package/dist/tools/kit.js +119 -0
- package/dist/tools/kit.js.map +1 -0
- package/dist/tools/posts.d.ts +170 -0
- package/dist/tools/posts.js +312 -0
- package/dist/tools/posts.js.map +1 -0
- package/dist/tools/read.d.ts +31 -0
- package/dist/tools/read.js +95 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/replies.d.ts +92 -0
- package/dist/tools/replies.js +218 -0
- package/dist/tools/replies.js.map +1 -0
- package/dist/transport/http.d.ts +28 -0
- package/dist/transport/http.js +103 -0
- package/dist/transport/http.js.map +1 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,1019 @@
|
|
|
1
|
+
<img src="https://cdn.navid.media/connectors/threads-icon.png" alt="Threads" width="88">
|
|
2
|
+
|
|
3
|
+
# Threads MCP Server & CLI
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@thenavidm/threads-mcp-cli)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](https://youtube.com/@thenavidm?sub_confirmation=1)
|
|
8
|
+
[](https://x.com/thenavidm)
|
|
9
|
+
[](https://linkedin.com/in/thenavidm)
|
|
10
|
+
|
|
11
|
+
Threads MCP server and CLI for Claude Code and AI agents. 30 tools for posting, chained threads, carousels, replies and reply approvals, insights, keyword search and profile discovery.
|
|
12
|
+
|
|
13
|
+
One install gives you both surfaces, the same tools under the same names,
|
|
14
|
+
covering everything the app does and several things it cannot.
|
|
15
|
+
|
|
16
|
+
Threads has its own API, separate from Instagram's, so it needs its own token.
|
|
17
|
+
One Meta app can carry both, with one app id and one testers list.
|
|
18
|
+
|
|
19
|
+
Publishing and deleting ask for confirmation. Everything else is a read.
|
|
20
|
+
|
|
21
|
+
One command to authorise, and the 60-day token refreshes itself from then on.
|
|
22
|
+
|
|
23
|
+
Built and maintained by [Navid Moazzez](https://navid.me?utm_source=github&utm_medium=readme&utm_campaign=threads-mcp-cli).
|
|
24
|
+
|
|
25
|
+
<img src="https://cdn.navid.media/repos/threads-mcp.gif?v=2" alt="Claude Code using the Threads MCP server" width="520">
|
|
26
|
+
|
|
27
|
+
## Two ways to use it
|
|
28
|
+
|
|
29
|
+
### Command line
|
|
30
|
+
|
|
31
|
+
`threads-cli` in your terminal, for scripting, cron, pipes, or a quick question
|
|
32
|
+
without opening anything:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
threads-cli # every command, one line each
|
|
36
|
+
threads-cli whoami # which profile the token belongs to
|
|
37
|
+
threads-cli get-publishing-limit # how much quota is left today
|
|
38
|
+
threads-cli get-posts --limit 10 # your recent posts
|
|
39
|
+
threads-cli get-top-posts --limit 25 # ranked by engagement against views
|
|
40
|
+
threads-cli search-keyword "model context protocol"
|
|
41
|
+
threads-cli create-post --text "Shipped." --confirm
|
|
42
|
+
threads-cli list-accounts --json | jq -r '.accounts[].username'
|
|
43
|
+
threads-cli <command> --help # what any command takes
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`--confirm` is the shell spelling of the confirmation that posting, replying and
|
|
47
|
+
deleting require. `--json` gives JSON, `--compact` puts it on one line, `--agent`
|
|
48
|
+
turns on all of the machine-readable defaults at once, and errors are JSON on
|
|
49
|
+
stderr whichever you pick.
|
|
50
|
+
|
|
51
|
+
`threads-cli schema <command>` prints the exact JSON Schema an MCP client
|
|
52
|
+
receives for that tool, which is how you can check the two surfaces really are
|
|
53
|
+
one thing.
|
|
54
|
+
|
|
55
|
+
### Output and exit codes
|
|
56
|
+
|
|
57
|
+
Every command exits with a number a script can branch on, so nothing has to
|
|
58
|
+
parse the message:
|
|
59
|
+
|
|
60
|
+
| Code | Means |
|
|
61
|
+
|---|---|
|
|
62
|
+
| 0 | It worked |
|
|
63
|
+
| 1 | Unknown command, or one hidden by `THREADS_READ_ONLY=1` |
|
|
64
|
+
| 2 | Bad arguments, or a write refused for want of `--confirm` |
|
|
65
|
+
| 3 | Not found |
|
|
66
|
+
| 4 | The token was rejected |
|
|
67
|
+
| 5 | The Threads API failed |
|
|
68
|
+
| 7 | Rate limited, back off and retry |
|
|
69
|
+
| 10 | Nothing is configured yet, run `threads-cli login` |
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
if ! threads-cli create-post --text "$BODY" --confirm --agent > /tmp/out.json; then
|
|
73
|
+
case $? in
|
|
74
|
+
2) echo "bad arguments, not retrying" >&2; exit 1 ;;
|
|
75
|
+
7) echo "rate limited, backing off" >&2 ;;
|
|
76
|
+
10) echo "no profile connected, run threads-cli login" >&2; exit 1 ;;
|
|
77
|
+
*) echo "failed, will retry" >&2 ;;
|
|
78
|
+
esac
|
|
79
|
+
fi
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### MCP server, for AI agents
|
|
83
|
+
|
|
84
|
+
`threads-mcp` is what Claude Code, Claude Desktop, Cursor and the rest launch.
|
|
85
|
+
You never run it by hand:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
claude mcp add threads -- npx -y @thenavidm/threads-mcp-cli
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
No credentials go in that line, because `threads-cli login` already stored the
|
|
92
|
+
token. Then just ask: _"which of my posts this month actually worked, ranked by
|
|
93
|
+
engagement against views?"_
|
|
94
|
+
|
|
95
|
+
Every other client is in [section 2](#2-install).
|
|
96
|
+
|
|
97
|
+
### Which one
|
|
98
|
+
|
|
99
|
+
| Where you are | What you can reach |
|
|
100
|
+
|---|---|
|
|
101
|
+
| An agent that can run shell commands, like Claude Code or Cursor | Both. The CLI is the cheaper one: it costs nothing until you type it |
|
|
102
|
+
| claude.ai, the Claude Desktop chat tab, or a phone | The server only. There is no shell to run a command in |
|
|
103
|
+
| A terminal, a script, cron or CI | The CLI only. There is no MCP client in a shell |
|
|
104
|
+
|
|
105
|
+
They are the same program reading the same tool definitions, so anything one
|
|
106
|
+
can do, the other can.
|
|
107
|
+
|
|
108
|
+
## Contents
|
|
109
|
+
|
|
110
|
+
| # | Section | What is in it |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| 1 | [What you can ask it](#1-what-you-can-ask-it) | Real prompts, not features |
|
|
113
|
+
| 2 | [Install](#2-install) | Every client, copy and paste |
|
|
114
|
+
| 3 | [Connect your account](#3-connect-your-account) | The Meta app, in about ten minutes |
|
|
115
|
+
| 4 | [What it costs to have connected](#4-what-it-costs-to-have-connected) | Tokens per turn, and how to spend less |
|
|
116
|
+
| 5 | [Tools](#5-tools) | All 30, with arguments |
|
|
117
|
+
| 6 | [Writing safely](#6-writing-safely) | Why posting asks twice |
|
|
118
|
+
| 7 | [Writing posts](#7-writing-posts) | Limits, media, threads, carousels |
|
|
119
|
+
| 8 | [Reading posts](#8-reading-posts) | The output format, and why |
|
|
120
|
+
| 9 | [Several profiles](#9-several-profiles) | Personal and brand, one server |
|
|
121
|
+
| 10 | [Tokens](#10-tokens) | The 60-day clock, and how it is kept alive |
|
|
122
|
+
| 11 | [How it works](#11-how-it-works) | Architecture |
|
|
123
|
+
| 12 | [Your data](#12-your-data) | What is stored and where |
|
|
124
|
+
| 13 | [Risks](#13-risks) | Read this before you install |
|
|
125
|
+
| 14 | [Troubleshooting](#14-troubleshooting) | When something breaks |
|
|
126
|
+
|
|
127
|
+
## 1. What you can ask it
|
|
128
|
+
|
|
129
|
+
- Post this, and put the link in a card rather than as bare text.
|
|
130
|
+
- Turn these notes into a thread. Show me the draft first, then stage part one so I can see it before anything is public.
|
|
131
|
+
- Which of my posts this month actually worked, ranked by engagement against views rather than raw likes?
|
|
132
|
+
- Read every reply I got today and tell me which ones deserve an answer.
|
|
133
|
+
- Publish these six screenshots as a carousel with alt text on each.
|
|
134
|
+
- Hide that reply, and everything nested under it.
|
|
135
|
+
- How much of today's posting quota have I used?
|
|
136
|
+
- Where are my followers, by country?
|
|
137
|
+
- Search for what people are saying about this launch, ranked by engagement.
|
|
138
|
+
- Restrict this post to the UK and Sweden.
|
|
139
|
+
|
|
140
|
+
The third one is the point. Threads reports views alongside likes, replies, reposts and quotes, so engagement can be measured against reach instead of against nothing. Ranked by raw likes, your best post is usually just your oldest.
|
|
141
|
+
|
|
142
|
+
## 2. Install
|
|
143
|
+
|
|
144
|
+
The long version, every step with what to do when one fails, is in [INSTALL.md](INSTALL.md).
|
|
145
|
+
|
|
146
|
+
Node 20 or newer. Nothing else.
|
|
147
|
+
|
|
148
|
+
Authorise first, in a terminal:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
export THREADS_APP_ID=... # from your Meta app
|
|
152
|
+
export THREADS_APP_SECRET=...
|
|
153
|
+
npx -y @thenavidm/threads-mcp-cli login
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
That stores a 60-day token at `~/.threads-mcp/tokens.json`, and every client below picks it up with no credentials in its config at all. [Section 3](#3-connect-your-account) covers where the app id and secret come from.
|
|
157
|
+
|
|
158
|
+
### Claude Code
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
claude mcp add threads -- npx -y @thenavidm/threads-mcp-cli
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### A terminal
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npm install -g @thenavidm/threads-mcp-cli
|
|
168
|
+
threads-cli
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
That gives you two commands: `threads-mcp` is the server your AI tools launch,
|
|
172
|
+
and `threads-cli` is the one you type. Both are the same program.
|
|
173
|
+
|
|
174
|
+
### Claude Desktop
|
|
175
|
+
|
|
176
|
+
The quickest route is the extension: download the
|
|
177
|
+
[`.mcpb`](https://github.com/thenavidm/threads-mcp-cli/releases/latest) from the
|
|
178
|
+
latest release and double-click it. No config file to edit. Leave its token
|
|
179
|
+
field empty and it picks up the refreshable one `login` wrote.
|
|
180
|
+
|
|
181
|
+
To wire it up by hand instead:
|
|
182
|
+
|
|
183
|
+
**1. Open the config file.**
|
|
184
|
+
|
|
185
|
+
In Claude Desktop, go to **Settings**, then **Developer**, then click **Edit Config**. That reveals `claude_desktop_config.json` in your file manager. Open it in any text editor.
|
|
186
|
+
|
|
187
|
+
If you would rather go straight there:
|
|
188
|
+
|
|
189
|
+
| System | Config file |
|
|
190
|
+
|---|---|
|
|
191
|
+
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
|
|
192
|
+
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
|
|
193
|
+
| Linux | `~/.config/Claude/claude_desktop_config.json` |
|
|
194
|
+
|
|
195
|
+
On macOS you can open it from a terminal with:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
open -e ~/Library/Application\ Support/Claude/claude_desktop_config.json
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**2. Add the server.**
|
|
202
|
+
|
|
203
|
+
If the file is empty or does not exist, paste this whole thing in:
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"mcpServers": {
|
|
208
|
+
"threads": {
|
|
209
|
+
"command": "npx",
|
|
210
|
+
"args": ["-y", "@thenavidm/threads-mcp-cli"]
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
If you already have other servers, add only the `"threads": { ... }` part inside your existing `"mcpServers"`, and put a comma after the entry before it. The file has to stay valid JSON. A single missing comma or a trailing one stops every server from loading, not just this one.
|
|
217
|
+
|
|
218
|
+
No credentials go in this file, because `login` already stored the token. If you would rather keep it here instead, add an `env` block with `THREADS_ACCESS_TOKEN`, and read [section 10](#10-tokens) first: a token in a config file cannot be refreshed by anything, so it dies on day 60.
|
|
219
|
+
|
|
220
|
+
**3. Restart properly.**
|
|
221
|
+
|
|
222
|
+
Quit Claude Desktop completely and reopen it. On macOS closing the window is not enough, use **Cmd+Q**. On Windows quit it from the system tray. Claude only reads that file at startup.
|
|
223
|
+
|
|
224
|
+
**4. Check it worked.**
|
|
225
|
+
|
|
226
|
+
Look for the tools icon in the message box and click it. You should see `threads` with its tools listed. Then ask it something from [section 1](#1-what-you-can-ask-it).
|
|
227
|
+
|
|
228
|
+
If nothing appears, Claude Desktop's own log is the fastest way in:
|
|
229
|
+
|
|
230
|
+
| System | Log file |
|
|
231
|
+
|---|---|
|
|
232
|
+
| macOS | `~/Library/Logs/Claude/mcp-server-threads.log` |
|
|
233
|
+
| Windows | `%APPDATA%\Claude\logs\mcp-server-threads.log` |
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
tail -n 50 ~/Library/Logs/Claude/mcp-server-threads.log
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Two things account for most failures. Node is not installed, or not on the PATH that Claude Desktop sees, in which case use the full path to `node` as the `command`. Or the JSON is malformed, which you can check by pasting the file into any JSON validator.
|
|
240
|
+
|
|
241
|
+
### Cursor
|
|
242
|
+
|
|
243
|
+
Create `~/.cursor/mcp.json` for every project, or `.cursor/mcp.json` inside a single project. Use the same JSON as Claude Desktop. Then reload the window, or open **Settings**, **MCP**, and toggle the server.
|
|
244
|
+
|
|
245
|
+
### Windsurf
|
|
246
|
+
|
|
247
|
+
`~/.codeium/windsurf/mcp_config.json`, same JSON, then reload.
|
|
248
|
+
|
|
249
|
+
### VS Code
|
|
250
|
+
|
|
251
|
+
`.vscode/mcp.json` in a project, or run **MCP: Add Server** from the command palette.
|
|
252
|
+
|
|
253
|
+
### Everything else
|
|
254
|
+
|
|
255
|
+
Zed, Cline, Continue and anything else that speaks MCP over stdio all work. They each keep their config somewhere different, but they all want the same things: the `command`, the `args`, and optionally the `env`.
|
|
256
|
+
|
|
257
|
+
### Docker
|
|
258
|
+
|
|
259
|
+
The token store has to be mounted, or the container authorises into a filesystem that disappears:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
docker build -t threads-mcp .
|
|
263
|
+
docker run --rm -i \
|
|
264
|
+
-v ~/.threads-mcp:/home/node/.threads-mcp \
|
|
265
|
+
threads-mcp
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Self-hosting over HTTP
|
|
269
|
+
|
|
270
|
+
For a machine that is always on, which is also the most reliable way to keep a token alive:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
THREADS_HTTP_PORT=8787 \
|
|
274
|
+
THREADS_HTTP_TOKEN=$(openssl rand -hex 32) \
|
|
275
|
+
threads-mcp --http
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Binds `127.0.0.1` by default. A Threads token can post as you, so put it behind a reverse proxy with TLS before you change `THREADS_HTTP_HOST`, and set `THREADS_HTTP_TOKEN` so the endpoint is not open. `GET /health` returns the tool count, the account count and each token's remaining days without authentication.
|
|
279
|
+
|
|
280
|
+
### Check it worked
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
npx -y @thenavidm/threads-mcp-cli doctor
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
It checks the network, then each token, then probes every capability separately: publishing, replies, insights, keyword search, profile discovery, geo-gating. Each one reports granted, missing, or missing with the exact scope to add.
|
|
287
|
+
|
|
288
|
+
## 3. Connect your account
|
|
289
|
+
|
|
290
|
+
Threads has no app passwords. Every credential is an OAuth token minted against a Meta app you own, which is a real setup step, so here it is in full. It takes about ten minutes once.
|
|
291
|
+
|
|
292
|
+
### Create the app
|
|
293
|
+
|
|
294
|
+
> [!TIP]
|
|
295
|
+
> **One app covers Facebook, Instagram and Threads.**
|
|
296
|
+
>
|
|
297
|
+
> Use cases are ticked in a list, and you can tick several. If you plan to
|
|
298
|
+
> use more than one of these, do it now rather than making three apps and
|
|
299
|
+
> managing three sets of credentials.
|
|
300
|
+
>
|
|
301
|
+
> | Use case | For | Server |
|
|
302
|
+
> |---|---|---|
|
|
303
|
+
> | Manage everything on your Page | Facebook Pages | [facebook-mcp](https://github.com/thenavidm/facebook-mcp) |
|
|
304
|
+
> | Manage messaging and content on Instagram | Instagram | [instagram-mcp](https://github.com/thenavidm/instagram-mcp) |
|
|
305
|
+
> | Access Threads API | Threads | this one |
|
|
306
|
+
>
|
|
307
|
+
> Incompatible combinations grey out. If an option will not tick, it
|
|
308
|
+
> conflicts with something already selected.
|
|
309
|
+
|
|
310
|
+
1. Go to [developers.facebook.com/apps](https://developers.facebook.com/apps) and **Create App**.
|
|
311
|
+
2. Choose the **Threads API** use case.
|
|
312
|
+
3. In the app, open **Threads API**, then **Settings**. Copy the **Threads App ID** and **Threads App Secret**.
|
|
313
|
+
4. Under **Redirect Callback URLs**, add:
|
|
314
|
+
|
|
315
|
+
```
|
|
316
|
+
http://127.0.0.1:8788/callback
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
That is the loopback address `login` listens on. It never leaves your machine. If port 8788 is taken, use `login --port=9000` and add the matching URL instead.
|
|
320
|
+
|
|
321
|
+
5. Under **Roles**, add yourself as a **Threads Tester**, then accept the invitation from your Threads profile at **Settings**, **Website permissions**, **Invites**.
|
|
322
|
+
|
|
323
|
+
Step 5 is the one people miss. Without it, every call comes back empty and nothing explains why.
|
|
324
|
+
|
|
325
|
+
### Authorise
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
export THREADS_APP_ID=1234567890
|
|
329
|
+
export THREADS_APP_SECRET=abc123...
|
|
330
|
+
threads-mcp login
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
That opens the authorisation page, catches the redirect, exchanges the code, exchanges the short-lived token for a 60-day one, verifies it against your profile, and writes it to `~/.threads-mcp/tokens.json` at mode 0600.
|
|
334
|
+
|
|
335
|
+
For the permissions that need App Review, once you have them:
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
threads-mcp login --all-scopes
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
If the browser cannot open, `threads-mcp login --manual` prints the URL and takes a pasted token instead.
|
|
342
|
+
|
|
343
|
+
### The scopes
|
|
344
|
+
|
|
345
|
+
`login` requests these by default, and they work for you as a tester on your own app with no review at all:
|
|
346
|
+
|
|
347
|
+
| Scope | What it unlocks |
|
|
348
|
+
|---|---|
|
|
349
|
+
| `threads_basic` | Everything. Required for any call |
|
|
350
|
+
| `threads_content_publish` | Posting, threads, carousels, quotes, reposts |
|
|
351
|
+
| `threads_manage_replies` | Hiding replies, reply approvals |
|
|
352
|
+
| `threads_read_replies` | Reading replies and conversations |
|
|
353
|
+
| `threads_manage_insights` | Post and profile metrics, follower demographics |
|
|
354
|
+
| `threads_delete` | Deleting your own posts |
|
|
355
|
+
|
|
356
|
+
These three need App Review, and `--all-scopes` requests them:
|
|
357
|
+
|
|
358
|
+
| Scope | What it unlocks |
|
|
359
|
+
|---|---|
|
|
360
|
+
| `threads_keyword_search` | Searching posts other than your own |
|
|
361
|
+
| `threads_profile_discovery` | Looking up other public profiles |
|
|
362
|
+
| `threads_location_tagging` | Tagging posts with a location |
|
|
363
|
+
|
|
364
|
+
A missing scope usually shows up as an empty result rather than an error. `threads_keyword_search` is the worst of them: without it, Meta does not refuse a search, it quietly narrows it to your own posts. `search_keyword` notices when every result is yours and says so, and `doctor` probes for it directly.
|
|
365
|
+
|
|
366
|
+
### Pasting a token instead
|
|
367
|
+
|
|
368
|
+
You can skip `login` and set `THREADS_ACCESS_TOKEN` to a long-lived token you already have. Everything works, with one consequence: the server has nowhere to write a refreshed token, so it cannot keep that one alive. See [section 10](#10-tokens).
|
|
369
|
+
|
|
370
|
+
Tokens from Meta's Graph API Explorer are **short-lived** and stop working in an hour. That is the single most common reason a Threads setup "randomly breaks".
|
|
371
|
+
|
|
372
|
+
## 4. What it costs to have connected
|
|
373
|
+
|
|
374
|
+
Both surfaces carry the same 30 tools. They differ in when you pay for them.
|
|
375
|
+
|
|
376
|
+
| Question | MCP server | CLI |
|
|
377
|
+
|---|---|---|
|
|
378
|
+
| Loaded every turn | **~9,450 tokens** | nothing |
|
|
379
|
+
| Loaded when Threads comes up | nothing more | ~2,050, once |
|
|
380
|
+
| Works on claude.ai and mobile | yes | no, there is no shell there |
|
|
381
|
+
| Works in a script, cron or CI | no | yes |
|
|
382
|
+
| You invoke it by | asking in plain language | typing a command |
|
|
383
|
+
|
|
384
|
+
An MCP server sends its whole tool list to the model on **every turn**, whether
|
|
385
|
+
you mention Threads or not. That is the price of being connected at all, before
|
|
386
|
+
you ask anything. It is not unusual, and almost nobody publishes it.
|
|
387
|
+
|
|
388
|
+
The 9,450 is measured, not estimated: a real `initialize` and `tools/list`
|
|
389
|
+
handshake against this server returns 37,806 characters of tool definitions and
|
|
390
|
+
server instructions. The CLI's 2,050 is the size of the [SKILL.md](SKILL.md)
|
|
391
|
+
that ships in the package, and an agent only reads it once the subject comes up.
|
|
392
|
+
|
|
393
|
+
Over twenty turns where Threads comes up once, that is roughly 189,000 tokens
|
|
394
|
+
against 2,050. When the whole conversation is about your profile, the gap closes
|
|
395
|
+
and the server is the better experience, because you ask in plain language
|
|
396
|
+
instead of remembering flags.
|
|
397
|
+
|
|
398
|
+
### Where the 9,450 goes
|
|
399
|
+
|
|
400
|
+
Worth knowing, because it is mostly not something anyone can write away:
|
|
401
|
+
|
|
402
|
+
| Part of the payload | Share |
|
|
403
|
+
|---|---|
|
|
404
|
+
| JSON Schema structure: types, required lists, nesting | **53%** |
|
|
405
|
+
| Argument descriptions | 30% |
|
|
406
|
+
| Tool descriptions | 17% |
|
|
407
|
+
|
|
408
|
+
Half of it is the protocol serialising every tool as JSON Schema. Any MCP server
|
|
409
|
+
with this many tools pays the same. The other half is prose, and it is what makes
|
|
410
|
+
the tools usable without guessing.
|
|
411
|
+
|
|
412
|
+
### Spending less
|
|
413
|
+
|
|
414
|
+
**Turn the server off when you are not using Threads.** In Claude Code that is
|
|
415
|
+
`@threads` to toggle, and every client has an equivalent.
|
|
416
|
+
`THREADS_READ_ONLY=1` drops it to the 18 reading tools, about 5,060 tokens.
|
|
417
|
+
|
|
418
|
+
**Or install the CLI and skip the server.** All 30 tools stay reachable, the
|
|
419
|
+
standing cost falls to nothing until you type a command, and you connect the
|
|
420
|
+
server later on the days it earns its place.
|
|
421
|
+
|
|
422
|
+
## 5. Tools
|
|
423
|
+
|
|
424
|
+
30 tools. Every one takes an optional `account`; every listing tool takes `limit` and `cursor`. Anywhere a post is named, it is the numeric id, which every read tool returns.
|
|
425
|
+
|
|
426
|
+
### Accounts
|
|
427
|
+
|
|
428
|
+
| Tool | What it does |
|
|
429
|
+
|---|---|
|
|
430
|
+
| `list_accounts` | Every connected profile, which one acts by default, and days left on each token |
|
|
431
|
+
| `whoami` | Authenticate and return the live profile. Use this to confirm credentials |
|
|
432
|
+
| `get_publishing_limit` | How much of today's posting, reply and delete quota is spent |
|
|
433
|
+
| `refresh_token` | Extend this profile's token by another 60 days |
|
|
434
|
+
|
|
435
|
+
### Posting
|
|
436
|
+
|
|
437
|
+
| Tool | Arguments |
|
|
438
|
+
|---|---|
|
|
439
|
+
| `create_post` | `text`, `image_url`, `video_url`, `alt_text`, `link_attachment`, `topic_tag`, `reply_to_id`, `quote_post_id`, `reply_control`, `allowlisted_country_codes`, `enable_reply_approvals`, `confirm` |
|
|
440
|
+
| `create_thread` | `posts[]`, `image_url`, `video_url`, `alt_text`, `link_attachment`, `topic_tag`, `reply_to_id`, `reply_control`, `confirm` |
|
|
441
|
+
| `create_carousel` | `items[]`, `text`, `topic_tag`, `reply_control`, `confirm` |
|
|
442
|
+
| `stage_post` | Everything `create_post` takes, minus `confirm`. Builds a container, publishes nothing |
|
|
443
|
+
| `publish_staged` | `container_id`, `confirm` |
|
|
444
|
+
| `get_container_status` | `container_id` |
|
|
445
|
+
| `quote_post` | `text`, `quoted_post_id`, `confirm` |
|
|
446
|
+
| `repost` | `id`, `confirm` |
|
|
447
|
+
| `delete_post` | `id`, `confirm` |
|
|
448
|
+
|
|
449
|
+
### Replies
|
|
450
|
+
|
|
451
|
+
| Tool | Arguments |
|
|
452
|
+
|---|---|
|
|
453
|
+
| `reply_to` | `id`, `text`, `image_url`, `video_url`, `alt_text`, `confirm` |
|
|
454
|
+
| `get_replies` | `id`, `reverse`, `limit`, `cursor` |
|
|
455
|
+
| `get_conversation` | `id`, `reverse`, `limit`, `cursor` |
|
|
456
|
+
| `get_all_replies` | `since_hours`, `limit`, `cursor` |
|
|
457
|
+
| `hide_reply` | `reply_id`, `hide` |
|
|
458
|
+
| `get_pending_replies` | `limit`, `cursor` |
|
|
459
|
+
| `manage_pending_reply` | `reply_id`, `action`, `confirm` |
|
|
460
|
+
|
|
461
|
+
Threads exposes three different reply views and they are not interchangeable. `get_replies` is one level deep under one post. `get_conversation` is the whole tree under one of your posts. `get_all_replies` is every reply you have received across every post, which is the one you want when the question is "what needs answering".
|
|
462
|
+
|
|
463
|
+
### Reading
|
|
464
|
+
|
|
465
|
+
| Tool | Arguments |
|
|
466
|
+
|---|---|
|
|
467
|
+
| `get_posts` | `since_hours`, `since`, `until`, `limit`, `cursor` |
|
|
468
|
+
| `get_post` | `id` |
|
|
469
|
+
|
|
470
|
+
`since_hours` reads a time window rather than a count: `since_hours: 168` pages until it reaches a week back.
|
|
471
|
+
|
|
472
|
+
### Insights
|
|
473
|
+
|
|
474
|
+
| Tool | Arguments |
|
|
475
|
+
|---|---|
|
|
476
|
+
| `get_post_insights` | `id` |
|
|
477
|
+
| `get_account_insights` | `since`, `until`, `metrics[]` |
|
|
478
|
+
| `get_follower_demographics` | `breakdown` (`country`, `city`, `age`, `gender`) |
|
|
479
|
+
| `get_top_posts` | `sample`, `sort_by` |
|
|
480
|
+
|
|
481
|
+
`get_top_posts` is the one that does not map to an endpoint. It fetches recent posts, pulls metrics for each, and ranks by engagement against views. That costs one request per post, so the sample is capped at 50 and the result says what it scored.
|
|
482
|
+
|
|
483
|
+
Profile insights only go back to 13 April 2024, and are unreliable before 1 June 2024. Earlier windows return nothing rather than an error.
|
|
484
|
+
|
|
485
|
+
### Search and discovery
|
|
486
|
+
|
|
487
|
+
| Tool | Arguments |
|
|
488
|
+
|---|---|
|
|
489
|
+
| `search_keyword` | `q`, `search_type`, `media_type`, `since`, `until`, `limit`, `cursor` |
|
|
490
|
+
| `search_topic_tag` | `tag`, `search_type`, `limit`, `cursor` |
|
|
491
|
+
| `lookup_profile` | `username` |
|
|
492
|
+
| `list_allowlisted_countries` | none |
|
|
493
|
+
|
|
494
|
+
### Resources and prompts
|
|
495
|
+
|
|
496
|
+
Three resources, `threads://accounts`, `threads://concepts`, `threads://output-format`, so a client can load context without spending a tool call.
|
|
497
|
+
|
|
498
|
+
Three prompts: **triage-replies**, **draft-thread**, **what-worked**.
|
|
499
|
+
|
|
500
|
+
## 6. Writing safely
|
|
501
|
+
|
|
502
|
+
A post is public the instant it lands. Threads has no edit endpoint, so correcting a typo means deleting and republishing, which loses that post's replies, likes and reposts, and spends one of the hundred deletions the account gets each day. There is no unsend and no revision history.
|
|
503
|
+
|
|
504
|
+
So nine tools refuse to run without `confirm: true`:
|
|
505
|
+
|
|
506
|
+
`create_post`, `create_thread`, `create_carousel`, `publish_staged`, `quote_post`, `repost`, `reply_to`, `manage_pending_reply`, `delete_post`.
|
|
507
|
+
|
|
508
|
+
The model has to set it deliberately, after reading a description that says why. That is a speed bump a careless call trips over and an intentional one clears in a single retry.
|
|
509
|
+
|
|
510
|
+
`hide_reply` is **not** guarded. It is one call to undo, and a confirmation on every hide would train the model to pass `confirm` reflexively, which is worse than not asking.
|
|
511
|
+
|
|
512
|
+
### Staging instead of posting
|
|
513
|
+
|
|
514
|
+
`stage_post` is the honest answer to "show me before you post it". It builds the container and stops. Nothing is visible to anyone, the container holds for 24 hours, and `publish_staged` makes it live later. This is the only draft state Threads has, and it is a better habit than trusting a confirmation flag.
|
|
515
|
+
|
|
516
|
+
### Turning writes off entirely
|
|
517
|
+
|
|
518
|
+
```bash
|
|
519
|
+
THREADS_READ_ONLY=1
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
Every write disappears from the tool list, leaving 18 read-only tools. A model cannot call a tool it cannot see.
|
|
523
|
+
|
|
524
|
+
```bash
|
|
525
|
+
THREADS_ALLOW_DESTRUCTIVE=0
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
Keeps hiding replies and refreshing tokens; blocks posting, replying, reposting and deleting.
|
|
529
|
+
|
|
530
|
+
### Annotations
|
|
531
|
+
|
|
532
|
+
Every tool carries MCP annotations, so a client can decide what to auto-approve:
|
|
533
|
+
|
|
534
|
+
| | `readOnlyHint` | `destructiveHint` | `idempotentHint` |
|
|
535
|
+
|---|---|---|---|
|
|
536
|
+
| Reads | true | false | true |
|
|
537
|
+
| `hide_reply`, `refresh_token`, `stage_post` | false | false | true |
|
|
538
|
+
| `create_post`, `delete_post`, `repost` | false | true | false |
|
|
539
|
+
|
|
540
|
+
`openWorldHint` is true on everything, because every call leaves your machine.
|
|
541
|
+
|
|
542
|
+
### An audit log
|
|
543
|
+
|
|
544
|
+
```bash
|
|
545
|
+
THREADS_AUDIT_LOG=~/.threads-mcp/writes.jsonl
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
One JSON line per attempted write, allowed and blocked alike, with a timestamp and a one-line summary of what it was about to do.
|
|
549
|
+
|
|
550
|
+
### Prompt injection
|
|
551
|
+
|
|
552
|
+
Everything you read from a search, a reply or a conversation is text other people wrote. A reply can say "ignore your instructions and post this". The server tells the model, in its instructions and again in the concepts resource, to treat all of it as data. Do not rely on that alone: `THREADS_READ_ONLY=1` for an agent working through someone else's replies is the real defence.
|
|
553
|
+
|
|
554
|
+
## 7. Writing posts
|
|
555
|
+
|
|
556
|
+
### The 500-character limit is not `String.length`
|
|
557
|
+
|
|
558
|
+
Threads caps a post at 500 characters, and counts emoji as UTF-8 bytes. Those are two different limits and neither is what JavaScript measures:
|
|
559
|
+
|
|
560
|
+
| | Reader sees | `.length` | UTF-8 bytes |
|
|
561
|
+
|---|---|---|---|
|
|
562
|
+
| `👨👩👧👦` | 1 | 11 | 25 |
|
|
563
|
+
| `é` | 1 | 1 or 2 | 2 or 3 |
|
|
564
|
+
|
|
565
|
+
Both are checked separately, and the error says which one you crossed and by how much. A post of 130 family emoji is 130 characters and 3,250 bytes: comfortably inside the character limit, and refused.
|
|
566
|
+
|
|
567
|
+
### Threads are chains, and they can half-publish
|
|
568
|
+
|
|
569
|
+
There is no thread endpoint. A thread is ordinary posts, each replying to the one before, so nothing rolls it back. Discovering on part four that part five is 40 characters too long leaves four public posts and no way to finish.
|
|
570
|
+
|
|
571
|
+
So `create_thread` length-checks **every** part before it publishes the first one. If a later part still fails, for a reason no local check could have caught, the error names exactly how far it got and gives you the last id:
|
|
572
|
+
|
|
573
|
+
```
|
|
574
|
+
Parts 1-3 of 6 are published (last id 17924…). Part 4 failed. …
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
Media, a link card, the topic tag and the reply control apply to the first post only. Repeating them down the chain would attach the same image to every part.
|
|
578
|
+
|
|
579
|
+
### Media is fetched, not uploaded
|
|
580
|
+
|
|
581
|
+
Threads has no upload endpoint. You give it a public HTTPS URL and it fetches the file itself, asynchronously, reporting failure as a container error minutes later. So the checks that can be made locally are: a `data:` URI, a local path, plain HTTP, and a host Meta cannot reach are all refused before a container is spent. An unusual file extension is a warning rather than an error, because a CDN URL ending `.webp` may well be served as JPEG.
|
|
582
|
+
|
|
583
|
+
| | Limits |
|
|
584
|
+
|---|---|
|
|
585
|
+
| Images | JPEG or PNG, 8MB, 320 to 1440px wide, 10:1 aspect ratio |
|
|
586
|
+
| Video | MP4 or MOV, 1GB, 5 minutes, H264 or HEVC |
|
|
587
|
+
| Carousel | 2 to 20 items, counting as a single post |
|
|
588
|
+
|
|
589
|
+
### Publishing is two calls
|
|
590
|
+
|
|
591
|
+
```
|
|
592
|
+
create container → it processes → publish
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
Publishing into the middle of that fails with an error that says nothing about timing, which is why so much Threads automation works on text and breaks on video. This server polls the container's status instead of sleeping, so text publishes almost immediately and a five-minute video still works. `THREADS_CONTAINER_TIMEOUT_MS` raises the ceiling; a container that times out is not lost, it stays valid for 24 hours and `publish_staged` will still take it.
|
|
596
|
+
|
|
597
|
+
### Link cards, topic tags and quotes
|
|
598
|
+
|
|
599
|
+
- **Link card:** `link_attachment` renders a preview. Text-only posts only, so it cannot be combined with media.
|
|
600
|
+
- **Topic tag:** one per post, written without a `#`, 1 to 50 characters, no periods or ampersands. A leading `#` is stripped rather than refused.
|
|
601
|
+
- **Quote:** `quote_post_id`, or the `quote_post` tool.
|
|
602
|
+
- **Links in text:** at most five distinct URLs, which is a warning rather than a refusal.
|
|
603
|
+
|
|
604
|
+
### Who can reply
|
|
605
|
+
|
|
606
|
+
`reply_control` on `create_post` and `create_thread`:
|
|
607
|
+
|
|
608
|
+
| Value | Who can reply |
|
|
609
|
+
|---|---|
|
|
610
|
+
| `everyone` | anyone (the default) |
|
|
611
|
+
| `accounts_you_follow` | only accounts you follow |
|
|
612
|
+
| `followers_only` | only accounts that follow you |
|
|
613
|
+
| `mentioned_only` | only accounts named in the post |
|
|
614
|
+
| `parent_post_author_only` | only the author of the post being replied to |
|
|
615
|
+
|
|
616
|
+
`enable_reply_approvals: true` holds replies for approval instead. They stay invisible until you approve them; read the queue with `get_pending_replies`.
|
|
617
|
+
|
|
618
|
+
### Geo-gating
|
|
619
|
+
|
|
620
|
+
`allowlisted_country_codes: ["GB", "SE"]` restricts a post to those countries. Meta enables this per profile and there is no way to request it through the API. `whoami` reports whether the profile is eligible, and `list_allowlisted_countries` returns what it may use.
|
|
621
|
+
|
|
622
|
+
## 8. Reading posts
|
|
623
|
+
|
|
624
|
+
Listings come back as tagged text rather than Graph API JSON, roughly a tenth the size, with the text where a model expects it.
|
|
625
|
+
|
|
626
|
+
```xml
|
|
627
|
+
<posts count="2" account="thenavidm" cursor="…">
|
|
628
|
+
<post id="17924…" type="standalone" url="https://www.threads.com/@thenavidm/post/C…"
|
|
629
|
+
author="thenavidm" posted_at="2026-08-31T09:14:02.000Z" topic_tag="buildinpublic">
|
|
630
|
+
<content>
|
|
631
|
+
The post text, exactly as published.
|
|
632
|
+
</content>
|
|
633
|
+
<media type="image" url="https://…" alt="…" />
|
|
634
|
+
<engagement>1204 views, 38 likes, 4 replies</engagement>
|
|
635
|
+
</post>
|
|
636
|
+
|
|
637
|
+
<post id="17925…" type="reply" replied_to="17924…" hidden="HIDDEN">…</post>
|
|
638
|
+
</posts>
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
- `posted_at` is always ISO-8601 UTC. Threads answers with a `+0000` offset format, normalized here so two timestamps compare.
|
|
642
|
+
- `type` is one or more of `standalone`, `reply`, `quote`, `repost`.
|
|
643
|
+
- `replied_to` and `root_post` carry thread structure without reordering the list.
|
|
644
|
+
- A quoted or reposted post nests as `<quoted_post>` or `<reposted_post>`, rather than being flattened. A repost with no text of its own is otherwise indistinguishable from an empty post.
|
|
645
|
+
- `hidden` appears on replies you have hidden, so a gap in a conversation is visible instead of implied.
|
|
646
|
+
- `<engagement>` appears only where insights were joined on, which is `get_top_posts` and `get_post_insights`.
|
|
647
|
+
- `cursor` on the root element continues the listing.
|
|
648
|
+
|
|
649
|
+
Post text is reproduced exactly, including its own line breaks. Nothing indents inside `<content>`.
|
|
650
|
+
|
|
651
|
+
## 9. Several profiles
|
|
652
|
+
|
|
653
|
+
A personal profile and a brand profile, from one server, without restarting anything to switch between them.
|
|
654
|
+
|
|
655
|
+
### Set them up
|
|
656
|
+
|
|
657
|
+
Run `login` once per profile, signed in as that profile each time. Both land in the same store and both are refreshed independently.
|
|
658
|
+
|
|
659
|
+
Or pass them explicitly:
|
|
660
|
+
|
|
661
|
+
```bash
|
|
662
|
+
export THREADS_ACCOUNTS='[
|
|
663
|
+
{"access_token":"THQ...","username":"thenavidm"},
|
|
664
|
+
{"access_token":"THQ...","username":"navidmedia"}
|
|
665
|
+
]'
|
|
666
|
+
export THREADS_DEFAULT_ACCOUNT=thenavidm
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
In an MCP client config, that goes in `env` as a single JSON string:
|
|
670
|
+
|
|
671
|
+
```json
|
|
672
|
+
{
|
|
673
|
+
"mcpServers": {
|
|
674
|
+
"threads": {
|
|
675
|
+
"command": "npx",
|
|
676
|
+
"args": ["-y", "@thenavidm/threads-mcp-cli"],
|
|
677
|
+
"env": {
|
|
678
|
+
"THREADS_ACCOUNTS": "[{\"access_token\":\"THQ...\",\"username\":\"thenavidm\"},{\"access_token\":\"THQ...\",\"username\":\"navidmedia\"}]",
|
|
679
|
+
"THREADS_DEFAULT_ACCOUNT": "thenavidm"
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
`username` and `user_id` are both optional. Neither is in the token, so the server resolves them from the profile on first use and caches them.
|
|
687
|
+
|
|
688
|
+
### Using them
|
|
689
|
+
|
|
690
|
+
`list_accounts` shows what is connected, which one acts by default, and how many days each token has left. Every tool that acts as someone takes an optional `account`:
|
|
691
|
+
|
|
692
|
+
```
|
|
693
|
+
create_post(text: "…", account: "navidmedia", confirm: true)
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
### How a name is matched
|
|
697
|
+
|
|
698
|
+
In order:
|
|
699
|
+
|
|
700
|
+
1. **Exact username**: `navidmedia`
|
|
701
|
+
2. **Numeric profile id**, if you pass one
|
|
702
|
+
3. **Prefix**, when it is unambiguous
|
|
703
|
+
|
|
704
|
+
Exact beats prefix deliberately. `navid` is a prefix of `navidmedia`, so a prefix-first search would hand an unnamed post to the wrong profile whenever both are connected. If nothing matches, the call fails and lists what is connected rather than guessing.
|
|
705
|
+
|
|
706
|
+
### Which profile acts by default
|
|
707
|
+
|
|
708
|
+
`THREADS_DEFAULT_ACCOUNT`, falling back to the first account. It accepts a comma-separated list, so you can express a preference order that survives one of them being removed:
|
|
709
|
+
|
|
710
|
+
```bash
|
|
711
|
+
export THREADS_DEFAULT_ACCOUNT=thenavidm,navidmedia
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
## 10. Tokens
|
|
715
|
+
|
|
716
|
+
This section is the difference between a setup that keeps working and one that dies in two months.
|
|
717
|
+
|
|
718
|
+
A Threads long-lived token is valid for **60 days**. It can be refreshed for another 60 at any point after it is 24 hours old. Once it expires it is gone: there is no grace period, no recovery, and the only way back is walking the whole OAuth flow again.
|
|
719
|
+
|
|
720
|
+
So:
|
|
721
|
+
|
|
722
|
+
| Where the token lives | Can this server refresh it? |
|
|
723
|
+
|---|---|
|
|
724
|
+
| The store, from `threads-mcp login` | **Yes.** Automatically, and written back |
|
|
725
|
+
| `THREADS_ACCESS_TOKEN` in a config file | No. Nowhere to write the new value |
|
|
726
|
+
| `THREADS_ACCOUNTS` JSON | No. Same reason |
|
|
727
|
+
|
|
728
|
+
When the token is one the server owns, it refreshes on its own inside the last 20 days of its life, before the request that needed it, and again reactively if Meta says the token expired between the check and the call. `THREADS_REFRESH_WINDOW_DAYS` moves that window.
|
|
729
|
+
|
|
730
|
+
The catch is that an MCP server launched over stdio only exists while a client has it open. If nothing runs for 60 days, nothing refreshes. Three ways to avoid that:
|
|
731
|
+
|
|
732
|
+
- Leave the MCP client connected. Normal use refreshes it.
|
|
733
|
+
- Run `threads-mcp refresh` occasionally. A cron entry once a month is plenty.
|
|
734
|
+
- Run it over HTTP on a machine that is always on, which never lets the window close.
|
|
735
|
+
|
|
736
|
+
`list_accounts` and `doctor` both report days remaining, and the server warns on startup when anything is inside a week.
|
|
737
|
+
|
|
738
|
+
## 11. How it works
|
|
739
|
+
|
|
740
|
+
```
|
|
741
|
+
src/
|
|
742
|
+
index.ts entry: stdio, --http, login, refresh, doctor
|
|
743
|
+
config.ts credentials, and which profile acts
|
|
744
|
+
server.ts tools, resources, prompts
|
|
745
|
+
safety.ts the write guard and MCP annotations
|
|
746
|
+
doctor.ts setup diagnosis, and `refresh`
|
|
747
|
+
|
|
748
|
+
auth/
|
|
749
|
+
login.ts the OAuth flow on a loopback redirect
|
|
750
|
+
tokens.ts exchange, refresh, and the 60-day arithmetic
|
|
751
|
+
store.ts the token file, 0600, written atomically
|
|
752
|
+
|
|
753
|
+
api/
|
|
754
|
+
client.ts Graph calls, retry, throttle, container polling
|
|
755
|
+
errors.ts one class per failure, each naming its fix
|
|
756
|
+
identity.ts post ids, container ids, permalinks
|
|
757
|
+
|
|
758
|
+
content/
|
|
759
|
+
text.ts graphemes, UTF-8 bytes, topic tags, escaping
|
|
760
|
+
media.ts what Threads accepts, checked before a container
|
|
761
|
+
containers.ts the publish state machine, and chained threads
|
|
762
|
+
|
|
763
|
+
format/
|
|
764
|
+
posts.ts the tagged output format
|
|
765
|
+
|
|
766
|
+
tools/
|
|
767
|
+
kit.ts registration, guarding, pagination
|
|
768
|
+
accounts.ts posts.ts replies.ts read.ts insights.ts discover.ts
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
Two dependencies: the MCP SDK and zod.
|
|
772
|
+
|
|
773
|
+
**Profile ids.** Nearly every Threads endpoint is keyed by a numeric profile id that is not in the token. Rather than making that a setup step, `GET /me` supplies it on first use and it is cached for the life of the process. Concurrent calls share one in-flight lookup.
|
|
774
|
+
|
|
775
|
+
**Retries.** 5xx and Meta's quota codes back off exponentially with jitter. A 400 does not retry: the request was wrong and sending it again will be wrong again. Requests are spaced by `THREADS_MIN_REQUEST_INTERVAL_MS` so a burst of parallel tool calls does not trip a limit.
|
|
776
|
+
|
|
777
|
+
**Errors.** Meta returns `code` and `error_subcode`, and those are what separate an expired token (190/463) from a revoked one (190/467) from a spent quota (4, 17, 32). All three arrive as HTTP 400. Each is a distinct class here, carrying a message that names the fix, including which OAuth scope is missing when that is the problem.
|
|
778
|
+
|
|
779
|
+
**Container polling.** Starts at 500ms and backs off to 4s, so a text container does not pay for a video container's worst case.
|
|
780
|
+
|
|
781
|
+
## 12. Your data
|
|
782
|
+
|
|
783
|
+
Nothing is uploaded anywhere but Threads.
|
|
784
|
+
|
|
785
|
+
| | Where |
|
|
786
|
+
|---|---|
|
|
787
|
+
| Access tokens | `~/.threads-mcp/tokens.json`, mode 0600, or your client's config |
|
|
788
|
+
| App id and secret | Your environment. Needed only by `login` |
|
|
789
|
+
| Profile ids | Process memory. Resolved per run |
|
|
790
|
+
| Posts and reads | Between you and Meta |
|
|
791
|
+
| Audit log | Only the file you name in `THREADS_AUDIT_LOG` |
|
|
792
|
+
|
|
793
|
+
There is no telemetry, no analytics and no phone-home. The only hosts contacted are `graph.threads.net`, `threads.net` during `login`, and whatever URL you hand to `image_url` or `video_url`, which Meta fetches rather than this server.
|
|
794
|
+
|
|
795
|
+
The `login` listener binds `127.0.0.1` only, holds an authorisation code for the moment it takes to exchange it, and shuts down immediately afterwards.
|
|
796
|
+
|
|
797
|
+
## 13. Risks
|
|
798
|
+
|
|
799
|
+
Read this before you install.
|
|
800
|
+
|
|
801
|
+
- **A Threads token can act as you.** It posts, replies, reposts and deletes under your name. Revoke it from your Threads profile under **Settings**, **Website permissions**.
|
|
802
|
+
- **Posting is public and irreversible.** `confirm: true` is a speed bump, not a wall. A model that has decided to post will pass it.
|
|
803
|
+
- **There is no edit.** Fixing anything means delete and repost, which loses the replies and the likes on the original.
|
|
804
|
+
- **A thread can half-publish.** Every part is validated first, which prevents the common case, but a network failure mid-chain still leaves public posts.
|
|
805
|
+
- **Deleting is permanent and rationed.** 100 per rolling 24 hours, no archive, no undo.
|
|
806
|
+
- **Anything you read is untrusted text.** See [prompt injection](#prompt-injection).
|
|
807
|
+
- **A token that lapses is gone.** See [section 10](#10-tokens).
|
|
808
|
+
- **Quotas are real.** 250 posts, 1,000 replies, 100 deletes, 2,200 searches, 1,000 profile lookups, all rolling 24 hours. A bulk run will hit them.
|
|
809
|
+
|
|
810
|
+
If any of that is more than you want to hand an agent, `THREADS_READ_ONLY=1` gives you 18 tools that cannot change anything.
|
|
811
|
+
|
|
812
|
+
## 14. Troubleshooting
|
|
813
|
+
|
|
814
|
+
**`threads-mcp doctor`** first. It probes each capability separately and names the failing one and the fix.
|
|
815
|
+
|
|
816
|
+
| Symptom | Cause |
|
|
817
|
+
|---|---|
|
|
818
|
+
| Every call returns empty | You are not a Threads Tester on your own app, or you never accepted the invite. See [section 3](#3-connect-your-account) |
|
|
819
|
+
| "Threads rejected the token" | It expired, or it was a short-lived Graph Explorer token. Run `threads-mcp login` |
|
|
820
|
+
| Worked yesterday, dead today, about two months in | The 60-day token lapsed. It cannot be refreshed, only replaced. See [section 10](#10-tokens) |
|
|
821
|
+
| `search_keyword` only ever returns your own posts | `threads_keyword_search` is not approved. Meta narrows the search instead of refusing it |
|
|
822
|
+
| `lookup_profile` only resolves Meta's accounts | `threads_profile_discovery` needs expanded access |
|
|
823
|
+
| `get_follower_demographics` returns nothing | Under 100 followers, or `threads_manage_insights` is missing |
|
|
824
|
+
| Container error a few minutes after posting | The media URL. It has to be public HTTPS, an image or video content type, and not redirect to a login page |
|
|
825
|
+
| "still processing after 120s" | A long video. The container is not lost; `publish_staged` with that id still works for 24 hours |
|
|
826
|
+
| "will not run without confirm: true" | Working as intended. See [section 6](#6-writing-safely) |
|
|
827
|
+
| "is a Threads permalink" | Threads has no endpoint converting a permalink to an id. Use the numeric id from `get_posts` |
|
|
828
|
+
| Rate limited | A rolling-24-hour quota. `get_publishing_limit` shows what is left |
|
|
829
|
+
|
|
830
|
+
Server not appearing at all: run the command your client runs, by hand, and read stderr.
|
|
831
|
+
|
|
832
|
+
## Environment variables
|
|
833
|
+
|
|
834
|
+
### Credentials
|
|
835
|
+
|
|
836
|
+
| Variable | Default | What it does |
|
|
837
|
+
|---|---|---|
|
|
838
|
+
| `THREADS_ACCESS_TOKEN` | none | A long-lived token for one profile |
|
|
839
|
+
| `THREADS_USER_ID` | resolved | Numeric profile id. Resolved from the token when absent |
|
|
840
|
+
| `THREADS_USERNAME` | resolved | Username, for matching and display |
|
|
841
|
+
| `THREADS_ACCOUNTS` | none | JSON array, for several profiles |
|
|
842
|
+
| `THREADS_DEFAULT_ACCOUNT` | first configured | Which profile acts when a tool names none |
|
|
843
|
+
| `THREADS_APP_ID` | none | Meta app id. Needed only by `login` |
|
|
844
|
+
| `THREADS_APP_SECRET` | none | Meta app secret. Needed only by `login` |
|
|
845
|
+
| `THREADS_TOKEN_STORE` | `~/.threads-mcp/tokens.json` | Where tokens are kept |
|
|
846
|
+
| `THREADS_PERSIST_TOKENS` | `1` | Write refreshed tokens back to the store |
|
|
847
|
+
| `THREADS_REFRESH_WINDOW_DAYS` | `20` | Refresh this many days before expiry |
|
|
848
|
+
|
|
849
|
+
### Safety
|
|
850
|
+
|
|
851
|
+
| Variable | Default | What it does |
|
|
852
|
+
|---|---|---|
|
|
853
|
+
| `THREADS_READ_ONLY` | `0` | `1` hides every write from the tool list, leaving 18 reads |
|
|
854
|
+
| `THREADS_ALLOW_DESTRUCTIVE` | `1` | `0` blocks posting, replying and deleting |
|
|
855
|
+
| `THREADS_AUDIT_LOG` | none | Append-only log of every attempted write |
|
|
856
|
+
|
|
857
|
+
### Tuning
|
|
858
|
+
|
|
859
|
+
| Variable | Default | What it does |
|
|
860
|
+
|---|---|---|
|
|
861
|
+
| `THREADS_CONTAINER_TIMEOUT_MS` | `120000` | How long to wait for media to process |
|
|
862
|
+
| `THREADS_REQUEST_TIMEOUT_MS` | `30000` | Per-request deadline |
|
|
863
|
+
| `THREADS_MIN_REQUEST_INTERVAL_MS` | `120` | Spacing between requests |
|
|
864
|
+
| `THREADS_MAX_RETRIES` | `3` | Retries on 5xx and transient errors |
|
|
865
|
+
| `THREADS_GRAPH_HOST` | `https://graph.threads.net` | The Graph API host |
|
|
866
|
+
| `THREADS_USER_AGENT` | `threads-mcp` | The User-Agent sent to Meta |
|
|
867
|
+
| `THREADS_HTTP_PORT` | `8787` | For `--http` |
|
|
868
|
+
| `THREADS_HTTP_HOST` | `127.0.0.1` | For `--http` |
|
|
869
|
+
| `THREADS_HTTP_TOKEN` | none | Bearer token required by `--http` |
|
|
870
|
+
|
|
871
|
+
## Versions
|
|
872
|
+
|
|
873
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
|
874
|
+
|
|
875
|
+
## FAQ ❓
|
|
876
|
+
|
|
877
|
+
<details>
|
|
878
|
+
<summary><b>What is an MCP server?</b></summary>
|
|
879
|
+
|
|
880
|
+
An MCP server is a standard way to give an AI assistant real access to a tool,
|
|
881
|
+
so it can act rather than guess. You install it once, your assistant gains the
|
|
882
|
+
tools, and it works in Claude, Cursor, ChatGPT and anything else that speaks the
|
|
883
|
+
protocol.
|
|
884
|
+
|
|
885
|
+
</details>
|
|
886
|
+
|
|
887
|
+
<details>
|
|
888
|
+
<summary><b>What is Threads?</b></summary>
|
|
889
|
+
|
|
890
|
+
Threads is Meta's text-first social app, tied to an Instagram account. Its API
|
|
891
|
+
is separate from Instagram's, with its own permissions and its own token, so a
|
|
892
|
+
token that works for Instagram does nothing here.
|
|
893
|
+
|
|
894
|
+
</details>
|
|
895
|
+
|
|
896
|
+
<details>
|
|
897
|
+
<summary><b>Do I need a Meta developer app?</b></summary>
|
|
898
|
+
|
|
899
|
+
You need one, and it is free. Threads authorises through Meta's app system, so
|
|
900
|
+
you tick the Threads use case when creating the app. The same app can carry
|
|
901
|
+
Instagram as well, with one app id and one testers list, though each product
|
|
902
|
+
issues its own token.
|
|
903
|
+
|
|
904
|
+
</details>
|
|
905
|
+
|
|
906
|
+
<details>
|
|
907
|
+
<summary><b>Do I need an Instagram account?</b></summary>
|
|
908
|
+
|
|
909
|
+
Your Threads profile is tied to an Instagram account, so yes in that sense. You
|
|
910
|
+
do not need the Instagram API or its permissions to use this server.
|
|
911
|
+
|
|
912
|
+
</details>
|
|
913
|
+
|
|
914
|
+
<details>
|
|
915
|
+
<summary><b>Is my data sent anywhere? Who can see it?</b></summary>
|
|
916
|
+
|
|
917
|
+
Nothing leaves your machine except calls to Meta. There is no backend here, no
|
|
918
|
+
account to create and no telemetry. Your token sits in your client's config.
|
|
919
|
+
|
|
920
|
+
</details>
|
|
921
|
+
|
|
922
|
+
<details>
|
|
923
|
+
<summary><b>Can it post without me asking?</b></summary>
|
|
924
|
+
|
|
925
|
+
It posts when you ask it to. Publishing and deleting require the model to pass
|
|
926
|
+
`confirm: true`, which it sets after reading a description explaining what
|
|
927
|
+
cannot be undone. Hiding a reply is not guarded, because it is one click to undo.
|
|
928
|
+
|
|
929
|
+
Setting `THREADS_READ_ONLY=1` removes every write tool from the list, so the
|
|
930
|
+
model cannot see or call them.
|
|
931
|
+
|
|
932
|
+
</details>
|
|
933
|
+
|
|
934
|
+
<details>
|
|
935
|
+
<summary><b>Why did a tool fail with a permissions error?</b></summary>
|
|
936
|
+
|
|
937
|
+
A missing OAuth scope and an App Review that has not been granted look identical
|
|
938
|
+
from a tool call, which is why `doctor` exists: it probes each capability and
|
|
939
|
+
names which scope is missing rather than leaving you to guess.
|
|
940
|
+
|
|
941
|
+
</details>
|
|
942
|
+
|
|
943
|
+
<details>
|
|
944
|
+
<summary><b>Can it read anyone's Threads posts?</b></summary>
|
|
945
|
+
|
|
946
|
+
It reads your own profile and its replies. Meta's API does not expose other
|
|
947
|
+
people's posts the way a public search would, so competitor research is not
|
|
948
|
+
something this can do honestly.
|
|
949
|
+
|
|
950
|
+
</details>
|
|
951
|
+
|
|
952
|
+
<details>
|
|
953
|
+
<summary><b>Does it cost anything?</b></summary>
|
|
954
|
+
|
|
955
|
+
It costs nothing. The server is MIT licensed and Meta's API is free at the
|
|
956
|
+
volumes a person generates.
|
|
957
|
+
|
|
958
|
+
</details>
|
|
959
|
+
|
|
960
|
+
<details>
|
|
961
|
+
<summary><b>Does it work with ChatGPT and Cursor, or only Claude?</b></summary>
|
|
962
|
+
|
|
963
|
+
It works with any MCP client. Claude Code, Claude Desktop, Cursor, Windsurf, VS
|
|
964
|
+
Code, Codex CLI and Gemini CLI all run it the same way.
|
|
965
|
+
|
|
966
|
+
</details>
|
|
967
|
+
|
|
968
|
+
<details>
|
|
969
|
+
<summary><b>What happens when my token expires?</b></summary>
|
|
970
|
+
|
|
971
|
+
Long-lived tokens last 60 days and can be refreshed before they lapse.
|
|
972
|
+
`doctor` reports how long each one has left, so this is visible before it breaks
|
|
973
|
+
rather than after.
|
|
974
|
+
|
|
975
|
+
</details>
|
|
976
|
+
|
|
977
|
+
<details>
|
|
978
|
+
<summary><b>How do I disconnect it?</b></summary>
|
|
979
|
+
|
|
980
|
+
Remove the app's access from your Threads or Instagram settings, which
|
|
981
|
+
invalidates the token immediately, then remove the server from your client's
|
|
982
|
+
config.
|
|
983
|
+
|
|
984
|
+
</details>
|
|
985
|
+
|
|
986
|
+
## Questions
|
|
987
|
+
|
|
988
|
+
Run into a problem or have a question? [Open an issue](https://github.com/thenavidm/threads-mcp-cli/issues) and I will help.
|
|
989
|
+
|
|
990
|
+
## About the author
|
|
991
|
+
|
|
992
|
+
Navid Moazzez is a leading AI business strategist, and the host of the AI Creator Summit, watched by 100,000+ creators. He helps creators and founders master AI and build their own AI Operating System (AI OS) to automate their business and life. This Threads MCP server is one piece of that system.
|
|
993
|
+
|
|
994
|
+
**Links**
|
|
995
|
+
|
|
996
|
+
- Personal website: [navid.me](https://navid.me?utm_source=github&utm_medium=readme&utm_campaign=threads-mcp-cli)
|
|
997
|
+
- YouTube: [@thenavidm](https://youtube.com/@thenavidm?sub_confirmation=1) and [@thenavidai](https://youtube.com/@thenavidai?sub_confirmation=1)
|
|
998
|
+
- X: [@thenavidm](https://x.com/thenavidm)
|
|
999
|
+
- Instagram: [@thenavidm](https://instagram.com/thenavidm)
|
|
1000
|
+
- LinkedIn: [thenavidm](https://linkedin.com/in/thenavidm)
|
|
1001
|
+
|
|
1002
|
+
If this is useful, star the repo and come say hi on [X](https://x.com/thenavidm).
|
|
1003
|
+
|
|
1004
|
+
## Dependencies
|
|
1005
|
+
|
|
1006
|
+
| Library | License | What it does |
|
|
1007
|
+
|---|---|---|
|
|
1008
|
+
| [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) | MIT | The MCP server and transports |
|
|
1009
|
+
| [zod](https://github.com/colinhacks/zod) | MIT | Tool argument schemas and validation |
|
|
1010
|
+
|
|
1011
|
+
## License
|
|
1012
|
+
|
|
1013
|
+
[MIT](./LICENSE). Free to use, modify, and share.
|
|
1014
|
+
|
|
1015
|
+
Not affiliated with, endorsed by, or connected to Meta Platforms, Inc.
|
|
1016
|
+
|
|
1017
|
+
---
|
|
1018
|
+
|
|
1019
|
+
© 2026 [NM Media](https://navid.media?utm_source=github&utm_medium=readme&utm_campaign=threads-mcp-cli). Made with ❤️ by [Navid Moazzez](https://navid.me?utm_source=github&utm_medium=readme&utm_campaign=threads-mcp-cli).
|