planpost-mcp 0.2.1 → 0.3.0

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.
Files changed (3) hide show
  1. package/README.md +175 -16
  2. package/dist/index.js +60 -10
  3. package/package.json +33 -9
package/README.md CHANGED
@@ -12,13 +12,36 @@ In the PlanPost dashboard go to **Settings → API Keys → Create Key** and sel
12
12
 
13
13
  ### 2. Add the server to your agent
14
14
 
15
- **Claude Code:**
15
+ Two ways to connect. **Hosted** is the default: point your client at
16
+ `https://mcp.planpo.st/mcp`, nothing to install, your key travels as a header.
17
+ **Local** runs the same server on your machine with `npx`, so the key never
18
+ leaves it. Both expose the same 47 tools.
19
+
20
+ Replace `pk_live_xxx` with your key throughout.
21
+
22
+ **Claude Code**
23
+
24
+ ```bash
25
+ claude mcp add --transport http planpost https://mcp.planpo.st/mcp \
26
+ --header "Authorization: Bearer pk_live_xxx"
27
+ ```
28
+
29
+ Local fallback:
16
30
 
17
31
  ```bash
18
32
  claude mcp add planpost -e PLANPOST_API_KEY=pk_live_xxx -- npx -y planpost-mcp
19
33
  ```
20
34
 
