使用说明
twitter-mcp + xdata
X (Twitter) data access for Claude Code, split across two front ends over one set of adapters:
xdataCLI — 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
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:
{
"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. 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 bycounts, location trends,usageand filtered stream; an app-only bearer is one call away from the consumer key/secret you already have:curl -s -u "<consumer_key>:<consumer_secret>" \ -d 'grant_type=client_credentials' https://api.x.com/oauth2/token -
cookie.*— DevTools → Application → Cookies →x.com→ copyct0andauth_token. Optional; only powers the reply fallback.ct0rotates every few hours.
Install the CLI and register the MCP server:
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).
bun run src/index.ts -config ~/.twitter-mcp/config.json
bunx tsc --noEmit
Tech stack
Bun · @modelcontextprotocol/sdk ·
oauth-1.0a · bun:sqlite · X internal GraphQL (no deps)
License
MIT