@omnisocials/mcp-server 1.18.0 → 1.19.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
@@ -218,6 +218,11 @@ Full API docs: [docs.omnisocials.com](https://docs.omnisocials.com)
218
218
 
219
219
  ## Changelog
220
220
 
221
+ ### 1.19.0 (2026-08-02)
222
+
223
+ - **Alt text now reaches Instagram and LinkedIn:** per-media alt text (`{ url, alt }` / `{ id, alt }` entries) is also delivered to Instagram (image posts and carousel image slides — not Reels/Stories) and LinkedIn (images only — not video or documents), alongside Mastodon, Bluesky, X, and Pinterest.
224
+ - **`duration_seconds` on `get_recent_platform_posts`:** video length in whole seconds where the platform's listing API reports it (currently TikTok and YouTube; `null` for images and platforms that don't expose it) — rendered as a Duration column (m:ss) in the summary table and included in the `Structured data` JSON block.
225
+
221
226
  ### 1.18.0
222
227
 
223
228
  - **`retry_post`:** retry the failed platforms of a `failed` or partially failed (`warning`) post on the same post — only failed platforms are re-published, succeeded ones never post twice. Async via the publishing queue; poll `get_post` for the outcome. Max 3 retries per platform. Backed by `POST /api/v1/posts/{id}/retry`.
