Docs › API reference › MCP server

MCP server: let an AI agent post through the API

Let an AI agent post through OmniPosts with the Model Context Protocol — the same operations, shapes and refusals as the API, as tools.

Updated

OmniPosts runs an MCP server at POST /mcp. An agent that connects to it gets tools for everything the API does for posting: see what each platform accepts, check a post, send or schedule it, follow each channel's result, retry, upload media from a link, and read metrics. The tools return the same JSON the API returns and refuse with the same codes, so what you read in these docs holds for the tools too.

Connect a client

The server speaks streamable HTTP and is stateless: every call carries its own credential, there is no session to start or keep. Any MCP client that supports streamable HTTP and can send a header works. For a step-by-step setup, see Connect an agent.

claude mcp add --transport http omniposts https://omniposts.dev/mcp \
  --header "Authorization: Bearer omniposts_..."
# ~/.codex/config.toml (or .codex/config.toml in a trusted project)
[mcp_servers.omniposts]
url = "https://omniposts.dev/mcp"
bearer_token_env_var = "OMNIPOSTS_API_KEY"

Codex sends the key from that environment variable as Authorization: Bearer …, so set OMNIPOSTS_API_KEY=omniposts_... where Codex runs. The Codex CLI and IDE extension share this file.

{
  "mcpServers": {
    "omniposts": {
      "url": "https://omniposts.dev/mcp",
      "headers": { "Authorization": "Bearer omniposts_..." }
    }
  }
}

Cursor and many other clients read the JSON config. Clients that can only sign in with OAuth, such as custom connectors in the Claude and ChatGPT apps, cannot connect yet: OAuth sign-in for agents is coming soon.

Use an API key with only the scopes the agent needs — posts:write, posts:read, channels:read and media:write cover posting — and give it an end date. See Authentication.

The tools

Tool Scope What it does
list_providers channels:read Every platform with its text limit, media limits, per-platform settings and whether it is open for posting. The same as GET /v1/providers
list_channels channels:read The connected accounts, with their health. GET /v1/channels
validate_post posts:write A dry run of create_post: the same checks, nothing written. POST /v1/groups/validate
create_post posts:write Publish now, at scheduledFor, or into the posting queue. POST /v1/groups
list_posts posts:read Posts newest first, filterable by status, channel and time. GET /v1/groups
get_post posts:read One post with a target per channel: status, link on the platform, failure. GET /v1/groups/{id}
cancel_post posts:write Cancel every channel not yet sent. POST /v1/groups/{id}/cancel
retry_post posts:write Re-queue the channels marked retryable. POST /v1/groups/{id}/retry
upload_media_from_url media:write Download a public https link and store it; use the id in mediaIds. POST /v1/media
start_channel_connect channels:manage Start connecting an OAuth platform; the result's handoffUrl is for a signed-in person to open. POST /v1/connect-sessions
get_connect_session channels:read Whether that connect is still pending, completed (channel set), failed or expired. GET /v1/connect-sessions/{id}
get_post_metrics metrics:read A target's platform counters. GET /v1/metrics/post/{targetId}

A tool whose scope the key lacks answers with an error result, code insufficient_scope, naming the scope.

What a good run looks like

  1. list_channels for the ids, and list_providers for the limits of the platforms behind them.
  2. Compose. validate_post with the exact arguments you will send; fix what rejections and problems say.
  3. create_post with an idempotencyKey, so a retried call can never post twice.
  4. The result has a target per channel. published, failed and cancelled are final; for anything else (publishing, awaiting_platform, unknown) call get_post again later.
  5. A failed target whose failure.retryable is true can go again with retry_post. One whose action is VERIFY_MANUALLY may be live on the platform: look there before doing anything, and never re-create the post.

Results and errors

Every tool returns structured content (the same JSON as the API endpoint in the table) and one sentence of text. A refusal is a tool result with isError and { "error": { "code", "message", "details" } }, using the codes in Errors: nothing_to_publish with its rejections, plan_limit_reached, posting_limit_reached, media_rejected, not_found, invalid_request, and so on.

A request with no key or a bad key is 401 at the HTTP level, before any JSON-RPC. Calls count against the WRITE rate limit (60 a minute per organization); a 429 carries Retry-After.

For agents reading this

/llms.txt points here and to /openapi.yaml. The REST API and the tools are the same system: a post created by a tool shows up in GET /v1/groups, in the web app and in webhooks, and the other way round.