X (Twitter) MCP server
An X MCP server that counts like X and warns before the expensive post.
Your agent drafts and posts to X through one hosted server. It counts characters the way X does, flags the $0.20 link post, and caps volume per account.
The short answer · Updated Nov 1, 2026
An X MCP server exposes posting to X (formerly Twitter) as tools an AI agent can call over the Model Context Protocol. Ours lives at https://socialsforai.com/api/mcp, speaks Streamable HTTP and takes a bearer API key. It posts through X's API v2 with OAuth 2.0, counts text with X's weighted rules (280, links 23, emoji 2), warns when a post contains a link because X charges $0.200 for it versus $0.015, and returns each post's URL.
X: live. Posting to X works on the API today.
What an X MCP server does
Posting to X from an agent takes an X developer app, an OAuth 2.0 flow with PKCE, token refresh, a character counter that matches X's, image uploads, and per-post API fees. Our server holds the app and the tokens and gives the agent eight tools. The agent asks for an account id, drafts, dry runs, and calls create_post.
create_post sends POST https://api.x.com/2/tweets; images go first to X's media upload endpoint. Access tokens refresh automatically through the offline.access scope, so an X account rarely needs reconnecting. X's per-post fees are covered on our X API pricing page; the shared protocol setup is on the MCP server overview.
The tools that matter for X
The eight tools and their inputs are documented once, in every tool and its inputs. For an X account, these five carry the details worth knowing.
list_platforms: X's entry ismaxChars: 280,maxImages: 4,supportsVideo: false,dailyPostCap: 100. The 280 is a weighted count, not a string length.availablereads false today because our production X app isn't configured yet; when it flips, X connect links start working.list_accountswithplatform: "x":handleis the X username we read fromGET /2/users/mewhen the account connected, anddisplay_nameis the profile name. The handle is what goes into the post URL.create_post: put the X wording inplatform_options.x.textand the longer version intext; the weighted 280 applies to whichever text X receives. Up to 4 images, each downloaded from its URL, refused over 5 MB, and uploaded toPOST /2/media/uploadbefore the post is created.get_post: an X target'surlishttps://x.com/<handle>/status/<id>andexternal_post_idis the id X returned. Right after publishing we fetch the post withGET /2/tweets/<id>; a successful read setsverified: true, and a 403 on that read leaves it false without failing the post. X's pricing lists post reads at $0.005 per resource, so that check is one read in X's terms.get_usage: active accounts, posts, plan and an estimated plan cost. X API fees aren't part of that estimate. The place an agent sees X cost is thex_link_costwarning in a dry run.
A worked session: dry run, fix, publish
You ask the agent to announce a launch on X with a link. The dry run returns one error and one warning. The error is length: X's weighted count is 301, over 280. The warning is cost: a post with a URL is billed at $0.200 instead of $0.015. The agent drops the link from the X version (a reply with the link would be billed at $0.200 too), shows you the new text, and publishes with an idempotency key once you approve.
Warnings don't block a publish. If you want the link anyway, say so and the agent can send it as is. Ids here are illustrative; the shapes are the server's real responses, trimmed with .... To try the flow without posting, run the same session against the sandbox platform.
// 1. Find the account
list_accounts({ "platform": "x" })
→ [{ "id": "acc_x7Qm2pLr9sTn4VwB", "object": "account", "platform": "x",
"handle": "yourhandle", "status": "active", ... }]
// 2. Dry run the draft
create_post({ "accounts": ["acc_x7Qm2pLr9sTn4VwB"],
"text": "<301-character draft with a link>", "dry_run": true })
→ { "object": "preflight", "ok": false,
"issues": [
{ "account_id": "acc_x7Qm2pLr9sTn4VwB", "platform": "x", "severity": "error",
"code": "text_too_long", "field": "platform_options.x.text",
"message": "Text is 301 characters; X allows 280.",
"fix": "Shorten it by 21+ characters, or set platform_options.x.text to a shorter version." },
{ "account_id": "acc_x7Qm2pLr9sTn4VwB", "platform": "x", "severity": "warning",
"code": "x_link_cost",
"message": "Posts with a URL cost $0.200 through the X API instead of $0.015 (docs.x.com pricing).",
"fix": "Remove the link, or send a link-free version for X in platform_options.x.text. A link in a reply is also billed at the with-URL rate." }
],
"preview": [{ "account_id": "acc_x7Qm2pLr9sTn4VwB", "platform": "x", "text": "<301-character draft with a link>" }] }
// 3. Publish the fixed version (after the human approves)
create_post({ "accounts": ["acc_x7Qm2pLr9sTn4VwB"],
"text": "Webhooks are live: signed events for every post and account change.",
"idempotency_key": "webhooks-launch-x" })
→ { "id": "post_Vb3kR8sLq2Xn0TfY", "object": "post", "status": "published",
"targets": [{ "account_id": "acc_x7Qm2pLr9sTn4VwB", "platform": "x", "status": "published",
"url": "https://x.com/yourhandle/status/<id>", "external_post_id": "<id>",
"attempts": 1, "verified": true, "error": null, ... }], ... }X rules the server handles
From X's developer docs and our X provider:
- Length: X allows 280 characters by weight. Per X's counting guide, every URL counts as 23 characters regardless of length, each emoji counts 2 (including joined sequences), CJK characters count 2, and text is NFC-normalized first. Our counter follows the same rules, so
text_too_longmatches what X would say. - Link cost: X's pricing page lists Post create at $0.015 per request and Post create with a URL at $0.200, and notes prices are subject to change. A link in an X post triggers the
x_link_costwarning before you spend it. - Fees: we pass X API fees through at cost, with an included credit on paid plans ($2 Hobby, $5 Pro, $15 Growth, $40 Scale). Paid billing isn't live yet.
- Images: up to 4 per post, 5 MB each. Video isn't supported yet and returns
video_not_supported. - Volume: our fair-use cap is 100 posts per X account per rolling 24 hours, plus the 24-hour duplicate check. Bursts and repeats are what get automated accounts limited.
- Per-network text: X is usually the shortest limit in a multi-network post, so set
platform_options.x.textand keep the long version for LinkedIn.
Before an agent posts to X for you
On X every post is a metered API request, so the human check is partly about wording and partly about money. The numbers below come from X's pricing page and our caps.
- Know what a runaway loop costs. At our cap of 100 posts per account per day, link-free posts come to $1.50 in X fees and posts that all carry a link come to $20.00. The cap (
daily_cap_reached) is what stops the loop; your approval prompt is what stops the first one. - Treat
x_link_costas a question. The warning doesn't block anything, so instruct the agent to stop and ask you whenever a dry run returns it, rather than publishing on its own judgment. - Don't move the link into a reply to save money. A reply with a URL is billed at the same $0.200 rate.
- Review before every publish. Keep your client's tool approval on for
create_post, and have the agent show you the dry-run preview of the X text specifically, since it's often a shorter rewrite of the shared draft. - Retries are safe with a key. A timed-out
create_postresent with the sameidempotency_keyand body returns the original post, so X isn't billed twice for one announcement. - Repeats are blocked. Identical text to the same X account within 24 hours fails preflight with
duplicate_content.
X errors and their fixes
The codes below are the ones with an X-specific cause. For the codes every network shares, including a 401 when the client connects, account_not_found and idempotency_key_reused, see troubleshooting by error code.
x_link_cost(warning): the text contains a link. Remove it, give X a link-free version inplatform_options.x.text, or publish anyway knowing X bills $0.200 for that request. Moving the link to a reply doesn't help: a reply with a URL is billed at $0.200 too.text_too_longon X while the text looks short: links count 23 and emoji count 2. Ask the agent to setplatform_options.x.textinstead of shortening the shared text.account_needs_reauthon X: the refresh token was revoked, for example because the owner removed the app in X's settings. Create a new connect link forx.too_many_mediaormedia_too_large: a fifth image, or one over 5 MB. X's limits are tighter than LinkedIn's, so a shared media list may need trimming for the X account.platform_rate_limited: X answered 429. We retry on our backoff schedule; pollget_postinstead of callingcreate_postagain.duplicate_contentafter preflight passed: X's own response called the post a duplicate. Change the wording for that account.platform_unavailablewhen creating an X connect link, orplatform_not_configuredin a dry run: X isn't enabled on the server you're calling. On the hosted server it's live;list_platformsshows the current status.
Status: what works today
X posting is live on the hosted server: OAuth 2.0 with PKCE and the scopes tweet.read tweet.write users.read media.write offline.access, text, images, refresh, read-back verification. Video isn't supported yet. Bluesky, LinkedIn (personal profiles), Mastodon and Telegram are live too; see the Bluesky MCP server and LinkedIn MCP server.
Connect X to your agent in five steps
- 01
Get a key
Create a workspace at /signup or with POST /v1/workspaces. You get an sk_ key on the Free plan.
- 02
Point your client at the server
Register the hosted URL with an Authorization: Bearer header. The Claude Code command is on this page; the other clients are on the MCP hub.
- 03
Check that X is available
Ask the agent to call list_platforms. The X row should say available: true. If it says false, the server you're calling doesn't have X enabled and the next step will be refused.
- 04
Authorize on X
Ask for a connect link for x. Open it, sign in to the X account that should post, and authorize the app on X's OAuth screen.
- 05
Dry run, read the cost warning, publish
Dry run the draft. Fix any length error, decide whether the link is worth $0.200, then approve the publish and keep the x.com status URL it returns.
Questions
What developers ask
Do I have to run an X MCP server myself?
No. Ours is hosted. You add one URL and an API key to your client; we hold the X app, refresh tokens and count characters the way X does.
Does using the X MCP server cost X API fees?
Yes. X charges per post: $0.015, or $0.200 with a link, per its pricing page. We pass that through at cost, with a small included credit on paid plans.
Do I need an X developer account?
No. The account owner approves our app on X's OAuth screen from a connect link.
Can an agent spam X with this?
We cap each X account at 100 posts per rolling 24 hours and block identical text to the same account within 24 hours.
Why does X say my 250-character post is too long?
X counts by weight: every URL is 23, every emoji 2, CJK characters 2. A dry run shows the weighted count before you post.
Can the agent schedule tweets?
Yes. Pass scheduled_at up to 365 days ahead. The post stays scheduled until then and can be canceled with cancel_post.
Will I have to reconnect my X account often?
Rarely. The offline.access scope gives us a refresh token, and we refresh before publishing when the access token is close to expiring. You reconnect only if the refresh token is revoked.
Get an API key and send your first post in five minutes.