---
name: postbean
description: Draft, schedule, edit and publish social media posts through a Postbean workspace over MCP, and connect the Postbean MCP server first if its tools are not available yet. Use when the user mentions Postbean, or asks to draft, schedule or publish a post to LinkedIn, Instagram, Facebook, Threads, Bluesky, X, TikTok, YouTube, Pinterest, Google Business or dev.to through Postbean.
---

# Postbean

Postbean is a social media scheduler: one workspace to write, schedule,
publish and measure posts across the networks its user connected. The MCP
server exposes the same actions the web app performs — on that user's
workspace, under that user's permissions, spending that user's AI credits.

This copy was served by `https://postbean.app`, so every address below points there.

## Are the tools connected?

Call `list_accounts` before anything else.

- It returns accounts → go to **Using the tools**.
- There are no `postbean` tools at all → **Install**, below.
- `401 Unauthenticated` → the API key is missing, wrong, or was revoked.
  Start at step 1.
- An error naming a permission (`… permiso requerido: posts:publish`) → the
  key works but was issued without that scope. Scopes are fixed when a key
  is created, so this needs a new key, not a retry.

## Install

### 1. Ask the user for an API key

The server authenticates with a workspace API key, and only the account
holder can issue one — this step is theirs, not yours. Ask them to:

