@omnisocials/mcp-server 1.14.0 → 1.15.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.
package/README.md CHANGED
@@ -207,6 +207,18 @@ Full API docs: [docs.omnisocials.com](https://docs.omnisocials.com)
207
207
 
208
208
  ## Changelog
209
209
 
210
+ ### 1.15.0
211
+
212
+ - **Reel cover read-back:** `get_post` now renders the Instagram Reel cover selection (`thumbnail_type`, `thumb_offset` shown as ms and m:ss, `cover_url`), so a cover set through the API can be verified without opening the dashboard.
213
+ - `update_post` now accepts `instagram.thumbnail_type`, `instagram.thumb_offset`, and `instagram.cover_url` (previously create-only on this server).
214
+ - Tool schemas now document that `thumb_offset` is in **milliseconds** (e.g. `3000` = 0:03).
215
+ - Companion server (`mcp.omnisocials.com`): workspace name now always renders correctly in tool output instead of the "Workspace 1" placeholder when `get_post`/`list_posts` was the session's first call.
216
+
217
+ ### 1.14.0
218
+
219
+ - **`get_recent_platform_posts` rebuilt for ingesting native posts:** returns a `Structured data` JSON block with one record per post — the platform's own post/media `id` (stable de-dupe key), a `permalink`, the full untruncated caption, and exact-integer metrics (no more rounded `1.3K`).
220
+ - **`permalink`** added on every recent-platform post (all platforms except Google Business); also on the REST `GET /posts/recent-platform` response.
221
+
210
222
  ### 1.13.1
211
223
 
212
224
  - Docs-only republish to refresh the README on npmjs.com — the changelog below had drifted (it stopped at 1.5.0) and the Social Inbox tools were missing from the tool list. No code changes from 1.13.0.
@@ -140,7 +140,7 @@ export function registerPostTools(server, getClient) {
140
140
  content: [{ type: "text", text: md }],
141
141
  };
142
142
  });
143
- server.tool("get_post", "Get details of a specific post by ID — content, channels, media (with URLs), first comment, Instagram collaborators/user tags/location/Trial Reel state, dates and live URLs, enough to fully verify a scheduled post without opening the dashboard. When a post has per-platform caption overrides (e.g. a shorter X version alongside the default), every variant is rendered as its own labeled block under `### Content` so you can see exactly what each platform will publish. X threads are rendered under `### X Thread` with each tweet labeled in publish order — read this to see the full chained tweet text, since thread-only posts have no caption in `content`. After publishing, includes `published_urls` — a map of platform → live URL for each platform that successfully posted (e.g. facebook, instagram, linkedin, x). Useful for polling: when a post's status is `published`, read `published_urls` to surface the live links.", {
143
+ server.tool("get_post", "Get details of a specific post by ID — content, channels, media (with URLs), first comment, Instagram collaborators/user tags/location/Trial Reel state/Reel cover (thumbnail_type + thumb_offset in ms), dates and live URLs, enough to fully verify a scheduled post without opening the dashboard. When a post has per-platform caption overrides (e.g. a shorter X version alongside the default), every variant is rendered as its own labeled block under `### Content` so you can see exactly what each platform will publish. X threads are rendered under `### X Thread` with each tweet labeled in publish order — read this to see the full chained tweet text, since thread-only posts have no caption in `content`. After publishing, includes `published_urls` — a map of platform → live URL for each platform that successfully posted (e.g. facebook, instagram, linkedin, x). Useful for polling: when a post's status is `published`, read `published_urls` to surface the live links.", {
144
144
  id: z.string().describe("The post ID"),
145
145
  }, async ({ id }) => {
146
146
  const result = await getClient().getPost(id);
@@ -300,6 +300,20 @@ export function registerPostTools(server, getClient) {
300
300
  : "manual (graduate it yourself in the Instagram app)";
301
301
  md += `\n\n**Instagram Trial Reel:** enabled — shown to non-followers first; graduation: ${strategy}\n`;
302
302
  }
303
+ // Reel cover selection read-back (thumb_offset is in MILLISECONDS).
304
+ // The API defaults these to ""/0 when never set, so only render values
305
+ // that were actually chosen.
306
+ if (igOpts && typeof igOpts === "object") {
307
+ const offsetMs = Number(igOpts.thumb_offset) || 0;
308
+ if (igOpts.thumbnail_type === "from-library" && igOpts.cover_url) {
309
+ md += `\n**Instagram Reel cover:** custom image — ${igOpts.cover_url}\n`;
310
+ }
311
+ else if (igOpts.thumbnail_type === "from-video" || offsetMs > 0) {
312
+ const totalSec = Math.floor(offsetMs / 1000);
313
+ const mmss = `${Math.floor(totalSec / 60)}:${String(totalSec % 60).padStart(2, "0")}`;
314
+ md += `\n**Instagram Reel cover:** video frame at ${mmss} (thumb_offset ${offsetMs} ms)\n`;
315
+ }
316
+ }
303
317
  return {
304
318
  content: [{ type: "text", text: md }],
305
319
  };
@@ -359,7 +373,7 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
359
373
  media_urls: z.union([
360
374
  z.array(z.string()),
361
375
  z.record(z.string(), z.array(z.string())),
362
- ]).optional().describe("External image/video URLs — flat array (same for all platforms) or object with platform keys: { default: [...], instagram: [...], pinterest: [...] }. Max 10 total. When using per-platform format, 'default' is the fallback for selected platforms without their own key. Pass an empty array (e.g. facebook: []) to opt a platform out of media."),
376
+ ]).optional().describe("External image/video URLs — flat array (same for all platforms) or object with platform keys: { default: [...], instagram: [...], pinterest: [...] }. Max 10 total, each file ≤ 100 MB. When using per-platform format, 'default' is the fallback for selected platforms without their own key. Pass an empty array (e.g. facebook: []) to opt a platform out of media. For files over 100 MB (up to 1 GB): upload_media with method 'url' first, then pass the returned media id in `media`."),
363
377
  type: z.enum(["post", "story", "reel"]).optional().describe("Content type: 'post' (default), 'story' (Instagram/Facebook/Snapchat), 'reel' (Instagram/Facebook/YouTube/TikTok)"),
364
378
  link_url: z.string().optional().describe("URL to share as a rich preview card on platforms that support link-share posts (LinkedIn and Facebook). The URL renders as a tile with thumbnail / title / description instead of plain text. Ignored on platforms that don't support link shares, and ignored on posts that already have media attached (media wins)."),
365
379
  link_title: z.string().optional().describe("Optional title for the link-share preview. LinkedIn uses this when set; Facebook ignores it and fetches OG metadata server-side. Omit to let LinkedIn auto-fetch the page title."),
@@ -392,9 +406,9 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
392
406
  }).optional().describe("YouTube Shorts options. Only applies when type is 'reel' and youtube is among the selected channels."),
