# MCP Server · PostLake Docs > Markdown version of https://docs.postlake.dev/mcp . The canonical page for humans. > PostLake is the social media API for AI agents: https://postlake.dev/llms.txt `cancel_post` supports scheduled-only preview and snapshot fencing. Use `id` for one post, or `ids`, `profile`, `from` and `to` for an atomic scheduled batch. Preview with `dryRun: true`, inspect the selection, then apply its `snapshot`. It never retracts published content. Upload responses include media retention terms; `GET /v1/media/policy` exposes the complete policy. PostLake runs a hosted **MCP server**: the open standard that lets AI agents use external tools. Connect once and your agent can publish, schedule and pull analytics across every network, just by you asking in plain language. ## Server URL ``` https://api.postlake.dev/mcp ``` ## Connect in three steps - **Open your agent's connector settings.** In Claude: Settings → Connectors → Add custom connector. ChatGPT, Cursor and others have an equivalent "add MCP server" option. - **Paste the URL above.** - **Approve on the PostLake screen.** You're sent to a PostLake **Authorize** screen. Sign in and click Approve. OAuth means you do not need to paste an existing API key into the client. The agent then appears under Agents in your dashboard. ## OAuth compatibility ### Protocol compatibility and safe requests - PostLake supports MCP `2026-07-28` and legacy `2025-11-25`, `2025-06-18` and `2025-03-26`. Existing clients can keep their initialization flow. Modern clients use per-request metadata and `server/discover`. - For modern requests, send matching `MCP-Protocol-Version`, `Mcp-Method` and, for a tool call, `Mcp-Name` headers. Include protocol version, client information and capabilities in `params._meta`. Unsupported versions return `-32022`; header mismatches return `-32020`. - Use `application/json` and a single JSON-RPC message, not a batch or response. Request bodies are limited to 2 MiB. Upload file bytes through the signed upload URL, never inside a tool call. Accepted notifications return `202` with no body and cannot execute tools. - Both `/mcp` and the legacy API-key endpoint `/v1/mcp` preserve agent identity and publishing guardrails. OAuth-connected agents and restricted API keys cannot mint unrestricted keys. Unrestricted API-key automation retains its existing setup capability. - Tool annotations are conservative hints, not permissions or a promise that a write is safe to retry. Keep the original post ID, snapshot and idempotency key. Unknown or uncertain outcomes must be recovered by reading the receipt or post, not creating it again. - Native clients can omit `Origin`. Browser origins are validated; custom browser integrations need their origin configured. Expired credentials return `401` with OAuth discovery, while database failures are not reported as invalid credentials. PostLake supports both registration methods used by MCP clients. Newer clients can identify themselves with a Client ID Metadata Document (CIMD). Clients that still use Dynamic Client Registration (DCR), including current Cursor versions, continue to work. The client chooses automatically from PostLake's OAuth metadata, so you do not need different URLs or setup for each service. Both paths require PKCE and an exact registered callback. The approval screen shows where the result will be returned before you continue. DCR client names are self-declared, so only approve a connection you started and whose return destination you recognize. Access tokens last one hour and clients refresh them automatically. Signed channel-connection links created by `get_connect_link` expire after 30 minutes. The hosted page is for managing connections, not just reconnecting. Owners can add another account on an already-connected platform, reconnect expired channels, or disconnect accounts. The heading stays "Manage your connections"; reconnect warnings appear beside the affected channels. The link's profile and platform restrictions still apply. When the target is known, prefer `get_connect_link` with `profile`, plus `platforms` if only one network is needed. Omit `profile` when the human needs to manage multiple profiles. Unscoped links keep access to all profiles; a profile switcher shows one at a time, starting with the first profile that has a channel needing reconnection within the platform filter (otherwise the first profile). Scoped links show only the chosen profile, with no switcher. After the owner reconnects, call `list_social_accounts` to confirm the channel is active. Streamable HTTP clients (Cursor and others that send `Accept: application/json, text/event-stream`) receive HTTP 401 with a `WWW-Authenticate` challenge on first connect. That is what opens the host's Connect / login prompt. Directories may still probe `initialize` and `tools/list` as plain JSON without a token. A green "connected" status with no login means the client skipped that 401; reconnect so it can complete OAuth. ## Per-agent setup Pick your host for the exact steps. Each connects to the URL above over OAuth, so you do not need to paste an existing API key: Claude Code One command, then approve the browser page that opens: ``` claude mcp add --transport http postlake https://api.postlake.dev/mcp ``` Start a fresh session and run `/mcp` to confirm `postlake` is connected. Claude Desktop Open **Settings → Connectors → Add custom connector**, paste the server URL, and approve the browser sign-in. PostLake's tools then appear in any chat. Cursor Open **Settings → Tools & MCP → New MCP server** (this opens `mcp.json`), merge this in, and save: ``` { "mcpServers": { "postlake": { "url": "https://api.postlake.dev/mcp" } } } ``` Then open **Customize → MCPs** (or `Ctrl+Shift+P` → **Open MCPs**) and click **Connect** / **Needs login**. The first handshake returns 401 on purpose so that button appears. Sign in and click **Approve** once. Cursor Desktop may use its native `cursor://anysphere.cursor-mcp/oauth/callback` callback or a local loopback callback. PostLake supports both. For something running **unattended**, send an API key instead and skip the browser step entirely. The same URL takes either credential: ``` { "mcpServers": { "postlake": { "url": "https://api.postlake.dev/mcp", "headers": { "Authorization": "Bearer sk_live_..." } } } } ``` An API key does not expire, so nothing has to refresh it and nobody has to approve anything after the key exists. It still appears as a named identity in Agent Control, where its publishing limits are enforced. An OAuth token lasts an hour and is tied to one client, which is what you want when a person is present. ChatGPT In ChatGPT's developer mode / custom connectors, add a new MCP server pointing at the URL above, then approve the sign-in. Gemini CLI ``` gemini mcp add postlake https://api.postlake.dev/mcp ``` Then approve the browser page to authorize. VS Code · GitHub Copilot In Copilot **Agent mode → MCP servers → Add**, point it at the URL above and sign in. Any other MCP host Point its “add MCP server” action at `https://api.postlake.dev/mcp` with no `Authorization` header. OAuth signs you in through your browser. The hosted `/mcp` endpoint accepts OAuth access tokens only; a normal `sk_live_` REST API key does not unlock it. Clients that cannot complete browser OAuth send `Authorization: Bearer sk_live_...` to the same `https://api.postlake.dev/mcp` URL. One endpoint takes either credential. API keys have their own agent identity and owner-set guardrails. (`POST /v1/mcp` is the same server under an older path and still works, so existing configs need no change.) ## The toolset `manage_media` lists account-owned uploads and previews irreversible file erasure. Use `action=preview_delete` with an upload ID, obtain the owner's confirmation, then use `action=delete` with its snapshot. Drafts and pending publications remain protected. This does not remove social posts, cancel schedules or refund publishing credits. `delete_post` also accepts `dryRun` and `snapshot` for network-specific removal previews. See [the removal guide](https://postlake.dev/media#deletion). Once connected, the agent gets tools for the full social workflow. Responses use a consistent, model-readable format throughout: | Group | What the agent can do | | --- | --- | | **Identity, limits & releases** | Check the connected account, agent publishing limits, credit balance, and confirmed product releases before acting. Name the authenticated agent when the owner permits it. `whoami`, `set_my_name`, `get_credits`, `get_changelog` | | **Profiles & channels** | List and manage profiles, inspect channel destinations and shoppable products, find a Facebook Page before mentioning it, read ordinary tagged posts and your own posts, prepare, start, inspect, and end Facebook Page live broadcasts, connect supported credential-based channels, create a human approval link for OAuth channels, or disconnect a channel. `list_profiles`, `create_profile`, `rename_profile`, `delete_profile`, `list_social_accounts`, `get_social_account`, `list_account_targets`, `search_facebook_pages`, `list_products`, `check_allowance`, `list_tagged_posts`, `list_own_posts`, `list_events`, `create_event`, `create_live_broadcast`, `start_live_broadcast`, `list_live_broadcasts`, `get_live_broadcast`, `end_live_broadcast`, `connect_account`, `get_connect_link`, `disconnect_account` | | **Publish & schedule** | Upload media from a public URL or directly from the agent's local disk, preview a timezone-aware cadence, dry-run a post, publish now or later, create a scheduled calendar atomically, confirm an asynchronous publish, save a draft for a person to approve and publish it once they have, inspect history, edit or cancel queued posts, and retract something already live. `upload_media`, `upload_media_batch`, `preview_cadence`, `validate_post`, `create_post`, `create_scheduled_calendar`, `get_post`, `confirm_post`, `list_posts`, `edit_post`, `edit_scheduled_posts_bulk`, `publish_draft`, `cancel_post`, `delete_post` | | **Read & respond** | Read notifications and comments, mark notifications seen, reply to comments, hide a reply you do not want on your post, delete a comment, engage with posts or people, and update supported social profiles. `list_notifications`, `mark_notifications_seen`, `read_comments`, `reply_to_comment`, `hide_comment`, `delete_comment`, `engage`, `update_profile` | | **Discover** | Look at the network itself, not just your own corner of it: search public posts by keyword or topic tag, look someone up before replying to them, read what an account has been publishing, and find a place to tag on a post. `search_posts`, `look_up_profile`, `read_profile_posts`, `search_places`, `find_creators` | | **Direct messages** | Read threads, explicitly mark a handled thread as read, reply, or start a supported conversation from a handle. `list_conversations`, `read_conversation`, `mark_conversation_read`, `send_message` | | **Platform rules** | Read each platform's limits, media rules, options and live creator-level publishing constraints. `get_platform_capabilities`, `get_publish_info` | | **Analytics** | Per-post and cross-platform performance, to decide what to post next. `get_analytics`, `get_post_analytics`. Stored reports need no provider calls. Period and Content modes share profile, channel, network, agent, interface and date filters across totals, charts and breakdowns. Set `refresh: true` for background collection and progress. `sort` selects views, likes, comments, engagement, engagementRate, impressions or clicks across the full population before returning up to 20 destinations. Structured results include the full report and content-only averageViews with a measured-destination denominator. [Methodology and filters](https://postlake.dev/analytics). | | **Automation handoff** | Account-level API key callers can create another full-access key for a trusted service. OAuth agents cannot see or call this tool because it would bypass their owner-set limits. `create_api_key` | | **Safe by default** | Idempotent retries mean a retrying agent never double-posts, even at 3am. Reads say which networks they could not reach, so an empty result never quietly means "we did not look". | `set_my_name` updates only the authenticated agent's display name. The same name appears in Agent Control and post attribution. The account owner can lock it in Agent Control. It does not rename the API key or OAuth client. Meta advertising features are deferred. Ad account discovery, Facebook and Instagram branded-partner lookup, and Facebook Partnership Ads permissions and branded-content post discovery are not available through MCP. Calls from cached tool lists return unavailable. Their retained REST endpoints return HTTP 501 with error type `not_implemented`, code `meta_ads_deferred`, and `retryable: false`. Reconnecting cannot enable them. Organic publishing, analytics, ordinary tagged posts, and non-ad branded-content publishing are unchanged. For Instagram, inspect `connectVariant` and `accountType` with `get_social_account`. Stories support both login routes, subject to Meta account eligibility. Facebook Login Stories require a Business account; Instagram Login does not need a Facebook Page. A Story needs one JPEG or PNG image (PostLake converts PNG automatically) or MP4/MOV video, and `platformOptions.instagram.placement: "story"` on `validate_post` and `create_post`. Older connections may not report `accountType`; Meta checks eligibility when publishing. The Facebook route also adds discovery, product tagging and Facebook-only comment controls, subject to account eligibility. ## Upload a local file `upload_media` supports both remote and local assets. Pass `url` for an existing public asset. For a file on the agent's machine, omit `url` and pass `contentType` plus the file's `sizeBytes`. The tool returns a preallocated `med_…` id, a five-minute HTTPS PUT target and the exact headers to send. The agent uploads the bytes with its local HTTP client, then passes that media id to `create_post`. For example, after `upload_media({ "contentType": "video/mp4", "sizeBytes": 123456 })`, it runs: ``` curl --fail-with-body --request PUT "$UPLOAD_URL" \ --header "Authorization: Bearer $UPLOAD_TOKEN" \ --header "Content-Type: video/mp4" \ --upload-file "/absolute/path/video.mp4" ``` No temporary public bucket or base64 in the model context is needed. The signed target is account-bound, expires after five minutes, accepts only the prepared type and size, and cannot be reused after a successful upload. The local path stays on the agent's machine; only the bytes are sent to PostLake. For a carousel, call `upload_media_batch` with one item per slide instead of `upload_media` once per file. Each local item still returns its own five-minute PUT target. Then call `validate_post` before `create_post`. TikTok photo posts need JPEG or WebP (max 1080×1920); a PNG is converted to JPEG with a warning. Photo posts also need a title via `platformOptions.tiktok.title`. When nothing should publish unless every selected network can accept the content, send `requireAllTargets: true` to `create_post`. To schedule "7am in London", pass `timezone: "Europe/London"` with a naive `scheduledAt` such as `2026-08-13T07:00:00`, or set an account default timezone. The stored fire time is always UTC. A naive datetime without `timezone` (and without an account default) is refused. `edit_post` can change text, media, `platformOptions`, the fire time, and the exact destination list through `accounts`. Send every account you want to keep; new accounts inherit the scheduled time and are checked before saving. An agent requiring approval must ask the owner to change destinations. If a scheduled channel was disconnected, use `targetAccounts` to map that target's old `acc_…` id to the exact reconnected channel. PostLake will never guess between same-platform accounts. `list_posts` accepts status, account, profile, network, agent, surface, approval, date filters, and literal caption search using `q` (case-insensitive, up to 200 characters). Filters apply before bounded cursor pagination. Date endpoints are inclusive; pass the returned `nextCursor` to continue. If `delete_post` cannot retract a live post, the reason includes the live URL. For a large calendar, use `list_posts` with `state: "scheduled"` and `sort: "scheduledAt"`. Its cursor continues after an earlier scheduled post is cancelled. Use `cancel_post` with scoped `ids`, or REST `POST /v1/posts/bulk/cancel`, for atomic scheduled cancellation. Preview with `dryRun: true` and apply its full-post `snapshot`. This is not an atomic replacement with a new calendar. For campaign edits, `edit_scheduled_posts_bulk` accepts up to 200 exact scheduled post IDs with text, per-platform overrides, destinations, campaign or asset metadata, or `shiftMinutes` to move each UTC publish time. Preview with `dryRun: true`, inspect each before/after diff, then apply with the returned `snapshot`. If any selected post changes or starts publishing, none are edited. Storage changes together; timers are re-armed best-effort and the due-post cron is the backstop. `list_posts` also filters by exact `campaign` and `targetCount` (`single` or `shared`). Use create_scheduled_calendar to preview and atomically create up to 200 scheduled posts, then apply with the returned snapshot and an idempotency key. For a scheduled destination edit, first call `get_post` and include `expectedAccounts` with its current target account IDs in order. PostLake rejects stale edits instead of silently removing a newer selection. Approval keeps the original ID, campaign, metadata and options. Use `expectedSnapshot` from `get_post` with `publish_draft` to fence all reviewed content. Guarded agents cannot approve themselves. Call `get_post` with `idempotencyKey` instead of `id` for read-only receipt recovery. Cancellation does not release the key. See [approval identity and recovery](https://postlake.dev/publishing). For a multi-user product, an MCP agent can create one profile per customer with `create_profile`, mint a profile-scoped handoff with `get_connect_link`, confirm the result with `list_social_accounts`, and then publish, read analytics, or manage messages through the same profile. PostLake-managed OAuth is ready by default. Optional customer-owned platform OAuth secrets are intentionally configured only through the secure dashboard, REST API, or SDK so a model is never asked to retain permanent third-party credentials. ## What comes back List and action tools usually answer in compact markdown that your assistant can render as a real table. Complete records such as `get_post` and detailed capability lookups return structured JSON when every field matters. Every result carries the ids needed for the next call. **list_social_accounts** (ids shortened here to fit the page): ``` | id | profile | platform | handle | status | | --- | --- | --- | --- | --- | | acc_29fd5066… | postlake | bluesky | crumblelake.bsky.social | active | | acc_6fb037b0… | postlake | x | yourbrand | active | | acc_1f756183… | postlake | youtube | @postlake | active | 3 rows ``` **list_posts** gives per-network status on one line, so a partial failure is visible at a glance: ``` | id | state | when | profile | targets | text | | --- | --- | --- | --- | --- | --- | | post_bf85a34c… | published | 2026-08-02 20:26 | postlake | bluesky/crumblelake.bsky.social:published | Release update | | post_9f8e7d6c… | partial | 2026-08-01 14:12 | postlake | bluesky/crumblelake:published x/yourbrand:failed | Product launch | 2 rows ``` **create_post** tells you what happened per network, and spells out anything that did not go: ``` post_9f8e7d6c: partial | platform | state | url | | --- | --- | --- | | bluesky | published | https://bsky.app/profile/you/post/3ms4t | | x | failed | | x did not publish: Not enough credits for an X post with a link (90 needed). ``` When a tool fails you get the sentence first, then the facts your agent can act on, so it can choose differently instead of retrying the same call: ``` This agent may post to bluesky, linkedin. Not x. type: agent_not_permitted retryable: no, trying again will fail the same way refused by: the platforms limit your owner set you may use instead: bluesky, linkedin ``` ## What it looks like in use Once connected you can just say: ``` "Post this photo to Instagram and LinkedIn, schedule a follow-up for Friday at 9am, and tell me which of last week's posts did best." ``` ### Local media files An MCP server runs remotely and cannot read a path on your computer directly. For a local image or video, call `upload_media` with its MIME type and no `url`. PostLake returns a short-lived authenticated `uploadUrl`, the required `PUT` method and headers, and a ready-to-run upload instruction. Your agent can upload the local bytes without making the file public or obtaining your API key. The `PUT` response contains the `med_…` id to pass to `create_post`. For a carousel, use `upload_media_batch`. For media already on the web, pass its public `url` to `upload_media` as before. PostLake fetches and validates it server-side. ## Security & revoking access Agents authorise over OAuth, so you do not paste an existing API key into the client or a chat. Every connection you authorise is listed under **Agents** in your dashboard. Click **Revoke** next to any one to cut off its access immediately; it would need to re-authorise to return. An approved OAuth connection can use the social workflow tools listed above, but it cannot create an unrestricted account API key. This prevents an agent from escaping the limits its owner set. Only approve clients you trust, and revoke any connection you no longer recognise or need. ## Per-agent limits OAuth connections appear under **Agents** in the dashboard. You can restrict which profiles and networks each connection may publish to, set a daily post limit, control whether it may spend purchased credit packs, and require your approval before anything publishes. When approval is required, `create_post` saves a draft even if the agent omits `draft: true`, and that OAuth agent cannot publish the draft itself. The owner can review and publish it from PostLake or with an account-level API key. Call `whoami` to see the active policy before planning work. Existing connections keep their current direct-publishing behavior until the owner enables approval.