21
- **Claude Desktop** (`claude_desktop_config.json`):
35
+ Run `claude mcp list` afterwards to confirm it is connected.
36
+
37
+ **Claude Desktop**
38
+
39
+ Hosted: Settings → Connectors → Add custom connector. Name `PlanPost`, URL
40
+ `https://mcp.planpo.st/mcp`, then add the header `Authorization: Bearer pk_live_xxx`.
41
+
42
+ Local fallback, in `claude_desktop_config.json` (macOS:
43
+ `~/Library/Application Support/Claude/`, Windows: `%APPDATA%\Claude\`), then
44
+ restart the app:
22
45
 
23
46
  ```json
24
47
  {
@@ -32,32 +55,87 @@ claude mcp add planpost -e PLANPOST_API_KEY=pk_live_xxx -- npx -y planpost-mcp
32
55
  }
33
56
  ```
34
57
 
35
- **Any other MCP client:** run `npx -y planpost-mcp` over stdio with the `PLANPOST_API_KEY` environment variable set.
58
+ **Cursor** (`~/.cursor/mcp.json`, or `.cursor/mcp.json` per project)
36
59
 
37
- ### Remote (HTTP) mode
60
+ ```json
61
+ {
62
+ "mcpServers": {
63
+ "planpost": {
64
+ "url": "https://mcp.planpo.st/mcp",
65
+ "headers": { "Authorization": "Bearer pk_live_xxx" }
66
+ }
67
+ }
68
+ }
69
+ ```
38
70
 
39
- The server can also run as a shared, multi-tenant HTTP endpoint — no local install for users, each request authenticated by its own key:
71
+ Local fallback: replace the entry with
72
+ `{ "command": "npx", "args": ["-y", "planpost-mcp"], "env": { "PLANPOST_API_KEY": "pk_live_xxx" } }`.
73
+ A project-level `.cursor/mcp.json` is committed with your repo, so prefer the
74
+ hosted form and keep the key out of the file.
40
75
 
41
- ```bash
42
- npx -y planpost-mcp --http --port 3020 # exposes POST /mcp (+ /health)
76
+ **OpenAI Codex CLI** (`~/.codex/config.toml`)
77
+
78
+ ```toml
79
+ [mcp_servers.planpost]
80
+ url = "https://mcp.planpo.st/mcp"
81
+ bearer_token_env_var = "PLANPOST_API_KEY"
43
82
  ```
44
83
 
45
- Behind a reverse proxy, bind to loopback so the port isn't reachable directly:
84
+ Export `PLANPOST_API_KEY` in your shell; Codex reads the token from the
85
+ environment, not the file. `codex mcp add` does the same thing interactively.
46
86
 
47
- ```bash
48
- HOST=127.0.0.1 npx -y planpost-mcp --http --port 3020
87
+ Local fallback:
88
+
89
+ ```toml
90
+ [mcp_servers.planpost]
91
+ command = "npx"
92
+ args = ["-y", "planpost-mcp"]
93
+
94
+ [mcp_servers.planpost.env]
95
+ PLANPOST_API_KEY = "pk_live_xxx"
96
+ ```
97
+
98
+ **Gemini CLI** (`~/.gemini/settings.json`, or `.gemini/settings.json` per project)
99
+
100
+ ```json
101
+ {
102
+ "mcpServers": {
103
+ "planpost": {
104
+ "httpUrl": "https://mcp.planpo.st/mcp",
105
+ "headers": { "Authorization": "Bearer pk_live_xxx" },
106
+ "timeout": 10000
107
+ }
108
+ }
109
+ }
49
110
  ```
50
111
 
51
- Connect from any HTTP-capable MCP client, e.g.:
112
+ Gemini CLI uses `httpUrl` for streamable HTTP, not `url`; the wrong key is a
113
+ silent no-op. Local fallback: the same `command`/`args`/`env` entry as Cursor.
114
+
115
+ **Any other MCP client:** connect to `https://mcp.planpo.st/mcp` (streamable
116
+ HTTP, `Authorization: Bearer pk_live_xxx` on every request), or run
117
+ `npx -y planpost-mcp` over stdio with `PLANPOST_API_KEY` set. Per-client
118
+ walkthroughs with screenshots: [planpo.st/docs/agents](https://planpo.st/docs/agents).
119
+
120
+ ### Self-hosting the HTTP endpoint
121
+
122
+ `mcp.planpo.st` is this same package running with `--http`. To run your own:
52
123
 
53
124
  ```bash
54
- claude mcp add --transport http planpost https://mcp.yourhost.com/mcp \
55
- --header "Authorization: Bearer pk_live_xxx"
125
+ npx -y planpost-mcp --http --port 3020 # exposes POST /mcp (+ /health)
56
126
  ```
57
127
 
58
- The transport is stateless (streamable HTTP); the `Authorization: Bearer pk_...` header is required on every request and scoped per request, so one hosted instance serves many users safely. Put it behind HTTPS (reverse proxy) in production.
128
+ Behind a reverse proxy, bind to loopback so the port isn't reachable directly:
129
+
130
+ ```bash
131
+ HOST=127.0.0.1 npx -y planpost-mcp --http --port 3020
132
+ ```
59
133
 
60
- `--host` / `HOST` default to `0.0.0.0`, so containers and LAN use work without extra flags.
134
+ The transport is stateless (streamable HTTP); the `Authorization: Bearer pk_...`
135
+ header is required on every request and scoped per request, so one hosted
136
+ instance serves many users safely. Put it behind HTTPS in production.
137
+ `--host` / `HOST` default to `0.0.0.0`, so containers and LAN use work without
138
+ extra flags.
61
139
 
62
140
  ### 3. Talk to it
63
141
 
@@ -78,6 +156,8 @@ The transport is stateless (streamable HTTP); the `Authorization: Bearer pk_...`
78
156
 
79
157
  ## Tools
80
158
 
159
+ 47 tools, grouped below. The permission column is the API-key scope a tool needs; a key without the scope gets a clear error rather than a silent failure.
160
+
81
161
  ### Posts & publishing
82
162
 
83
163
  | Tool | Required permission | Description |
@@ -88,6 +168,11 @@ The transport is stateless (streamable HTTP); the `Authorization: Bearer pk_...`
88
168
  | `update_post` | `posts:write` | Update a draft/scheduled post |
89
169
  | `delete_post` | `posts:write` | Delete a post |
90
170
  | `publish_post` | `posts:write` | Publish immediately |
171
+ | `preview_post` | `posts:read` | Show exactly what each platform will receive (text after overrides, attachments, warnings) |
172
+ | `bulk_create_posts` | `posts:write` | Create a whole campaign of posts from one manifest |
173
+ | `list_campaigns` | `posts:read` | Campaign tags in use, with post counts by status and platform |
174
+ | `delete_posts` | `posts:write` | Bulk-delete posts matching a filter (a filter is required) |
175
+ | `list_media` | `media:read` | Media already uploaded, so an asset can be reused instead of re-uploaded |
91
176
  | `upload_media` | `media:write` | Get a presigned URL to upload an image/video |
92
177
  | `repurpose_content` | `repurpose:write` | Save repurposed content as draft posts |
93
178
 
@@ -98,8 +183,31 @@ The transport is stateless (streamable HTTP); the `Authorization: Bearer pk_...`
98
183
  | `list_connections` | `connections:read` | Connected social accounts |
99
184
  | `list_account_groups` | `groups:read` | Named account groups (e.g. "Polish Pages") |
100
185
  | `get_account_group` | `groups:read` | Resolve a group by id/name/default → connection IDs |
186
+ | `create_account_group` | `groups:write` | Create a named group bundling connections |
187
+ | `update_account_group` | `groups:write` | Rename, recolour, set default, or replace a group's connections |
188
+ | `delete_account_group` | `groups:write` | Delete a group (the accounts stay connected) |
189
+ | `add_connections_to_group` | `groups:write` | Add connections to a group |
190
+ | `remove_connections_from_group` | `groups:write` | Remove connections from a group |
191
+ | `get_connection_link` | `connections:read` | Link the user opens in a browser to connect a new social account (OAuth needs a human) |
101
192
 
102
- Connecting a new social account requires OAuth in the dashboard — agents can target any account that's already connected.
193
+ 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.
194
+
195
+ ### Approvals
196
+
197
+ | Tool | Required permission | Description |
198
+ |---|---|---|
199
+ | `submit_for_approval` | `approvals:write` | Send a post to a human reviewer (required when the API key is approval-gated) |
200
+ | `list_pending_approvals` | `approvals:read` | Posts waiting for review, with outcomes |
201
+ | `get_approval_status` | `approvals:read` | Pending / approved / rejected / changes requested for one submission |
202
+
203
+ ### Scheduling
204
+
205
+ | Tool | Required permission | Description |
206
+ |---|---|---|
207
+ | `get_best_times` | `schedule:read` | Best posting times per platform from the account's engagement history |
208
+ | `suggest_schedule` | `schedule:read` | Spread N posts across a date range at optimal times; returns concrete timestamps |
209
+ | `get_posting_schedule` | `schedule:read` | Weekly queue slots, global plus per-group overrides |
210
+ | `set_posting_schedule` | `schedule:write` | Replace the weekly queue slots for the global schedule or one group (Pro) |
103
211
 
104
212
  ### Video & carousels
105
213
 
@@ -113,6 +221,7 @@ Connecting a new social account requires OAuth in the dashboard — agents can t
113
221
  | `render_carousel` | `carousels:write` | Render slides into branded PNGs in the media library |
114
222
  | `get_carousel_render` | `carousels:read` | Poll a render; completed renders return post-ready media assets |
115
223
  | `list_brand_kits` | `brand-kits:read` | Saved brand kits (palette, fonts, logo, handle) for `render_carousel` |
224
+ | `generate_image` | `images:write` | Generate an image with AI for a post (1 AI credit); pass the URL to `create_post` |
116
225
 
117
226
  ### Content sources
118
227
 
@@ -133,6 +242,13 @@ Connecting a new social account requires OAuth in the dashboard — agents can t
133
242
 
134
243
  Revenue data is read-only over the API; connecting Stripe/RevenueCat/Shopify happens in the dashboard (Settings → Revenue).
135
244
 
245
+ ### Brand voice (Pro)
246
+
247
+ | Tool | Required permission | Description |
248
+ |---|---|---|
249
+ | `get_brand_voices` | `brand-voice:read` | Tone-of-voice profiles analysed from the user's content |
250
+ | `get_brand_voice_prompt` | `brand-voice:read` | Ready-to-use system prompt for a voice; apply it when drafting posts |
251
+
136
252
  ## A typical agent workflow
137
253
 
138
254
  1. `get_account_group` (`default: true`) → connection IDs
@@ -152,11 +268,54 @@ Or, for a branded carousel post end-to-end:
152
268
  ```bash
153
269
  npm install
154
270
  npm run build # tsc → dist/
271
+ npm test # validates server.json against the registry schema, boots the stdio server
155
272
  PLANPOST_API_KEY=pk_test_xxx node dist/index.js
156
273
  ```
157
274
 
158
275
  The server speaks MCP over stdio. It calls the PlanPost public API (`/api/public/v1`) with your key; nothing else is stored or sent anywhere.
159
276
 
277
+ ## Maintainers
278
+
279
+ Releasing a version means publishing twice: the code to npm, then the metadata
280
+ to the [official MCP Registry](https://registry.modelcontextprotocol.io), which
281
+ only stores `server.json` and checks that the npm package it points at exists
282
+ and carries the matching `mcpName`. Glama and other directories import from the
283
+ registry, so the second step is what makes a release discoverable.
284
+
285
+ 1. Bump `version` in `package.json` **and** in `server.json` (top level and
286
+ `packages[0].version`). `npm test` fails if they disagree.
287
+ 2. Publish to npm first; the registry rejects a version npm does not have yet:
288
+
289
+ ```bash
290
+ npm publish --access public
291
+ ```
292
+
293
+ 3. Install the publisher CLI once (`brew install mcp-publisher`, or the tarball
294
+ from the registry's GitHub releases), then log in. The `io.github.lukBB/*`
295
+ namespace is proven by GitHub device-flow login as that account:
296
+
297
+ ```bash
298
+ mcp-publisher login github
299
+ ```
300
+
301
+ 4. Validate and publish from this directory:
302
+
303
+ ```bash
304
+ mcp-publisher validate
305
+ mcp-publisher publish
306
+ ```
307
+
308
+ 5. Confirm the listing:
309
+
310
+ ```bash
311
+ curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.lukBB/planpost"
312
+ ```
313
+
314
+ An expired token shows as "Invalid or expired Registry JWT token": run the login
315
+ again. `mcp-publisher status` deprecates or deletes a published version.
316
+ Directory listings beyond the registry (mcp.so, Cursor Directory, outreach) are
317
+ tracked in [DISTRIBUTION.md](./DISTRIBUTION.md).
318
+
160
319
  ## License
161
320
 
162
321
  MIT
package/dist/index.js CHANGED
@@ -74,7 +74,10 @@ function buildServer() {
74
74
  // ─── Tools ───
75
75
  function registerTools(server) {
76
76
  server.tool("list_posts", "List your scheduled and published social media posts. Filter by status, platform, campaign, label, post type, or free text.", {
77
- status: z.enum(["draft", "scheduled", "processing", "published", "failed", "cancelled"]).optional().describe("Filter by post status"),
77
+ // `needs_approval` is a first-class status, not an internal one: a post
78
+ // parked for human sign-off is exactly what an agent needs to be able to
79
+ // find again after submitting it.
80
+ status: z.enum(["draft", "needs_approval", "scheduled", "processing", "published", "failed", "cancelled"]).optional().describe("Filter by post status ('needs_approval' = waiting on human sign-off)"),
78
81
  platform: z.string().optional().describe("Filter by platform (instagram, twitter, linkedin, etc.)"),
79
82
  campaign: z.string().optional().describe("Filter by campaign tag, e.g. 'garmin-cirqa-2026' (matches utm.campaign; slugified automatically)"),
80
83
  label: z.string().optional().describe("Filter by internal label (case-insensitive substring), e.g. 'Garmin CIRQA'"),
@@ -112,7 +115,7 @@ function registerTools(server) {
112
115
  const data = await api(`/posts/${id}`);
113
116
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
114
117
  });
115
- server.tool("create_post", "Create a new social media post. Saves as draft or schedules it for a specific time.", {
118
+ server.tool("create_post", "Create a new social media post. Saves as draft or schedules it for a specific time. X (Twitter): nothing sent to X may contain a link (URL, bare domain or t.co) — not the post, any thread segment, or a first comment (posted to X as a reply). The X API bills each link post at $0.20 vs $0.015, so the API rejects them; put a link-free version in platformContent.twitter.", {
116
119
  content: z.string().describe("Post text content"),
117
120
  connectionIds: z.array(z.string()).describe("Array of social connection IDs to post to. Use list_connections or list_account_groups to find IDs."),
118
121
  platformContent: z.record(z.string(), z.string()).optional().describe("Per-platform content overrides, e.g. { twitter: 'shorter version', linkedin: 'longer version' }"),
@@ -193,7 +196,9 @@ function registerTools(server) {
193
196
  const media = pl.mediaCount === 0
194
197
  ? `no media${pl.mediaSource === "override" ? " (text-only override)" : ""}`
195
198
  : `${pl.mediaCount} ${pl.mediaCount === 1 ? "item" : "items"} (${pl.media.map((m) => m.filename ?? m.type).join(", ")})`;
196
- const warn = pl.warnings?.length ? `\n warnings: ${pl.warnings.map((w) => w.code).join(", ")}` : "";
199
+ const warn = pl.warnings?.length
200
+ ? `\n warnings:\n${pl.warnings.map((w) => ` - [${w.code}] ${w.message}`).join("\n")}`
201
+ : "";
197
202
  return [
198
203
  `── ${pl.platform.toUpperCase()} ─────────────`,
199
204
  ` text (${pl.contentSource}): ${limit}${pl.linksClickable ? "" : " [links NOT clickable]"}`,
@@ -212,7 +217,7 @@ function registerTools(server) {
212
217
  }],
213
218
  };
214
219
  });
215
- server.tool("bulk_create_posts", "Create a whole campaign of posts from one manifest. Each entry accepts the same fields as create_post; `defaults` supplies values shared by every entry (campaign, connectionIds, timezone, ...) so you don't repeat them. Rows are independent — one bad row fails alone and the rest still get created. Max 50 per call. Returns a per-row result with ids, warnings and edit links.", {
220
+ server.tool("bulk_create_posts", "Create a whole campaign of posts from one manifest. Each entry accepts the same fields as create_post; `defaults` supplies values shared by every entry (campaign, connectionIds, timezone, ...) so you don't repeat them. Rows are independent — one bad row fails alone and the rest still get created. Max 50 per call. Returns a per-row result with ids, warnings and edit links. X (Twitter): nothing sent to X may contain a link (URL, bare domain or t.co) — not the post, any thread segment, or a first comment (posted to X as a reply). The X API bills each link post at $0.20 vs $0.015, so the API rejects them; put a link-free version in platformContent.twitter.", {
216
221
  posts: z.array(z.record(z.string(), z.any())).describe("Array of post payloads. Each accepts every create_post field: content, connectionIds, platformContent, platformSettings, postType, threadSegments, label, campaign, schedule/scheduledFor, media, platformMedia, utm."),
217
222
  defaults: z.record(z.string(), z.any()).optional().describe("Fields merged into every entry before its own values (which always win). Ideal for campaign, connectionIds and timezone."),
218
223
  }, async ({ posts, defaults }) => {
@@ -280,7 +285,7 @@ function registerTools(server) {
280
285
  const data = await api(`/media?${params}`);
281
286
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
282
287
  });
283
- server.tool("update_post", "Update a draft or scheduled post.", {
288
+ server.tool("update_post", "Update a draft or scheduled post. X (Twitter): nothing sent to X may contain a link (URL, bare domain or t.co) — not the post, any thread segment, or a first comment (posted to X as a reply). The X API bills each link post at $0.20 vs $0.015, so the API rejects them; put a link-free version in platformContent.twitter.", {
284
289
  id: z.string().describe("Post ID to update"),
285
290
  content: z.string().optional().describe("New post text content"),
286
291
  connectionIds: z.array(z.string()).optional().describe("New connection IDs"),
@@ -356,7 +361,7 @@ function registerTools(server) {
356
361
  await api(`/posts/${id}`, { method: "DELETE" });
357
362
  return { content: [{ type: "text", text: `Post ${id} deleted.` }] };
358
363
  });
359
- server.tool("publish_post", "Publish a draft or scheduled post immediately to all connected platforms.", {
364
+ server.tool("publish_post", "Publish a draft or scheduled post immediately to all connected platforms. X (Twitter): nothing sent to X may contain a link (URL, bare domain or t.co) — not the post, any thread segment, or a first comment (posted to X as a reply). The X API bills each link post at $0.20 vs $0.015, so the API rejects them; put a link-free version in platformContent.twitter.", {
360
365
  id: z.string().describe("Post ID to publish now"),
361
366
  }, async ({ id }) => {
362
367
  const data = await api(`/posts/${id}/publish`, { method: "POST" });
@@ -774,6 +779,31 @@ function registerTools(server) {
774
779
  });
775
780
  return { content: [{ type: "text", text: JSON.stringify(data.data, null, 2) }] };
776
781
  });
782
+ server.tool("get_posting_schedule", "Read the weekly posting queue schedule: the global schedule plus any per-group overrides. Slots are {day: 1-7 (Mon=1..Sun=7), time: 'HH:MM' 24h} in the user's timezone. Groups without an override inherit the global schedule. Needs the schedule:read permission.", {}, async () => {
783
+ const data = await api("/posting-queue/queues");
784
+ return { content: [{ type: "text", text: JSON.stringify(data.data, null, 2) }] };
785
+ });
786
+ server.tool("set_posting_schedule", "Replace the weekly posting queue slots for the global schedule or one account group (Pro feature; needs the schedule:write permission). Pass the FULL desired slot list — it replaces the existing one. An empty array on a group removes its override so it inherits the global schedule again.", {
787
+ groupId: z
788
+ .string()
789
+ .optional()
790
+ .describe("Account group id (from list_account_groups); omit for the global schedule"),
791
+ slots: z
792
+ .array(z.object({
793
+ day: z.number().int().min(1).max(7).describe("ISO weekday: 1=Monday .. 7=Sunday"),
794
+ time: z
795
+ .string()
796
+ .regex(/^([01]\d|2[0-3]):[0-5]\d$/)
797
+ .describe("24h time 'HH:MM' in the user's timezone, e.g. '09:30'"),
798
+ }))
799
+ .describe("The complete weekly slot list to store"),
800
+ }, async ({ groupId, slots }) => {
801
+ const data = await api(`/posting-queue/queues/${encodeURIComponent(groupId ?? "global")}`, {
802
+ method: "PUT",
803
+ body: JSON.stringify({ slots }),
804
+ });
805
+ return { content: [{ type: "text", text: JSON.stringify(data.data, null, 2) }] };
806
+ });
777
807
  }
778
808
  // ─── Start ───
779
809
  async function startStdio() {
@@ -815,22 +845,42 @@ function readBody(req) {
815
845
  */
816
846
  async function startHttp(port, host) {
817
847
  const httpServer = createHttpServer(async (req, res) => {
848
+ // An API host must never rank: Google indexed the bare /mcp 405 JSON as a
849
+ // page under the brand. The header, not robots.txt, is the fix on purpose:
850
+ // a crawler has to be able to fetch the URL to see the noindex, so
851
+ // robots.txt below allows crawling and this header does the de-indexing.
852
+ res.setHeader("X-Robots-Tag", "noindex, nofollow");
818
853
  const fail = (status, message) => {
819
- res.writeHead(status, { "Content-Type": "application/json" });
854
+ // charset is load-bearing: without it, any non-ASCII byte in a message
855
+ // renders as mojibake in browsers that assume latin-1 for JSON.
856
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
820
857
  res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message }, id: null }));
821
858
  };
822
859
  try {
823
860
  const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
824
861
  if (url.pathname === "/health") {
825
- res.writeHead(200, { "Content-Type": "application/json" });
862
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
826
863
  res.end(JSON.stringify({ status: "ok" }));
827
864
  return;
828
865
  }
866
+ if (url.pathname === "/robots.txt") {
867
+ // Allow crawling so the X-Robots-Tag noindex is actually seen.
868
+ res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
869
+ res.end("User-agent: *\nDisallow:\n");
870
+ return;
871
+ }
872
+ if (url.pathname === "/" && (req.method === "GET" || req.method === "HEAD")) {
873
+ // A human (or crawler) landing on the bare host gets the docs page
874
+ // instead of a JSON error.
875
+ res.writeHead(302, { Location: "https://planpo.st/mcp" });
876
+ res.end();
877
+ return;
878
+ }
829
879
  if (url.pathname !== "/mcp")
830
- return fail(404, "Not found — MCP endpoint is /mcp");
880
+ return fail(404, "Not found. The MCP endpoint is /mcp");
831
881
  if (req.method !== "POST") {
832
882
  // Stateless mode: no SSE resumption stream, no sessions to delete.
833
- return fail(405, "Method not allowed — stateless transport accepts POST only");
883
+ return fail(405, "Method not allowed. This stateless transport accepts POST only; setup guide at https://planpo.st/mcp");
834
884
  }
835
885
  const auth = req.headers.authorization || "";
836
886
  const key = auth.startsWith("Bearer ") ? auth.slice(7) : "";
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "planpost-mcp",
3
- "version": "0.2.1",
4
- "//mcpName": "Ownership marker for the official MCP Registry: must equal `name` in server.json, or publishing fails validation.",
5
- "mcpName": "st.planpo/mcp",
6
- "description": "PlanPost MCP server — let Claude and other AI agents schedule posts, manage social media, create videos and carousels, and read analytics via the PlanPost public API",
3
+ "version": "0.3.0",
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
+ "mcpName": "io.github.lukBB/planpost",
6
+ "description": "PlanPost MCP server: schedule and publish social posts to Instagram, TikTok, LinkedIn, X, Facebook, Threads and YouTube from Claude, Cursor, Codex or any MCP client; render video and carousels, read analytics and revenue.",
7
7
  "license": "MIT",
8
8
  "type": "module",
9
9
  "bin": {
@@ -19,21 +19,43 @@
19
19
  "engines": {
20
20
  "node": ">=18"
21
21
  },
22
- "homepage": "https://planpo.st/agent",
22
+ "homepage": "https://planpo.st/mcp",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/lukBB/social-poster-v2.git",
26
+ "directory": "packages/mcp-server"
27
+ },
28
+ "bugs": {
29
+ "url": "https://planpo.st/support"
30
+ },
23
31
  "keywords": [
24
32
  "mcp",
25
33
  "model-context-protocol",
26
- "claude",
27
- "ai-agent",
34
+ "mcp-server",
28
35
  "social-media",
36
+ "social-media-scheduling",
29
37
  "scheduling",
38
+ "instagram",
39
+ "tiktok",
40
+ "linkedin",
41
+ "twitter",
42
+ "facebook",
43
+ "threads",
44
+ "youtube",
45
+ "claude",
46
+ "claude-code",
47
+ "cursor",
48
+ "codex",
49
+ "gemini-cli",
50
+ "ai-agent",
30
51
  "planpost"
31
52
  ],
32
53
  "scripts": {
33
54
  "build": "tsc",
34
55
  "dev": "tsc --watch",
35
56
  "start": "node dist/index.js",
36
- "prepublishOnly": "npm run build"
57
+ "prepublishOnly": "npm run build",
58
+ "test": "node --test test/*.test.mjs"
37
59
  },
38
60
  "dependencies": {
39
61
  "@modelcontextprotocol/sdk": "^1.12.1",
@@ -41,6 +63,8 @@
41
63
  },
42
64
  "devDependencies": {
43
65
  "typescript": "^5.7.0",
44
- "@types/node": "^22.0.0"
66
+ "@types/node": "^22.0.0",
67
+ "ajv": "^8.17.1",
68
+ "ajv-formats": "^3.0.1"
45
69
  }
46
70
  }