393
407
  instagram: z.object({
394
408
  share_to_feed: z.boolean().optional(),
395
- thumbnail_type: z.enum(["from-video", "from-library"]).optional(),
396
- thumb_offset: z.number().optional(),
397
- cover_url: z.string().optional(),
409
+ thumbnail_type: z.enum(["from-video", "from-library"]).optional().describe("How the Reel cover is chosen: 'from-video' picks a frame at thumb_offset; 'from-library' uses the image at cover_url. get_post reads the chosen cover back."),
410
+ thumb_offset: z.number().optional().describe("Reel cover frame timestamp in MILLISECONDS from the start of the video (e.g. 3000 = 0:03). Used with thumbnail_type 'from-video'."),
411
+ cover_url: z.string().optional().describe("Custom Reel cover image URL. Used with thumbnail_type 'from-library'."),
398
412
  audio_id: z.string().optional().describe("Reels only. Licensed music track to attach — a numeric audio ID from search_instagram_audio. Requires a Facebook account connected to the workspace whose Page links this Instagram account."),
399
413
  audio_volume: z.number().int().min(0).max(100).optional().describe("Volume of the attached music track, 0-100 (default 100). Only used with audio_id."),
400
414
  video_volume: z.number().int().min(0).max(100).optional().describe("Volume of the video's own audio, 0-100 (default 100). Set 0 for a music-only Reel. Only used with audio_id."),
@@ -537,7 +551,7 @@ Do NOT call without required media — it will fail.`, {
537
551
  media_urls: z.union([
538
552
  z.array(z.string()),
539
553
  z.record(z.string(), z.array(z.string())),
540
- ]).optional().describe("External image/video URLs — flat array or per-platform object. Max 10 total. 'default' key is fallback for platforms without their own key. Empty array opts out."),
554
+ ]).optional().describe("External image/video URLs — flat array or per-platform object. Max 10 total, each file ≤ 100 MB (larger, up to 1 GB: upload_media with method 'url' → pass the media id in `media`). 'default' key is fallback for platforms without their own key. Empty array opts out."),
541
555
  type: z.enum(["post", "story", "reel"]).optional().describe("Content type: 'post' (default), 'story', 'reel'"),
542
556
  location_id: z.string().optional().describe("Instagram only. Facebook Place ID of a single physical venue to tag the post's location. Applied to single-image and carousel Instagram feed posts. Use the `search_locations` tool to find a valid ID. Ignored by other platforms."),
543
557
  collaborators: z.array(z.string()).max(3).optional().describe("Instagram only. Up to 3 public Instagram usernames to invite as co-authors (the 'Collab' feature). Works on image, carousel, and reel posts — NOT Stories. A leading '@' is stripped; usernames are case-insensitive. Private or non-existent usernames are rejected by Instagram at publish time. Ignored by other platforms."),
@@ -561,6 +575,9 @@ Do NOT call without required media — it will fail.`, {
561
575
  first_comment: z.string().max(10000).optional().describe("Text auto-posted as the first comment on the video right after it publishes. The video must have comments enabled."),
562
576
  }).optional().describe("YouTube Shorts options. Only applies when type is 'reel' and youtube is among the selected channels."),
563
577
  instagram: z.object({
578
+ thumbnail_type: z.enum(["from-video", "from-library"]).optional().describe("How the Reel cover is chosen: 'from-video' picks a frame at thumb_offset; 'from-library' uses the image at cover_url. get_post reads the chosen cover back."),
579
+ thumb_offset: z.number().optional().describe("Reel cover frame timestamp in MILLISECONDS from the start of the video (e.g. 3000 = 0:03). Used with thumbnail_type 'from-video'."),
580
+ cover_url: z.string().optional().describe("Custom Reel cover image URL. Used with thumbnail_type 'from-library'."),
564
581
  audio_id: z.string().optional().describe("Reels only. Licensed music track to attach — a numeric audio ID from search_instagram_audio. Requires a Facebook account connected to the workspace whose Page links this Instagram account."),
565
582
  audio_volume: z.number().int().min(0).max(100).optional().describe("Volume of the attached music track, 0-100 (default 100). Only used with audio_id."),
566
583
  video_volume: z.number().int().min(0).max(100).optional().describe("Volume of the video's own audio, 0-100 (default 100). Set 0 for a music-only Reel. Only used with audio_id."),
@@ -674,7 +691,7 @@ Do NOT call without required media — it will fail.`, {
674
691
  media_urls: z.union([
675
692
  z.array(z.string()),
676
693
  z.record(z.string(), z.array(z.string())),
677
- ]).optional().describe("External URLs — flat array or per-platform object. Max 10 total. 'default' key is fallback for platforms without their own key. Empty array opts out."),
694
+ ]).optional().describe("External URLs — flat array or per-platform object. Max 10 total, each file ≤ 100 MB (larger, up to 1 GB: upload_media with method 'url' → pass the media id in `media`). 'default' key is fallback for platforms without their own key. Empty array opts out."),
678
695
  location_id: z.string().optional().describe("Instagram only. Facebook Place/Page ID to tag the post's location with. Send an empty string to clear an existing location tag. Ignored by other platforms."),
679
696
  collaborators: z.array(z.string()).max(3).optional().describe("Instagram only. Up to 3 public Instagram usernames to invite as co-authors. Replaces the existing collaborator list. Send an empty array to clear collaborators. Works on image, carousel, and reel posts — NOT Stories. Ignored by other platforms."),
680
697
  user_tags: z.array(z.object({
@@ -702,9 +719,9 @@ Do NOT call without required media — it will fail.`, {
702
719
  }).optional().describe("Pinterest-specific options. Attach 2–5 images via media_urls.pinterest (or default) to publish a single carousel pin instead of separate pins. Carousel slides MUST share the same aspect ratio (1% tolerance) — the API returns 400 validation_error with `mismatched_slides: [n, ...]` if you pass mixed-ratio images and try to schedule or publish. Drafts are exempt so you can iterate."),
703
720
  instagram: z.object({
704
721
  share_to_feed: z.boolean().optional(),
705
- thumbnail_type: z.enum(["from-video", "from-library"]).optional(),
706
- thumb_offset: z.number().optional(),
707
- cover_url: z.string().optional(),
722
+ thumbnail_type: z.enum(["from-video", "from-library"]).optional().describe("How the Reel cover is chosen: 'from-video' picks a frame at thumb_offset; 'from-library' uses the image at cover_url. get_post reads the chosen cover back."),
723
+ thumb_offset: z.number().optional().describe("Reel cover frame timestamp in MILLISECONDS from the start of the video (e.g. 3000 = 0:03). Used with thumbnail_type 'from-video'."),
724
+ cover_url: z.string().optional().describe("Custom Reel cover image URL. Used with thumbnail_type 'from-library'."),
708
725
  audio_id: z.string().optional().describe("Reels only. Licensed music track to attach — a numeric audio ID from search_instagram_audio. Requires a Facebook account connected to the workspace whose Page links this Instagram account."),
709
726
  audio_volume: z.number().int().min(0).max(100).optional().describe("Volume of the attached music track, 0-100 (default 100). Only used with audio_id."),
710
727
  video_volume: z.number().int().min(0).max(100).optional().describe("Volume of the video's own audio, 0-100 (default 100). Set 0 for a music-only Reel. Only used with audio_id."),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnisocials/mcp-server",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
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",