Skip to content
SocialsForAI
Menu

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 is maxChars: 280, maxImages: 4, supportsVideo: false, dailyPostCap: 100. The 280 is a weighted count, not a string length. available reads false today because our production X app isn't configured yet; when it flips, X connect links start working.
  • list_accounts with platform: "x": handle is the X username we read from GET /2/users/me when the account connected, and display_name is the profile name. The handle is what goes into the post URL.
  • create_post: put the X wording in platform_options.x.text and the longer version in text; the weighted 280 applies to whichever text X receives. Up to 4 images, each downloaded from its URL, refused over 5 MB, and uploaded to POST /2/media/upload before the post is created.
  • get_post: an X target's url is https://x.com/<handle>/status/<id> and external_post_id is the id X returned. Right after publishing we fetch the post with GET /2/tweets/<id>; a successful read sets verified: 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 the x_link_cost warning 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.

text
// 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_long matches 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_cost warning 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.text and 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_cost as 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_post resent with the same idempotency_key and 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.

Set up for X: add the server, then authorize on X

Register the server in your client first. The Claude Code version is below; Cursor and Claude Desktop take a JSON entry each, shown in client setup.

Connecting an X account is an OAuth 2.0 authorization with PKCE (S256). The agent calls create_connect_link with platforms: ["x"]; you open the link and are sent to x.com/i/oauth2/authorize, which asks for tweet.read tweet.write users.read media.write offline.access. We exchange the code, read your handle, and keep the tokens encrypted. Before each publish we refresh the access token if it expires within a minute, and if X still answers 401 we refresh once and retry the post. If the refresh is refused, or X rejects the new token too, the account moves to needs_reauth.

bash
export SOCIALS_API_KEY=sk_...
claude mcp add --transport http socials https://socialsforai.com/api/mcp \
  --header "Authorization: Bearer $SOCIALS_API_KEY"

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 in platform_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_long on X while the text looks short: links count 23 and emoji count 2. Ask the agent to set platform_options.x.text instead of shortening the shared text.
  • account_needs_reauth on X: the refresh token was revoked, for example because the owner removed the app in X's settings. Create a new connect link for x.
  • too_many_media or media_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; poll get_post instead of calling create_post again.
  • duplicate_content after preflight passed: X's own response called the post a duplicate. Change the wording for that account.
  • platform_unavailable when creating an X connect link, or platform_not_configured in a dry run: X isn't enabled on the server you're calling. On the hosted server it's live; list_platforms shows 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

  1. 01

    Get a key

    Create a workspace at /signup or with POST /v1/workspaces. You get an sk_ key on the Free plan.

  2. 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.

  3. 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.

  4. 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.

  5. 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.