MCP Server
Octopost provides a Model Context Protocol (MCP) server that lets AI assistants work with your Octopost account directly. Create posts, manage your queue, check connected accounts, and more from Claude, ChatGPT, your editor or your terminal.
Server URL: https://octopost.ink/mcp (Streamable HTTP)
Network status: X, Bluesky, Mastodon and LinkedIn (personal profiles) are live. Threads opens after Meta approves the Octopost app. Instagram, Facebook, TikTok, YouTube, Pinterest and Reddit are in platform review and not available yet. Discord, Slack and Telegram are not supported.
Two ways to connect#
- Sign in (OAuth). Add the URL to your client. The first time it connects, an Octopost page opens: sign in, check the account and click Allow. No key to copy. This is the only way that works in claude.ai, Claude Desktop and ChatGPT, and it works in Claude Code, Cursor, VS Code and Codex too. Every app you connect this way is listed under Connected apps on the API keys page; Disconnect there ends its access right away.
- API key. Send
Authorization: Bearer op_...with every request. Use this on CI boxes and in clients that can't sign in. Keys are on the API keys page: the account key at the top and any key you generate there both work. Scoped keys are refused; MCP needs a full-access key. If a key has default accounts set, MCP posts as those accounts unless the call names others.
Setup#
You only need one of these. Pick the app you use. Each one also has its own page with the exact steps: Claude Code, claude.ai, Claude Desktop, ChatGPT, Cursor, Codex, VS Code, Windsurf, Gemini CLI.
Claude Code#
claude mcp add --transport http octopost https://octopost.ink/mcpThen start Claude Code, run /mcp, pick octopost and choose Authenticate. Add --scope user to use Octopost in every project.
With an API key instead:
claude mcp add --transport http octopost https://octopost.ink/mcp --header "Authorization: Bearer op_YOUR_API_KEY"claude.ai and Claude Desktop#
- Open Customize > Connectors, click + Add and choose Add custom connector.
- Name it Octopost, paste
https://octopost.ink/mcpas the URL and click Continue. - Keep the settings Claude detects and click Continue. Choose Sign in now, leave the OAuth client on the recommended option, add no request headers and click Add.
- Sign in to Octopost and click Allow.
- In a chat, open the + menu below the message box, then Connectors, and switch Octopost on.
Connectors added on claude.ai also show up in Claude Desktop and the mobile apps. On Team and Enterprise plans an owner adds it first in Organization settings > Connectors (Add, Custom, Web); members then open Customize > Connectors, find Octopost and click Connect. Free accounts get one custom connector.
ChatGPT#
- In ChatGPT on the web, open Plugins, click +, and choose Add custom MCP server.
- Name it Octopost, paste
https://octopost.ink/mcpas the server URL, choose OAuth for authentication, and create it. - Sign in to Octopost in the window that opens and click Allow.
- In a chat, type @ and pick Octopost.
Custom MCP servers depend on your ChatGPT plan; on Business and Enterprise an admin may need to allow them.
Cursor#
Add to ~/.cursor/mcp.json (or .cursor/mcp.json in a project):
{
"mcpServers": {
"octopost": {
"url": "https://octopost.ink/mcp"
}
}
}Then open Cursor's MCP settings, find octopost and click Connect (or Needs login). With an API key, add "headers": { "Authorization": "Bearer op_YOUR_API_KEY" } next to url and keep the file out of git.
OpenAI Codex CLI#
codex mcp add octopost --url https://octopost.ink/mcp
codex mcp login octopostWith an API key instead, in ~/.codex/config.toml:
[mcp_servers.octopost]
url = "https://octopost.ink/mcp"
http_headers = { "Authorization" = "Bearer op_YOUR_API_KEY" }VS Code#
Add to .vscode/mcp.json in your workspace:
{
"servers": {
"octopost": {
"type": "http",
"url": "https://octopost.ink/mcp"
}
}
}Start the server from the file; VS Code asks you to sign in. With an API key, add "headers": { "Authorization": "Bearer op_YOUR_API_KEY" }.
Windsurf#
Add to ~/.config/devin/mcp_config.json (older Windsurf builds: ~/.codeium/windsurf/mcp_config.json):
{
"mcpServers": {
"octopost": {
"serverUrl": "https://octopost.ink/mcp",
"headers": {
"Authorization": "Bearer op_YOUR_API_KEY"
}
}
}
}Gemini CLI#
gemini mcp add --transport http --header "Authorization: Bearer op_YOUR_API_KEY" octopost https://octopost.ink/mcpAny other MCP client#
Add https://octopost.ink/mcp as a remote (Streamable HTTP) server. If the client supports MCP authorization it finds the sign-in on its own: the server answers an unauthenticated request with 401 and WWW-Authenticate: Bearer resource_metadata="https://octopost.ink/.well-known/oauth-protected-resource". Otherwise send the Authorization: Bearer op_... header.
Claude Code plugin (skill + server)#
The plugin bundles the server config and the Octopost agent skill, which tells Claude Code how to post, thread, schedule and queue for approval. Run these inside Claude Code, then run /mcp, pick octopost and choose Authenticate:
/plugin marketplace add octopost-ink/octopost-plugin
/plugin install octopost@octopostSource: github.com/octopost-ink/octopost-plugin (skill and config only, MIT).
Agent skill only#
Any agent that reads Agent Skills can add the skill. Connect the server separately with one of the options above:
npx skills add octopost-ink/octopost-pluginErrors you might see#
Every error says what happened and what to do next. The common ones on a first call:
- 401 "Octopost needs you to sign in": no credentials were sent. Sign in when your app asks, or send
Authorization: Bearer op_.... - 401 "does not match any Octopost account": the key was revoked, regenerated or mistyped. Copy a current one from the API keys page.
- 401 "this Octopost sign-in has expired": your app should refresh it on its own. If it doesn't, remove the connector and add the URL again.
- "no active plan": finish the $1 trial checkout on the billing page.
- "No X (twitter) account is connected": connect X on the accounts page, then retry.
- "scheduled_for ... has no timezone" or "is in the past": send an ISO 8601 time with an offset or
Z, in the future. - "content is N characters on X; the limit is 280": shorten it or split it into a thread with
segments(split_threaddoes it for you). Nothing was posted. - "has reached its limit of N posts a day" (
code: daily_cap): the account owner capped this key or app on the API keys page. Nothing was saved. - "This is the same text as post ..." (
code: duplicate): the same text went to that network in the last 7 days. Change it, or passallow_duplicate: trueif repeating it is intended. held: true, "Held for approval: contains blocked term": the owner's blocked terms (Settings) caught it. It's saved as pending approval and nothing was published; a person decides.
Available Tools#
Once connected, your AI assistant has these tools. Each one carries a title and read-only / destructive hints, so clients can ask before the ones that publish or delete.
Posts#
- create_post - Publish a post now, with optional thread segments
- schedule_post - Publish at an exact time
- publish_draft - Publish a saved draft, scheduled or queued post right now
- preview_post - Dry run: character counts per network, X units, media checks, which account posts, guardrails. Saves nothing
- split_thread - Split long text into thread parts that fit every network you name
- create_upload - One-time upload URL for a file on your machine; returns the
media_urlto attach - list_posts - List your posts, newest first, filtered by status, paged with
cursor/next_cursor - get_post - Everything about one post: text, segments, per-network variants, media with alt text, queue place, posting account, per-network results and links, last error
- update_post - Edit a draft or scheduled post
- delete_post - Delete a post (and the published copies where the network allows it)
- retry_failed - Retry the networks that failed on a post
- cancel_scheduled - Turn a scheduled post back into a draft
- reschedule_post - Move a scheduled post to a new time
Queue#
- queue_post - Add a post to the publishing queue, optionally held for approval
- approve_queued_post - Approve a post waiting for approval. Refused unless the account owner lets agents approve, and never for a post the same key or app made; people approve from the emailed link or the dashboard. See Approvals
- reject_queued_post - Take a queued post out of the queue (back to a draft) with an optional note. Posts waiting for approval are a person's to approve or reject
- reorder_queue - Set the order queued posts go out in
- get_queue_status - See what is queued and when the next post goes out
- update_queue_settings - Change queue interval, active hours, timezone, or pause it
Feeds and evergreen#
- list_feeds - The RSS/Atom feeds this account turns into posts. See RSS Feeds
- add_feed - Turn a feed into posts: each new item waits for approval (default) or joins the queue. The first check posts only the latest item
- remove_feed - Stop a feed (posts it already made are kept)
- set_evergreen - Mark a post evergreen: the queue posts it again when it runs empty, at most once every 30 days
Analytics#
- get_analytics - Likes, reposts, replies, quotes and X views: one post (with snapshots at about 1 hour, 24 hours and 7 days) or a summary over a date range with totals per network and the top posts. Works for X, Bluesky and Mastodon.
refresh: truereads fresh numbers first. See Analytics
Accounts and reference#
- list_accounts - See the connected social accounts (call this first)
- get_usage - X units used and left this month, connected accounts vs the plan, and this connection's daily post limit
- list_presets - Your saved publishing presets (platform groups you can pass to the create tools)
- get_guide - Guides on getting started, scheduling, the queue, and each network
What publishing tools return#
create_post and retry_failed wait for every network and return JSON text like this:
{
"success": true,
"status": "partial",
"postId": "6f1c2d3e-...",
"summary": "Published to X. Failed: Bluesky: Bluesky session expired. Reconnect the account.. Fix the cause above, then call retry_failed.",
"results": {
"twitter": { "platform": "twitter", "status": "published", "postId": "1234567890", "url": "https://x.com/you/status/1234567890" },
"bluesky": { "platform": "bluesky", "status": "failed", "error": "Bluesky session expired. Reconnect the account.", "errorCode": "AUTH_FAILED", "retryable": false }
},
"platforms": { "twitter": { "id": "1234567890", "url": "https://x.com/you/status/1234567890" }, "bluesky": { "id": "", "error": "..." } }
}statusispublished(live everywhere),partial(live on some networks) orfailed(live nowhere). The tool result hasisError: trueonly forfailed.resultshas one entry per network withstatus, the liveurl, orerror+errorCode+retryable. The codes are listed in Errors.- A publish that takes longer than about 20 seconds (a Threads post with media can take a few minutes) answers with
status: "publishing", thepostIdand whatever is live so far, and keeps going. Check it withget_postinstead of callingcreate_postagain.publish_draftandretry_failedwork the same way. summaryis one sentence an agent can repeat to the person, including what to do next.retry_failedrepublishes only what isn't live; live networks and live thread parts are never posted twice.get_postreturns the samestatusandresultsfor a stored post.list_postsgives each post'sstatus(which can also bescheduled,queued,pending_approval,retrying,publishingorneeds_review); callget_postfor itsresults.
create_post, schedule_post and queue_post take an optional idempotency_key (1 to 255 characters, a UUID works well). If the call times out and the agent repeats it with the same key, Octopost returns the original post with "idempotent_replay": true instead of posting again. Send one with every create_post: an agent whose client timed out can't tell whether the post went out, and the key makes repeating the call safe.
Example Usage#
Once set up, you can ask your AI assistant things like:
- "Post 'Just shipped a new feature!' to X"
- "Add this to my queue for later and hold it for my approval: 'Check out our new docs'"
- "Post about my new project and drop the URL as a reply so it doesn't get algorithmically down-ranked"
- "Show me my recent posts"
- "What accounts do I have connected?"
- "Update my queue to post every 3 hours"
- "Which of last week's posts did best? Write three more like it and queue them for my approval"
Threading via MCP#
create_post, schedule_post, queue_post, and update_post all accept a segments array for threaded posts. On X, Threads, Bluesky and Mastodon each segment becomes a reply in a chain; on LinkedIn each segment is added as a comment on the first post. The canonical use case is the "link in reply" pattern: put the main message in content and the URL in a segment, so the main post isn't penalized for containing a link on X/Threads.
{
"content": "I shipped a thing.",
"platforms": ["twitter"],
"segments": [
{ "content": "https://example.com" }
]
}See Threaded Posts for full details.
Different text per network#
create_post, schedule_post, queue_post, update_post and preview_post accept variants: a different version of the post for some networks. Every network you leave out gets the main post.
{
"content": "We just shipped v2: faster sync and offline mode.",
"platforms": ["twitter", "linkedin"],
"variants": {
"linkedin": { "content": "Today we shipped v2. Here is what changed and why..." },
"twitter": { "segments": [{ "content": "https://example.com/v2" }] }
}
}Each variant can set content, segments and media_urls (and on instagram, instagram_post_type: feed, carousel, story or reel); whatever it leaves out comes from the main post (segments: [] = a single post there, media_urls: [] = no media there). Each version is checked against its own network's limit, duplicates are compared per network, and blocked terms are checked in every version. get_post returns variants; in results, a network that got its own version has variant: true. On update_post, an object replaces all variants and null removes them. With cross-linking only the primary network's version is used.
Media, local files and alt text#
Every media_urls item (top level and in segments) is a public URL or {"url": ..., "alt": ...}. Alt text (up to 1,000 characters) goes to X, Bluesky, Mastodon, LinkedIn and Threads. Plain URL strings still work.
For a file on your machine, call create_upload, send the bytes to the upload_url it returns, then use the media_url from the response:
curl -T ./screenshot.png "https://octopost.ink/api/media-uploads/up_..."
# {"media_url": "https://octopost.ink/uploads/posts/.../1762....png", "type": "image/png", ...}{
"content": "New dashboard is live.",
"platforms": ["twitter", "bluesky"],
"media_urls": [{ "url": "https://octopost.ink/uploads/posts/.../1762....png", "alt": "The new dashboard with the weekly chart" }]
}The upload URL works once and for 15 minutes, needs no auth header, and takes JPEG, PNG, GIF, WebP, MP4, MOV or WebM (checked from the file itself) up to the plan's size limit (max_bytes: 25 MB, 50 MB on Premium, 100 MB on Business).
Guardrails#
The account owner can put limits on what agents post, and Octopost enforces them for MCP and the REST API:
- Daily post limit per key or connected app, set on the API keys page.
get_usageshows how many are left. - Blocked terms, set in Settings. A post that contains one is held for approval (
held: true,heldReason) instead of going out, through the same approval flow as any held post: the owner gets the approval email, and auto-approve rules don't apply to it. - Duplicate check: the same text (case and spacing ignored) to the same network within 7 days is refused, naming the earlier post. Pass
allow_duplicate: truewhen a repeat is intended.
preview_post reports all of these before you post.