Docs
Docs/AI Agents/MCP Server

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#

  1. 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.
  2. 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/mcp

Then 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#

  1. Open Customize > Connectors, click + Add and choose Add custom connector.
  2. Name it Octopost, paste https://octopost.ink/mcp as the URL and click Continue.
  3. 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.
  4. Sign in to Octopost and click Allow.
  5. 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#

  1. In ChatGPT on the web, open Plugins, click +, and choose Add custom MCP server.
  2. Name it Octopost, paste https://octopost.ink/mcp as the server URL, choose OAuth for authentication, and create it.
  3. Sign in to Octopost in the window that opens and click Allow.
  4. 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 octopost

With 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/mcp

Any 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@octopost

Source: 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-plugin

Errors 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_thread does 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 pass allow_duplicate: true if 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_url to 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: true reads 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": "..." } }
}
  • status is published (live everywhere), partial (live on some networks) or failed (live nowhere). The tool result has isError: true only for failed.
  • results has one entry per network with status, the live url, or error + 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", the postId and whatever is live so far, and keeps going. Check it with get_post instead of calling create_post again. publish_draft and retry_failed work the same way.
  • summary is one sentence an agent can repeat to the person, including what to do next.
  • retry_failed republishes only what isn't live; live networks and live thread parts are never posted twice.
  • get_post returns the same status and results for a stored post. list_posts gives each post's status (which can also be scheduled, queued, pending_approval, retrying, publishing or needs_review); call get_post for its results.

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_usage shows 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: true when a repeat is intended.

preview_post reports all of these before you post.

Related guides