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
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 sendinitializefirst; same URL, the server answers in their dialect. Mcp-Methodis required on every request. AddMcp-Namefortools/callandprompts/get(=params.name) and forresources/read(=params.uri). A mismatch is400 -32020.- The
_metaenvelope needsprotocolVersionandclientCapabilities, both prefixedio.modelcontextprotocol/;clientInfois optional but recommended — it shows up in the workspace audit trail. Acceptshould list bothapplication/jsonandtext/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 withisError: true; the machine code sits in_meta["com.popkit/error"].code.
The tools
Workspace & accounts
| Tool | Scope (any) | Tier | Idempotency | What it does |
|---|---|---|---|---|
| get_workspace_context | workspace:read | read | — | Who am I, which workspace, role, available platforms, quota left, and this credential's scopes and publish tiers. Call it first. |
| list_accounts | accounts:read | read | — | Connected social accounts, groups, authorization status and expiry. |
Reading content
| Tool | Scope (any) | Tier | Idempotency | What it does |
|---|---|---|---|---|
| list_posts | posts:read | read | — | Drafts, in-review, scheduled, published and failed posts; cursor-paginated; filter by status and platform. |
| get_post | posts:read | read | — | One post in full: body, per-platform variants, media, per-account delivery results. Poll it after publishing. |
| check_publish_constraints | posts:read | read | — | 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
| Tool | Scope (any) | Tier | Idempotency | What it does |
|---|---|---|---|---|
| create_post | posts:write schedule:write publish:execute | write / publish (by action) | required for publish | Create a post with action = draft / schedule (with scheduledAt) / publish. publish requires clientRequestId; OAuth (class A) credentials must also complete confirm_publish. |
| update_post | posts:write schedule:write publish:execute | write / publish (by action) | required for publish | Edit an existing post (body, variants, media, tier); same rules as create_post. |
| attach_media | posts:write | write | optional (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_covers | posts:write | write | optional (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_cover | posts:write | write | optional (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_post | schedule:write posts:write | write | optional (recommended) | Move a scheduled post, or pull it back to draft. |
| submit_for_approval | approvals:write | write | optional (recommended) | Send a draft into the approval flow (the natural endpoint for editor-role credentials). |
Publishing
| Tool | Scope (any) | Tier | Idempotency | What it does |
|---|---|---|---|---|
| confirm_publish | publish:execute | publish | required | Second step of two-step confirmation: { postId, confirmToken } → the post actually goes out. Token is single-use, valid 10 minutes. |
| publish_post | publish:execute schedule:write | publish | required | Publish an existing post, or retry one failed account. |
Comments
| Tool | Scope (any) | Tier | Idempotency | What it does |
|---|---|---|---|---|
| list_comments | comments:read | read | — | Comments on published posts (optionally unreplied only); each carries canReply and why not. |
| reply_comment | comments:write | publish | required | Reply 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
| Tool | Scope (any) | Tier | Idempotency | What it does |
|---|---|---|---|---|
| list_ideas | ideas:read | read | — | Idea groups and ideas from the planning board. |
| create_idea | ideas:write | write | optional (recommended) | Add an idea to the planning board; no external side effects. |
| update_idea | ideas:write | write | optional (recommended) | Edit an existing idea; omitted fields keep their current values, and editors can only change their own. |
Analytics
| Tool | Scope (any) | Tier | Idempotency | What it does |
|---|---|---|---|---|
| get_analytics_summary | analytics:read | read | — | 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.
| Prompt | Needs | What it does |
|---|---|---|
| weekly_content_plan | accounts:read analytics:read posts:read | Plan this week's content from accounts and the last 30 days of data. |
| draft_from_idea | ideas:read posts:write accounts:read | Expand one idea into multi-platform drafts (ideaId supports completion). |
| postmortem_last_campaign | posts:read analytics:read | Post-mortem on the latest batch of published posts. |
| comment_triage | comments:read | Triage comments and suggest replies; only suggests replying when comments:write is granted. |
| Resource URI | Needs | Content |
|---|---|---|
| popkit://workspace/{workspaceId}/accounts | accounts:read | Account list |
| popkit://analytics/{workspaceId}/summary | analytics:read | 30-day metrics summary |
| popkit://post/{postId} | posts:read | One post (template) |
Rules that apply to every call
- Publishing is asynchronous —
create_post/confirm_publishreturn{ postId, status: "publishing", deepLink }right away; pollget_postfor per-account results. Stuck for ~15 minutes counts as failed. - Which accounts —
targetAccountIdsomitted means *every usable account* on the requested platforms (expired ones are skipped silently), capped at 20 per call. Pass it explicitly to choose. Onupdate_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 getinvalid_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_limitedwithretryAfterSec. - 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_limitedwithparams.reason = "comment_reply_cap". - Manual channels (Douyin / Xiaohongshu / WeChat MP) have no platform API:
publishonly records the post and reminds an admin to paste it — nothing goes out automatically. - Media cannot be an external link. Call
attach_mediafirst (direct upload or transload) to get amediaUrl, then pass it asmedia[].dataUrl.
What each failure means: Errors & fixes. What a credential is allowed to do: Auth & security.