cometpost MCP server

Connect Claude, ChatGPT, Claude Code or Cursor to the cometpost MCP server, with every tool and its arguments.

Seventeen tools that let an agent discover constraints, reuse media, find earlier posts, draft, edit and schedule social posts through cometpost.

It is a client of the public /v1 API, exactly like the web dashboard is (ADR-0006). It holds no database connection, no platform token and no Credential of its own: every tool call forwards the caller's own bearer Credential to /v1 and returns what the API said. Anything this server can do, a customer's own script can do with the same key — and anything it cannot do is a gap in the API rather than a gap here.

The tools are shaped by the domain, not by the routes. For the raw HTTP API behind them, fetch the OpenAPI 3.1 description at https://api.cometpost.app/v1/openapi.json (no key needed to read it) or read docs/api/v1.md.

Connecting a client #

The server is hosted at https://mcp.cometpost.app/mcp and speaks Streamable HTTP. It accepts two kinds of bearer token in the Authorization header: an OAuth access token from a connected assistant, or a cometpost cp_live_ Credential. Both reach the same tools with the same constraints.

Claude and ChatGPT: connect by URL #

No key to copy. The assistant discovers cometpost's sign-in itself, sends you to Google, and asks what it may touch.

  • Claude: Settings → Connectors → Add custom connector → https://mcp.cometpost.app/mcp
  • ChatGPT: enable developer mode, then add a connector with the same URL

On the consent page you choose, once:

  • Channels: specific Channels, or all of them including ones connected later. A connected assistant only ever sees the Channels it can use, so cometpost_list_channels is the honest list of what is available to it.
  • Let it schedule posts without my approval: off by default. Off, the assistant creates Posts that land as drafts and a human publishes them. cometpost_create_post says so in its answer rather than pretending it scheduled something.

Consent is remembered. Reconnecting an assistant that is already connected skips the consent page and keeps its grant. To change its Channels or scheduling, revoke it in Settings → Connected assistants in the dashboard and connect again. Revoking takes effect on its next request. Access tokens last an hour and refresh tokens 30 days, rotated on use; the assistant handles that itself. The full contract is in docs/api/v1.md under OAuth agents.

Claude Code, Cursor and scripts: a pasted key #

Mint a Credential in the dashboard and send it as the bearer token:

{
  "mcpServers": {
    "cometpost": {
      "url": "https://mcp.cometpost.app/mcp",
      "headers": { "Authorization": "Bearer cp_live_7f3a…" }
    }
  }
}

The same two things are chosen when the key is minted and cannot be changed from here: may_schedule (off by default, so Posts land as drafts) and the Channel scope.

The tools #

ToolArgumentsReturns
cometpost_list_channels—The Channels this Credential may publish through: id, handle, platform, status
cometpost_list_platforms—Supported caption limits, media requirements and Publish Options
cometpost_prepare_channelchannel_idWhat one Channel can do right now: legal Publish Options, problems standing between it and publishing (e.g. Instagram Page Publishing Authorization), and publishing quota where the platform volunteers it. A problem is a result, not an error
cometpost_list_media—Workspace media IDs, readiness and expiry for reuse
cometpost_create_postbody, channels, scheduled_for?, media_ids?, variants?, publish_options?, idempotency_key?Post id, whether it was scheduled or left as a draft, the effective time, and one sentence to relay. Idempotent: every call carries an Idempotency-Key (generated, or your idempotency_key), so repeating a call whose reply was lost with the same key returns the original Post
cometpost_validate_postthe content arguments of cometpost_create_post (no idempotency_key)A dry run through the same checks as create: valid: true with outcome (scheduled, or draft for a credential that cannot schedule), or valid: false with the reason and each coded problems entry. Creates nothing and cannot know whether the platform will accept the content at publish time. A scope or permission refusal (403), rate limit or server fault is a tool error, as for create
cometpost_edit_postpost_id, optional body, variants, publish_options, media_ids, channelsSaved changes; null variant reverts to the base, frozen destinations retain their snapshot
cometpost_list_postsstatus?, channel_id?, from?, to?, range_on?, q?, limit?, cursor?Posts newest first, compact: id, state, caption preview (280 characters), Delivery count, next publish time, plus next_cursor. Default page is 20. from/to apply to creation time, or to publish time with range_on: "scheduled"
cometpost_get_postpost_id, detail?Per-Channel outcome, permalinks, failure reasons, and the Delivery ids. detail: "full" adds the caption, per-platform variants, media, publish options, provenance and every Delivery's Attempt history
cometpost_cancel_postpost_idHow many still-pending Deliveries were cancelled, and which had already started publishing and could not be
cometpost_reschedule_deliverydelivery_id, effective_atThe new time, or why it can no longer move
cometpost_schedule_postpost_idRe-arm unscheduled destinations after a workflow-start failure
cometpost_upload_mediasource_url, or data + content_typeA Media Asset id to pass to cometpost_create_post (URL up to 64 MiB, inline up to 5 MiB)
cometpost_start_media_uploadfilename, content_type, size_bytes, width?, height?A presigned upload_url and exactly what to PUT to it (method, Content-Type, body length), for a file over 64 MiB (up to 512 MiB) or with no public URL. Only for an agent that can make an HTTP PUT. The tool never receives the file
cometpost_complete_media_uploadmedia_idJudges what landed and returns the asset: ready with size, type and video facts, or the reason it was refused. media_not_uploaded means the PUT has not arrived; safe to repeat
cometpost_list_webhooks—GET /v1/webhooks: each webhook endpoint's id, URL, subscribed events and whether it is enabled. Never a signing secret. Needs a credential that covers every Channel
cometpost_list_webhook_deliveriesendpoint_id?, status?, event_type?, limit? (default 20), cursor?GET /v1/webhook-deliveries: newest first, each with event id and type, status, attempts, last HTTP status and a short reply snippet, plus next_cursor. Read only: registering, rotating and redelivering are REST-only

