twitter-mcp
by armatrix
README.md
# twitter-mcp + xdata
X (Twitter) data access for Claude Code, split across two front ends over one set of adapters:
- **`xdata` CLI** — all content reads. This is the entry point you want.
- **MCP server** — writes, post-volume counts, and usage only.
Zero compilation — edit and run instantly with Bun.
## Why reads live in the CLI, not in MCP
Both data providers bill **per returned record**, and an MCP tool result lands in the model's
context *in full*. One 20-post search is 20–40k tokens of JSON. From a shell the same call can be
trimmed, piped through `jq`, or written straight to disk with only a summary coming back.
Measured, not estimated:
| | tools | schema in context | a 20-post search in context |
|---|---|---|---|
before (reads in MCP) | 21 | ~1,898 tok | 20,000–40,000 tok |
now (reads in CLI) | 9 | ~986 tok | ~600 tok (`--out`: ~30 tok) |
The 900-token schema saving is the small half. The point is that large payloads no longer have a
path into the conversation at all.
## Architecture
```
┌───────────────────────────┐ ┌──────────────────────────────────────┐
│ xdata CLI │ │ MCP server (twitter-mcp v2.0) │
│ all content reads │ │ writes · counts · usage · feeds │
└─────────────┬─────────────┘ └──────────────┬───────────────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌────────────────────┐
│ ReaderClient (port) │ │ WriterClient │ OAuth 1.0a
├──────────────────────┤ │ CookieClient │ ct0 + auth_token
│ primary │ │ CountsClient │ OAuth2 app-only
│ twitterapi.io │ └────────────────────┘
│ $0.00015/post │
│ │ on 401/402/429/5xx or an HTTP-200 `status:error`
│ ▼ │
│ fallback │ every fallback is tagged in the response —
│ official X API v2 │ a silent switch would hide a 33x price jump
│ $0.005/post │
└──────────────────────┘
```
### Which channel for what
| Need | Channel | Cost shape |
|---|---|---|
Content (search, timelines, replies, profiles, graph) | twitterapi.io | **$0.00015/post**, $0.00018/profile |
Same, when the primary is out of credits or down | official v2 (auto, tagged) | $0.005/post, $0.010/user — 33x |
**Volume over time** (trends, ranking, spotting peaks) | official `counts` | **$0.005 per _request_** — any match volume, and it does not consume the 3M post-read cap |
Writes (post/like/retweet/delete) | official OAuth 1.0a | $0.015/post ($0.20 with a link) |
Replies the Free tier blocks (403) | cookie GraphQL | free |
**The cheap pattern is two-stage**: `counts` first to find which query and which window matter
(effectively free), then pull posts for only that window. Large blind searches are the expensive
mistake.
## Setup
```bash
git clone https://github.com/armatrix/twitter-mcp.git && cd twitter-mcp && bun install
mkdir -p ~/.twitter-mcp && cp config.example.json ~/.twitter-mcp/config.json && chmod 600 ~/.twitter-mcp/config.json
```
`~/.twitter-mcp/config.json` is the single source of truth for every credential:
```json
{
"reader": { "provider": "twitterapi.io", "api_key": "new1_..." },
"writer": { "provider": "twitter-official", "api_key": "...", "api_secret_key": "...",
"access_token": "...", "access_token_secret": "..." },
"official": { "bearer": "AAAA..." },
"cookie": { "ct0": "...", "auth_token": "..." },
"defaults": { "timeline_count": 20, "search_count": 20 }
}
```
- `reader.api_key` — [twitterapi.io](https://twitterapi.io). Optional: without it every read falls
back to the official API and is tagged as such (33x the price, same capability).
- `writer.*` — X Developer Portal → your App → Keys and Tokens.
- `official.bearer` — **derive it, don't go hunting for it.** OAuth 1.0a is rejected by `counts`,
location trends, `usage` and filtered stream; an app-only bearer is one call away from the
consumer key/secret you already have:
```bash
curl -s -u "<consumer_key>:<consumer_secret>" \
-d 'grant_type=client_credentials' https://api.x.com/oauth2/token
```
- `cookie.*` — DevTools → Application → Cookies → `x.com` → copy `ct0` and `auth_token`.
Optional; only powers the reply fallback. `ct0` rotates every few hours.
Install the CLI and register the MCP server:
```bash
ln -sf "$PWD/bin/xdata" ~/.local/bin/xdata # or point a wrapper at src/cli.ts
claude mcp add twitter -- bun run "$PWD/src/index.ts" -config ~/.twitter-mcp/config.json
```
## CLI
```
xdata usage # both providers: balance, spend, cap
xdata counts '<query>' --day|--hour [--all] # per-request billing → trends & ranking
xdata search '<q> min_faves:30' --top -n 20 # min_faves is server-side (official v2 lacks it)
xdata user|about|timeline|mentions <handle>
xdata replies|quotes|thread|retweeters <tweetId>
xdata tweets <id,id,...> # batch by id
xdata batch-users <id,id,...> # bulk profiles, cheaper per user
xdata followers|followings <handle>
xdata follower-ids <handle> # IDs only: 0.45cr vs 18cr per profile — 40x cheaper
xdata verified-followers <handle> # $0.3/1000
xdata usersearch <query> # no official equivalent exists
xdata follows <a> <b> # mutual-follow check (100cr)
xdata article <tweetId> # long-form body (no official equivalent)
xdata trends [woeid]
```
Output control — this is where the token savings are:
```
--out FILE full JSON to disk, one summary line to stdout
--json raw --jsonl one record per line (pipe to jq)
-n N max records -w N text column width -q no summary line
```
Every command prints `cost≈`, because both providers bill per record and it is otherwise invisible
until the dashboard catches up.
## MCP tools (9)
| Tool | Channel | Notes |
|---|---|---|
`post_tweet` | OAuth 1.0a → cookie | Up to 4 images; auto-falls back to cookie auth on a reply 403 |
`like_tweet` `unlike_tweet` `retweet` `delete_tweet` | OAuth 1.0a | |
`get_tweet_counts` | official app-only | Per-request billing; counts only (no text/authors) |
`get_api_usage` | official app-only | Post reads against the monthly cap + estimated spend |
`get_for_you_feed` `get_following_feed` | cookie | The authenticated account's own timelines |
There are deliberately **no content-read tools here**. The server says so on startup.
### Reply fallback
```
post_tweet(text, reply_to_tweet_id) → OAuth 1.0a POST /2/tweets
├─ 200 → done (method: "oauth")
└─ 403 (Free tier reply restriction) → cookie GraphQL CreateTweet (method: "cookie")
→ or an error if no cookie is configured
```
## Endpoint index
`docs/endpoint-index.md` — 264 endpoints across both providers, generated from their machine-readable
specs (`api.x.com/2/openapi.json` v2.168 / 157 paths + twitterapi.io / 76 paths), grouped by function
with auth requirements and billing per row. Consult it before assuming a capability is missing.
⚠️ **A spec is a lead, not a fact.** `/twitter/user/tweet_timeline` is documented with a top-level
`tweets` array but actually answers `data.tweets`; the adapter accepts both, because reading only one
shape yields an empty array indistinguishable from "no results".
## Known limitations
| Issue | Reality |
|---|---|
`/twitter/user/following` looks broken | It isn't a route — the path is `followings` (plural). Wrong URLs answer HTTP 200 `status:error msg:"user not found"` |
`last_tweets` ignores `count` | No such parameter; a page is always ~20 and you are billed per returned tweet. Narrow with `cursor`, and prefer the stream API for monitoring — the endpoint docs say polling it frequently "will cost you a lot" |
Official `counts`/`trends`/`usage`/stream return 403 | They reject OAuth 1.0a. Use `official.bearer` |
Official v2 has no user search, and no long-form article body | Only twitterapi.io has these |
`firehose` / `sample` streams | 403 — Enterprise tier |
Filtered stream billing | Not in the pricing page; rule slots are visible (`cap_per_client_app: 5`, `cap_per_project: 15`) but per-delivery cost is **unverified** |
Free-tier OAuth blocks replies to non-interacted posts | Cookie fallback handles it |
`ct0` rotates every few hours | Re-copy from the browser when cookie auth starts failing |
## Project structure
```
src/
├── cli.ts # xdata — all content reads
├── index.ts # MCP server — writes, counts, usage, feeds
├── config.ts
├── clients/
│ ├── reader.ts # port + router (primary → tagged fallback)
│ ├── reader-twitterapi.ts # primary adapter
│ ├── reader-official.ts # fallback adapter (X API v2, OAuth 1.0a)
│ ├── counts.ts # app-only bearer: counts / trends / usage
│ ├── writer.ts # OAuth 1.0a writes + media upload
│ └── cookie.ts # GraphQL via browser session
├── lib/
│ ├── normalize-tweet.ts # GraphQL → NormalizedTweet
│ └── v2-to-normalized.ts # X API v2 → NormalizedTweet
└── tools/
├── discovery.ts # get_tweet_counts, get_api_usage, cookie feeds
└── write.ts # post/like/unlike/retweet/delete
docs/
├── endpoint-index.md # both providers, generated from specs
PATCH-reader-dual-channel.md # why it is built this way + what was learned the hard way
```
## Development
No build step. Edit and restart the server (**stdio servers do not hot-reload — `/mcp` reconnect
after changing anything the server loads**).
```bash
bun run src/index.ts -config ~/.twitter-mcp/config.json
bunx tsc --noEmit
```
## Tech stack
Bun · [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk) ·
[oauth-1.0a](https://github.com/nicobytes/oauth-1.0a) · `bun:sqlite` · X internal GraphQL (no deps)
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues