planpost-mcp 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +16 -12
- package/dist/index.js +55 -8
- package/package.json +1 -1
- package/skills/planpost/SKILL.md +15 -22
- package/skills/send-to-plan-post/SKILL.md +0 -152
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `planpost-mcp`. Versions follow [semver](https://semver.org).
|
|
4
4
|
|
|
5
|
+
## 0.4.1 — 2026-11-14
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- Agents that sign in with OAuth see 34 tools: the scheduling, delivery,
|
|
9
|
+
analytics, revenue and approval tools. The video, carousel, brand kit,
|
|
10
|
+
content source and repurposing tools are left out for them, because those
|
|
11
|
+
features are parked in the PlanPost app; an API key still gets all 49.
|
|
12
|
+
- `create_post` no longer points agents at the carousel tools; several
|
|
13
|
+
attached images publish as a carousel where the network supports one.
|
|
14
|
+
- Descriptions in the README, `server.json`, the plugin manifests, GEMINI.md
|
|
15
|
+
and the skill describe the product as it is now: an agentic scheduler, with
|
|
16
|
+
approval as a choice the user makes (off by default).
|
|
17
|
+
- `generate_image` is among the tools left out for OAuth sign-ins: it calls a
|
|
18
|
+
generative image model, which the Claude directory does not allow in a
|
|
19
|
+
listed server.
|
|
20
|
+
- `.claude-plugin/marketplace.json` has a marketplace description.
|
|
21
|
+
|
|
22
|
+
### Removed
|
|
23
|
+
- The `send-to-plan-post` skill. It described one account's own groups, not
|
|
24
|
+
the product. `skills/planpost` is the skill to install.
|
|
25
|
+
|
|
5
26
|
## 0.4.0 — 2026-11-09
|
|
6
27
|
|
|
7
28
|
### Added
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# planpost-mcp
|
|
2
2
|
|
|
3
|
-
MCP (Model Context Protocol) server for [PlanPost](https://planpo.st), the agentic social scheduler
|
|
3
|
+
MCP (Model Context Protocol) server for [PlanPost](https://planpo.st), the agentic social scheduler your AI agent can run. It lets Claude, ChatGPT, Cursor, Gemini CLI and other AI agents draft, schedule and publish posts to Instagram, Facebook, LinkedIn, X, Threads, TikTok and YouTube, see what failed and why, and read analytics and revenue from your PlanPost account.
|
|
4
4
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
@@ -9,8 +9,9 @@ MCP (Model Context Protocol) server for [PlanPost](https://planpo.st), the agent
|
|
|
9
9
|
Add `https://mcp.planpo.st/mcp` to your agent as a remote MCP server (a
|
|
10
10
|
"connector" in Claude and ChatGPT) and leave the authentication fields empty.
|
|
11
11
|
The first time the agent uses it, a browser opens on planpo.st: log in, review
|
|
12
|
-
what the agent may do, and choose whether its posts wait for your approval
|
|
13
|
-
|
|
12
|
+
what the agent may do, and choose whether its posts wait for your approval
|
|
13
|
+
(they go out at the time the agent sets unless you turn that on). No key to
|
|
14
|
+
create or paste.
|
|
14
15
|
|
|
15
16
|
```bash
|
|
16
17
|
claude mcp add --transport http planpost https://mcp.planpo.st/mcp
|
|
@@ -20,8 +21,8 @@ claude mcp add --transport http planpost https://mcp.planpo.st/mcp
|
|
|
20
21
|
{ "mcpServers": { "planpost": { "url": "https://mcp.planpo.st/mcp" } } }
|
|
21
22
|
```
|
|
22
23
|
|
|
23
|
-
Connected agents are listed
|
|
24
|
-
where you can
|
|
24
|
+
Connected agents are listed on the **Agent** page under **Connected apps**,
|
|
25
|
+
where you can turn approval on or off for each one, or remove it, at any time. They use the same permissions and the
|
|
25
26
|
same monthly call allowance as API keys.
|
|
26
27
|
|
|
27
28
|
The repository also ships ready-made bundles: a Claude Code plugin
|
|
@@ -39,16 +40,17 @@ OAuth support, for CI, or when you want a key limited to a few permissions.
|
|
|
39
40
|
|
|
40
41
|
### 1. Get an API key
|
|
41
42
|
|
|
42
|
-
In the PlanPost dashboard
|
|
43
|
+
In the PlanPost dashboard open the **Agent** page and choose **Create key** and select the permissions you need (see the table below). The key (`pk_live_…`) is shown **once** — copy it.
|
|
43
44
|
|
|
44
|
-
> API/MCP access is included
|
|
45
|
+
> API/MCP access is included on every plan: Free 200 calls a month, Creator 1,000, Pro 10,000. Keys are scoped: a key with only `posts:read` cannot publish.
|
|
45
46
|
|
|
46
47
|
### 2. Add the server to your agent
|
|
47
48
|
|
|
48
49
|
Two ways to connect. **Hosted** is the default: point your client at
|
|
49
50
|
`https://mcp.planpo.st/mcp`, nothing to install, your key travels as a header.
|
|
50
51
|
**Local** runs the same server on your machine with `npx`, so the key never
|
|
51
|
-
leaves it. Both expose the same 49 tools
|
|
52
|
+
leaves it. Both expose the same 49 tools to an API key; an agent that signs in
|
|
53
|
+
with OAuth gets 34 of them (see the note under Tools).
|
|
52
54
|
|
|
53
55
|
Replace `pk_live_xxx` with your key throughout.
|
|
54
56
|
|
|
@@ -190,9 +192,9 @@ endpoint, and never forwards them upstream.
|
|
|
190
192
|
|
|
191
193
|
> "Schedule this post to my Polish Pages group for tomorrow 9:00: …"
|
|
192
194
|
>
|
|
193
|
-
> "
|
|
195
|
+
> "Here is my new blog post. Write one version per network and spread them across next week."
|
|
194
196
|
>
|
|
195
|
-
> "
|
|
197
|
+
> "What failed to publish this week, and what do I need to fix?"
|
|
196
198
|
>
|
|
197
199
|
> "How much revenue did my posts drive last 30 days?"
|
|
198
200
|
|
|
@@ -200,12 +202,12 @@ endpoint, and never forwards them upstream.
|
|
|
200
202
|
|
|
201
203
|
| Variable | Required | Description |
|
|
202
204
|
|---|---|---|
|
|
203
|
-
| `PLANPOST_API_KEY` | stdio only | API key from
|
|
205
|
+
| `PLANPOST_API_KEY` | stdio only | API key from the Agent page in PlanPost (`pk_live_…`). Not needed with `--http`, where each request brings its own credentials. |
|
|
204
206
|
| `PLANPOST_API_URL` | — | API base URL (default `https://api.planpo.st`) |
|
|
205
207
|
|
|
206
208
|
## Tools
|
|
207
209
|
|
|
208
|
-
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.
|
|
210
|
+
49 tools, grouped below. An agent that signs in with OAuth (the hosted server with no key, which is how Claude, ChatGPT and Cursor connect) is offered 34: the AI image tool and the video, carousel, brand kit, content source and repurposing tools are left out, because those features are parked in the PlanPost app. An API key gets all 49. 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.
|
|
209
211
|
|
|
210
212
|
### Posts & publishing
|
|
211
213
|
|
|
@@ -243,6 +245,8 @@ endpoint, and never forwards them upstream.
|
|
|
243
245
|
|
|
244
246
|
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.
|
|
245
247
|
|
|
248
|
+
X needs a paid plan. On the free plan `get_connection_link` answers with an upgrade message for `twitter`, `list_connections` marks an X account that is already connected as not on the current plan, and a post that targets X is saved as a draft with the `x_requires_paid_plan` error.
|
|
249
|
+
|
|
246
250
|
### Approvals
|
|
247
251
|
|
|
248
252
|
| Tool | Required permission | Description |
|
package/dist/index.js
CHANGED
|
@@ -83,17 +83,58 @@ function formatLinks(links) {
|
|
|
83
83
|
// ─── MCP Server ───
|
|
84
84
|
// Tool registration is a function so HTTP mode can build a fresh server per
|
|
85
85
|
// request (stateless transport); stdio builds one at startup.
|
|
86
|
-
function buildServer() {
|
|
86
|
+
function buildServer(omit = NO_TOOLS) {
|
|
87
87
|
const server = new McpServer(SERVER_INFO);
|
|
88
|
-
registerTools(server);
|
|
88
|
+
registerTools(server, omit);
|
|
89
89
|
return server;
|
|
90
90
|
}
|
|
91
|
+
const NO_TOOLS = new Set();
|
|
92
|
+
/**
|
|
93
|
+
* Tools left out for agents that signed in with OAuth, which is how every
|
|
94
|
+
* directory-listed client (Claude, ChatGPT, Cursor) connects.
|
|
95
|
+
*
|
|
96
|
+
* - generate_image calls a generative image model. The Claude directory's
|
|
97
|
+
* compliance step requires the statement "My MCP server does not use AI
|
|
98
|
+
* models to generate images, video, or audio", and a tool that is merely
|
|
99
|
+
* discouraged would make that statement false.
|
|
100
|
+
* - Video and carousel templates, brand kits, content sources and repurposing
|
|
101
|
+
* belong to features parked in the app since 2026-11-11 (PARKED_NAV in
|
|
102
|
+
* apps/web/lib/config/nav.tsx). A signed-in agent is offered what the
|
|
103
|
+
* product sells: scheduling, delivery, results and approvals.
|
|
104
|
+
*
|
|
105
|
+
* API-key connections, which the user sets up by hand, keep the full set.
|
|
106
|
+
* To bring a parked feature back, remove its tools here and its nav entry.
|
|
107
|
+
*/
|
|
108
|
+
const OMITTED_FOR_SIGNED_IN_AGENTS = new Set([
|
|
109
|
+
"generate_image",
|
|
110
|
+
// Video templates
|
|
111
|
+
"create_video",
|
|
112
|
+
"get_video_status",
|
|
113
|
+
"list_video_templates",
|
|
114
|
+
// Carousel templates and brand kits
|
|
115
|
+
"generate_carousel",
|
|
116
|
+
"render_carousel",
|
|
117
|
+
"get_carousel_render",
|
|
118
|
+
"list_carousel_styles",
|
|
119
|
+
"list_brand_kits",
|
|
120
|
+
// Content sources and repurposing
|
|
121
|
+
"add_content_source",
|
|
122
|
+
"delete_content_source",
|
|
123
|
+
"fetch_source_content",
|
|
124
|
+
"list_content_sources",
|
|
125
|
+
"list_imported_content",
|
|
126
|
+
"repurpose_content",
|
|
127
|
+
]);
|
|
91
128
|
// ─── Tools ───
|
|
92
|
-
function registerTools(target) {
|
|
129
|
+
function registerTools(target, omit = NO_TOOLS) {
|
|
93
130
|
// Every tool below is registered through this shim so its annotations
|
|
94
131
|
// (title, read-only / destructive hints) come from annotations.ts.
|
|
95
132
|
const server = {
|
|
96
|
-
tool: ((name, description, schema, handler) =>
|
|
133
|
+
tool: ((name, description, schema, handler) => {
|
|
134
|
+
if (omit.has(name))
|
|
135
|
+
return undefined;
|
|
136
|
+
return target.tool(name, description, schema, annotationsFor(name), handler);
|
|
137
|
+
}),
|
|
97
138
|
};
|
|
98
139
|
server.tool("list_posts", "List your scheduled and published social media posts. Filter by status, platform, campaign, label, post type, or free text.", {
|
|
99
140
|
// `needs_approval` is a first-class status, not an internal one: a post
|
|
@@ -142,7 +183,7 @@ function registerTools(target) {
|
|
|
142
183
|
connectionIds: z.array(z.string()).describe("Array of social connection IDs to post to. Use list_connections or list_account_groups to find IDs."),
|
|
143
184
|
platformContent: z.record(z.string(), z.string()).optional().describe("Per-platform content overrides, e.g. { twitter: 'shorter version', linkedin: 'longer version' }"),
|
|
144
185
|
platformSettings: z.record(z.string(), z.any()).optional().describe("Per-platform settings, e.g. { instagram: { isReel: true }, youtube: { title: '...', visibility: 'public' } }. TikTok: a scheduled or published post MUST carry { tiktok: { privacyLevel } } with one of PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY (TikTok makes the user choose who can view each video; without it the API returns 400). Optional tiktok flags: disableComment, disableDuet, disableStitch (default true = off), brandedContent, yourBrand."),
|
|
145
|
-
postType: z.enum(["post", "reel", "short", "story", "thread"]).optional().describe("Explicit post type (clearer than platformSettings flags, and takes precedence over them). 'reel' needs Instagram/Facebook; 'short' needs YouTube/Instagram/TikTok/Facebook; 'story' needs Instagram
|
|
186
|
+
postType: z.enum(["post", "reel", "short", "story", "thread"]).optional().describe("Explicit post type (clearer than platformSettings flags, and takes precedence over them). 'reel' needs Instagram/Facebook; 'short' needs YouTube/Instagram/TikTok/Facebook; 'story' needs Instagram (Facebook Stories are not supported); 'thread' is X/Twitter-only and requires threadSegments. Defaults to 'post', or is inferred from platformSettings flags (e.g. { instagram: { isReel: true } } → reel). NOTE: a carousel is NOT a postType — attach several images (or media with type 'carousel') and it publishes as a carousel where the network supports one."),
|
|
146
187
|
threadSegments: z.array(z.string()).optional().describe("Ordered tweet segments for a thread (only used when postType='thread', X/Twitter-only). 2–25 segments, each 1–280 characters."),
|
|
147
188
|
label: z.string().optional().describe("Internal working name shown in the PlanPost dashboard list, e.g. 'Garmin CIRQA - IG Carousel'. Never published to any platform — it just makes a campaign's drafts tellable apart. Highly recommended when creating several posts at once."),
|
|
148
189
|
campaign: z.string().optional().describe("Campaign tag, e.g. 'garmin-cirqa-2026' (slugified automatically). Sets utm.campaign and derives a utm.content token from the format (e.g. 'ig-carousel', 'fb-static'). Group/filter/bulk-delete by this with list_campaigns, list_posts({campaign}) and delete_posts({campaign})."),
|
|
@@ -486,9 +527,15 @@ function registerTools(target) {
|
|
|
486
527
|
return " ⚠️ last publish failed, will retry";
|
|
487
528
|
return ` ⛔ cannot publish${h.reason ? ` [${h.reason}]` : ""}${h.lastError ? `: ${String(h.lastError).slice(0, 120)}` : ""}`;
|
|
488
529
|
};
|
|
489
|
-
|
|
530
|
+
// `planBlocked` means the platform is not on the account's current plan
|
|
531
|
+
// (X needs a paid plan). The account is connected and healthy, but a post
|
|
532
|
+
// that targets it fails preflight with `x_requires_paid_plan`, so flag it
|
|
533
|
+
// here before the agent picks it.
|
|
534
|
+
const planTag = (c) => c.planBlocked ? ` ⛔ not on the current plan, needs ${c.requiredPlan === "pro" ? "Pro" : "Creator or Pro"}` : "";
|
|
535
|
+
const summary = connections.map((c) => `${c.provider} — ${c.metadata?.accountName || c.providerUsername} (ID: ${c.id})${planTag(c) || healthTag(c)}`).join("\n");
|
|
490
536
|
const dead = connections.filter((c) => c.health?.status === "dead").length;
|
|
491
|
-
const
|
|
537
|
+
const planBlocked = connections.filter((c) => c.planBlocked).length;
|
|
538
|
+
const headline = `${connections.length} connected accounts${dead ? `, ${dead} cannot publish until fixed` : ""}${planBlocked ? `, ${planBlocked} not on the current plan` : ""}:`;
|
|
492
539
|
return { content: [{ type: "text", text: `${headline}\n\n${summary}\n\nFull data:\n${JSON.stringify(connections, null, 2)}` }] };
|
|
493
540
|
});
|
|
494
541
|
server.tool("list_account_groups", "List account groups the user has set up in PlanPost (e.g. 'Polish Pages', 'English Pages'). Each group bundles social connections so you can target multiple accounts in one create_post call. Returns id, name, connectionCount, isDefault, and the underlying connections.", {}, async () => {
|
|
@@ -1069,7 +1116,7 @@ async function startHttp(port, host) {
|
|
|
1069
1116
|
}
|
|
1070
1117
|
}
|
|
1071
1118
|
const body = await readBody(req);
|
|
1072
|
-
const server = buildServer();
|
|
1119
|
+
const server = buildServer(credential.kind === "grant" ? OMITTED_FOR_SIGNED_IN_AGENTS : NO_TOOLS);
|
|
1073
1120
|
const transport = new StreamableHTTPServerTransport({
|
|
1074
1121
|
sessionIdGenerator: undefined, // stateless
|
|
1075
1122
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "planpost-mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"//mcpName": "Ownership marker for the official MCP Registry: must equal `name` in server.json, or `mcp-publisher publish` fails validation. Namespace is the GitHub account that owns this repo, verified with `mcp-publisher login github`.",
|
|
5
5
|
"mcpName": "io.github.lukBB/planpost",
|
|
6
6
|
"description": "PlanPost MCP server, the agentic social scheduler with revenue attribution: schedule and publish posts to Instagram, TikTok, LinkedIn, X, Facebook, Threads and YouTube from Claude, Cursor, Codex or any MCP client, render video and carousels, and read revenue per post.",
|
package/skills/planpost/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: planpost
|
|
3
|
-
description: Draft, schedule and publish social media posts through PlanPost, the agentic social scheduler
|
|
3
|
+
description: Draft, schedule and publish social media posts through PlanPost, the agentic social scheduler your AI agent can run, then check what went out and what worked. Use when the user wants to post, schedule, queue or bulk-plan content for Instagram, TikTok, LinkedIn, X (Twitter), Facebook, Threads or YouTube, check a post's status or a failed publish, or ask which posts drove clicks, signups or revenue. Requires the PlanPost MCP server (https://mcp.planpo.st/mcp).
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
homepage: https://planpo.st/mcp
|
|
@@ -8,8 +8,9 @@ metadata:
|
|
|
8
8
|
|
|
9
9
|
# PlanPost
|
|
10
10
|
|
|
11
|
-
PlanPost schedules and publishes to seven social networks
|
|
12
|
-
|
|
11
|
+
PlanPost schedules and publishes to seven social networks, reports what
|
|
12
|
+
failed and why, and reads back analytics and revenue (revenue per post on the
|
|
13
|
+
Pro plan). This skill is the operating manual for its
|
|
13
14
|
MCP tools. Tool names below are the ones the server exposes; your client may
|
|
14
15
|
prefix them (for example `mcp__planpost__create_post`).
|
|
15
16
|
|
|
@@ -18,7 +19,7 @@ prefix them (for example `mcp__planpost__create_post`).
|
|
|
18
19
|
If no PlanPost tools are available, the server is not connected. Tell the user
|
|
19
20
|
to add `https://mcp.planpo.st/mcp` as a remote MCP server (connector) in their
|
|
20
21
|
agent and sign in when the browser opens. Details per client:
|
|
21
|
-
https://planpo.st/docs
|
|
22
|
+
https://planpo.st/docs. Do not ask the user to paste an API key into
|
|
22
23
|
the chat.
|
|
23
24
|
|
|
24
25
|
## The rules that prevent bad posts
|
|
@@ -33,9 +34,10 @@ the chat.
|
|
|
33
34
|
3. **Say the time back.** Pass `scheduledFor` as ISO 8601 plus the user's
|
|
34
35
|
`timezone`, and repeat the local time in your answer. If they gave no time
|
|
35
36
|
and want one, `suggest_schedule` or `get_best_times` picks it.
|
|
36
|
-
4. **Approval is not a failure.**
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
4. **Approval is not a failure.** If the user turned on approval for this
|
|
38
|
+
connection, a post comes back `needs_approval`: it is waiting for them in
|
|
39
|
+
PlanPost under Posts, In review. Report that and stop; do not retry or
|
|
40
|
+
re-create it. `get_approval_status` tells you when it clears.
|
|
39
41
|
5. **Read warnings.** Responses carry `warnings[]` (what will be truncated or
|
|
40
42
|
dropped at publish time) and sometimes `preflight.errors[]` (the post was
|
|
41
43
|
saved as a draft because it would fail). Fix what they name, then
|
|
@@ -70,20 +72,13 @@ Reuse before uploading: `list_media` shows what is already in the library.
|
|
|
70
72
|
For a new file, `upload_media` returns a presigned URL; upload to it, then
|
|
71
73
|
pass the returned asset in `media`.
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
`list_brand_kits` → `render_carousel` → poll `get_carousel_render` until
|
|
77
|
-
`completed` → `create_post` with the returned `assets` as `media`.
|
|
78
|
-
|
|
79
|
-
**Video**
|
|
80
|
-
|
|
81
|
-
`list_video_templates` → `create_video` → poll `get_video_status` →
|
|
82
|
-
`create_post` with `postType: "reel"` or `"short"`.
|
|
75
|
+
Several images attached to one post publish as a carousel where the network
|
|
76
|
+
supports one. For a Reel or Short, attach the video and set `postType: "reel"`
|
|
77
|
+
or `"short"`.
|
|
83
78
|
|
|
84
79
|
**Turn an article into posts**
|
|
85
80
|
|
|
86
|
-
|
|
81
|
+
Read the article (open the URL the user gives you, or ask them to paste it) →
|
|
87
82
|
write per-network variants (use `get_brand_voice_prompt` when a brand voice
|
|
88
83
|
exists) → `create_post` as drafts.
|
|
89
84
|
|
|
@@ -103,12 +98,10 @@ money, not the ones with the most likes.
|
|
|
103
98
|
## Limits to respect
|
|
104
99
|
|
|
105
100
|
- Calls count against the user's monthly API allowance; prefer one
|
|
106
|
-
`bulk_create_posts` over many `create_post` calls, and do not poll
|
|
107
|
-
|
|
101
|
+
`bulk_create_posts` over many `create_post` calls, and do not poll a status
|
|
102
|
+
faster than every few seconds.
|
|
108
103
|
- A tool that answers with a missing-permission error means the user did not
|
|
109
104
|
grant that scope. Say which capability is missing; do not look for a
|
|
110
105
|
workaround.
|
|
111
|
-
- Image, video and carousel generation spend the user's AI credits or render
|
|
112
|
-
quota. Confirm before generating more than a couple.
|
|
113
106
|
- Connecting a new social account needs a human in a browser:
|
|
114
107
|
`get_connection_link` returns the link to send them.
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: send-to-plan-post
|
|
3
|
-
description: Compose and route social posts through the user's PlanPost app. Resolves named account groups ("Polish Pages", "English Pages", "SaaS", "Personal", etc.) into PlanPost connection IDs and creates draft, scheduled, or immediately-published posts. MANDATORY TRIGGERS - "send to plan post", "/send-to-plan-post", "post to PlanPost", "publish via PlanPost", "save as draft on PlanPost", "post to my <group> pages", "post this to my <group> group", "draft on PlanPost", "schedule on PlanPost", "post to Polish Pages", "post to English Pages". Also trigger when the user pastes social copy and references any known group name. Requires the planpost-mcp server registered with PLANPOST_API_KEY.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# send-to-plan-post
|
|
7
|
-
|
|
8
|
-
## What this skill does
|
|
9
|
-
|
|
10
|
-
Routes user-provided content to the right set of social accounts in PlanPost, using account groups as the primary targeting concept (with a fallback to individual handle targeting). Supports draft, scheduled, and immediate-publish modes. Handles image/video attachments via PlanPost's presigned S3 upload flow.
|
|
11
|
-
|
|
12
|
-
## Required MCP tools (`planpost-mcp`)
|
|
13
|
-
|
|
14
|
-
The user must have the `planpost-mcp` server installed and authenticated with `PLANPOST_API_KEY`. This skill calls:
|
|
15
|
-
|
|
16
|
-
- `list_account_groups` — resolve a group name into its connections
|
|
17
|
-
- `get_account_group` — fetch one group (by id, name, or default flag)
|
|
18
|
-
- `list_connections` — fallback when targeting individual accounts
|
|
19
|
-
- `upload_media` — presigned-URL upload for attached files
|
|
20
|
-
- `create_post` — draft or scheduled creation
|
|
21
|
-
- `publish_post` — immediate publish of a created post
|
|
22
|
-
- (read-only) `list_posts`, `get_post` — for "show me what I just sent"
|
|
23
|
-
|
|
24
|
-
The API key needs these permissions: `groups:read`, `connections:read`, `posts:read`, `posts:write`, `media:write`.
|
|
25
|
-
|
|
26
|
-
If the MCP server is missing or the API key is unset, stop and tell the user how to install/configure it (see `packages/mcp-server/README.md`). Do not fall back to web scraping or computer-use clicking through the dashboard.
|
|
27
|
-
|
|
28
|
-
## Flow
|
|
29
|
-
|
|
30
|
-
### Step 1 — Parse the request
|
|
31
|
-
|
|
32
|
-
From the user's message extract:
|
|
33
|
-
|
|
34
|
-
- **Content** — the post body. Preserve the user's language exactly (Polish stays Polish, English stays English). Do not "improve" copy unless asked.
|
|
35
|
-
- **Target** — one of: a group name ("Polish Pages"), a list of handles ("@triathlon_info_pl on IG"), or "default".
|
|
36
|
-
- **Mode** — `draft` | `schedule` | `publish-now`. **Default to `draft` when ambiguous.** Only schedule or publish if the user explicitly asks for it.
|
|
37
|
-
- **Schedule time** — if mode is `schedule`, an ISO datetime or natural phrase ("tomorrow 9am"). Resolve to ISO 8601 in the user's timezone before sending.
|
|
38
|
-
- **Media** — any files attached in the chat or URLs the user pasted.
|
|
39
|
-
- **Platform overrides** — per-platform variants (`platformContent`) or settings (`platformSettings`, e.g. `{ instagram: { isReel: true } }`) only if the user gave them explicitly.
|
|
40
|
-
|
|
41
|
-
### Step 2 — Resolve targets
|
|
42
|
-
|
|
43
|
-
- **Group by name** → call `list_account_groups`, do a case-insensitive exact-name match first; if no hit, try substring; if multiple match, ask the user to pick from the matches.
|
|
44
|
-
- **Default group** → call `get_account_group` with `{ default: true }`.
|
|
45
|
-
- **Specific handles** → call `list_connections`, filter by `provider` + `providerUsername` (lowercase compare, strip leading `@`).
|
|
46
|
-
- **No target resolved** → stop, list every group the user has, ask which one.
|
|
47
|
-
|
|
48
|
-
Capture the resolved `connectionIds[]` and the human-readable list (`provider @handle`) for the confirmation step.
|
|
49
|
-
|
|
50
|
-
### Step 3 — Handle media
|
|
51
|
-
|
|
52
|
-
For each attached file:
|
|
53
|
-
|
|
54
|
-
1. Call `upload_media` with `{ filename, contentType, size }` to get a presigned PUT URL.
|
|
55
|
-
2. PUT the file bytes.
|
|
56
|
-
3. Capture the returned `{ id, type, url, key, filename, mimeType, size }` and append to a `media[]` array.
|
|
57
|
-
|
|
58
|
-
For pasted URLs: fetch the bytes (respect link-safety rules — verify unfamiliar URLs with the user before fetching), then upload via the same flow. If a URL fails to fetch, ask the user to attach the file directly instead.
|
|
59
|
-
|
|
60
|
-
### Step 4 — Compose and confirm
|
|
61
|
-
|
|
62
|
-
Before any write that publishes publicly, show a confirmation block:
|
|
63
|
-
|
|
64
|
-
```
|
|
65
|
-
About to <draft | schedule | publish-now>:
|
|
66
|
-
|
|
67
|
-
Group: Polish Pages (4 accounts)
|
|
68
|
-
- facebook @triathloninfopl
|
|
69
|
-
- instagram @triathlon_info_pl
|
|
70
|
-
- threads @triathlon_info_pl
|
|
71
|
-
- twitter @triathlon_info_pl
|
|
72
|
-
When: <Now | 2026-05-19 09:00 Europe/Warsaw | Draft only>
|
|
73
|
-
Content: <body, truncated to ~200 chars with [...] if longer>
|
|
74
|
-
Media: <N items, names>
|
|
75
|
-
Mode: <publish-now | schedule | draft>
|
|
76
|
-
|
|
77
|
-
Reply "go" to execute, or describe edits.
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
Rules:
|
|
81
|
-
|
|
82
|
-
- **Draft**: a confirmation summary is shown but the skill may proceed without an explicit "go" if the user's instruction was already unambiguous. (Drafts are reversible.)
|
|
83
|
-
- **Schedule**: require explicit "go".
|
|
84
|
-
- **Publish-now**: require explicit "go". If 5+ accounts are targeted, require a second `"yes publish to all <N>"` confirmation.
|
|
85
|
-
|
|
86
|
-
### Step 5 — Execute
|
|
87
|
-
|
|
88
|
-
- Call `create_post` with `{ content, connectionIds, media, scheduledFor?, platformContent?, platformSettings?, timezone? }`.
|
|
89
|
-
- If mode is `publish-now`: take the returned post id and call `publish_post`.
|
|
90
|
-
- If mode is `schedule`: setting `scheduledFor` on `create_post` is enough; do not call `publish_post`.
|
|
91
|
-
- If mode is `draft`: do not set `scheduledFor` and do not call `publish_post`.
|
|
92
|
-
|
|
93
|
-
### Step 6 — Report back
|
|
94
|
-
|
|
95
|
-
Format the result tightly:
|
|
96
|
-
|
|
97
|
-
```
|
|
98
|
-
Created post <id> as <status>.
|
|
99
|
-
|
|
100
|
-
Targets:
|
|
101
|
-
- facebook @triathloninfopl queued
|
|
102
|
-
- instagram @triathlon_info_pl queued
|
|
103
|
-
- threads @triathlon_info_pl failed (token expired — re-auth at planpo.st)
|
|
104
|
-
|
|
105
|
-
Dashboard: https://planpo.st/posts/<id>
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Surface per-platform results (`platformResults[]`) from the API response. **Do not swallow partial failures.**
|
|
109
|
-
|
|
110
|
-
## Examples
|
|
111
|
-
|
|
112
|
-
### Draft to a group
|
|
113
|
-
|
|
114
|
-
> "draft a post on PlanPost to my English Pages — 'New SoloOperatorStack post on building Claude skills over PlanPost. Link in bio.'"
|
|
115
|
-
|
|
116
|
-
→ Resolve "English Pages" via `list_account_groups`. Show confirmation. Call `create_post` with no `scheduledFor`. Report id.
|
|
117
|
-
|
|
118
|
-
### Publish-now to specific handles
|
|
119
|
-
|
|
120
|
-
> "post to @triathlon_info_pl on Instagram and Facebook only: 'Trening interwałowy 6x800 — kto dziś biegnie?'"
|
|
121
|
-
|
|
122
|
-
→ Skip group resolution. Filter `list_connections` to those two. Confirm (4+ rule does not apply). `create_post` → `publish_post`.
|
|
123
|
-
|
|
124
|
-
### Schedule with image
|
|
125
|
-
|
|
126
|
-
> User drops `cover.jpg`: "schedule this for tomorrow 9am to Polish Pages: 'Lista startów na weekend ...'"
|
|
127
|
-
|
|
128
|
-
→ `upload_media` for the image. Resolve Polish Pages via `get_account_group`. Resolve "tomorrow 9am" to ISO in `Europe/Warsaw`. Confirm. `create_post` with `scheduledFor`.
|
|
129
|
-
|
|
130
|
-
### Discovery
|
|
131
|
-
|
|
132
|
-
> "what groups do I have on PlanPost?"
|
|
133
|
-
|
|
134
|
-
→ `list_account_groups`. Format as table: name, account count, default flag, color.
|
|
135
|
-
|
|
136
|
-
## Safety rules
|
|
137
|
-
|
|
138
|
-
- Never publish without explicit "go" / "publish" / "yes" from the user.
|
|
139
|
-
- Never invent a group name — if the user names a group that doesn't exist, list what does exist and ask.
|
|
140
|
-
- Never silently drop a failed-per-account result — always surface it in the final report.
|
|
141
|
-
- Preserve the user's language and tone in the post body. Do not paraphrase.
|
|
142
|
-
- For pasted URLs, follow link-safety rules: confirm unfamiliar URLs with the user before fetching their contents.
|
|
143
|
-
|
|
144
|
-
## Failure modes and how to handle them
|
|
145
|
-
|
|
146
|
-
- **API key missing** → "Set `PLANPOST_API_KEY` in your MCP config. See `packages/mcp-server/README.md`."
|
|
147
|
-
- **`groups:read` permission missing** → "Your API key doesn't have the `groups:read` permission. Regenerate it at planpo.st/settings/api-keys with that scope checked, or pass connection IDs directly via `list_connections`."
|
|
148
|
-
- **Group not found** → "I don't see a group called 'X'. You have: <list>. Want me to use one of these?"
|
|
149
|
-
- **Ambiguous group name (HTTP 300)** → list the candidates returned by the API, ask the user to pick by id.
|
|
150
|
-
- **Connection inactive (token expired)** → call this out before composing; suggest re-auth in the dashboard.
|
|
151
|
-
- **Upload fails** → retry once, then ask the user to attach again or paste a different URL.
|
|
152
|
-
- **Backend 5xx on `create_post`** → do not retry automatically. Confirm with the user first to avoid duplicate posts.
|