@omnisocials/mcp-server 1.3.6 → 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 +10 -0
- package/build/tools/accounts.js +17 -5
- package/build/tools/analytics.js +54 -31
- package/build/tools/posts.js +43 -7
- package/build/types.d.ts +9 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -191,6 +191,16 @@ 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
|
+
|
|
199
|
+
### 1.3.7
|
|
200
|
+
|
|
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.
|
|
202
|
+
- Tool descriptions clarify that `content` is the post caption / body, and for YouTube Shorts becomes the video **description** — **not** the Short's title. To rename a Short, use `youtube.title`.
|
|
203
|
+
|
|
194
204
|
### 1.3.6
|
|
195
205
|
|
|
196
206
|
- Metadata-only republish of 1.3.5 to refresh the README on npmjs.com. No code changes from 1.3.5.
|
package/build/tools/accounts.js
CHANGED
|
@@ -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),
|
|
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 +=
|
|
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
|
-
|
|
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`;
|
package/build/tools/analytics.js
CHANGED
|
@@ -11,48 +11,64 @@ export function registerAnalyticsTools(server, getClient) {
|
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
13
|
const d = result.data;
|
|
14
|
-
|
|
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: [{
|
|
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(
|
|
23
|
-
md += `| **Engagements** | ${formatNumber(
|
|
24
|
-
md += `| **Likes** | ${formatNumber(
|
|
25
|
-
md += `| **Comments** | ${formatNumber(
|
|
26
|
-
md += `| **Shares** | ${formatNumber(
|
|
27
|
-
if (
|
|
28
|
-
const rate = ((
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
53
|
-
period: z.string().optional().describe("
|
|
54
|
-
start_date: z.string().optional().describe(
|
|
55
|
-
end_date: z.string().optional().describe(
|
|
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
|
|
65
|
-
|
|
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 | `;
|
package/build/tools/posts.js
CHANGED
|
@@ -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."),
|
|
@@ -378,9 +382,11 @@ Do NOT call without required media — it will fail.`, {
|
|
|
378
382
|
});
|
|
379
383
|
server.tool("update_post", `Update an existing post. Only draft and scheduled posts can be updated.
|
|
380
384
|
|
|
385
|
+
**Per-platform options (\`youtube\`, \`pinterest\`, \`instagram\`, \`tiktok\`)** are accepted here, same shape as in \`create_post\`. Pass an object — never a JSON-encoded string. For YouTube Shorts, the **title** lives at \`youtube.title\`; the **video description** lives in \`content\` (or \`content.youtube\` for a per-platform override). They are not the same field — changing the caption does NOT rename the Short.
|
|
386
|
+
|
|
381
387
|
**X threads**: To convert an existing draft into a chained X thread, pass \`x.thread_parts\` as a 2–25 entry array of \`{ text }\` objects (each ≤ 280 chars). Pass \`null\` to revert to single-tweet mode. Do NOT shove "1/", "2/" into \`content\` — that's a single tweet, not a thread.`, {
|
|
382
388
|
id: z.string().describe("The post ID to update"),
|
|
383
|
-
content: z.union([z.string(), z.record(z.string(), z.string())]).optional().describe("Updated post
|
|
389
|
+
content: z.union([z.string(), z.record(z.string(), z.string())]).optional().describe("Updated post caption / body text. For YouTube Shorts this becomes the video description, NOT the title — to rename the Short, use `youtube.title`. String or object with platform keys: { \"default\": \"fallback\", \"linkedin\": \"long\" }."),
|
|
384
390
|
scheduled_at: z.string().optional().describe("Updated scheduled date (ISO 8601)"),
|
|
385
391
|
channels: z.array(z.string()).optional().describe("Updated channel IDs. Note: `linkedin` (personal profile) and `linkedin_page` (company page) are independent channels."),
|
|
386
392
|
media_ids: z.union([
|
|
@@ -391,6 +397,36 @@ Do NOT call without required media — it will fail.`, {
|
|
|
391
397
|
z.array(z.string()),
|
|
392
398
|
z.record(z.string(), z.array(z.string())),
|
|
393
399
|
]).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."),
|
|
400
|
+
youtube: z.object({
|
|
401
|
+
title: z.string().optional().describe("Short title shown on YouTube. To change the Short's title on an existing draft, set this — do NOT use `content` (which is the description)."),
|
|
402
|
+
tags: z.array(z.string()).optional(),
|
|
403
|
+
privacy_status: z.enum(["public", "private", "unlisted"]).optional(),
|
|
404
|
+
category_id: z.string().optional(),
|
|
405
|
+
made_for_kids: z.boolean().optional(),
|
|
406
|
+
notify_subscribers: z.boolean().optional(),
|
|
407
|
+
contains_synthetic_media: z.boolean().optional(),
|
|
408
|
+
}).optional().describe("YouTube Shorts options. Only applies when type is 'reel' and youtube is among the selected channels."),
|
|
409
|
+
pinterest: z.object({
|
|
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)"),
|
|
415
|
+
}).optional().describe("Pinterest-specific options"),
|
|
416
|
+
instagram: z.object({
|
|
417
|
+
share_to_feed: z.boolean().optional(),
|
|
418
|
+
thumbnail_type: z.enum(["from-video", "from-library"]).optional(),
|
|
419
|
+
thumb_offset: z.number().optional(),
|
|
420
|
+
cover_url: z.string().optional(),
|
|
421
|
+
}).optional().describe("Instagram Reel options"),
|
|
422
|
+
tiktok: z.object({
|
|
423
|
+
privacy_level: z.enum(["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR", "SELF_ONLY"]).optional(),
|
|
424
|
+
disable_comment: z.boolean().optional(),
|
|
425
|
+
disable_duet: z.boolean().optional(),
|
|
426
|
+
disable_stitch: z.boolean().optional(),
|
|
427
|
+
is_aigc: z.boolean().optional(),
|
|
428
|
+
brand_content_toggle: z.boolean().optional(),
|
|
429
|
+
}).optional().describe("TikTok options"),
|
|
394
430
|
x: z.object({
|
|
395
431
|
reply_settings: z.enum(["", "following", "mentionedUsers"]).optional(),
|
|
396
432
|
paid_partnership: 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
|
-
|
|
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