MCP integration
Errors & fixes
Sorted by what you actually see. Read the HTTP status first — it tells you which of the three layers failed.
Three layers, three places to look
- HTTP layer —
401/400/404/405/415/403. The request never reached a tool. Wrong credential, or the wrong shape. - JSON-RPC layer — HTTP 200 but the body carries
error. Mostly-32602: that tool name does not exist *for this credential*. - Business layer — HTTP 200,
isError: true, machine code in_meta["com.popkit/error"].code. The call was understood and refused, or it failed downstream. This is the normal path for rate limits and permissions — branch on the code, don't treat it as an exception.
{ "isError": true,
"content": [{ "type": "text",
"text": "rate_limited: rate limit for create_post: 10 calls per 60s; retry after 42s {\"limit\":10,\"windowSec\":60,\"retryAfterSec\":42}" }],
"_meta": { "com.popkit/error": {
"code": "rate_limited",
"params": { "limit": 10, "windowSec": 60, "retryAfterSec": 42 } } } }HTTP layer
| You see | It means | Fix |
|---|---|---|
401 + WWW-Authenticate | No token, expired, or the credential was disabled / revoked. Auth runs before anything else, so a wrong token gives 401 even for a malformed request | Class A: run the authorization flow. Class B: check the key, ask an admin |
400 -32020 | Mcp-Method / Mcp-Name disagree with the body, or the header is missing | Send the header, make it match params.name (or params.uri) |
400 -32602 | The _meta envelope is missing a required key or the io.modelcontextprotocol/ prefix | Add protocolVersion and clientCapabilities, both prefixed |
404, empty body | MCP is not enabled in this environment, or the URL is wrong | Check the URL / environment |
404 -32601 | Method name misspelled (tools/lists…) | Check the spelling |
405 | You sent GET or DELETE. This endpoint is stateless — expected and harmless; the MCP SDKs treat it as normal | Nothing to fix if it came from an SDK's listener stream |
415 | Content-Type is not application/json | Set the header |
403 | The Origin header is not allowed (only browsers send one) | Call it server-side, or from the site's own origin |
Business machine codes
| code | Meaning | What to do |
|---|---|---|
| unauthenticated | No usable identity. Business tools are then absent from tools/list; calling one yields -32602 | Refresh / replace the credential; check authorized from ping |
| workspace_not_found | The workspace is gone or dissolved, or the authorizing user is no longer a member | Class A: re-authorize. Class B: ask an admin to reissue |
| forbidden | The role matrix, the credential's scopes or its publish tiers say no | Drop a tier (action:"draft"), or ask for more scopes / tiers |
| not_found | The object does not exist, or belongs to another workspace (not distinguished, to prevent enumeration) | Check the id |
| invalid_input | Bad input (length / enum / date / cursor / idempotency key); params carries the detail | Fix per params and retry |
| plan_limit | Plan quota exhausted (currently: monthly scheduled posts; retries of failed posts count too) | Wait for next month or upgrade — do not retry |
| no_account | No usable account on the target platform | Connect one in popkit first |
| needs_reauth | The account's platform authorization expired | A human must re-authorize in popkit; an external system cannot |
| rate_limited | A tier limit, the per-account interval, the daily publish cap, or one of the two comment-reply caps. params.reason names it: comment_reply_cap (too many replies on one comment within 24 h) or comment_reply_daily_cap (this credential's replies for the day are used up). The other sources carry no reason | Wait params.retryAfterSec, then retry — except on comment_reply_cap, where retrying will not help; reply to a different comment |
| replay | The previous call with the same clientRequestId is still running (the placeholder lives ≤ 2 min) | Wait a few seconds and retry with the same key — you get the original result |
| external_writes_off | Outbound publishing is switched off in this environment (platform channels only) | Drafts / schedules only, or switch environment |
| platform_error | The platform rejected or failed (policy / duplicate / 5xx); params.reason has a machine code | Not fixable by input; a human checks the deepLink |
| internal | Unclassified server error (logged) | Retry later; contact us if it persists |
By symptom
- Everything is 401 — the credential is the problem, not the network. If the
curlsmoke test passes with the same token, it is your client's header config. pingreturnsauthorized: false— the credential is valid but resolves to no workspace: it was revoked, or the person who created it is no longer a member. Not a scope problem.- A tool is missing from
tools/list— the credential lacks that scope. Class A: re-authorize and tick it. Class B: scopes are fixed at issue, ask an admin to reissue. forbiddenalthough the scopes are all there — either the role is too low, or that publish tier was not ticked. Checkcredential.allowedActionsfromget_workspace_context.invalid_inputonly on a retry — the sameclientRequestIdwas reused with different arguments. Retries must resend the *identical* payload; if the content changed, use a new key.- Published but nothing happened — publishing is asynchronous. Poll
get_postand readtargets[].status/errorCodeper account. - A flood of
GETrequests from n8n — old n8n versions leaked MCP sessions after a run. Upgrade to 2.33 or later.
Still stuck? Send us the failing call — request headers, request body and the full response, with the credential masked — plus what ping returns for the same credential. Those two together identify the layer almost every time. Setup for your specific client: Client setup.