@omnisocials/mcp-server 1.3.3 → 1.3.5

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/README.md CHANGED
@@ -191,6 +191,11 @@ Full API docs: [docs.omnisocials.com](https://docs.omnisocials.com)
191
191
 
192
192
  ## Changelog
193
193
 
194
+ ### 1.3.4
195
+
196
+ - **Prevent accidental publishing.** Tool descriptions now front-load the difference: `create_post` is explicitly the **draft** path, `create_and_publish_post` carries a `STOP` gate listing every phrasing ("draft", "schedule", "review first", "save it", etc.) that should make the agent fall back to `create_post`. Default is "create draft" whenever there's ambiguity.
197
+ - **Draft + publish responses now include an `Open in OmniSocials` link.** Drafts and scheduled posts link to the editor (`/create-post/:id`) so the user can review or edit before it goes live. Published posts link to the details view (`/posts/:id`). Applies to `create_post`, `create_and_publish_post`, `update_post`, and `get_post`.
198
+
194
199
  ### 1.3.2 / 1.3.3
195
200
 
196
201
  - **Fixed:** `create_post` and `create_and_publish_post` no longer throw `str.replace is not a function` when the API returns per-platform `content` as an object. Response formatters now wrapped in a safe fallback so a formatting throw can never surface as a tool error (closes the duplicate-on-retry trap).
@@ -39,6 +39,19 @@ function selectedChannels(p) {
39
39
  function displayDate(p) {
40
40
  return p?.schedule_at ?? p?.scheduled_at ?? p?.published_at ?? p?.created_at ?? null;
41
41
  }
42
+ // Direct link into the OmniSocials dashboard so the user can verify a draft
43
+ // or jump to a published post. Drafts and scheduled posts open in the editor
44
+ // at /create-post/:id; published posts open in the details view at /posts/:id.
45
+ function postAppUrl(p) {
46
+ const id = p?.id;
47
+ if (!id)
48
+ return null;
49
+ const status = String(p?.status || "").toLowerCase();
50
+ const isEditable = status === "draft" || status === "scheduled" || status === "" || status === "failed";
51
+ return isEditable
52
+ ? `https://app.omnisocials.com/create-post/${id}`
53
+ : `https://app.omnisocials.com/posts/${id}`;
54
+ }
42
55
  export function registerPostTools(server, getClient) {
43
56
  server.tool("list_posts", "List all posts in the workspace. Optionally filter by status. The content column shows a preview of the default caption; if a post has per-platform overrides (e.g. a custom X version), the preview is suffixed with *(per-platform)* — call `get_post` to see every variant.", {
44
57
  status: z.string().optional().describe("Filter by status: draft, scheduled, published, failed"),
@@ -106,6 +119,7 @@ export function registerPostTools(server, getClient) {
106
119
  md += `| Field | Value |\n`;
107
120
  md += `|-------|-------|\n`;
108
121
  const schedAt = p.schedule_at ?? p.scheduled_at;
122
+ const appUrl = postAppUrl(p);
109
123
  md += `| **ID** | \`${p.id}\` |\n`;
110
124
  md += `| **Status** | ${capitalize(p.status || "—")} |\n`;
111
125
  md += `| **Type** | ${p.type || "post"} |\n`;
@@ -115,6 +129,8 @@ export function registerPostTools(server, getClient) {
115
129
  if (p.published_at)
116
130
  md += `| **Published** | ${formatDateTime(p.published_at)} |\n`;
117
131
  md += `| **Created** | ${formatDateTime(p.created_at)} |\n`;
132
+ if (appUrl)
133
+ md += `| **Open in OmniSocials** | ${appUrl} |\n`;
118
134
  if (p.media?.length) {
119
135
  md += `| **Media** | ${p.media.length} file(s) |\n`;
120
136
  }
@@ -150,7 +166,11 @@ export function registerPostTools(server, getClient) {
150
166
  content: [{ type: "text", text: md }],
151
167
  };
152
168
  });
153
- server.tool("create_post", `Create a new social media post, story, or reel.
169
+ server.tool("create_post", `Create a **DRAFT** post, story, or reel in OmniSocials. The post is saved to the workspace but is NOT published to any social platform until you explicitly call \`publish_post\` or the user schedules it for a future time.
170
+
171
+ USE THIS TOOL FOR: drafts, scheduled posts, "save it for later", review-before-publish, "make a draft", "write a draft post", "create a post" (when the user has NOT explicitly asked to publish/post right now), or any case where the user might want to review or tweak the post before it goes live.
172
+
173
+ DO NOT use \`create_and_publish_post\` instead unless the user has explicitly said something like "post it now", "publish immediately", "go live", "send it". When in doubt, default to this tool — drafts can always be published later via \`publish_post\` and that's reversible; publishing is not.
154
174
 
155
175
  IMPORTANT — Before calling this tool, make sure you have all required information from the user. If anything is missing, ASK the user before calling:
156
176
 
@@ -196,14 +216,14 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
196
216
  link: z.string().optional(),
197
217
  }).optional().describe("Pinterest-specific options"),
198
218
  youtube: z.object({
199
- title: z.string().optional(),
219
+ title: z.string().optional().describe("Short title shown on YouTube. Falls back to \"YouTube Short\" when omitted."),
200
220
  tags: z.array(z.string()).optional(),
201
221
  privacy_status: z.enum(["public", "private", "unlisted"]).optional(),
202
222
  category_id: z.string().optional(),
203
223
  made_for_kids: z.boolean().optional(),
204
224
  notify_subscribers: z.boolean().optional(),
205
225
  contains_synthetic_media: z.boolean().optional(),
206
- }).optional().describe("YouTube Shorts options"),
226
+ }).optional().describe("YouTube Shorts options. Only applies when type is 'reel' and youtube is among the selected channels."),
207
227
  instagram: z.object({
208
228
  share_to_feed: z.boolean().optional(),
209
229
  thumbnail_type: z.enum(["from-video", "from-library"]).optional(),
@@ -238,10 +258,11 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
238
258
  // The post is already created by the time this runs. If response
239
259
  // formatting throws, we must NOT surface it as a tool error — agents
240
260
  // retry on errors and that produces orphan duplicate posts.
261
+ const appUrl = postAppUrl(p);
241
262
  try {
242
263
  const schedAt = p.schedule_at ?? p.scheduled_at;
243
264
  const channels = selectedChannels(p);
244
- let md = `## Post Created\n\n`;
265
+ let md = `## Draft Created (not yet published)\n\n`;
245
266
  md += `| Field | Value |\n`;
246
267
  md += `|-------|-------|\n`;
247
268
  md += `| **ID** | \`${p.id}\` |\n`;
@@ -250,17 +271,31 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
250
271
  md += `| **Scheduled** | ${formatDateTime(schedAt)} |\n`;
251
272
  if (channels.length)
252
273
  md += `| **Channels** | ${channels.join(", ")} |\n`;
253
- md += `\n**Content:** ${truncate(p.content, 100)}`;
274
+ if (appUrl)
275
+ md += `| **Open in OmniSocials** | ${appUrl} |\n`;
276
+ md += `\n**Content:** ${truncate(p.content, 100)}\n\n`;
277
+ md += `_Share the link above with the user so they can review or edit the draft. Publish later with \`publish_post\` once they confirm._`;
254
278
  return { content: [{ type: "text", text: md }] };
255
279
  }
256
280
  catch {
257
281
  // Fall back to the minimum the agent needs to confirm success — never error.
282
+ const fallback = `## Draft Created\n\n- **ID:** \`${p.id}\`\n- **Status:** ${p.status || "draft"}${appUrl ? `\n- **Open in OmniSocials:** ${appUrl}` : ""}\n\n_Share the link above with the user so they can review the draft._`;
258
283
  return {
259
- content: [{ type: "text", text: `## Post Created\n\n- **ID:** \`${p.id}\`\n- **Status:** ${p.status || "draft"}\n\n_Post was created successfully. Use \`get_post\` with the ID above to see full details._` }],
284
+ content: [{ type: "text", text: fallback }],
260
285
  };
261
286
  }
262
287
  });
263
- server.tool("create_and_publish_post", `Create a new post and publish it immediately (no scheduling).
288
+ server.tool("create_and_publish_post", `Create a post AND publish it to every selected social platform IMMEDIATELY. The post goes live within seconds, is publicly visible to followers on each platform, and **cannot be unpublished** through the OmniSocials API — the user would have to delete each post on each platform manually.
289
+
290
+ **STOP — do NOT call this tool if the user said any of the following anywhere in the conversation:**
291
+ - "draft", "make a draft", "save as draft", "save it"
292
+ - "schedule", "later", "tomorrow", "next week", a specific time
293
+ - "review first", "let me see it", "preview", "show me"
294
+ - "create a post" without an explicit "now"/"publish"/"go live"
295
+
296
+ In any of those cases, use \`create_post\` instead. It saves a draft you can show to the user, edit, and publish later via \`publish_post\` — that path is reversible at every step. This path is not. When in doubt, choose \`create_post\`.
297
+
298
+ USE THIS TOOL ONLY when the user has explicitly asked to publish/post right now: "publish it", "post it now", "send it", "go live", "fire it off", etc.
264
299
 
265
300
  IMPORTANT — Before calling this tool, make sure you have all required information. If anything is missing, ASK the user first:
266
301
 
@@ -294,10 +329,10 @@ Do NOT call without required media — it will fail.`, {
294
329
  link: z.string().optional(),
295
330
  }).optional().describe("Pinterest-specific options"),
296
331
  youtube: z.object({
297
- title: z.string().optional(),
332
+ title: z.string().optional().describe("Short title shown on YouTube."),
298
333
  tags: z.array(z.string()).optional(),
299
334
  privacy_status: z.enum(["public", "private", "unlisted"]).optional(),
300
- }).optional().describe("YouTube Shorts options"),
335
+ }).optional().describe("YouTube Shorts options. Only applies when type is 'reel' and youtube is among the selected channels."),
301
336
  tiktok: z.object({
302
337
  privacy_level: z.enum(["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR", "SELF_ONLY"]).optional(),
303
338
  }).optional().describe("TikTok options"),
@@ -318,6 +353,7 @@ Do NOT call without required media — it will fail.`, {
318
353
  };
319
354
  }
320
355
  const p = result.data;
356
+ const appUrl = postAppUrl(p);
321
357
  try {
322
358
  const channels = selectedChannels(p);
323
359
  let md = `## Post Created & Publishing\n\n`;
@@ -327,13 +363,16 @@ Do NOT call without required media — it will fail.`, {
327
363
  md += `| **Status** | ${capitalize(p.status || "publishing")} |\n`;
328
364
  if (channels.length)
329
365
  md += `| **Channels** | ${channels.join(", ")} |\n`;
366
+ if (appUrl)
367
+ md += `| **Open in OmniSocials** | ${appUrl} |\n`;
330
368
  md += `\n**Content:** ${truncate(p.content, 100)}`;
331
369
  return { content: [{ type: "text", text: md }] };
332
370
  }
333
371
  catch {
334
372
  // The post is already queued; never let a formatting throw look like an API error.
373
+ const fallback = `## Post Created & Publishing\n\n- **ID:** \`${p.id}\`\n- **Status:** ${p.status || "publishing"}${appUrl ? `\n- **Open in OmniSocials:** ${appUrl}` : ""}\n\n_Post was queued successfully._`;
335
374
  return {
336
- content: [{ type: "text", text: `## Post Created & Publishing\n\n- **ID:** \`${p.id}\`\n- **Status:** ${p.status || "publishing"}\n\n_Post was queued successfully. Use \`get_post\` with the ID above for full details._` }],
375
+ content: [{ type: "text", text: fallback }],
337
376
  };
338
377
  }
339
378
  });
@@ -370,6 +409,7 @@ Do NOT call without required media — it will fail.`, {
370
409
  }
371
410
  const p = result.data;
372
411
  const schedAt = p.schedule_at ?? p.scheduled_at;
412
+ const appUrl = postAppUrl(p);
373
413
  let md = `## Post Updated\n\n`;
374
414
  md += `| Field | Value |\n`;
375
415
  md += `|-------|-------|\n`;
@@ -377,6 +417,8 @@ Do NOT call without required media — it will fail.`, {
377
417
  md += `| **Status** | ${capitalize(p.status || "—")} |\n`;
378
418
  if (schedAt)
379
419
  md += `| **Scheduled** | ${formatDateTime(schedAt)} |\n`;
420
+ if (appUrl)
421
+ md += `| **Open in OmniSocials** | ${appUrl} |\n`;
380
422
  return {
381
423
  content: [{ type: "text", text: md }],
382
424
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnisocials/mcp-server",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "description": "MCP server for OmniSocials API - manage social media posts, media, accounts, analytics, and webhooks",
5
5
  "type": "module",
6
6
  "main": "build/index.js",