cometpost_create_post answers compactly on purpose — an id, a time and a sentence, not the Delivery array. That is an order of magnitude fewer tokens for information a model does not use, and the detail is one cometpost_get_post away. cometpost_get_post is compact by default for the same reason, and detail: "full" is opt-in because the Attempt history and variants can run long for a Post with several failed retries; ask for it when reading a post before editing it or explaining a failure. Draft approval and deliberate terminal-failure retry require a human session in the dashboard. The prototype that proved this surface (#21) is the evidence: a fresh agent scheduled and then cancelled a Post without ever calling cometpost_get_post, because the create result already carried what cancelling needed.

cometpost_upload_media takes a URL, or small bytes inline #

source_url is the normal path: cometpost fetches the media itself, behind the SSRF guards in docs/api/v1.md. data is base64 for a small image, up to 5MB.

Large or local files: cometpost_start_media_upload and cometpost_complete_media_upload #

URL pull stops at 64 MiB and needs a public URL, so a 100 MiB video on the agent's disk had no way in. Intake limits are stated in cometpost_list_platforms as mediaIntake (direct upload 512 MiB, URL pull 64 MiB, inline 5 MiB), and the large path is the REST presigned upload split at its edges:

  1. cometpost_start_media_upload with the file's filename, content_type and exact size_bytes returns upload_url and what to send.
  2. The agent PUTs exactly size_bytes bytes to upload_url with the Content-Type header, for example curl --fail --upload-file clip.mp4 -H 'Content-Type: video/mp4' '<upload_url>'. The bytes go from the agent straight to storage.
  3. cometpost_complete_media_upload with the id judges what landed and returns the asset; only a ready one can go in media_ids.

This is only for an agent that can make an HTTP PUT (a shell, code execution, a CLI). One that cannot should ask the human for a public URL and use cometpost_upload_media. The MCP server never receives the file in this workflow: the tools carry the name, type, size and the URL, so it stays a thin client over /v1 and not a data plane (ADR-0006). Opening an upload reserves its declared size against the Workspace's daily media allowance, exactly as the REST call does.

Deliberately not tools #

  • Connecting a Channel — needs a human completing a platform consent screen. A tool that cannot succeed is worse than no tool.
  • Publishing a draft — costs a session, always. Publishing is terminal.
  • Anything touching billing — no agent should change what a Workspace pays.
  • Minting a Credential — a key that can mint keys has no blast radius.

Errors #

Every failure arrives in the API's own shape:

{ "code": "may_not_schedule", "message": "…", "retryable": false }

message states the next action, not just the fault, and retryable is the one judgement an agent cannot make for itself.

Two optional fields are added where the API provides them. A rate limit (rate_limited) carries retry_after_seconds, taken from the API's Retry-After, plus rate_limit (limit, remaining, reset_at) when the API reports it, so a retry can be scheduled instead of guessed. A refused request with individually coded content problems (content_rejected) carries them as problems. Each MCP request spends at least two API requests against the daily allowance (the transport's credential check, then the tool's own call), so the allowance is not a count of tool calls.

One case is worth knowing about. A resource outside the Credential's Channel scope answers 404, not 403 — deliberately indistinguishable from "no such thing", because 403 would let a leaked key enumerate the Workspace by watching which ids it confirmed. So a not_found message names scope as a possible cause: check cometpost_list_channels rather than retrying. An agent that reads "not found" and retries forever is worse off than one told its key is too narrow.

Running it #

Local development; production runs the same code as a Railway service (.railway/railway.ts, built from the root Dockerfile with APP=mcp).

pnpm --filter @cometpost/mcp dev      # tsx watch
pnpm --filter @cometpost/mcp start
VariableDefaultMeaning
COMETPOST_API_URLhttps://api.cometpost.appWhere /v1 answers. Use http://127.0.0.1:8787 for a local API.
MCP_PORTPORT, else 8788The port /mcp and /health listen on. Railway injects PORT; set MCP_PORT only to override it locally.
MCP_HOST127.0.0.1The bind address. Production sets 0.0.0.0.
MCP_ALLOWED_HOSTS—Comma-separated hostnames, without ports. Required when MCP_HOST is not loopback. Production: mcp.cometpost.app,healthcheck.railway.app (Railway's healthcheck sends the second as Host; a browser cannot be made to, so allowing it costs nothing).
MCP_PUBLIC_URL—Where clients reach this server, e.g. https://mcp.cometpost.app. With OAUTH_ISSUER, turns on OAuth discovery.
OAUTH_ISSUER—The authorization server clients sign in with: https://app.cometpost.app/api/auth.

Set MCP_PUBLIC_URL and OAUTH_ISSUER together or not at all. Both set, /mcp answers a token-less request with 401 and a WWW-Authenticate header naming /.well-known/oauth-protected-resource, which names the issuer. Neither set (a local run), it sends the plain Bearer challenge and no connector can find its way in. This server holds no secrets: these are public URLs, and it never verifies a token itself, it forwards the token to /v1.

The API needs MCP_PUBLIC_URL too (the same value) so the tokens it issues name this server as their resource, and OAUTH_REGISTRATION_ENABLED=true before any assistant can register. See docs/deploy-railway.md.

/v1 lives on api.cometpost.app and not on app.cometpost.app: only /api/* is proxied through the dashboard, because routing agent traffic through it would make an agent's latency and availability depend on a Next.js deployment that exists for humans (ADR-0012).

MCP_ALLOWED_HOSTS is not optional paperwork. Binding off loopback means a browser on someone else's machine can be pointed at this server, and the allow-list is the DNS-rebinding defence; the process refuses to start without one rather than warning into a log nobody reads. The check reads the Host header on every route, /health and the discovery metadata included, and compares the hostname with the port stripped: MCP_ALLOWED_HOSTS=localhost admits Host: localhost:8788. A wrong value looks like a healthcheck that never passes, with Invalid Host in the response body.

Every MCP request verifies the bearer, key or OAuth token alike, against GET /v1/channels before dispatch. That costs one extra API call per request, and buys a real 401 with WWW-Authenticate at the transport layer — which is where a client looks — for a key that is revoked or expired. Learning the same fact inside a tool call would surface a dead key as a tool error for the model to puzzle over.

Tests #

pnpm test                             # the whole workspace
pnpm vitest run apps/mcp              # just this app

src/server.test.ts drives the tools through a real MCP client over a real transport, so the schemas, the dispatch and the result shapes are the shipping ones. src/app.test.ts drives the HTTP entry with the official Streamable HTTP client — the negotiation the prototype proved by hand, run on every commit.

/v1 is stubbed in both, which is the boundary this app is built on: it imports no database, no domain and no API internals, so what these tests prove is what the API's HTTP contract lets them prove. The enforcement behind that contract — that a scoped Credential cannot read or write outside its scope — is proven against real routes and real Postgres in apps/api/src/authorization.test.ts.

This page is generated from the product documentation. Spotted a mistake? Email hello@cometpost.app.