Repository avatar
Monitoring
v0.4.0
active

planpost

io.github.lukBB/planpost

Schedule posts to 7 social networks, render video and carousels, read analytics and revenue.

Documentation

planpost-mcp

MCP (Model Context Protocol) server for PlanPost, the agentic social scheduler with revenue attribution. It lets Claude, ChatGPT, Cursor, Gemini CLI and other AI agents schedule social media posts, create videos and carousels, manage content sources, and read analytics and revenue data by talking to your PlanPost account.

Quick start

The short way: sign in

Add https://mcp.planpo.st/mcp to your agent as a remote MCP server (a "connector" in Claude and ChatGPT) and leave the authentication fields empty. The first time the agent uses it, a browser opens on planpo.st: log in, review what the agent may do, and choose whether its posts wait for your approval. No key to create or paste.

claude mcp add --transport http planpost https://mcp.planpo.st/mcp
{ "mcpServers": { "planpost": { "url": "https://mcp.planpo.st/mcp" } } }

Connected agents are listed under Settings → API Keys → Connected apps, where you can remove one at any time. They use the same permissions and the same monthly call allowance as API keys.

The repository also ships ready-made bundles: a Claude Code plugin (.claude-plugin/), a Cursor plugin (.cursor-plugin/), a Gemini CLI extension (gemini-extension.json) and an agent skill:

npx skills add lukBB/planpost-mcp --skill planpost
gemini extensions install https://github.com/lukBB/planpost-mcp
claude plugin marketplace add lukBB/planpost-mcp && claude plugin install planpost@planpost-plugins

The rest of this section is the API-key route: use it for clients without OAuth support, for CI, or when you want a key limited to a few permissions.

1. Get an API key

In the PlanPost dashboard go to Settings → API Keys → Create Key and select the permissions you need (see the table below). The key (pk_live_…) is shown once — copy it.

API/MCP access is included in every paid plan — Creator: 1,000 calls/month, Pro: 10,000 calls/month. Keys are scoped: a key with only posts:read cannot publish.

2. Add the server to your agent

Two ways to connect. Hosted is the default: point your client at https://mcp.planpo.st/mcp, nothing to install, your key travels as a header. Local runs the same server on your machine with npx, so the key never leaves it. Both expose the same 49 tools.

Replace pk_live_xxx with your key throughout.

Claude Code

claude mcp add --transport http planpost https://mcp.planpo.st/mcp \
  --header "Authorization: Bearer pk_live_xxx"

Local fallback:

claude mcp add planpost -e PLANPOST_API_KEY=pk_live_xxx -- npx -y planpost-mcp

Run claude mcp list afterwards to confirm it is connected.

Claude Desktop

Hosted: Settings → Connectors → Add custom connector. Name PlanPost, URL https://mcp.planpo.st/mcp, then add the header Authorization: Bearer pk_live_xxx.

Local fallback, in claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/, Windows: %APPDATA%\Claude\), then restart the app:

{
  "mcpServers": {
    "planpost": {
      "command": "npx",
      "args": ["-y", "planpost-mcp"],
      "env": { "PLANPOST_API_KEY": "pk_live_xxx" }
    }
  }
}

Cursor (~/.cursor/mcp.json, or .cursor/mcp.json per project)

{
  "mcpServers": {
    "planpost": {
      "url": "https://mcp.planpo.st/mcp",
      "headers": { "Authorization": "Bearer pk_live_xxx" }
    }
  }
}

Local fallback: replace the entry with { "command": "npx", "args": ["-y", "planpost-mcp"], "env": { "PLANPOST_API_KEY": "pk_live_xxx" } }. A project-level .cursor/mcp.json is committed with your repo, so prefer the hosted form and keep the key out of the file.

OpenAI Codex CLI (~/.codex/config.toml)

[mcp_servers.planpost]
url = "https://mcp.planpo.st/mcp"
bearer_token_env_var = "PLANPOST_API_KEY"

Export PLANPOST_API_KEY in your shell; Codex reads the token from the environment, not the file. codex mcp add does the same thing interactively.

Local fallback:

[mcp_servers.planpost]
command = "npx"
args = ["-y", "planpost-mcp"]

[mcp_servers.planpost.env]
PLANPOST_API_KEY = "pk_live_xxx"

Gemini CLI (~/.gemini/settings.json, or .gemini/settings.json per project)

{
  "mcpServers": {
    "planpost": {
      "httpUrl": "https://mcp.planpo.st/mcp",
      "headers": { "Authorization": "Bearer pk_live_xxx" },
      "timeout": 10000
    }
  }
}

Gemini CLI uses httpUrl for streamable HTTP, not url; the wrong key is a silent no-op. Local fallback: the same command/args/env entry as Cursor.

