MCP connector
xpost speaks Model Context Protocol natively, on every plan. One hosted URL gives any MCP client 17 tools — posting, media, delivery receipts, and the engagement insights that let an agent learn what works. Everything goes through the same API as the dashboard, so key scopes and rate limits all apply, and a project that holds agent posts for approval holds the ones made through MCP too — an agent cannot get past its own project's approval queue, by design.
https://xpost.to/api/mcp · Streamable HTTP · Bearer auth
Create the key under Dashboard → AI agent (agent key). That tab generates all of the snippets below with your key filled in.
Claude Code
claude mcp add --transport http xpost https://xpost.to/api/mcp \ --header "Authorization: Bearer xp_live_YOUR_KEY"
Claude Desktop & claude.ai
Easiest is the xpost plugin from Claude's directory: Customize → Plugins, search xpost, Add — then Customize → Connectors, xpost, Connect. Adding the plugin doesn't sign you in; Connect does. Without the plugin: Customize → Connectors → Add → Custom connector and paste this URL. Either way the client finds our sign-in on its own: you sign in, pick the project, press Allow. No key to copy.
https://xpost.to/api/mcp
The connection shows up under Project → AI agent as “connected by signing in”, with a Disconnect button. A connector UI that can't open a browser can still use the keyed form, https://xpost.to/api/mcp/xp_live_YOUR_KEY— treat that URL as a secret.
ChatGPT
Nothing to paste. xpost is in ChatGPT's plugin directory — add it there and sign in. Connect xpost to ChatGPT has the two steps.
Cursor
Add to .cursor/mcp.json (project) or ~/.cursor/mcp.json (global):
{
"mcpServers": {
"xpost": {
"url": "https://xpost.to/api/mcp",
"headers": { "Authorization": "Bearer xp_live_YOUR_KEY" }
}
}
}Any other MCP client
The connector is standard Streamable HTTP at https://xpost.to/api/mcp with Authorization: Bearer <key>. Clients that can't send headers can use the /api/mcp/<key> form.
Local stdio server (self-hosting)
Running xpost on localhost, or want the agent to upload files straight from the machine it runs on? Use the stdio server that ships in the repo (mcp/server.mjs):
claude mcp add xpost \ -e XPOST_API_KEY=xp_live_YOUR_KEY \ -e XPOST_URL=http://localhost:3001 \ -- node /path/to/xpost/mcp/server.mjs
Same tools as the hosted connector, plus upload_media can read local file paths (the hosted connector accepts URLs and base64 only).
The 17 tools
The project's settings, read before writing: timezone (the clock every bare time is on), queue (its posting schedule and next free slots — scheduled_at: "next_slot" takes one), approval (whether agent posts wait for a person), posts_today (this key's remaining daily allowance) and publishing (whether anything written here can go out at all — read it before writing, not after).
The connected accounts and what each will publish. ref ("x:someone") is what create_post accounts takes. limits carries caption_chars_available (room for your words), max_media, media_required and video_seconds; can carries placements, first comment, people tags, documents and mixed media. These describe the connection, not the platform's brochure. posts_left_today (where present) is how many more posts that account can publish before resets_at (midnight UTC) — schedule past it, not into a full day, which is refused. connect_url is the page where an account is connected — hand it over whenever a platform they asked for is missing. An empty list adds connect_message; a project that has never published adds never_posted_message, which says what to offer them next.
Every platform_configurations key each account accepts, with kind, accepted values, ceilings and placement gates — the catalog to read before setting anything beyond a caption. A key that is absent cannot be published through that connection: sent anyway it is refused or loses the delivery. Pinterest accounts also list boards, each with eligible. ONE POST CARRIES EVERY ACCOUNT: list them all in create_post's accounts and put a platform's own words in platform_configurations.<platform>.caption, its own options beside them. One post per platform is not how this works — it spends the free first post on a single network, walls the rest, and hands the person one card per network to read instead of one.
Attach an image, video or PDF and get a media_id for create_post. Sources: file (an attachment from this conversation, as the host provides it), a public https url, base64 data with mime_type. Instagram, TikTok, YouTube and Pinterest need media; a PDF becomes a LinkedIn document post. When an attached file cannot be provided from here, create the post with media_pending: true instead and the person adds it in the approval card. Formats and caps: https://xpost.to/docs/api#media.
Create a post to connected accounts — X/Twitter, Instagram, LinkedIn, Facebook, TikTok, YouTube, Threads, Bluesky, Pinterest. CREATING IT SENDS IT: no scheduled_at publishes now, one publishes then; a project may hold it instead (status says). The answer carries message and approval_link or preview_link. accounts takes each account's ref. Media is media_ids, files (attachments from this conversation) or media_pending: true, which holds the post until the picture arrives — the reply's upload_url takes a POST of the file (upload_curl), or the person adds it in the card; then it goes out on its own unless the project holds agent posts. "next_slot" takes the queue; is_draft saves without sending. Undo: update_post or delete_post before it goes out; take_down_post after, where networks allow.
Recent posts with status, publishes, mediaIds and, on a rejected post, rejectionReason — the person's own words on what to change with update_post (often empty after a one-tap rejection). Filter with status (one or several), limit, and from/to on the scheduled time. A post still waiting carries approval_link. filter echoes what applied. An empty list with nothing filtered adds never_posted_message, which says what to offer them next.
Remove a post this key created that has not gone out. A draft, a post waiting for approval, a rejected or a failed one is deleted with its delivery records. A post already holding a time is taken off the schedule instead, and unscheduled with message says so. Posts a person approved are refused, and so is one that has gone out — removing THAT from the platform is take_down_post.
Change a post that has not gone out — caption, media, time, per-platform options; anything left out is kept. The reply is a NEW post with a new id and a new link; replaced names the old one, which is gone. add_media_ids and files append to what the post holds; media_ids replaces it. A picture the person added in the approval card is already on the post. Editing an approved or rejected post puts it back in the approval queue, and message says which of the two happened. A post already tried is not editable: retry_delivery handles the delivery that failed. Send a draft with is_draft: false — creating it again instead posts the words twice.
Send one failed delivery again, by its id from get_delivery_receipt — a delivery id, not a post id. Skip it when the receipt says retry_helps: false: the same failure comes back. A delivery that succeeded is refused. A refusal carries error_code — already_succeeded (with post_url, or taken_down), in_flight, already_claimed, not_found, or is_post_id (a post id was passed; its deliveries are in the answer, pick the failed one) — and retriable, false when asking again cannot change the answer.
Delete a published post from its platforms, by its post_id — every platform it is live on, or only platform. There is no need to call get_delivery_receipt first. There is no undo; reposting is a new post with a new URL. This is the person's own decision about their live post, not a cleanup step. Each delivery answers in deliveries: error_code never_went_out, account_gone or unsupported means the person removes it on the platform themselves; only platform_refused is retriable. delivery_id still takes down one delivery; a post id passed there answers is_post_id with the post's deliveries.
Accounts that cannot publish right now — an expired token, a platform out of reach — each with what happened. The fix is the person reconnecting that account: connect_url is where they do it. An empty list means every account is healthy.
Per-account delivery receipt for a post: status, the live URL, plain_reason on failure, and note where less was published than was attached. approval_link is the post as a page. Not needed straight after create_post or update_post: their card already follows the delivery live. Not needed before take_down_post either: it takes the post id and draws the post once it is down.
Create AND SEND up to 100 DIFFERENT posts in one call: each row goes out at its scheduled_at, or now, unless the project holds agent posts. Each row runs the full single-post path — allowance, approval — and reports on its own, so some rows can land while others refuse. A row's media is media_ids (from upload_media) or media_urls. A held row carries its own approval_link. An idempotency_key per row makes a resend after a timeout answer repeated: true instead of a second post. NOT for one post going to several accounts: that is ONE create_post with every account in accounts, with any per-platform wording in platform_configurations.<platform>.caption.
Per-platform engagement for one published post: views, likes, comments, shares, reach, saves, engagement, and the live URL. A null metric is one the platform does not report, not zero; empty data means the numbers have not synced yet (roughly every 6 hours).
Engagement over a window: totals, per platform, per format. coverage says how much of the window the totals rest on: with_metrics + unreportable + awaiting = deliveries. Stories are never reported on by Instagram or Facebook, so format: "story" totals are zero however many went out. ever_delivered separates an empty window from an empty history.
Best-performing deliveries ranked by a metric, with caption, platform, username, metrics and live URL — what worked for this audience, as a study aid rather than a template.
Posting slots (weekday × hour, UTC) ranked by this workspace's own past engagement. signal says what the order is worth: none means no engagement is recorded and the order is arbitrary; thin means some slots rest on one or two posts (samples); ok means three or more each.
What your agent should expect
- · pending_approval is not an error. In copilot mode every agent post waits for a human. The right behavior is to tell the user and stop — not retry.
- · 422 = the post was refused. The violations name the reason. Fix it and try once; never try to get around it.
- · Check receipts. Delivery is per platform — report partial failures honestly.
- · Learn before drafting.
get_top_postsandget_best_timesread this project's own results — use them to pick angles and times.