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.
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
list_channelsfor the ids, andlist_providersfor the limits of the platforms behind them.- Compose.
validate_postwith the exact arguments you will send; fix whatrejectionsandproblemssay. create_postwith anidempotencyKey, so a retried call can never post twice.- The result has a target per channel.
published,failedandcancelledare final; for anything else (publishing,awaiting_platform,unknown) callget_postagain later. - A
failedtarget whosefailure.retryableistruecan go again withretry_post. One whoseactionisVERIFY_MANUALLYmay 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.