Any other MCP client: connect to https://mcp.planpo.st/mcp (streamable HTTP, Authorization: Bearer pk_live_xxx on every request), or run npx -y planpost-mcp over stdio with PLANPOST_API_KEY set. Per-client walkthroughs with screenshots: planpo.st/docs/agents.

Self-hosting the HTTP endpoint

mcp.planpo.st is this same package running with --http. To run your own:

npx -y planpost-mcp --http --port 3020     # exposes POST /mcp (+ /health)

Behind a reverse proxy, bind to loopback so the port isn't reachable directly:

HOST=127.0.0.1 npx -y planpost-mcp --http --port 3020

The transport is stateless (streamable HTTP); credentials are read from every request and scoped to it, so one hosted instance serves many users safely. Put it behind HTTPS in production. --host / HOST default to 0.0.0.0, so containers and LAN use work without extra flags.

A self-hosted endpoint accepts API keys (Authorization: Bearer pk_...) out of the box. OAuth sign-in is switched on by three variables, and needs a PlanPost API that runs the authorization server (/oauth/*):

VariableDescription
PLANPOST_MCP_PUBLIC_URLPublic URL of this endpoint, e.g. https://mcp.example.com/mcp. Tokens are issued for exactly this URL.
PLANPOST_MCP_SERVICE_SECRET32+ random characters shared with the API (MCP_SERVICE_SECRET there).
PLANPOST_OAUTH_ISSUERAuthorization server origin. Defaults to PLANPOST_API_URL.
OPENAI_APPS_CHALLENGEOptional. Token served at /.well-known/openai-apps-challenge for ChatGPT directory domain verification.

With those set the server follows the MCP authorization spec: it serves /.well-known/oauth-protected-resource, answers unauthenticated requests with 401 and a WWW-Authenticate: Bearer resource_metadata="…" challenge, validates access tokens by introspection, checks they were issued for this endpoint, and never forwards them upstream.

3. Talk to it

"Schedule this post to my Polish Pages group for tomorrow 9:00: …"

"Make a quote video from this testimonial and publish it as a reel."

"Pull the latest posts from my WordPress blog and draft a LinkedIn carousel from the newest one."

"How much revenue did my posts drive last 30 days?"

Environment variables

VariableRequiredDescription
PLANPOST_API_KEYstdio onlyAPI key from Settings → API Keys (pk_live_…). Not needed with --http, where each request brings its own credentials.
PLANPOST_API_URL—API base URL (default https://api.planpo.st)

Tools

49 tools, grouped below. The permission column is the scope a tool needs, whether it comes from an API key or from OAuth sign-in; a credential without the scope gets a clear error rather than a silent failure. Every tool carries MCP annotations (a title plus read-only, destructive and open-world hints) so clients know which calls to confirm.

Posts & publishing

ToolRequired permissionDescription
list_postsposts:readList posts, filter by status/platform
get_postposts:readFetch one post
create_postposts:writeCreate a draft or scheduled post (per-platform content/settings/media, UTM overrides, timezone)
update_postposts:writeUpdate a draft/scheduled post
delete_postposts:writeDelete a post
publish_postposts:writePublish immediately
retry_failedposts:writeRe-queue the platforms that failed for a transient reason; never re-posts one that went out
get_delivery_reportposts:readThe morning check: recent failed or partly failed posts with each platform's error and fix, unhealthy connections, approvals running late
preview_postposts:readShow exactly what each platform will receive (text after overrides, attachments, warnings)
bulk_create_postsposts:writeCreate a whole campaign of posts from one manifest
list_campaignsposts:readCampaign tags in use, with post counts by status and platform
delete_postsposts:writeBulk-delete posts matching a filter (a filter is required)
list_mediamedia:readMedia already uploaded, so an asset can be reused instead of re-uploaded
upload_mediamedia:writeGet a presigned URL to upload an image/video
repurpose_contentrepurpose:writeSave repurposed content as draft posts

Accounts

ToolRequired permissionDescription
list_connectionsconnections:readConnected social accounts
list_account_groupsgroups:readNamed account groups (e.g. "Polish Pages")
get_account_groupgroups:readResolve a group by id/name/default → connection IDs
create_account_groupgroups:writeCreate a named group bundling connections
update_account_groupgroups:writeRename, recolour, set default, or replace a group's connections
delete_account_groupgroups:writeDelete a group (the accounts stay connected)
add_connections_to_groupgroups:writeAdd connections to a group
remove_connections_from_groupgroups:writeRemove connections from a group
get_connection_linkconnections:readLink the user opens in a browser to connect a new social account (OAuth needs a human)

Connecting a new social account requires OAuth in a browser: get_connection_link hands the user the link, and the agent can target the account once it is connected.

Approvals

ToolRequired permissionDescription
submit_for_approvalapprovals:writeSend a post to a human reviewer (required when the API key is approval-gated)
list_pending_approvalsapprovals:readPosts waiting for review, with outcomes
get_approval_statusapprovals:readPending / approved / rejected / changes requested for one submission

Scheduling

ToolRequired permissionDescription
get_best_timesschedule:readBest posting times per platform from the account's engagement history
suggest_scheduleschedule:readSpread N posts across a date range at optimal times; returns concrete timestamps
get_posting_scheduleschedule:readWeekly queue slots, global plus per-group overrides
set_posting_scheduleschedule:writeReplace the weekly queue slots for the global schedule or one group (Pro)

Video & carousels

ToolRequired permissionDescription
list_video_templatesvideos:readBrowse video templates and their editable fields
create_videovideos:writeSubmit a render job from a template
get_video_statusvideos:readPoll a render; completed renders return download URLs
generate_carouselcarousels:writeDecompose long-form content into carousel slide text
list_carousel_stylescarousels:readTemplate families, 40-palette library, formats, font pairings
render_carouselcarousels:writeRender slides into branded PNGs in the media library
get_carousel_rendercarousels:readPoll a render; completed renders return post-ready media assets
list_brand_kitsbrand-kits:readSaved brand kits (palette, fonts, logo, handle) for render_carousel
generate_imageimages:writeGenerate an image with AI for a post (1 AI credit); pass the URL to create_post

Content sources

ToolRequired permissionDescription
list_content_sourcescontent-sources:readList WordPress/RSS sources
add_content_sourcecontent-sources:writeConnect a WordPress site or RSS feed
delete_content_sourcecontent-sources:writeRemove a source
fetch_source_contentcontent-sources:writeImport the latest content now
list_imported_contentcontent-sources:readList imported content for repurposing

Analytics & revenue

ToolRequired permissionDescription
get_analyticsanalytics:readPost performance (impressions, likes, comments, shares)
get_revenue_overviewrevenue:readRevenue totals + trend; optionally recent transactions and Stripe/RevenueCat MRR

Revenue data is read-only over the API; connecting Stripe/RevenueCat/Shopify happens in the dashboard (Settings → Revenue).

Brand voice (Pro)

ToolRequired permissionDescription
get_brand_voicesbrand-voice:readTone-of-voice profiles analysed from the user's content
get_brand_voice_promptbrand-voice:readReady-to-use system prompt for a voice; apply it when drafting posts

A typical agent workflow

  1. get_account_group (default: true) → connection IDs
  2. list_video_templates → pick a template → create_video → poll get_video_status
  3. upload_media (or use the render URL) → create_post with scheduledFor
  4. Later: get_analytics and get_revenue_overview to report results

Or, for a branded carousel post end-to-end:

  1. generate_carousel (long-form content → slide text)
  2. list_carousel_styles / list_brand_kits → pick a family + palette or kit
  3. render_carousel → poll get_carousel_render → completed assets
  4. create_post with those assets as media (they're already attachment-shaped)

Development

npm install
npm run build      # tsc → dist/
npm test           # builds, validates server.json and the plugin manifests, boots stdio, exercises hosted OAuth mode
PLANPOST_API_KEY=pk_test_xxx node dist/index.js

The server speaks MCP over stdio, or streamable HTTP with --http. It calls the PlanPost public API (/api/public/v1) with your credentials; nothing else is stored or sent anywhere. Changes are listed in CHANGELOG.md.

Maintainers

Releasing a version means publishing twice: the code to npm, then the metadata to the official MCP Registry, which only stores server.json and checks that the npm package it points at exists and carries the matching mcpName. Glama and other directories import from the registry, so the second step is what makes a release discoverable.

  1. Bump version in package.json and in server.json (top level and packages[0].version), gemini-extension.json, .claude-plugin/plugin.json and .cursor-plugin/plugin.json, and add a CHANGELOG.md entry. npm test fails if any of them disagree.

  2. Publish to npm first; the registry rejects a version npm does not have yet:

    npm publish --access public
    
  3. Install the publisher CLI once (brew install mcp-publisher, or the tarball from the registry's GitHub releases), then log in. The io.github.lukBB/* namespace is proven by GitHub device-flow login as that account:

    mcp-publisher login github
    
  4. Validate and publish from this directory:

    mcp-publisher validate
    mcp-publisher publish
    
  5. Confirm the listing:

    curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.lukBB/planpost"
    

An expired token shows as "Invalid or expired Registry JWT token": run the login again. mcp-publisher status deprecates or deletes a published version. A tagged GitHub release is what the Gemini CLI extension gallery and the plugin marketplaces pick up, so tag vX.Y.Z after publishing.

License

MIT

NPM
planpost-mcp
Install Command
npm install planpost-mcp
Runtime: npx

Remote