posthive-mcp 0.1.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 +121 -0
  2. package/dist/index.js +357 -0
  3. package/package.json +53 -0
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # posthive-mcp
2
+
3
+ MCP server for [Posthive](https://posthive.co) — schedule posts to 11 social platforms directly from Claude, Cursor, Windsurf, or any MCP-compatible AI agent.
4
+
5
+ **Supported platforms:** Bluesky, Threads, Instagram, LinkedIn, Mastodon, YouTube, Facebook Pages, Pinterest, Telegram, X (Twitter), Nostr
6
+
7
+ ---
8
+
9
+ ## Quick start
10
+
11
+ You need a Posthive account and an API key. Get one at [posthive.co](https://posthive.co) → Settings → API Keys (Pro/Team plan).
12
+
13
+ ### Claude Desktop
14
+
15
+ Add this to your `claude_desktop_config.json`:
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "posthive": {
21
+ "command": "npx",
22
+ "args": ["posthive-mcp"],
23
+ "env": {
24
+ "POSTHIVE_API_KEY": "ph_your_key_here",
25
+ "POSTHIVE_API_URL": "https://api.posthive.co"
26
+ }
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ ### Claude Code (CLI)
33
+
34
+ ```bash
35
+ claude mcp add posthive -e POSTHIVE_API_KEY=ph_xxx -e POSTHIVE_API_URL=https://api.posthive.co -- npx posthive-mcp
36
+ ```
37
+
38
+ ### Cursor / Windsurf
39
+
40
+ Add to your MCP config (`.cursor/mcp.json` or `.windsurf/mcp.json`):
41
+
42
+ ```json
43
+ {
44
+ "mcpServers": {
45
+ "posthive": {
46
+ "command": "npx",
47
+ "args": ["posthive-mcp"],
48
+ "env": {
49
+ "POSTHIVE_API_KEY": "ph_your_key_here",
50
+ "POSTHIVE_API_URL": "https://api.posthive.co"
51
+ }
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ ### Self-hosted Posthive
58
+
59
+ Replace `https://api.posthive.co` with your own API URL (e.g. `http://localhost:3001`).
60
+
61
+ ---
62
+
63
+ ## Environment variables
64
+
65
+ | Variable | Required | Description |
66
+ |----------|----------|-------------|
67
+ | `POSTHIVE_API_KEY` | Yes | API key from Posthive Settings → API Keys |
68
+ | `POSTHIVE_API_URL` | Yes | Base URL of your Posthive API |
69
+
70
+ ---
71
+
72
+ ## Available tools
73
+
74
+ | Tool | What it does |
75
+ |------|-------------|
76
+ | `list_accounts` | List all connected social accounts and their IDs |
77
+ | `create_post` | Create a post (saved as draft by default) |
78
+ | `get_post` | Get full details and publish status of a post |
79
+ | `list_scheduled_posts` | List upcoming scheduled posts and drafts |
80
+ | `approve_draft` | Approve a draft and schedule it for publishing |
81
+ | `update_post` | Edit content or scheduled time of a pending post |
82
+ | `duplicate_post` | Clone a post as a new draft |
83
+ | `delete_post` | Delete a pending or draft post |
84
+ | `list_templates` | List saved post templates |
85
+ | `create_from_template` | Create a post from a template |
86
+
87
+ ### Draft-first by default
88
+
89
+ All posts created via MCP are saved as **drafts** unless you explicitly pass `schedule_directly: true`. Drafts show up in Posthive → Posts for review before anything goes live. Use `approve_draft` to promote a draft to the scheduled queue.
90
+
91
+ ---
92
+
93
+ ## Example prompts
94
+
95
+ ```
96
+ Schedule a Bluesky post about our new feature launch for tomorrow at 9 AM.
97
+
98
+ Draft a LinkedIn post summarising this article and add a link in the first comment.
99
+
100
+ List all my pending posts for this week.
101
+
102
+ Duplicate last week's top post and reschedule it for Monday.
103
+
104
+ Create posts for all my accounts announcing today's product update.
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Links
110
+
111
+ - [Posthive](https://posthive.co)
112
+ - [Documentation](https://posthive.co/docs#mcp-overview)
113
+ - [For Developers](https://posthive.co/for-developers)
114
+ - [GitHub](https://github.com/AstaBlackClove/posthive)
115
+ - [Issues](https://github.com/AstaBlackClove/posthive/issues)
116
+
117
+ ---
118
+
119
+ ## License
120
+
121
+ AGPL-3.0 — see [LICENSE](https://github.com/AstaBlackClove/posthive/blob/main/LICENSE) for details.
package/dist/index.js ADDED
@@ -0,0 +1,357 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Posthive MCP Server
4
+ *
5
+ * Exposes Posthive's scheduling API as MCP tools for use with Claude Code,
6
+ * Cursor, or any MCP-compatible agent.
7
+ *
8
+ * Required env vars:
9
+ * POSTHIVE_API_KEY — API key from Posthive Settings → API Keys
10
+ * POSTHIVE_API_URL — Base URL of your Posthive API (e.g. https://api.posthive.co)
11
+ */
12
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
15
+ const API_KEY = process.env.POSTHIVE_API_KEY;
16
+ const API_URL = (process.env.POSTHIVE_API_URL ?? "").replace(/\/$/, "");
17
+ if (!API_KEY) {
18
+ process.stderr.write("Error: POSTHIVE_API_KEY environment variable is required\n");
19
+ process.exit(1);
20
+ }
21
+ if (!API_URL) {
22
+ process.stderr.write("Error: POSTHIVE_API_URL environment variable is required\n");
23
+ process.exit(1);
24
+ }
25
+ // ─── HTTP helper ─────────────────────────────────────────────────────────────
26
+ async function apiCall(method, path, body) {
27
+ const res = await fetch(`${API_URL}${path}`, {
28
+ method,
29
+ headers: {
30
+ Authorization: `Bearer ${API_KEY}`,
31
+ "Content-Type": "application/json",
32
+ },
33
+ body: body !== undefined ? JSON.stringify(body) : undefined,
34
+ });
35
+ const text = await res.text();
36
+ let json;
37
+ try {
38
+ json = JSON.parse(text);
39
+ }
40
+ catch {
41
+ json = { raw: text };
42
+ }
43
+ if (!res.ok) {
44
+ const msg = json?.error ??
45
+ json?.message ??
46
+ text;
47
+ throw new Error(`API ${res.status}: ${typeof msg === "string" ? msg : JSON.stringify(msg)}`);
48
+ }
49
+ return json;
50
+ }
51
+ function ok(data) {
52
+ return {
53
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
54
+ };
55
+ }
56
+ // ─── Tool definitions ─────────────────────────────────────────────────────────
57
+ const TOOLS = [
58
+ {
59
+ name: "list_accounts",
60
+ description: "List all connected social media accounts for this Posthive workspace. " +
61
+ "Returns account IDs, platforms, and display names. Use these IDs when creating posts.",
62
+ inputSchema: { type: "object", properties: {}, required: [] },
63
+ },
64
+ {
65
+ name: "create_post",
66
+ description: "Create a post in Posthive. " +
67
+ "BY DEFAULT posts are saved as DRAFTS and require manual approval in the Posthive review queue before anything is published. " +
68
+ "Nothing is published automatically unless schedule_directly is explicitly set to true with a future scheduled_time. " +
69
+ "Use list_accounts to get valid account IDs before calling this tool.",
70
+ inputSchema: {
71
+ type: "object",
72
+ properties: {
73
+ content: { type: "string", description: "The post text content." },
74
+ account_ids: {
75
+ type: "array",
76
+ items: { type: "string" },
77
+ description: "Array of account IDs to post to. Use list_accounts to get IDs.",
78
+ },
79
+ media_urls: {
80
+ type: "array",
81
+ items: { type: "string" },
82
+ description: "Array of already-uploaded media URLs (images or video). Upload via POST /api/v1/upload first to get URLs.",
83
+ },
84
+ media_type: {
85
+ type: "string",
86
+ enum: ["post", "reel", "story"],
87
+ description: "Instagram media type. 'post' = feed image/carousel, 'reel' = video reel, 'story' = story. Only relevant for Instagram accounts.",
88
+ },
89
+ youtube_type: {
90
+ type: "string",
91
+ enum: ["short", "video"],
92
+ description: "YouTube video type. 'short' = YouTube Short (vertical, ≤60s), 'video' = standard upload. Only relevant for YouTube accounts.",
93
+ },
94
+ first_comment: { type: "string", description: "Optional first comment to fire immediately after the post goes live." },
95
+ scheduled_time: {
96
+ type: "string",
97
+ description: "ISO 8601 datetime for when the post should go live. Required when schedule_directly is true.",
98
+ },
99
+ schedule_directly: {
100
+ type: "boolean",
101
+ description: "When true, post is scheduled directly and will publish at scheduled_time without manual approval. " +
102
+ "Requires scheduled_time. Defaults to false (saves as draft).",
103
+ },
104
+ per_account: {
105
+ type: "object",
106
+ description: "Per-account content overrides. Keys are account IDs.",
107
+ additionalProperties: {
108
+ type: "object",
109
+ properties: { text: { type: "string" }, commentText: { type: "string" } },
110
+ },
111
+ },
112
+ dry_run: { type: "boolean", description: "When true, runs the full pipeline without real API calls." },
113
+ },
114
+ required: ["content", "account_ids"],
115
+ },
116
+ },
117
+ {
118
+ name: "get_post",
119
+ description: "Get full details of a single post by ID, including per-platform publish status.",
120
+ inputSchema: {
121
+ type: "object",
122
+ properties: {
123
+ post_id: { type: "string", description: "Post ID from list_scheduled_posts or create_post." },
124
+ },
125
+ required: ["post_id"],
126
+ },
127
+ },
128
+ {
129
+ name: "list_scheduled_posts",
130
+ description: "List upcoming scheduled posts and drafts in the queue.",
131
+ inputSchema: {
132
+ type: "object",
133
+ properties: {
134
+ status: {
135
+ type: "string",
136
+ enum: ["pending", "draft", "done", "failed"],
137
+ description: "Filter by post status. Omit to return all.",
138
+ },
139
+ limit: { type: "number", description: "Maximum number of posts to return (default 20, max 100)." },
140
+ },
141
+ required: [],
142
+ },
143
+ },
144
+ {
145
+ name: "approve_draft",
146
+ description: "Approve a draft post and schedule it for publishing at a specific time. " +
147
+ "Promotes a draft to the scheduled queue — it will publish at scheduled_time without further approval. " +
148
+ "Only works on posts with status 'draft'.",
149
+ inputSchema: {
150
+ type: "object",
151
+ properties: {
152
+ post_id: { type: "string", description: "Post ID of the draft to approve." },
153
+ scheduled_time: { type: "string", description: "ISO 8601 datetime when to publish. Must be in the future." },
154
+ },
155
+ required: ["post_id", "scheduled_time"],
156
+ },
157
+ },
158
+ {
159
+ name: "update_post",
160
+ description: "Update content or scheduled time of a queued or draft post. Only pending/draft posts can be updated.",
161
+ inputSchema: {
162
+ type: "object",
163
+ properties: {
164
+ post_id: { type: "string", description: "The ID of the post to update." },
165
+ content: { type: "string", description: "Updated post text content." },
166
+ scheduled_time: { type: "string", description: "Updated scheduled time (ISO 8601)." },
167
+ first_comment: { type: "string", description: "Updated first comment text." },
168
+ per_account: {
169
+ type: "object",
170
+ description: "Updated per-account content overrides.",
171
+ additionalProperties: {
172
+ type: "object",
173
+ properties: { text: { type: "string" }, commentText: { type: "string" } },
174
+ },
175
+ },
176
+ },
177
+ required: ["post_id"],
178
+ },
179
+ },
180
+ {
181
+ name: "duplicate_post",
182
+ description: "Clone an existing post as a new draft. Copies content, accounts, and per-account overrides.",
183
+ inputSchema: {
184
+ type: "object",
185
+ properties: {
186
+ post_id: { type: "string", description: "Post ID to duplicate." },
187
+ },
188
+ required: ["post_id"],
189
+ },
190
+ },
191
+ {
192
+ name: "delete_post",
193
+ description: "Delete a pending or draft post from the queue.",
194
+ inputSchema: {
195
+ type: "object",
196
+ properties: {
197
+ post_id: { type: "string", description: "The ID of the post to delete." },
198
+ },
199
+ required: ["post_id"],
200
+ },
201
+ },
202
+ {
203
+ name: "list_templates",
204
+ description: "List all saved post templates in this Posthive workspace.",
205
+ inputSchema: { type: "object", properties: {}, required: [] },
206
+ },
207
+ {
208
+ name: "create_from_template",
209
+ description: "Create a draft post using a saved template as the base content. " +
210
+ "You can override the text before saving. Use list_templates to get template IDs.",
211
+ inputSchema: {
212
+ type: "object",
213
+ properties: {
214
+ template_id: { type: "string", description: "Template ID from list_templates." },
215
+ account_ids: {
216
+ type: "array",
217
+ items: { type: "string" },
218
+ description: "Account IDs to post to. Use list_accounts to get IDs.",
219
+ },
220
+ content_override: { type: "string", description: "Override the template text. Leave blank to use template text as-is." },
221
+ first_comment_override: { type: "string", description: "Override the template first comment." },
222
+ schedule_directly: {
223
+ type: "boolean",
224
+ description: "When true, schedule immediately instead of saving as draft. Requires scheduled_time.",
225
+ },
226
+ scheduled_time: { type: "string", description: "ISO 8601 publish time. Required when schedule_directly is true." },
227
+ },
228
+ required: ["template_id", "account_ids"],
229
+ },
230
+ },
231
+ ];
232
+ // ─── Server ───────────────────────────────────────────────────────────────────
233
+ const server = new Server({ name: "posthive", version: "0.1.0" }, { capabilities: { tools: {} } });
234
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
235
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
236
+ const { name, arguments: args } = req.params;
237
+ const a = (args ?? {});
238
+ try {
239
+ switch (name) {
240
+ case "list_accounts": {
241
+ const data = await apiCall("GET", "/api/v1/accounts");
242
+ return ok(data);
243
+ }
244
+ case "create_post": {
245
+ const scheduleDirectly = a.schedule_directly === true;
246
+ const scheduledTime = a.scheduled_time;
247
+ if (scheduleDirectly && !scheduledTime) {
248
+ throw new Error("scheduled_time is required when schedule_directly is true");
249
+ }
250
+ const body = {
251
+ content: a.content,
252
+ accountIds: a.account_ids,
253
+ draft: !scheduleDirectly,
254
+ ...(scheduleDirectly && scheduledTime ? { scheduledFor: scheduledTime } : {}),
255
+ ...(a.media_urls ? { images: a.media_urls } : {}),
256
+ ...(a.media_type ? { mediaType: a.media_type } : {}),
257
+ ...(a.youtube_type ? { youtubeType: a.youtube_type } : {}),
258
+ ...(a.first_comment ? { commentText: a.first_comment } : {}),
259
+ ...(a.per_account ? { perAccount: a.per_account } : {}),
260
+ ...(a.dry_run ? { dryRun: true } : {}),
261
+ };
262
+ const data = await apiCall("POST", "/api/v1/posts", body);
263
+ return ok({
264
+ ...data,
265
+ _note: scheduleDirectly
266
+ ? "Post scheduled — will publish at scheduled_time."
267
+ : "Post saved as DRAFT. Use approve_draft to schedule it, or review it in Posthive → Posts.",
268
+ });
269
+ }
270
+ case "get_post": {
271
+ const data = await apiCall("GET", `/api/v1/posts/${a.post_id}`);
272
+ return ok(data);
273
+ }
274
+ case "list_scheduled_posts": {
275
+ const params = new URLSearchParams();
276
+ if (a.status)
277
+ params.set("status", a.status);
278
+ if (a.limit)
279
+ params.set("limit", String(a.limit));
280
+ const qs = params.size ? `?${params}` : "";
281
+ const data = await apiCall("GET", `/api/v1/posts${qs}`);
282
+ return ok(data);
283
+ }
284
+ case "approve_draft": {
285
+ const data = await apiCall("POST", `/api/v1/posts/${a.post_id}/approve`, {
286
+ scheduledFor: a.scheduled_time,
287
+ });
288
+ return ok({
289
+ ...data,
290
+ _note: "Draft approved and scheduled. It will publish at the specified time.",
291
+ });
292
+ }
293
+ case "update_post": {
294
+ const { post_id, content, scheduled_time, first_comment, per_account } = a;
295
+ const body = {
296
+ ...(content !== undefined ? { content } : {}),
297
+ ...(scheduled_time !== undefined ? { scheduledFor: scheduled_time } : {}),
298
+ ...(first_comment !== undefined ? { commentText: first_comment } : {}),
299
+ ...(per_account !== undefined ? { perAccount: per_account } : {}),
300
+ };
301
+ if (Object.keys(body).length === 0) {
302
+ throw new Error("Provide at least one field to update");
303
+ }
304
+ const data = await apiCall("PATCH", `/api/v1/posts/${post_id}`, body);
305
+ return ok(data);
306
+ }
307
+ case "duplicate_post": {
308
+ const data = await apiCall("POST", `/api/v1/posts/${a.post_id}/duplicate`);
309
+ return ok({
310
+ ...data,
311
+ _note: "Post duplicated as a new draft. Use approve_draft to schedule it.",
312
+ });
313
+ }
314
+ case "delete_post": {
315
+ const data = await apiCall("DELETE", `/api/v1/posts/${a.post_id}`);
316
+ return ok(data);
317
+ }
318
+ case "list_templates": {
319
+ const data = await apiCall("GET", "/api/v1/templates");
320
+ return ok(data);
321
+ }
322
+ case "create_from_template": {
323
+ const scheduleDirectly = a.schedule_directly === true;
324
+ const scheduledTime = a.scheduled_time;
325
+ if (scheduleDirectly && !scheduledTime) {
326
+ throw new Error("scheduled_time is required when schedule_directly is true");
327
+ }
328
+ const body = {
329
+ accountIds: a.account_ids,
330
+ draft: !scheduleDirectly,
331
+ ...(scheduleDirectly && scheduledTime ? { scheduledFor: scheduledTime } : {}),
332
+ ...(a.content_override ? { contentOverride: a.content_override } : {}),
333
+ ...(a.first_comment_override ? { firstCommentOverride: a.first_comment_override } : {}),
334
+ };
335
+ const data = await apiCall("POST", `/api/v1/templates/${a.template_id}/use`, body);
336
+ return ok({
337
+ ...data,
338
+ _note: scheduleDirectly
339
+ ? "Post created from template and scheduled."
340
+ : "Post created from template as DRAFT. Use approve_draft to schedule it.",
341
+ });
342
+ }
343
+ default:
344
+ throw new Error(`Unknown tool: ${name}`);
345
+ }
346
+ }
347
+ catch (err) {
348
+ const message = err instanceof Error ? err.message : String(err);
349
+ return {
350
+ content: [{ type: "text", text: `Error: ${message}` }],
351
+ isError: true,
352
+ };
353
+ }
354
+ });
355
+ // ─── Start ────────────────────────────────────────────────────────────────────
356
+ const transport = new StdioServerTransport();
357
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "posthive-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Posthive — schedule posts to 11 social platforms from Claude, Cursor, Windsurf, or any MCP-compatible AI agent",
5
+ "type": "module",
6
+ "bin": {
7
+ "posthive-mcp": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "dev": "tsc --watch",
15
+ "start": "node dist/index.js",
16
+ "prepublishOnly": "tsc"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "model-context-protocol",
21
+ "posthive",
22
+ "social-media",
23
+ "scheduler",
24
+ "bluesky",
25
+ "threads",
26
+ "linkedin",
27
+ "instagram",
28
+ "mastodon",
29
+ "claude",
30
+ "cursor",
31
+ "ai-agent"
32
+ ],
33
+ "author": "Posthive",
34
+ "license": "AGPL-3.0",
35
+ "homepage": "https://posthive.co/for-developers",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/AstaBlackClove/posthive"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/AstaBlackClove/posthive/issues"
42
+ },
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "^1.12.1"
45
+ },
46
+ "devDependencies": {
47
+ "typescript": "^5.5.4",
48
+ "@types/node": "^20.14.12"
49
+ },
50
+ "engines": {
51
+ "node": ">=20"
52
+ }
53
+ }