@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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1019 -0
  3. package/SKILL.md +203 -0
  4. package/dist/api/client.d.ts +105 -0
  5. package/dist/api/client.js +305 -0
  6. package/dist/api/client.js.map +1 -0
  7. package/dist/api/errors.d.ts +92 -0
  8. package/dist/api/errors.js +195 -0
  9. package/dist/api/errors.js.map +1 -0
  10. package/dist/api/identity.d.ts +33 -0
  11. package/dist/api/identity.js +52 -0
  12. package/dist/api/identity.js.map +1 -0
  13. package/dist/auth/login.d.ts +32 -0
  14. package/dist/auth/login.js +204 -0
  15. package/dist/auth/login.js.map +1 -0
  16. package/dist/auth/store.d.ts +37 -0
  17. package/dist/auth/store.js +88 -0
  18. package/dist/auth/store.js.map +1 -0
  19. package/dist/auth/tokens.d.ts +54 -0
  20. package/dist/auth/tokens.js +96 -0
  21. package/dist/auth/tokens.js.map +1 -0
  22. package/dist/cli.d.ts +59 -0
  23. package/dist/cli.js +444 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/config.d.ts +98 -0
  26. package/dist/config.js +185 -0
  27. package/dist/config.js.map +1 -0
  28. package/dist/content/containers.d.ts +89 -0
  29. package/dist/content/containers.js +210 -0
  30. package/dist/content/containers.js.map +1 -0
  31. package/dist/content/media.d.ts +61 -0
  32. package/dist/content/media.js +125 -0
  33. package/dist/content/media.js.map +1 -0
  34. package/dist/content/text.d.ts +68 -0
  35. package/dist/content/text.js +106 -0
  36. package/dist/content/text.js.map +1 -0
  37. package/dist/doctor.d.ts +14 -0
  38. package/dist/doctor.js +218 -0
  39. package/dist/doctor.js.map +1 -0
  40. package/dist/format/posts.d.ts +41 -0
  41. package/dist/format/posts.js +153 -0
  42. package/dist/format/posts.js.map +1 -0
  43. package/dist/index.d.ts +13 -0
  44. package/dist/index.js +167 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/safety.d.ts +52 -0
  47. package/dist/safety.js +85 -0
  48. package/dist/safety.js.map +1 -0
  49. package/dist/server.d.ts +20 -0
  50. package/dist/server.js +232 -0
  51. package/dist/server.js.map +1 -0
  52. package/dist/tools/accounts.d.ts +27 -0
  53. package/dist/tools/accounts.js +162 -0
  54. package/dist/tools/accounts.js.map +1 -0
  55. package/dist/tools/discover.d.ts +56 -0
  56. package/dist/tools/discover.js +146 -0
  57. package/dist/tools/discover.js.map +1 -0
  58. package/dist/tools/index.d.ts +3 -0
  59. package/dist/tools/index.js +16 -0
  60. package/dist/tools/index.js.map +1 -0
  61. package/dist/tools/insights.d.ts +55 -0
  62. package/dist/tools/insights.js +223 -0
  63. package/dist/tools/insights.js.map +1 -0
  64. package/dist/tools/kit.d.ts +90 -0
  65. package/dist/tools/kit.js +119 -0
  66. package/dist/tools/kit.js.map +1 -0
  67. package/dist/tools/posts.d.ts +170 -0
  68. package/dist/tools/posts.js +312 -0
  69. package/dist/tools/posts.js.map +1 -0
  70. package/dist/tools/read.d.ts +31 -0
  71. package/dist/tools/read.js +95 -0
  72. package/dist/tools/read.js.map +1 -0
  73. package/dist/tools/replies.d.ts +92 -0
  74. package/dist/tools/replies.js +218 -0
  75. package/dist/tools/replies.js.map +1 -0
  76. package/dist/transport/http.d.ts +28 -0
  77. package/dist/transport/http.js +103 -0
  78. package/dist/transport/http.js.map +1 -0
  79. 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
+ [![npm](https://img.shields.io/npm/v/@thenavidm%2Fthreads-mcp-cli?color=orange&label=npm)](https://www.npmjs.com/package/@thenavidm/threads-mcp-cli)
6
+ [![License](https://img.shields.io/badge/License-MIT-green)](./LICENSE)
7
+ [![YouTube](https://img.shields.io/badge/YouTube-@thenavidm-red?logo=youtube&logoColor=white)](https://youtube.com/@thenavidm?sub_confirmation=1)
8
+ [![X](https://img.shields.io/badge/X-@thenavidm-black?logo=x)](https://x.com/thenavidm)
9
+ [![LinkedIn](https://img.shields.io/badge/LinkedIn-thenavidm-0A66C2?logo=linkedin&logoColor=white)](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).