x-mcp
This MCP server lets an AI agent publish to X (Twitter) using a saved logged-in browser session, plus verify the session, with no API keys.
x_check_session: Check whether the saved X session is live and which handle it is logged in as.x_submit_post: Publish a new standalone post publicly and immediately.x_submit_reply: Reply to a tweet by URL or id.x_submit_quote: Quote-post a tweet with added text.x_submit_repost: Repost a tweet with no added text.All tools take an optional account (default via
X_ACCOUNT), drive a real headed Chromium session, confirm success from X's own responses, and return an unverified status with a screenshot instead of assuming success when confirmation fails.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@x-mcp post 'Hello world!' to X"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
x-mcp
MCP server for posting to X (Twitter) through a saved browser session (Playwright + stealth), plus a set of read scripts that drive the same session against X's internal GraphQL API.
The MCP surface is write-only by design. Reading X does not need an MCP round-trip and is better done in bulk at the terminal, which is what the scripts in scripts/ do. The server does the one thing that genuinely needs a browser inside an agent loop: publishing something and confirming it actually landed.
No API keys, no developer account, no paid API tier. The identity is a browser session you log into yourself.
Not affiliated with, endorsed by, or connected to X Corp. This drives your own logged-in account through a normal browser. Read the Notes before using it.
How it works
X serves its timelines and its mutations from an internal GraphQL API. Scraping the rendered DOM means chasing class names that change every few weeks, so this does not do that. Every page gets a small init script that wraps fetch and XMLHttpRequest and keeps the JSON the X web app itself receives; the code then walks that JSON for anything that looks like a tweet, in whatever envelope it arrived (a timeline entry, a visibility wrapper, a module item, a quoted tweet).
Two useful consequences. Long posts come back whole out of note_tweet instead of truncated at 280 characters. And a post is confirmed by reading X's own CreateTweet response for the new tweet's id, rather than sleeping two seconds and assuming it worked.
Chromium runs headed (non-headless) throughout. X blocks headless browsers.
Related MCP server: x-use
Prerequisites
Python 3.11+
An X account
Google Chrome installed. The code drives your real Chrome rather than Playwright's bundled build, which identifies itself as "Google Chrome for Testing" and is easier for X to flag. Without Chrome it falls back to the bundled build and says so on stderr.
1. Install
python3 -m venv .venv
.venv/bin/pip install -e .
.venv/bin/python -m playwright install chromium2. Save a browser session
There are two ways. Prefer the first one.
a. Import cookies from a browser you are already signed into (recommended)
.venv/bin/python scripts/import_session.pyLogging in through an automated browser is the step X flags, and a failed attempt can get the account's logins temporarily limited. If you are already signed into X normally, this skips that step entirely. The script asks for two cookie values, without echoing them, and writes the session file directly.
To find them, in the browser where you are already signed in: open x.com, press F12 (or Cmd+Option+I), go to Application (Chrome) or Storage (Firefox) -> Cookies -> https://x.com, and copy the values of auth_token and ct0.
auth_token is a full credential for the account, equivalent to a password. It is prompted for hidden so it stays out of your shell history, and the session file is written chmod 600.
Add --verify to open a browser afterwards and confirm the session works.
b. Log in through the automated browser
.venv/bin/x-mcp-setupA Chrome window opens on x.com. Log in normally, including whatever 2FA X asks for. Nothing is typed for you and no password is stored. When the home timeline appears, the cookies are saved to session.json (gitignored) and the window closes.
This drives your real installed Google Chrome (channel="chrome"), not Playwright's bundled build, and keeps a persistent profile under profiles/<account>/ so repeat runs look like a returning browser rather than a fresh one. Playwright's bundled binary identifies itself as "Google Chrome for Testing" and is much easier to flag; if real Chrome cannot be launched the code says so loudly on stderr rather than quietly falling back.
If X answers with "We've temporarily limited your login", stop. Wait it out rather than retrying, since repeated attempts compound the flag, and use method (a) instead.
Either way
For a second account, add --account <name>; that reads and writes session-<name>.json instead. Every read script takes the same --account, and every MCP tool takes an account argument. X_ACCOUNT sets the default when the argument is left empty.
3. Configure your AI tool
Claude Code
claude mcp add --scope user x /path/to/x_mcp/.venv/bin/x-mcpGeneric stdio config (Qwen Code, Cursor, etc.)
{
"mcpServers": {
"x": {
"command": "/path/to/x_mcp/.venv/bin/x-mcp"
}
}
}Available tools
Tool | Description |
| Check whether the saved session is live, and which handle it is logged in as. |
| Publish a new standalone post. |
| Reply to a tweet, by URL or bare id. |
| Quote-post a tweet with added text. |
| Repost a tweet with no added text. |
All of them drive a real (headed) Chromium window with the saved session, so they behave like a human posting - no API keys, no OAuth app. Every tool takes an optional account.
Run x_check_session first: it reports whether the session is live and posts nothing.
What the posting tools guarantee
The target is verified before anything is typed. A reply, quote or repost navigates to the tweet and confirms that exact id is present in the captured GraphQL. If it cannot be confirmed, the tool raises rather than typing into whatever page it landed on.
Success is observed, not assumed. A post, reply or quote returns the new tweet's real id and URL, read out of X's
CreateTweetresponse. A repost is confirmed by the button flipping to its unreposted state.Ambiguity is reported as ambiguity. If the action cannot be confirmed the result is
{"status": "unverified", ...}with a screenshot path, never a hopeful"posted". Do not retry an unverified result automatically: a retry on an ambiguous outcome double-posts, or undoes a repost that did land.Every tool publishes publicly and immediately. There is no draft state. In an MCP client, the client's own approval prompt is the confirmation step.
Reading X (scripts/)
Every script opens a headed Chromium window with the saved session and reads the captured GraphQL. Run them with the venv Python.
Script | What it does |
| Recent posts from one or more accounts. |
| A search, Latest tab by default ( |
| A post plus its replies, ranked by engagement. The context you need to reply well. |
| One pass over a worklist of accounts and searches, ranked, skipping anything already handled. |
| Build a session from cookies copied out of a browser you are already signed into. |
| Same as |
Example:
.venv/bin/python scripts/read_user.py nasa esa --max-age-h 48
.venv/bin/python scripts/search.py '"launch window" min_faves:50' --max-age-h 24
.venv/bin/python scripts/read_thread.py https://x.com/nasa/status/1234567890123456789The digest
digest.py reads targets.json (gitignored). Copy the template and edit it:
cp targets.example.json targets.jsonThree tiers, walked in order:
always_reply- accounts whose every new post you want to answer.ecosystem- larger accounts worth landing a reply under.queries- open search, filtered bymin_followers_for_open_searchso you get posts with an audience rather than noise.
.venv/bin/python scripts/digest.py --per-tier 10 --max-age-h 72
# after you reply to something, so it stops coming back
.venv/bin/python scripts/digest.py --replied 1234567890123456789
.venv/bin/python scripts/digest.py --ignore 1234567890123456789State lives in state/ (gitignored): replied.json, ignored.json.
Bundled agent skill
.claude/skills/x-outreach/SKILL.md ships with the repo. Open the repo in Claude Code and it loads automatically; to make it available from anywhere:
ln -s "$(pwd)/.claude/skills/x-outreach" ~/.claude/skills/x-outreachIt tells an agent which surface to use for what (reading is the CLIs, posting is the MCP), the approval rule for anything that publishes, what to do with an unverified result, and the bar a reply has to clear. Worth reading yourself even if you never point an agent at this.
Compatibility
Verified working against X in September 2026.
This reads X's internal GraphQL, which is not a public API and changes without notice. It is built to survive the common kinds of change: responses are walked for tweet objects rather than parsed by a fixed path, and the old field locations are kept as fallbacks. Field moves still happen. A recent one relocated user fields out of legacy into core, profile_bio and relationship_counts, which silently zeroed every follower count until the parser learned both shapes.
When something does break, it surfaces in one of three ways rather than as bad data:
A read raises instead of returning an empty list.
A posting tool raises naming the selector it looked for, with a screenshot under
state/.A post that cannot be confirmed returns
"status": "unverified"rather than a hopeful"posted".
If a field goes quietly empty rather than raising, that is a bug worth reporting.
HTTP transport (Docker)
The server speaks stdio by default. Set MCP_TRANSPORT=streamable-http (plus MCP_HOST / MCP_PORT, default port 8201) to run it as an HTTP MCP server; the included Dockerfile does this under xvfb-run.
Notes
This automates your own logged-in account, which X's terms restrict. Your account is the thing at risk. Keep the volume human, and do not point this at anything resembling bulk posting.
X rate-limits reading, and it does not take much. About fifty profile loads in ten minutes was enough to limit a healthy account. Every page load therefore goes through a throttle that enforces a minimum interval (
X_MIN_NAV_INTERVAL, default 5s), caps loads per run (X_MAX_NAVS, default 40), and raisesRateLimitedas soon as X serves a rate-limit page. If that raises, stop for several hours. Raising the cap to push through is how accounts get restricted.One browser window per run, targets read sequentially. A digest over ten accounts and five searches takes a few minutes. That pacing is deliberate.
Reads fail loudly. A login redirect or an empty capture raises instead of printing an empty list, so a dead session never looks like a quiet day.
The DOM selectors (
tweetTextarea_0,tweetButtonInline,retweet) are X's published test ids. If X changes them, the tools raise with the selector they looked for and a screenshot understate/, rather than clicking something else.One account per session file; the session file is the identity.
Layout
src/x_mcp/session.py session files, browser context, the capture script
src/x_mcp/tweets.py parsing captured GraphQL into one flat tweet shape
src/x_mcp/post.py the posting client
src/x_mcp/server.py the MCP server
scripts/ the read CLIs, sharing the same installed package
.claude/skills/ the bundled agent skillLicense
MIT. See LICENSE.
Available Tools
5 toolsx_check_sessionA
Check whether the saved X session for account is live, and which handle it is logged in as.
Inputs: account. Returns: result
x_submit_postA
Publish a new standalone post on X for account. Publishes PUBLICLY and IMMEDIATELY - no draft step.
Inputs: text (required), account. Returns: result
x_submit_quoteA
Quote-post a tweet (URL or id) with added text as account on X. Publishes PUBLICLY and IMMEDIATELY - no draft step.
Inputs: text (required), target (required), account. Returns: result
x_submit_replyA
Reply to a tweet (URL or id) as account on X. Publishes PUBLICLY and IMMEDIATELY - no draft step.
Inputs: text (required), target (required), account. Returns: result
x_submit_repostA
Repost a tweet (URL or id) with no added text as account on X. Publishes PUBLICLY and IMMEDIATELY - no draft step.
Inputs: target (required), account. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 5 tools
Each tool has a clearly distinct purpose: creating a new post, replying, quoting, reposting, and checking session state. There is no overlap or ambiguity between them.
All submission tools follow the consistent x_submit_* pattern (post, reply, quote, repost), and the session check uses x_check_session. The naming is predictable and uniform.
With only 5 tools, the server is well-scoped for its purpose of publishing to X. Each tool serves a necessary function without unnecessary bloat or gaps.
The tool set covers the core submission actions (post, reply, quote, repost) and session verification, which aligns with its stated purpose. Missing features like deleting posts or fetching timelines are not critical for a submission-focused server, but a minor gap exists for listing or managing existing content.
Maintenance
Related MCP Connectors
Publish and schedule tweets and threads on X (Twitter) from your AI assistant.
Draft, preview, schedule and publish posts to X, LinkedIn, Bluesky and 5 more social networks.
The zero-dashboard social autopilot. Publish to X and LinkedIn directly from AI chat.
Draft, check and schedule posts to your social accounts from Claude, ChatGPT, Cursor or VS Code.
Related MCP Servers
- FlicenseNot gradedqualityCmaintenanceEnables AI assistants to interact with Twitter/X using session cookies. Supports posting, searching, managing tweets, viewing trends, and user profiles.-
- AlicenseBqualityBmaintenanceMulti-account X (Twitter) automation that drives a real Chrome session with your own cookies, so no X API key is needed. 32 tools for posting, replies, search, engagement, and media, with draft approval on by default.82182MIT
- AlicenseNot gradedqualityDmaintenanceEnables posting single tweets, threads, and saving drafts to X (Twitter) automatically using Playwright.MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI assistants and local operators to publish approved posts, linear threads, replies, and Community posts to X through a Playwright-driven authenticated browser session, without X API credits. Also exposes read-only tools to check session status and list Communities available to the account, via stdio or bearer-protected Streamable HTTP.MIT