@@ -226,7 +231,7 @@ Full API docs: [docs.omnisocials.com](https://docs.omnisocials.com)
226
231
 
227
232
  ### 1.17.0
228
233
 
229
- - **Per-media alt text (accessibility descriptions):** `media_urls` / `media_ids` entries now accept `{ url, alt }` / `{ id, alt }` objects everywhere, including thread parts. Delivered to Mastodon (media description), Bluesky (embed alt), X (photos/GIFs), and Pinterest (pin `alt_text` fallback). `get_post` reads alt text back.
234
+ - **Per-media alt text (accessibility descriptions):** `media_urls` / `media_ids` entries now accept `{ url, alt }` / `{ id, alt }` objects everywhere, including thread parts. Delivered to Mastodon (media description), Bluesky (embed alt), X (media metadata, photos/GIFs only) and Pinterest (pin alt_text fallback). `get_post` reads alt text back.
230
235
 
231
236
  ### 1.16.0
232
237
 
package/build/index.js CHANGED
@@ -29,7 +29,7 @@ const sessionState = { activeIndex: 0 };
29
29
  const getActiveClient = () => workspaceClients[sessionState.activeIndex].client;
30
30
  const server = new McpServer({
31
31
  name: "OmniSocials",
32
- version: "1.18.0",
32
+ version: "1.19.0",
33
33
  });
34
34
  // Register all tools - pass getter function so tools always use the active workspace's client
35
35
  registerPostTools(server, getActiveClient);
@@ -142,7 +142,7 @@ When the user provides an image in the conversation (not a URL), use base64_data
142
142
  if (compatibility && compatibility.compatible === false && compatibility.summary) {
143
143
  md += `\n\n⚠️ **${compatibility.summary}** It will still post to your other connected platforms. Ask the user whether to continue before adding it to a post.`;
144
144
  }
145
- md += `\n\nUse this Media ID with \`media_ids\` when creating posts (including inside \`x.thread_parts[].media_ids\`). The public URL above also works anywhere \`media_urls\` is accepted. To attach alt text (an accessibility description), pass \`{ id: "<Media ID>", alt: "..." }\` instead of the bare ID — delivered to Mastodon, Bluesky, X (photos/GIFs), and Pinterest.`;
145
+ md += `\n\nUse this Media ID with \`media_ids\` when creating posts (including inside \`x.thread_parts[].media_ids\`). The public URL above also works anywhere \`media_urls\` is accepted. To attach alt text (an accessibility description), pass \`{ id: "<Media ID>", alt: "..." }\` instead of the bare ID — delivered to Mastodon, Bluesky, X, Pinterest, Instagram (images) and LinkedIn (images).`;
146
146
  return {
147
147
  content: [{ type: "text", text: md }],
148
148
  };
@@ -5,9 +5,11 @@ import { formatDateTime, truncate, capitalize, formatNumber, metricRows } from "
5
5
  // or an object carrying `alt`: an accessibility description (alt text, max
6
6
  // 1500 chars) for that file. Alt text is delivered to Mastodon (media
7
7
  // description — the community strongly values alt text), Bluesky (embed alt),
8
- // X (photos/GIFs only), and Pinterest (used as the pin alt_text when
9
- // pinterest.alt_text is not set); other platforms ignore it for now.
10
- const ALT_TEXT_DESCRIBE = "Accessibility description (alt text) for this file, max 1500 chars. Delivered to Mastodon (media description), Bluesky (embed alt), X (photos/GIFs only), and Pinterest (used as the pin alt_text when pinterest.alt_text is not set); other platforms ignore it.";
8
+ // X (media metadata, photos/GIFs only), Pinterest (pin alt_text fallback),
9
+ // Instagram (images and carousel image slides not Reels/Stories, capped at
10
+ // 1000 chars) and LinkedIn (images only not video or documents); other
11
+ // platforms ignore it for now.
12
+ const ALT_TEXT_DESCRIBE = "Accessibility description (alt text) for this file, max 1500 chars. Delivered to Mastodon (media description), Bluesky (embed alt), X (media metadata, photos/GIFs only), Pinterest (pin alt_text fallback), Instagram (images and carousel image slides — not Reels/Stories) and LinkedIn (images only — not video or documents). Other platforms ignore it.";
11
13
  const mediaUrlEntry = z.union([
12
14
  z.string(),
13
15
  z.object({
@@ -421,11 +423,11 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
421
423
  media_ids: z.union([
422
424
  z.array(mediaIdEntry),
423
425
  z.record(z.string(), z.array(mediaIdEntry)),
424
- ]).optional().describe("Media IDs from upload — flat array (same for all platforms) or object with platform keys: { default: [...], instagram: [...] }. Each entry is a plain ID string or { id, alt } to attach alt text (accessibility description, max 1500 chars) to that file — delivered to Mastodon, Bluesky, X (photos/GIFs), and Pinterest."),
426
+ ]).optional().describe("Media IDs from upload — flat array (same for all platforms) or object with platform keys: { default: [...], instagram: [...] }. Each entry is a plain ID string or { id, alt } to attach alt text (accessibility description, max 1500 chars) to that file — delivered to Mastodon, Bluesky, X, Pinterest, Instagram (images) and LinkedIn (images)."),
425
427
  media_urls: z.union([
426
428
  z.array(mediaUrlEntry),
427
429
  z.record(z.string(), z.array(mediaUrlEntry)),
428
- ]).optional().describe("External image/video URLs — flat array (same for all platforms) or object with platform keys: { default: [...], instagram: [...], pinterest: [...] }. Each entry is a plain URL string or { url, alt } to attach alt text (accessibility description, max 1500 chars) to that file — delivered to Mastodon, Bluesky, X (photos/GIFs), and 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`."),
430
+ ]).optional().describe("External image/video URLs — flat array (same for all platforms) or object with platform keys: { default: [...], instagram: [...], pinterest: [...] }. Each entry is a plain URL string or { url, alt } to attach alt text (accessibility description, max 1500 chars) to that file — delivered to Mastodon, Bluesky, X, Pinterest, Instagram (images) and LinkedIn (images). 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`."),
429
431
  type: z.enum(["post", "story", "reel"]).optional().describe("Content type: 'post' (default), 'story' (Instagram/Facebook/Snapchat), 'reel' (Instagram/Facebook/YouTube/TikTok)"),
430
432
  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)."),
431
433
  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."),
