Home / Docs / MCP server
MCP server

The Postozo MCP server

MCP (Model Context Protocol) is the standard way for AI apps to use other tools. Add Postozo to your AI app and ask it to plan, check and schedule posts in plain English.

Developer docs: Overview, API guide, API reference, MCP server, Webhooks, CLI, OAuth for apps. Machine readable: openapi.json.

Server URL

https://app.postozo.com/mcp

Setup steps for each app are on the AI agents pages: Claude, Claude Code, ChatGPT, Cursor, VS Code and Windsurf. This page is the technical reference behind them.

Sign-in

MethodHowUse it when
OAuth (best)Paste the URL in your app. The app finds the sign-in details, registers itself (dynamic client registration), opens a Postozo page where you sign in and press Allow, and gets a pz_oat_ token. Details on the OAuth page.Your app supports OAuth for remote MCP servers, like Claude and Claude Code.
API key as Bearer tokenSend Authorization: Bearer pz_live_YOUR_KEY with every request. A CLI token (pz_cli_) works the same way.Your app lets you add headers, like Cursor, VS Code and Claude Code.
Key in the URLhttps://app.postozo.com/mcp/pz_live_YOUR_KEYOnly when your app can neither sign in nor send headers.

Why the key in the URL is less safe: a URL is easy to leak. It ends up in app settings, browser history, screenshots, shared config files and the logs of proxies between you and us. Anyone with it can use your whole workspace. Postozo hides the key in its own logs, but it cannot clean the others. If you use it, never share the URL, and rotate the key in Settings, Developers if it leaks.

Without a token, the server answers 401 with a WWW-Authenticate: Bearer resource_metadata="https://app.postozo.com/.well-known/oauth-protected-resource" header. That header is how apps find the sign-in. A token that belongs to a person acts with their role in the workspace; the workspace key acts as an admin.

Transport

  • Streamable HTTP, stateless. Send each JSON-RPC message as a POST to /mcp. There is no session: no Mcp-Session-Id header, and every request can go to any server.
  • The Accept header must list both application/json and text/event-stream, or the answer is 406. Answers come back as plain JSON (application/json), not as a stream.
  • Notifications (messages without an id) get 202 with no body.
  • GET and DELETE on /mcp answer 405 with Allow: POST, because there is no stream or session to open or close.
  • Protocol version 2025-06-18 is used in the examples below. The server answers with the version it agrees on in the initialize reply.
  • Limit: 600 requests a minute per workspace. Over it, the answer is HTTP 429 with a Retry-After header in seconds.
  • Errors before a request reaches MCP (no token, a paused team member, the limit, GET or DELETE) are JSON with error and msg.

The server sends these instructions to the AI in its initialize reply:

Postozo schedules social media posts. Start with list_channels. Read get_channel_rules before posting to a network. Use find_free_slot when no time is given. Confirm text, channels and time with the user before schedule_post with type schedule or now.

Try it with curl

curl -X POST "https://app.postozo.com/mcp" \
  -H "Authorization: Bearer pz_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"my-app","version":"1.0"}}}'
{
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": {},
      "prompts": {}
    },
    "serverInfo": {
      "name": "postozo",
      "version": "0.1.0"
    },
    "instructions": "Postozo schedules social media posts. Start with list_channels. Read get_channel_rules before posting to a network. Use find_free_slot when no time is given. Confirm text, channels and time with the user before schedule_post with type schedule or now."
  },
  "jsonrpc": "2.0",
  "id": 1
}

Then call a tool. The result has the data twice: as JSON text in content (for the AI) and as structuredContent (lists are wrapped in { "items": [...] }). Failures come back as a normal result with isError: true and { "error": "...", "msg": "..." } (the same message twice, as in the API), plus fix when there is 1; rule problems add problems.