1. Sign in at https://postbean.app
2. Open **Settings → Developers** (https://postbean.app/panel/settings/developers). The developer API is a paid
   add-on; that page states the price.
3. **Create API key** — name it after the client you are installing into
   (`claude-code`, `cursor`, `apx`…), choose the **project** the key
   operates on, and tick only the permissions you need (see below).
4. Copy the token. It is shown once and never again.

Never echo the token back in your replies and never put it in a file that
gets committed. Ask for it in the environment instead:

```bash
export POSTBEAN_API_KEY='<token>'
```

### 2. Register the server

There are two servers. Same tools, same key, same workspace — they differ
in where they run, which only matters when you have a file to upload:

|                   | Runs on              | `upload_media({ path })`                                        |
| ----------------- | -------------------- | --------------------------------------------------------------- |
| **Hosted (HTTP)** | Postbean's servers   | Cannot see your disk; a local file goes up through REST (below)   |
| **Local (stdio)** | Your machine, `npx`  | Reads your files directly. Needs Node 18+                         |

Pick the hosted one unless the agent uploads files it has locally.

#### Hosted (HTTP)

|           |                                                              |
| --------- | ------------------------------------------------------------ |
| Endpoint  | `POST https://postbean.app/mcp/postbean`                                      |
| Transport | Streamable HTTP, JSON-RPC 2.0 (`GET` and `DELETE` answer 405) |
| Auth      | `Authorization: Bearer <token>`                               |

**Claude Code**

```bash
claude mcp add --transport http postbean https://postbean.app/mcp/postbean --header "Authorization: Bearer $POSTBEAN_API_KEY"
```

**Cursor, Claude Desktop, or any client with an MCP JSON config**
(`~/.cursor/mcp.json`, `.mcp.json`, `claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "postbean": {
      "type": "http",
      "url": "https://postbean.app/mcp/postbean",
      "headers": { "Authorization": "Bearer ${POSTBEAN_API_KEY}" }
    }
  }
}
```

Use the literal token there only if the client does not expand environment
variables in its config — Claude Code and Cursor do.

**APX**

```bash
apx mcp add postbean --transport http --url https://postbean.app/mcp/postbean --header "Authorization: Bearer $POSTBEAN_API_KEY"
apx mcp run postbean list_accounts
```

#### Local (stdio)

Published on npm as `@postbean/mcp`, so `npx` fetches it — nothing to
clone or build. It takes the key in the environment (as
`POSTBEAN_API_TOKEN`) instead of a header, and `POSTBEAN_BASE_URL` points
it back at the deployment that served you this file.

**Claude Code**

```bash
claude mcp add postbean --env POSTBEAN_API_TOKEN=$POSTBEAN_API_KEY --env POSTBEAN_BASE_URL=https://postbean.app -- npx -y @postbean/mcp
```

**Cursor, Claude Desktop, or any client with an MCP JSON config**

```json
{
  "mcpServers": {
    "postbean": {
      "command": "npx",
      "args": ["-y", "@postbean/mcp"],
      "env": {
        "POSTBEAN_API_TOKEN": "${POSTBEAN_API_KEY}",
        "POSTBEAN_BASE_URL": "https://postbean.app"
      }
    }
  }
}
```

**APX**

```bash
apx mcp add postbean --scope runtime --command npx --env POSTBEAN_API_TOKEN=$POSTBEAN_API_KEY --env POSTBEAN_BASE_URL=https://postbean.app -- -y @postbean/mcp
```

### 3. Verify

Call `list_accounts` again. If the client caches its tool list, restart it
first — a missing tool after a correct install is almost always that.

## Uploading a local file

On the hosted (HTTP) server, `upload_media` needs something *it* can reach.
When your file has no public URL, send the bytes in three calls — same
key, same `drafts:write` scope, no hosting involved. (On the stdio server
this section is unnecessary: pass `path` and it does exactly this for you.)

```bash
# 1. Reserve. size_bytes is required and is what the limits are checked against.
curl -s -X POST https://postbean.app/api/media/upload-url \
  -H "Authorization: Bearer $POSTBEAN_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"file_name":"clip.mp4","mime_type":"video/mp4","size_bytes":41236994}'
# -> {"media_id":51,"upload_url":"https://..."}

# 2. Send the bytes to that URL. No Authorization header (the URL is already
#    signed), but Content-Type MUST match the mime you declared in step 1.
curl -s -X PUT "<upload_url>" -H 'Content-Type: video/mp4' --data-binary @clip.mp4

# 3. Confirm. Returns the media; dimensions and thumbnail follow in background.
curl -s -X POST https://postbean.app/api/media \
  -H "Authorization: Bearer $POSTBEAN_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"media_id":51}'
```

Then attach it with `update_post(post_id, media_ids=[51])`. The limits are
the same on both routes: 100 MB per video, 8 MB per image.

This is not a way around a tool that refused you — it is the same key under
the same scopes, and a missing permission fails here exactly as it would
there. It is how bytes get in when there is no URL to give.

## Permissions

A key can only do what it was ticked for, and a scope cannot be added to an
existing key. Ask for the narrowest set that does the job: a key that writes
drafts but cannot publish or delete is a real safety net, both for the user
and for you.

| Scope | What it allows |
| ----- | -------------- |
| `accounts:read` | View connected accounts |
| `credits:read` | View credits |
| `posts:read` | Read posts and renders |
| `drafts:write` | Create and edit drafts |
| `posts:publish` | Publish to networks |
| `posts:delete` | Delete posts |
| `ai:generate` | Generate content with AI |
| `campaigns:generate` | Generate campaigns |

## Using the tools

The schemas arrive with the tools. These are the things they do not say:

- **Ids.** A post id is a public ULID (`01…`). Account ids and media ids are
  integers.
- **Times are the user's, never UTC.** "5pm" means five in the workspace's
  zone. Write it exactly like that — `2026-08-28 17:00`, no offset — and it
  is read as wall-clock time there. A datetime that *does* carry `Z` or an
  offset is taken as that precise instant, so never staple a `Z` onto a
  local hour: `17:00Z` asked for from Argentina is 14:00 local, an hour
  already gone, and the post goes out on every network at once. That is a
  real incident, not a hypothetical. `get_workspace` names the zone and
  gives the local clock; every response carrying dates repeats it
  (`timezone`, `now`, `utc_offset`) and returns its dates in that zone with
  the offset attached. A time in the past is refused, and the refusal tells
  you what time it is there. To publish now, use `publish_post` — not a
  date that has already passed.
- **Edit, never re-create.** `create_post_draft` always makes a *new* draft.
  Changing an existing post — caption, media, schedule, accounts — is
  `update_post`. Creating a second draft to "edit" one is the mistake this
  API sees most.
- **`media_ids` is the final list**, not an addition. What you send is what
  the post keeps.
- **Media.** `upload_media` takes exactly one of `url`, `path` or `base64`.
  Video goes by `url`; base64 is for small images only. Passing `post_id`
  attaches the file and, with `replace` (default true), drops the previous
  ones.
- **`path` is read by the server process, not by you.** On the local
  (stdio) server that is your own machine, so `path` works. On the hosted
  HTTP one it is Postbean's, where your file does not exist — there,
  `path` will never find it. When the file is only local and you are on
  HTTP, do not go set up hosting for it: send the bytes yourself, below.
- **A `url` upload downloads in the background.** The tool answers straight
  away with `ingest_status: pending` and the file lands seconds later. Poll
  `get_media` until it reads `ready` (or `failed`, which carries the
  reason). Never re-send `upload_media` because the first call felt slow:
  that uploads the file twice. Publishing waits on its own — a queued post
  goes out as soon as its media is there.
- **TikTok.** Call `get_tiktok_creator_info` first and use the audiences it
  returns: `tiktok.privacy_level` is required and an invented value is
  rejected. Comments, duet and stitch default to on unless the account has
  them blocked, and commercial content needs its disclosure fields before
  the post is accepted.
- **Publishing is explicit.** Call `publish_post` only when the user asked
  for it in those words. Everything else ends as a draft or a scheduled post.
- **AI tools spend credits.** `get_credits` shows the balance; with none
  left, the tool refuses instead of charging.
- **A permission error is an answer, not an obstacle.** If a tool says the
  key lacks a scope, or that the user may not touch that post, tell the
  user. Do not retry it, do not reach for another tool to get around it,
  and do not go looking for the REST endpoint behind it: `/api/*` takes
  the same key and enforces the same scopes, so the only thing at the end
  of that road is the same refusal. What unblocks it is a new key, and
  only the user can issue one.

## Full reference

- Docs: https://postbean.app/docs/support/api/
- This file: https://postbean.app/mcp/skill.md — re-fetch it to update the skill.