@@ -602,11 +604,11 @@ Do NOT call without required media — it will fail.`, {
602
604
  media_ids: z.union([
603
605
  z.array(mediaIdEntry),
604
606
  z.record(z.string(), z.array(mediaIdEntry)),
605
- ]).optional().describe("Media IDs from upload — flat array or per-platform object. Each entry is a plain ID string or { id, alt } to attach alt text — delivered to Mastodon, Bluesky, X (photos/GIFs), and Pinterest."),
607
+ ]).optional().describe("Media IDs from upload — flat array or per-platform object. Each entry is a plain ID string or { id, alt } to attach alt text — delivered to Mastodon, Bluesky, X, Pinterest, Instagram (images) and LinkedIn (images)."),
606
608
  media_urls: z.union([
607
609
  z.array(mediaUrlEntry),
608
610
  z.record(z.string(), z.array(mediaUrlEntry)),
609
- ]).optional().describe("External image/video URLs — flat array or per-platform object. Each entry is a plain URL string or { url, alt } to attach alt text — delivered to Mastodon, Bluesky, X (photos/GIFs), and Pinterest. 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."),
611
+ ]).optional().describe("External image/video URLs — flat array or per-platform object. Each entry is a plain URL string or { url, alt } to attach alt text — delivered to Mastodon, Bluesky, X, Pinterest, Instagram (images) and LinkedIn (images). 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."),
610
612
  type: z.enum(["post", "story", "reel"]).optional().describe("Content type: 'post' (default), 'story', 'reel'"),
611
613
  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."),
612
614
  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."),
@@ -745,11 +747,11 @@ Do NOT call without required media — it will fail.`, {
745
747
  media_ids: z.union([
746
748
  z.array(mediaIdEntry),
747
749
  z.record(z.string(), z.array(mediaIdEntry)),
748
- ]).optional().describe("Media IDs — flat array or per-platform object. Each entry is a plain ID string or { id, alt } to attach alt text — delivered to Mastodon, Bluesky, X (photos/GIFs), and Pinterest."),
750
+ ]).optional().describe("Media IDs — flat array or per-platform object. Each entry is a plain ID string or { id, alt } to attach alt text — delivered to Mastodon, Bluesky, X, Pinterest, Instagram (images) and LinkedIn (images)."),
749
751
  media_urls: z.union([
750
752
  z.array(mediaUrlEntry),
751
753
  z.record(z.string(), z.array(mediaUrlEntry)),
752
- ]).optional().describe("External URLs — flat array or per-platform object. Each entry is a plain URL string or { url, alt } to attach alt text — delivered to Mastodon, Bluesky, X (photos/GIFs), and Pinterest. 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."),
754
+ ]).optional().describe("External URLs — flat array or per-platform object. Each entry is a plain URL string or { url, alt } to attach alt text — delivered to Mastodon, Bluesky, X, Pinterest, Instagram (images) and LinkedIn (images). 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."),
753
755
  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."),
754
756
  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."),
755
757
  user_tags: z.array(z.object({
@@ -985,7 +987,7 @@ Notes: only tracks Meta licenses for third-party publishing appear, so the selec
985
987
  });
986
988
  return { content: [{ type: "text", text: md }] };
987
989
  });