curl -X POST "https://app.postozo.com/mcp" \
  -H "Authorization: Bearer pz_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"find_free_slot","arguments":{"channelIds":["de53f029-255d-4104-b090-974efcca2c86"],"tzOffsetMinutes":330}}}'
{
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\n  \"date\": \"2026-10-01T03:30:00.000Z\"\n}"
      }
    ],
    "structuredContent": {
      "date": "2026-10-01T03:30:00.000Z"
    },
    "isError": false
  },
  "jsonrpc": "2.0",
  "id": 2
}

find_free_slot reads each channel's posting times as local times of day. tzOffsetMinutes: 330 is India (UTC+5:30), so a posting time of 9:00 comes back as 03:30 UTC. Without tzOffsetMinutes, the time zone in the profile of the person who signed in is used (the owner's for the workspace key), or UTC. It gives the same answer as GET /find-slot in the API.

When the workspace cannot use the API

MCP follows the same rule as the public API. When the workspace has no plan (the trial ended without a plan, or a cancelled plan ended), or the 7 day grace after a failed payment is over, initialize, tools/list and the prompts still answer, so your AI app can connect and show the problem. Every tools/call is refused with the API's message and fix, and the API's HTTP status in status (401 for no plan, 402 for a failed payment):

{
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{ \"error\": \"No subscription found\", ... }"
      }
    ],
    "structuredContent": {
      "error": "No subscription found",
      "msg": "No subscription found",
      "code": "no_subscription",
      "fix": "The workspace owner should choose a plan on the Billing page of the app. Nothing was deleted.",
      "status": 401
    },
    "isError": true
  },
  "jsonrpc": "2.0",
  "id": 3
}

Nothing is deleted. Tool calls work again as soon as the owner picks a plan or the payment goes through. During the 7 day grace everything works.

The tools

14 tools only read. The 6 that change data (schedule_post, change_post_date, retry_post, delete_post, upload_media_from_url and generate_image) have readOnlyHint: false, and delete_post also has destructiveHint: true, so your app can ask you before it runs them. This list is read from the server code when this page is built.

list_channels

List the connected social channels (accounts, pages) with their id, network, name and status. Call this first to get channel ids.

No arguments.

list_networks

List every network this server supports and whether it can be connected. Useful to explain what is possible.

No arguments.

list_customers

List customers. A customer groups channels (agencies use 1 per client).

No arguments.

get_channel_rules

Get the posting rules for 1 channel: max length, media needs, whether comments are supported, and the settings schema (some settings are required, for example a title on YouTube or a board on Pinterest). Read before scheduling to that channel.

ArgumentTypeRequiredDescription
channelIdstringYesChannel id

find_free_slot

Find the next free posting time for these channels, using each channel's posting times (times of day in the user's local time). Returns an ISO date in UTC.

ArgumentTypeRequiredDescription
channelIds[]array of stringNoChannel ids. Leave out for all channels.
tzOffsetMinutesnumberNoThe user's time zone as minutes to add to UTC to get local time: 330 for India (UTC+5:30), -300 for New York in winter (UTC-5), 0 for UTC. Leave out to use the time zone in the user's profile.

check_post

Check a post against every channel's rules without saving it. Returns a list of problems (empty means it is fine).

ArgumentTypeRequiredDescription
typestring
draft, schedule, now, review
Yesdraft = save only. schedule = publish at "date". now = publish right away. review = send to the client for approval first.
datestringNoWhen to publish, ISO 8601 with timezone, for example 2026-10-01T09:30:00+05:30. Not needed for type now. Use find_free_slot to pick a good time.
contentstringNoThe main post text. Plain text. Used for every channel unless the channel entry gives its own content.
media[]array of stringNoMedia ids from upload_media_from_url or list_media.
thread[]array of objectNoFollow-up comments or thread items, posted after the main post in order.
thread[].contentstringYes
thread[].media[]array of stringNoMedia ids
thread[].delaynumberNoMinutes to wait after the previous item.
channels[]array of objectYesWhere to post. Each entry needs channelId. Add content, media or settings only to change them for that channel. Read get_channel_rules first to learn the required settings.
channels[].channelIdstringYes
channels[].contentstringNo
channels[].media[]array of stringNoMedia ids for this channel only
channels[].settingsobjectNoNetwork settings, keys from get_channel_rules.
tags[]array of stringNoTag names.
repeatDaysnumberNoRepeat the post every N days. Leave out for no repeat.
shortLinkbooleanNoReplace links with trackable short links.
groupIdstringNoTo edit an existing post, pass its id.

