MCP integration

Auth & security

What a credential is allowed to do, how to take it away, and where the boundary sits.

How capability is computed

The user's role in the workspace ∩ the credential's scopes — and for writes, ∩ the publish tiers ticked when the credential was issued. Scopes only ever narrow; a credential can never do more than the person behind it.

A tool you lack scope for does not appear in tools/list — it is not an error, it is simply absent. So «the tool is missing» always means scope, never a bug.

Every single call re-checks that the authorizing user (class A) or the key's creator (class B) is still an active, unbanned member of that workspace. Remove them from the team and the credential stops working on its next call — no extra revocation step needed.

Scopes

scopeGrants
workspace:readWorkspace info, member count, quota left
accounts:readConnected social accounts and their auth status
posts:readDrafts, schedules, publish history
posts:writeCreate / edit drafts, upload media
approvals:writeSubmit drafts for approval
schedule:writeSchedule posts
publish:executePublish immediately
comments:readRead comments on published posts
comments:writeReply to comments as the workspace's accounts
analytics:readMetrics and trends
ideas:readRead the idea board
ideas:writeWrite ideas to the board

The three publish tiers

actionNeedsWhat happens
draftposts:writeSaved as a draft. No external effect — start here.
scheduleschedule:writeQueued in one call, no confirmation step — popkit publishes for real at the time you set. scheduledAt must carry a timezone and be ≥ 60 s away. Counts against the monthly plan quota *and* a daily cap (class A shares the publish cap; class B has its own, wider one). The same-account interval does not apply.
publishpublish:executeGoes out now, irreversibly. Class A first gets status: "draft" + confirm.token and must call confirm_publish (single-use, 10 min); a class-B key issued as pre-authorized publishes in one step.

publish_post is a fourth path and takes one call with no confirmation, but only for content that is already approved (a human approved it in the app — there is no approve tool) or failed (a retry of something already sent once).

Tiers are ticked at issue time and cannot be widened later — reissue instead. get_workspace_context reports the ones you have under credential.allowedActions. Which accounts a publish reaches is covered in the calling rules (omitted targetAccountIds = every usable account, capped at 20).

Credential lifecycle

A · OAuth tokenB · long-lived key
Who issues itThe end user, on the consent screenA workspace admin or owner, in Settings
LifetimeAccess token 1 h, refresh 30 dUnlimited, or 1–365 days if you set an expiry
Where it is storedHeld by the clientShown once at issue; we store only a hash
Revoking itSettings → Connected systems. An already-issued access token stays valid for up to 1 hDisable or revoke — the next call fails, there is no grace period
If the person leavesStops working on the next callStops working on the next call; the key is disabled, not deleted, so the team can take it over

Admins are notified 7 and 1 days before an expiring key lapses, and when a key starts failing a lot — an unattended integration that quietly breaks is the failure mode worth catching early.

Boundary and practice

  • One credential sees one workspace — the workspace is fixed at issue time and cannot be switched by any parameter.
  • Treat popkit_it_… like a password: secret store only, never in a repo, never in a prompt. If it leaks, revoke and reissue — there is no way to recover the plaintext.
  • Writes that have an external effect are audited in the workspace; class-B calls are tagged «via external system \<credential name\>». The call log records input digests and key names, never your raw content.
  • A credential without accounts:read cannot read account handles — the publish precheck masks them, so it cannot be used to enumerate the workspace's accounts.
  • OAuth metadata (class-A SDKs read these automatically; you only need them if you hand-roll the flow): https://popkit.loxily.com/.well-known/oauth-authorization-server and https://popkit.loxily.com/.well-known/oauth-protected-resource/api/mcp.
Production advertises only authorization_code and refresh_token: there is no client_credentials path here. Unattended systems use a long-lived key. To identify a client without registering it, use CIMD — the client_id is an HTTPS URL serving the client's own metadata document, which popkit fetches at authorization time.