Skip to content

MCP server

POST /mcp, Streamable HTTP. One JSON-RPC message per request, one JSON response. No batches.

The server keeps no session and has no server-to-client stream: GET /mcp and DELETE /mcp answer 405.

Version How the client says it
2026-07-28 _meta["io.modelcontextprotocol/protocolVersion"] in every request, mirrored in the MCP-Protocol-Version header, plus Mcp-Method (and Mcp-Name for tools/call). No initialize; server/discover is available.
2025-11-25, 2025-06-18 initialize, then the MCP-Protocol-Version header.
2025-03-26 initialize, no header.

An unknown version gets error -32022 with the supported list in data.supported. A header that disagrees with _meta gets -32020.

Two kinds of bearer token:

  • Access tokens from pickpost’s sign-in service (Logto), obtained with the MCP authorization flow. Their audience is this server’s /mcp URL, so they work here and nowhere else. They carry the scopes the user approved.
  • Personal API tokens (pp_…), the same as the HTTP API. All tool scopes, in the workspace the token was created in. Approving, inviting and billing stay in the app.

An access token from the sign-in service acts in the workspace the user last opened in the app. Tools can also answer with a plan limit (for example scheduling to a network the workspace’s plan does not include): the result has isError: true and says what the plan does not cover and what lifts it.

Without a valid token, /mcp answers 401 with:

WWW-Authenticate: Bearer realm="pickpost", resource_metadata="https://your-pickpost-host/.well-known/oauth-protected-resource/mcp", scope="openid offline_access posts:read posts:write posts:publish accounts:manage"

error="invalid_token" is added when a token was sent but refused (wrong audience or issuer, expired, bad signature).

  1. The client reads the protected resource metadata (RFC 9728) at the URL in resource_metadata: resource, authorization_servers, scopes_supported.
  2. It reads the authorization server’s metadata at <issuer>/.well-known/openid-configuration. client_id_metadata_document_supported is true and code_challenge_methods_supported is ["S256"].
  3. It sends the user to the authorization endpoint with its client ID metadata document URL as client_id, PKCE, resource=<this /mcp URL> (RFC 8707) and the scope from the challenge.
  4. The user signs in and approves the listed permissions. The client exchanges the code for an access token.

Ask for openid along with the Pickpost scopes, as the challenge does: the sign-in service refuses metadata-document clients whose request lacks it. No refresh token is issued to such clients yet, so they repeat the flow after the token’s hour.

Scope Allows
posts:read read-only tools
posts:write creating and editing drafts
posts:publish asking to schedule, publish or delete posts (each becomes an approval card)
accounts:manage asking to disconnect an account (an approval card)

tools/list only returns the tools the token’s scopes allow (cacheScope: "private"). Calling another tool answers 403 with WWW-Authenticate: Bearer error="insufficient_scope", scope="openid offline_access <needed scope>", ..., so the client can ask the user for more.

Requests that carry an Origin header are accepted only from Pickpost’s own public URL. Anything else gets 403 “Origin not allowed”. This blocks web pages from calling a local server through your browser. Desktop and CLI clients send no Origin and are not affected.

tools/list returns the 15 tools in the tool reference. Tool names use _ where the REST API uses . (posts_create for posts.create).

Each tool carries annotations:

Annotation Set on
readOnlyHint: true Tools that change nothing (posts_list, accounts_list, …).
destructiveHint: true posts_delete, accounts_delete.
openWorldHint: true posts_schedule, posts_publishNow: they reach the social networks.

tools/call results carry both content (text for the model) and structuredContent (the action’s JSON output).

posts_schedule, posts_publishNow, posts_delete and accounts_delete never run from MCP. The call returns right away with:

{
"status": "pending_approval",
"action": "posts.schedule",
"approvalUrl": "https://your-pickpost-host/#/posts?thread=ec6fa4aa0e9e43f0"
}

in structuredContent, isError: false, and a text telling the model not to call it again. In the app, the request becomes a card in a conversation named “Requests from {client}”, and open tabs show a toast. The action runs as the user when they click Approve.

The client name comes from clientInfo (title, then name) in _meta, or from the User-Agent for older clients.

initialize and server/discover return instructions that tell the model to read first (accounts_list, providers_limits, posts_list), draft with posts_create/posts_update, check with posts_validate, and not to repeat calls that are waiting for approval.