schedule_post (changes data)

Create or edit a post for 1 or more channels. Supports drafts, scheduling, posting now, client review, threads with delays, per-channel text, network settings, tags and repeats. Returns the post id, its status and a preview link.

ArgumentTypeRequiredDescription
typestring
draft, schedule, now, review
Yesdraft = save only. schedule = publish at "date". now = publish right away. review = send to the client for approval first.
datestringNoWhen to publish, ISO 8601 with timezone, for example 2026-10-01T09:30:00+05:30. Not needed for type now. Use find_free_slot to pick a good time.
contentstringNoThe main post text. Plain text. Used for every channel unless the channel entry gives its own content.
media[]array of stringNoMedia ids from upload_media_from_url or list_media.
thread[]array of objectNoFollow-up comments or thread items, posted after the main post in order.
thread[].contentstringYes
thread[].media[]array of stringNoMedia ids
thread[].delaynumberNoMinutes to wait after the previous item.
channels[]array of objectYesWhere to post. Each entry needs channelId. Add content, media or settings only to change them for that channel. Read get_channel_rules first to learn the required settings.
channels[].channelIdstringYes
channels[].contentstringNo
channels[].media[]array of stringNoMedia ids for this channel only
channels[].settingsobjectNoNetwork settings, keys from get_channel_rules.
tags[]array of stringNoTag names.
repeatDaysnumberNoRepeat the post every N days. Leave out for no repeat.
shortLinkbooleanNoReplace links with trackable short links.
groupIdstringNoTo edit an existing post, pass its id.

list_posts

List posts on the calendar between 2 dates (default: last 7 days to next 30 days).

ArgumentTypeRequiredDescription
startstringNoISO date
endstringNoISO date
customerIdstringNoOnly this customer

get_post

Get 1 post with its text per channel, state per channel (queued, published, error...), links to the live posts, errors with a fix, comments and history.

ArgumentTypeRequiredDescription
idstringYesPost id

change_post_date (changes data)

Move a post to a new date and time.

ArgumentTypeRequiredDescription
idstringYesPost id
datestringYesNew ISO 8601 date

retry_post (changes data)

Retry the channels of a post that failed or could not be confirmed. Only use after the user confirms the post is not already live.

ArgumentTypeRequiredDescription
idstringYesPost id

delete_post (changes data)

Delete a post from the calendar. Posts already live on a network stay live there.

ArgumentTypeRequiredDescription
idstringYesPost id

upload_media_from_url (changes data)

Import an image or video from a public URL into the media library. Returns a media id to use in schedule_post.

ArgumentTypeRequiredDescription
urlstringYesPublic http(s) URL of the file

list_media

List files in the media library.

ArgumentTypeRequiredDescription
searchstringNoFilter by name
typestring
image, video
Noimage or video

list_tags

List tags used to label posts.

No arguments.

get_analytics

Get account statistics for 1 channel from its network (7, 30 or 90 days). Some networks do not share statistics.

ArgumentTypeRequiredDescription
channelIdstringYesChannel id
daysnumber
7, 30, 90
No

get_post_analytics

Get statistics for 1 published post on 1 channel. Use the per-channel post id from get_post (channels[].root.id).

ArgumentTypeRequiredDescription
postIdstringYesPer-channel post id

get_workspace_stats

Counts from this workspace: posts by state, published posts per channel, and short link clicks.

ArgumentTypeRequiredDescription
daysnumberNo

run_channel_tool

