MCP integration

Tool reference

Every call is one POST. 20 tools, grouped by what they touch, plus the rules that apply to all of them.

The shape of a call

tools/call (2026-07-28)
POST https://popkit.loxily.com/api/mcp
Authorization: Bearer <token>
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: create_post
Accept: application/json, text/event-stream
Content-Type: application/json

{ "jsonrpc": "2.0", "id": 1, "method": "tools/call",
  "params": {
    "name": "create_post",
    "arguments": {
      "action": "schedule",
      "scheduledAt": "2026-09-20T10:00:00+08:00",
      "content": "…", "platforms": ["x", "youtube"],
      "clientRequestId": "acme-2026-09-20-001"
    },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": { "name": "acme-scheduler", "version": "1.4.0" },
      "io.modelcontextprotocol/clientCapabilities": {}
    } } }
  • 2026 clients have no handshake — your first message can be tools/list. 2025-era clients send initialize first; same URL, the server answers in their dialect.
  • Mcp-Method is required on every request. Add Mcp-Name for tools/call and prompts/get (= params.name) and for resources/read (= params.uri). A mismatch is 400 -32020.
  • The _meta envelope needs protocolVersion and clientCapabilities, both prefixed io.modelcontextprotocol/; clientInfo is optional but recommended — it shows up in the workspace audit trail.
  • Accept should list both application/json and text/event-stream: 2026 calls normally answer as plain JSON, but 2025-era calls are answered as SSE.
  • Tool output is in structuredContent. A business failure is still HTTP 200 with isError: true; the machine code sits in _meta["com.popkit/error"].code.

The tools

Workspace & accounts

ToolScope (any)TierIdempotencyWhat it does
get_workspace_contextworkspace:readread—Who am I, which workspace, role, available platforms, quota left, and this credential's scopes and publish tiers. Call it first.
list_accountsaccounts:readread—Connected social accounts, groups, authorization status and expiry.

Reading content

ToolScope (any)TierIdempotencyWhat it does
list_postsposts:readread—Drafts, in-review, scheduled, published and failed posts; cursor-paginated; filter by status and platform.
get_postposts:readread—One post in full: body, per-platform variants, media, per-account delivery results. Poll it after publishing.
check_publish_constraintsposts:readread—Dry-run publish check (no writes): per platform and account — length, media specs, authorization, rate caps, required fields. Same rules as a real publish.

Writing content

ToolScope (any)TierIdempotencyWhat it does
create_postposts:write schedule:write publish:executewrite / publish (by action)required for publishCreate a post with action = draft / schedule (with scheduledAt) / publish. publish requires clientRequestId; OAuth (class A) credentials must also complete confirm_publish.
update_postposts:write schedule:write publish:executewrite / publish (by action)required for publishEdit an existing post (body, variants, media, tier); same rules as create_post.
attach_mediaposts:writewriteoptional (recommended)Hand media to popkit: mode=upload returns a 5-minute direct-upload URL you PUT to; mode=transload takes a public image URL and copies it. Put the returned mediaUrl into create_post's media[].dataUrl.
suggest_coversposts:writewriteoptional (recommended)Suggest covers from a post's media: frames picked from its video (call twice — trigger, then fetch after ~10–30 s) or the best first image among 2+ images, ranked with a reason. Does not use AI quota.
set_post_coverposts:writewriteoptional (recommended)Apply a suggested candidate as the post's cover: videos get it as the cover (plus the TikTok cover frame), image posts get it moved to the first image.
reschedule_postschedule:write posts:writewriteoptional (recommended)Move a scheduled post, or pull it back to draft.
submit_for_approvalapprovals:writewriteoptional (recommended)Send a draft into the approval flow (the natural endpoint for editor-role credentials).

Publishing

ToolScope (any)TierIdempotencyWhat it does
confirm_publishpublish:executepublishrequiredSecond step of two-step confirmation: { postId, confirmToken } → the post actually goes out. Token is single-use, valid 10 minutes.
publish_postpublish:execute schedule:writepublishrequiredPublish an existing post, or retry one failed account.

Comments

ToolScope (any)TierIdempotencyWhat it does
list_commentscomments:readread—Comments on published posts (optionally unreplied only); each carries canReply and why not.
reply_commentcomments:writepublishrequiredReply to a comment as the workspace's social account — outbound, publish-tier rate limit; replying twice needs explicit force, and one comment takes at most 3 replies per 24 h.

Idea board

ToolScope (any)TierIdempotencyWhat it does
list_ideasideas:readread—Idea groups and ideas from the planning board.
create_ideaideas:writewriteoptional (recommended)Add an idea to the planning board; no external side effects.
update_ideaideas:writewriteoptional (recommended)Edit an existing idea; omitted fields keep their current values, and editors can only change their own.

Analytics

ToolScope (any)TierIdempotencyWhat it does
get_analytics_summaryanalytics:readread—Metrics overview and trends (optionally per account / date range).

Prompts and resources

Prompt templates package popkit's playbooks — the data is fetched and the rules are baked in, so the model does not have to be told twice. Resources are cacheable read-only snapshots. Both are pruned by scope, exactly like tools.

PromptNeedsWhat it does
weekly_content_planaccounts:read analytics:read posts:readPlan this week's content from accounts and the last 30 days of data.
draft_from_ideaideas:read posts:write accounts:readExpand one idea into multi-platform drafts (ideaId supports completion).
postmortem_last_campaignposts:read analytics:readPost-mortem on the latest batch of published posts.
comment_triagecomments:readTriage comments and suggest replies; only suggests replying when comments:write is granted.
Resource URINeedsContent
popkit://workspace/{workspaceId}/accountsaccounts:readAccount list
popkit://analytics/{workspaceId}/summaryanalytics:read30-day metrics summary
popkit://post/{postId}posts:readOne post (template)

Rules that apply to every call

  • Publishing is asynchronous — create_post / confirm_publish return { postId, status: "publishing", deepLink } right away; poll get_post for per-account results. Stuck for ~15 minutes counts as failed.
  • Which accounts — targetAccountIds omitted means *every usable account* on the requested platforms (expired ones are skipped silently), capped at 20 per call. Pass it explicitly to choose. On update_post, omitting the field keeps the existing selection.
  • Idempotency — write tools take clientRequestId (8–128 chars of [A-Za-z0-9_.:-]). A replay within 24 h returns the original result flagged _meta["com.popkit/replayed"]; it is required on the publish tier. The key is bound to the arguments: reuse it with different arguments and you get invalid_input.
  • Rate limits, each counted over a different scope: read 60 / write 10 / publish 5 per minute per credential × workspace × tool; 20 outbound publishes per day per credential × workspace; 600 s between two publishes to the same account per workspace — that last one is shared, so several credentials posting to one account throttle each other. Over any of them → rate_limited with retryAfterSec.
  • Comment replies are counted separately. The daily publish cap and the same-account interval do not apply to them — replying to a batch of comments is normal use and must not be throttled by quotas designed for posting content. The one limit that does apply: a single comment accepts at most 3 replies from you per 24 h (counted per platform comment, shared across workspaces), which bounds force; and 500 replies per day per credential × workspace as a backstop. Over it → rate_limited with params.reason = "comment_reply_cap".
  • Manual channels (Douyin / Xiaohongshu / WeChat MP) have no platform API: publish only records the post and reminds an admin to paste it — nothing goes out automatically.
  • Media cannot be an external link. Call attach_media first (direct upload or transload) to get a mediaUrl, then pass it as media[].dataUrl.

What each failure means: Errors & fixes. What a credential is allowed to do: Auth & security.