@omnisocials/mcp-server 1.3.7 → 1.3.8

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.8
195
+
196
+ - **Fixed:** Pinterest pins created via `create_post` / `create_and_publish_post` / `update_post` now publish with title, link, video cover, and alt text. Companion server fix — the API was storing those fields under unprefixed keys while the publisher read them under `pinterest_*` prefixes, so pins published with no metadata even when the agent passed it. Existing clients on 1.3.7 benefit automatically once the backend deploys.
197
+ - **Added:** `pinterest.video_cover` and `pinterest.alt_text` to the tool schemas. `alt_text` improves accessibility + Pinterest's discoverability for visually impaired users; `video_cover` sets a custom cover image for video pins (otherwise Pinterest uses a 1s video keyframe).
198
+
194
199
  ### 1.3.7
195
200
 
196
201
  - **Fixed:** `update_post` now accepts per-platform options (`youtube`, `pinterest`, `instagram`, `tiktok`) — same shape as `create_post`. Previously these were missing from the `update_post` schema, so agents that tried to rename a scheduled YouTube Short via `youtube.title` had the field silently stripped before it reached the server. Renaming a Short, changing privacy, adjusting a Pinterest board, etc. on an existing draft now work.
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { fetchImageAsBase64, capitalize } from "../client.js";
3
3
  export function registerAccountTools(server, getClient) {
4
- server.tool("list_accounts", "List all connected social media accounts in the workspace. Each account includes its platform, display name, channel ID (used for create_post), and supported content_types (post, story, reel). Pinterest accounts also include a `boards` array with `{id, name}` — use the board `id` as `board_id` when creating Pinterest posts. X accounts with Premium include `platform_details` with `subscription_type` (e.g. \"Premium\", \"PremiumPlus\"). LinkedIn appears as two independent platforms: `linkedin` for a personal profile and `linkedin_page` for a company page. A workspace can have both connected at the same time and post to each separately. Call this to help users pick which platforms to post to.", {}, async () => {
4
+ server.tool("list_accounts", "List all connected social media accounts in the workspace. Each account includes its platform, display name, channel ID (used for create_post), supported content_types (post, story, reel), and `needs_reconnect` (true when the OAuth token has been revoked/expired and the user must reconnect before posts can succeed; also reflected in `status` as `needs_reconnect` instead of `active`). Pinterest accounts include a `boards` array with `{id, name}` — use the board `id` as `board_id` when creating Pinterest posts. X accounts with Premium include `platform_details` with `subscription_type` (e.g. \"Premium\", \"PremiumPlus\"). LinkedIn appears as two independent platforms: `linkedin` for a personal profile and `linkedin_page` for a company page. A workspace can have both connected at the same time and post to each separately. Call this to help users pick which platforms to post to.", {}, async () => {
5
5
  const result = await getClient().listAccounts();
6
6
  if (result.error) {
7
7
  return {
@@ -16,8 +16,8 @@ export function registerAccountTools(server, getClient) {
16
16
  }
17
17
  // Build markdown table
18
18
  let md = `## Connected Accounts (${accounts.length})\n\n`;
19
- md += `| # | Platform | Account | Content Types | Channel ID |\n`;
20
- md += `|---|----------|---------|---------------|------------|\n`;
19
+ md += `| # | Platform | Account | Content Types | Channel ID | Status |\n`;
20
+ md += `|---|----------|---------|---------------|------------|--------|\n`;
21
21
  for (let i = 0; i < accounts.length; i++) {
22
22
  const a = accounts[i];
23
23
  const name = a.username ? `@${a.username}` : a.display_name || "—";
@@ -26,7 +26,16 @@ export function registerAccountTools(server, getClient) {
26
26
  const sub = a.platform_details?.subscription_type && a.platform_details.subscription_type !== "None"
27
27
  ? ` (${a.platform_details.subscription_type})`
28
28
  : "";
29
- md += `| ${i + 1} | ${platform}${sub} | ${name} | ${types} | \`${a.id}\` |\n`;
29
+ const statusCell = a.needs_reconnect
30
+ ? `⚠️ needs_reconnect`
31
+ : "active";
32
+ md += `| ${i + 1} | ${platform}${sub} | ${name} | ${types} | \`${a.id}\` | ${statusCell} |\n`;
33
+ }
34
+ const reconnects = accounts.filter((a) => a.needs_reconnect);
35
+ if (reconnects.length) {
36
+ md += `\n⚠️ ${reconnects.length} account(s) need reconnecting before posting will succeed: ${reconnects
37
+ .map((a) => capitalize(a.platform))
38
+ .join(", ")}. Ask the user to reconnect in OmniSocials (Settings → Organisation → Workspaces).\n`;
30
39
  }
31
40
  md += `\nUse the **Channel ID** as the \`channels\` parameter when creating posts.`;
32
41
  // Fetch profile pictures
@@ -74,7 +83,10 @@ export function registerAccountTools(server, getClient) {
74
83
  md += `| **Username** | ${a.username ? `@${a.username}` : "—"} |\n`;
75
84
  md += `| **Display Name** | ${a.display_name || "—"} |\n`;
76
85
  md += `| **Content Types** | ${(a.content_types || []).join(", ")} |\n`;
77
- md += `| **Status** | ${a.status} |\n`;
86
+ md += `| **Status** | ${a.needs_reconnect ? "⚠️ needs_reconnect" : a.status} |\n`;
87
+ if (a.needs_reconnect && a.reauth_reason) {
88
+ md += `| **Reconnect reason** | ${a.reauth_reason} |\n`;
89
+ }
78
90
  md += `| **Connected** | ${a.connected_at ? new Date(a.connected_at).toLocaleDateString() : "—"} |\n`;
79
91
  if (a.platform_details?.subscription_type) {
80
92
  md += `| **Subscription** | ${a.platform_details.subscription_type} |\n`;
@@ -11,48 +11,64 @@ export function registerAnalyticsTools(server, getClient) {
11
11
  };
12
12
  }
13
13
  const d = result.data;
14
- if (!d) {
14
+ // API returns { post_id, platforms: { <platform>: { metrics: {...} } } }.
15
+ // Aggregate the per-platform metrics into totals; the legacy flat shape
16
+ // (d.impressions / d.platform_stats) doesn't exist on this endpoint and
17
+ // reading those keys returned "—" for every field.
18
+ const platforms = d?.platforms || {};
19
+ const platformEntries = Object.entries(platforms);
20
+ if (platformEntries.length === 0) {
15
21
  return {
16
- content: [{ type: "text", text: "No analytics found for this post." }],
22
+ content: [{
23
+ type: "text",
24
+ text: "No analytics collected yet for this post. Stats are fetched periodically after publishing; check back in a few hours, or verify the post finished publishing successfully.",
25
+ }],
17
26
  };
18
27
  }
28
+ const totals = { impressions: 0, engagements: 0, likes: 0, comments: 0, shares: 0 };
29
+ const perPlatform = [];
30
+ for (const [platform, entry] of platformEntries) {
31
+ const m = entry?.metrics || {};
32
+ const impressions = platform === "instagram"
33
+ ? Number(m.reach ?? m.impressions ?? 0)
34
+ : Number(m.views ?? m.impressions ?? 0);
35
+ const likes = Number(m.likes ?? m.favorites ?? m.reactions ?? 0);
36
+ const comments = Number(m.comments ?? m.replies ?? 0);
37
+ const shares = Number(m.shares ?? m.retweets ?? m.reposts ?? 0);
38
+ const engagements = m.engagement != null ? Number(m.engagement) : likes + comments + shares;
39
+ totals.impressions += impressions;
40
+ totals.engagements += engagements;
41
+ totals.likes += likes;
42
+ totals.comments += comments;
43
+ totals.shares += shares;
44
+ perPlatform.push({ platform, impressions, engagements, likes, comments, shares });
45
+ }
19
46
  let md = `## Post Analytics\n\n`;
20
47
  md += `| Metric | Value |\n`;
21
48
  md += `|--------|-------|\n`;
22
- md += `| **Impressions** | ${formatNumber(d.impressions ?? 0)} |\n`;
23
- md += `| **Engagements** | ${formatNumber(d.engagements ?? 0)} |\n`;
24
- md += `| **Likes** | ${formatNumber(d.likes ?? 0)} |\n`;
25
- md += `| **Comments** | ${formatNumber(d.comments ?? 0)} |\n`;
26
- md += `| **Shares** | ${formatNumber(d.shares ?? 0)} |\n`;
27
- if (d.impressions > 0) {
28
- const rate = ((d.engagements ?? 0) / d.impressions * 100).toFixed(2);
49
+ md += `| **Impressions** | ${formatNumber(totals.impressions)} |\n`;
50
+ md += `| **Engagements** | ${formatNumber(totals.engagements)} |\n`;
51
+ md += `| **Likes** | ${formatNumber(totals.likes)} |\n`;
52
+ md += `| **Comments** | ${formatNumber(totals.comments)} |\n`;
53
+ md += `| **Shares** | ${formatNumber(totals.shares)} |\n`;
54
+ if (totals.impressions > 0) {
55
+ const rate = ((totals.engagements / totals.impressions) * 100).toFixed(2);
29
56
  md += `| **Engagement Rate** | ${rate}% |\n`;
30
57
  }
31
- if (d.platform_stats && Object.keys(d.platform_stats).length > 0) {
32
- md += `\n### Per-Platform Breakdown\n\n`;
33
- for (const [platform, stats] of Object.entries(d.platform_stats)) {
34
- const s = stats;
35
- md += `**${capitalize(platform)}:** `;
36
- const parts = [];
37
- if (s.impressions !== undefined)
38
- parts.push(`${formatNumber(s.impressions)} impressions`);
39
- if (s.engagements !== undefined)
40
- parts.push(`${formatNumber(s.engagements)} engagements`);
41
- if (s.likes !== undefined)
42
- parts.push(`${formatNumber(s.likes)} likes`);
43
- if (s.comments !== undefined)
44
- parts.push(`${formatNumber(s.comments)} comments`);
45
- md += parts.join(", ") + "\n\n";
46
- }
58
+ md += `\n### Per-Platform Breakdown\n\n`;
59
+ md += `| Platform | Impressions | Engagements | Likes | Comments | Shares |\n`;
60
+ md += `|----------|-------------|-------------|-------|----------|--------|\n`;
61
+ for (const p of perPlatform) {
62
+ md += `| ${capitalize(p.platform)} | ${formatNumber(p.impressions)} | ${formatNumber(p.engagements)} | ${formatNumber(p.likes)} | ${formatNumber(p.comments)} | ${formatNumber(p.shares)} |\n`;
47
63
  }
48
64
  return {
49
65
  content: [{ type: "text", text: md }],
50
66
  };
51
67
  });
52
- server.tool("get_analytics_overview", "Get an overview of analytics across all posts and accounts for a time period.", {
53
- period: z.string().optional().describe("Time period: 7d, 30d, 90d (default: 30d)"),
54
- start_date: z.string().optional().describe("Custom start date (ISO 8601)"),
55
- end_date: z.string().optional().describe("Custom end date (ISO 8601)"),
68
+ server.tool("get_analytics_overview", "Get analytics overview with total posts, impressions, engagements, engagement rate, and per-platform breakdown. Use `period` for a rolling window (7d / 30d / 90d) or `start_date` + `end_date` for a custom range. The response echoes the resolved range and today's date — read those before assuming a year from training data (e.g. when the user says 'April', use the most recent April, not April from your training cutoff).", {
69
+ period: z.string().optional().describe("Rolling window: 7d, 30d, 90d (default: 30d). Ignored if start_date/end_date are provided."),
70
+ start_date: z.string().optional().describe('Custom start date. Accepts "YYYY-MM-DD" (e.g. "2026-04-01") or "YYYY-MM" month-shorthand (e.g. "2026-04" → first of the month).'),
71
+ end_date: z.string().optional().describe('Custom end date. Same formats as start_date; "YYYY-MM" expands to the last day of the month.'),
56
72
  }, async (params) => {
57
73
  const result = await getClient().getAnalyticsOverview(params);
58
74
  if (result.error) {
@@ -61,8 +77,15 @@ export function registerAnalyticsTools(server, getClient) {
61
77
  };
62
78
  }
63
79
  const d = result.data;
64
- const period = result.period || params.period || "30d";
65
- let md = `## Analytics Overview (Last ${period})\n\n`;
80
+ const resolvedStart = result.start_date;
81
+ const resolvedEnd = result.end_date;
82
+ const currentDate = result.current_date;
83
+ const periodLabel = resolvedStart && resolvedEnd
84
+ ? `${resolvedStart} → ${resolvedEnd}`
85
+ : result.period || params.period || "30d";
86
+ let md = `## Analytics Overview (${periodLabel})\n\n`;
87
+ if (currentDate)
88
+ md += `_Today: ${currentDate}_\n\n`;
66
89
  md += `**${d.total_posts} posts** across **${d.total_platforms} platforms** | `;
67
90
  md += `**${formatNumber(d.total_impressions)}** impressions | `;
68
91
  md += `**${formatNumber(d.total_engagement)}** engagements | `;
@@ -211,9 +211,11 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
211
211
  ]).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."),
212
212
  type: z.enum(["post", "story", "reel"]).optional().describe("Content type: 'post' (default), 'story' (Instagram/Facebook/Snapchat), 'reel' (Instagram/Facebook/YouTube/TikTok)"),
213
213
  pinterest: z.object({
214
- board_id: z.string().optional(),
215
- title: z.string().optional(),
216
- link: z.string().optional(),
214
+ board_id: z.string().optional().describe("Pinterest board ID. Required for Pinterest. Use get_account to list boards."),
215
+ title: z.string().optional().describe("Pin title (max 100 characters)"),
216
+ link: z.string().optional().describe("Destination URL the pin clicks through to"),
217
+ video_cover: z.string().optional().describe("Cover image URL for video pins (JPEG/PNG). Falls back to a video keyframe if omitted."),
218
+ alt_text: z.string().optional().describe("Accessibility alt text for the pin image (max 500 characters)"),
217
219
  }).optional().describe("Pinterest-specific options"),
218
220
  youtube: z.object({
219
221
  title: z.string().optional().describe("Short title shown on YouTube. Falls back to \"YouTube Short\" when omitted."),
@@ -324,9 +326,11 @@ Do NOT call without required media — it will fail.`, {
324
326
  ]).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."),
325
327
  type: z.enum(["post", "story", "reel"]).optional().describe("Content type: 'post' (default), 'story', 'reel'"),
326
328
  pinterest: z.object({
327
- board_id: z.string().optional(),
328
- title: z.string().optional(),
329
- link: z.string().optional(),
329
+ board_id: z.string().optional().describe("Pinterest board ID. Required for Pinterest. Use get_account to list boards."),
330
+ title: z.string().optional().describe("Pin title (max 100 characters)"),
331
+ link: z.string().optional().describe("Destination URL the pin clicks through to"),
332
+ video_cover: z.string().optional().describe("Cover image URL for video pins (JPEG/PNG). Falls back to a video keyframe if omitted."),
333
+ alt_text: z.string().optional().describe("Accessibility alt text for the pin image (max 500 characters)"),
330
334
  }).optional().describe("Pinterest-specific options"),
331
335
  youtube: z.object({
332
336
  title: z.string().optional().describe("Short title shown on YouTube."),
@@ -403,9 +407,11 @@ Do NOT call without required media — it will fail.`, {
403
407
  contains_synthetic_media: z.boolean().optional(),
404
408
  }).optional().describe("YouTube Shorts options. Only applies when type is 'reel' and youtube is among the selected channels."),
405
409
  pinterest: z.object({
406
- board_id: z.string().optional(),
407
- title: z.string().optional(),
408
- link: z.string().optional(),
410
+ board_id: z.string().optional().describe("Pinterest board ID. Required for Pinterest. Use get_account to list boards."),
411
+ title: z.string().optional().describe("Pin title (max 100 characters)"),
412
+ link: z.string().optional().describe("Destination URL the pin clicks through to"),
413
+ video_cover: z.string().optional().describe("Cover image URL for video pins (JPEG/PNG). Falls back to a video keyframe if omitted."),
414
+ alt_text: z.string().optional().describe("Accessibility alt text for the pin image (max 500 characters)"),
409
415
  }).optional().describe("Pinterest-specific options"),
410
416
  instagram: z.object({
411
417
  share_to_feed: z.boolean().optional(),
package/build/types.d.ts CHANGED
@@ -41,7 +41,15 @@ export interface Account {
41
41
  display_name: string;
42
42
  profile_picture: string | null;
43
43
  content_types: string[];
44
- status: string;
44
+ /**
45
+ * "active" while the OAuth token is healthy. Flips to "needs_reconnect"
46
+ * when the platform has revoked/expired the token — posts to this account
47
+ * will fail until the user reconnects.
48
+ */
49
+ status: "active" | "needs_reconnect" | string;
50
+ needs_reconnect: boolean;
51
+ /** Short reason from the platform; only present when needs_reconnect is true. */
52
+ reauth_reason?: string | null;
45
53
  connected_at: string | null;
46
54
  boards?: {
47
55
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnisocials/mcp-server",
3
- "version": "1.3.7",
3
+ "version": "1.3.8",
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",