Run a network helper for 1 channel, for example list Pinterest boards, Reddit flairs or YouTube playlists, to fill a setting. get_channel_rules lists the tools for each channel.

ArgumentTypeRequiredDescription
channelIdstringYesChannel id
toolstringYesTool name
dataobjectNoTool input

generate_image (changes data)

Create an image with AI from a description and save it to the media library. Uses 1 image credit. Returns a media id. Only listed when the server has AI images turned on.

ArgumentTypeRequiredDescription
promptstringYesWhat the image should show

The 2 prompts

PromptWhat it doesArgumentText sent to the AI
plan_weekPlan a week of posts for my channelstopic (required): What the posts are aboutPlan 5 posts for the next 7 days about: <topic>. List my channels first, check each network's rules, pick free slots, and show me the plan as a table. Save them as drafts only after I say yes.
repurposeTurn a link or text into posts for each channelsource (required): A link or the text to reuseTurn this into 1 post per connected channel, following each network's rules: <source>. Show me the drafts before saving.

prompts/get with a name that is not in this list gets a JSON-RPC error with code -32602 (invalid params), for example {"code": -32602, "message": "MCP error -32602: Unknown prompt nope. Use prompts/list to see the names: plan_week, repurpose."}.

Example conversations

"Schedule our autumn menu post on Instagram and Facebook tomorrow morning." A well behaved app does this:

  1. list_channels to find the Instagram and Facebook channel ids.
  2. get_channel_rules for each, and learns that Instagram needs a photo or video.
  3. Asks you for a picture, or imports 1 with upload_media_from_url.
  4. check_post with the text, media and channels. An empty problems list means it fits.
  5. find_free_slot with your time zone offset when you did not give an exact time.
  6. Shows you the post and the time, and after your yes calls schedule_post:
{
  "type": "schedule",
  "date": "2026-10-01T09:30:00+05:30",
  "content": "Our autumn menu is here. Pumpkin soup, apple pie and hot cider.",
  "media": [
    "281e94f5-fe31-40ed-8718-8dd3d1fbcd6d"
  ],
  "channels": [
    {
      "channelId": "de53f029-255d-4104-b090-974efcca2c86"
    },
    {
      "channelId": "f451da5e-2cd3-4674-bbbb-dc6bc5e12b55",
      "content": "Our autumn menu is here. See you this weekend!"
    }
  ]
}

"Why did my LinkedIn post fail yesterday?" The app calls list_posts for yesterday, then get_post, which returns each channel's error with a fix, for example "This channel is missing a permission." and "Reconnect the channel and accept all requested permissions."

"Move everything on Friday to Monday." list_posts for Friday, then change_post_date for each post, after you confirm.

Local server for Claude Desktop

Apps that only start local servers can use the postozo mcp command. It reads JSON-RPC on standard input and passes each message to https://app.postozo.com/mcp with your CLI sign-in.

{
  "mcpServers": {
    "postozo": {
      "command": "postozo",
      "args": [
        "mcp"
      ]
    }
  }
}
FAQ

Questions

Which AI apps work?

Any app that supports remote MCP servers over HTTP. We have step by step guides for Claude, Claude Code, ChatGPT, Cursor, VS Code and Windsurf.

Can the AI publish without asking me?

The server tells the AI to confirm text, channels and time with you before it schedules or publishes, and the 6 tools that change data are marked so your app can ask before it runs them. Ask for drafts or client review when a person must check first.

Do I need an API key?

No, if your app supports OAuth sign-in: paste the server URL and sign in. Apps without OAuth can send the API key as a Bearer token.

Does the server keep a session?

No. It is stateless: every POST is handled on its own, so there is no session id and GET or DELETE on /mcp answer 405.

What about Claude Desktop with a local server?

Run postozo mcp from the CLI. It is a small local server that passes every message to the remote server, so you get the same tools.

Get your API key

Start a 7-day free trial. The API, MCP server, webhooks and CLI are in every plan.

Create your free account