988
- server.tool("get_recent_platform_posts", "Fetch the user's most recent posts straight from their connected platform APIs (Instagram, TikTok, X, YouTube, Facebook, LinkedIn, and more), INCLUDING content published outside OmniSocials. Use this when list_posts is empty — e.g. a brand-new workspace that has not published through OmniSocials yet — so you can still analyze the user's real content. Each post includes normalized `engagement` plus every raw metric the platform reported (Instagram: reach/views/saves/shares from per-post insights). Metrics only appear where the platform exposes them for historical posts (X, TikTok, Bluesky, Mastodon, Instagram, Facebook, YouTube); Threads, Pinterest, and Google Business return captions only. LinkedIn personal profiles can't be listed live (LinkedIn grants apps no such permission), so their results are posts published through OmniSocials with their latest collected stats. Fetched live, so expect a few seconds of latency. Output is a human-readable summary table PLUS a 'Structured data' JSON block carrying, for every post, the platform's own post id (the stable dedupe key), a permalink, the FULL untruncated caption, and exact-integer metrics — use that block when ingesting or storing native posts rather than the rounded/truncated table. Requires the analytics:read scope.", {
990
+ server.tool("get_recent_platform_posts", "Fetch the user's most recent posts straight from their connected platform APIs (Instagram, TikTok, X, YouTube, Facebook, LinkedIn, and more), INCLUDING content published outside OmniSocials. Use this when list_posts is empty — e.g. a brand-new workspace that has not published through OmniSocials yet — so you can still analyze the user's real content. Each post includes normalized `engagement` plus every raw metric the platform reported (Instagram: reach/views/saves/shares from per-post insights). Metrics only appear where the platform exposes them for historical posts (X, TikTok, Bluesky, Mastodon, Instagram, Facebook, YouTube); Threads, Pinterest, and Google Business return captions only. Records also carry `duration_seconds` — the video length in whole seconds — where the platform's listing API reports it (currently TikTok and YouTube); null for images and platforms that don't expose it. LinkedIn personal profiles can't be listed live (LinkedIn grants apps no such permission), so their results are posts published through OmniSocials with their latest collected stats. Fetched live, so expect a few seconds of latency. Output is a human-readable summary table PLUS a 'Structured data' JSON block carrying, for every post, the platform's own post id (the stable dedupe key), a permalink, the FULL untruncated caption, and exact-integer metrics — use that block when ingesting or storing native posts rather than the rounded/truncated table. Requires the analytics:read scope.", {
989
991
  limit: z
990
992
  .string()
991
993
  .optional()
@@ -1006,6 +1008,14 @@ Notes: only tracks Meta licenses for third-party publishing appear, so the selec
1006
1008
  const errors = result.errors || {};
1007
1009
  const errEntries = Object.entries(errors);
1008
1010
  const clean = (s) => String(s || "").replace(/[|\n\r]+/g, " ").trim();
1011
+ // Video length as m:ss (e.g. 95 → "1:35"); blank for images and
1012
+ // platforms whose listing API reports no duration.
1013
+ const fmtVideoDuration = (secs) => {
1014
+ if (typeof secs !== "number" || !Number.isFinite(secs))
1015
+ return "—";
1016
+ const s = Math.round(secs);
1017
+ return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
1018
+ };
1009
1019
  if (!posts.length) {
1010
1020
  const note = result.note || "No recent platform posts found.";
1011
1021
  const errLines = errEntries.map(([p, m]) => `- ${capitalize(p)}: ${m}`).join("\n");
@@ -1014,8 +1024,8 @@ Notes: only tracks Meta licenses for third-party publishing appear, so the selec
1014
1024
  };
1015
1025
  }
1016
1026
  let md = `## Recent platform posts (${posts.length} across ${connected.length} platform${connected.length === 1 ? "" : "s"})\n\n`;
1017
- md += `| # | Platform | Format | Content | Engagement | Metrics | Date |\n`;
1018
- md += `|---|----------|--------|---------|------------|---------|------|\n`;
1027
+ md += `| # | Platform | Format | Duration | Content | Engagement | Metrics | Date |\n`;
1028
+ md += `|---|----------|--------|----------|---------|------------|---------|------|\n`;
1019
1029
  posts.forEach((p, i) => {
1020
1030
  const hasMetrics = p.metrics && Object.keys(p.metrics).length > 0;
1021
1031
  const content = truncate(clean(p.text), 45) || "*(no caption)*";
@@ -1027,7 +1037,7 @@ Notes: only tracks Meta licenses for third-party publishing appear, so the selec
1027
1037
  .map(([label, n]) => `${formatNumber(n)} ${label.toLowerCase()}`)
1028
1038
  .join(" · ");
1029
1039
  const date = p.timestamp ? formatDateTime(p.timestamp) : "—";
1030
- md += `| ${i + 1} | ${capitalize(p.platform)} | ${p.format || "post"} | ${content} | ${eng} | ${detail || "—"} | ${date} |\n`;
1040
+ md += `| ${i + 1} | ${capitalize(p.platform)} | ${p.format || "post"} | ${fmtVideoDuration(p.duration_seconds)} | ${content} | ${eng} | ${detail || "—"} | ${date} |\n`;
1031
1041
  });
1032
1042
  if (result.note)
1033
1043
  md += `\n${result.note}\n`;
@@ -1049,6 +1059,7 @@ Notes: only tracks Meta licenses for third-party publishing appear, so the selec
1049
1059
  text: p.text ?? "",
1050
1060
  format: p.format ?? "post",
1051
1061
  media_count: p.media_count ?? 1,
1062
+ duration_seconds: p.duration_seconds ?? null,
1052
1063
  timestamp: p.timestamp ?? null,
1053
1064
  image_url: p.image_url ?? null,
1054
1065
  engagement: p.engagement ?? null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnisocials/mcp-server",
3
- "version": "1.18.0",
3
+ "version": "1.19.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",