@omnisocials/mcp-server 1.21.0 → 1.23.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
@@ -188,7 +188,7 @@ Apply a set when creating a post: `create_post` / `create_and_publish_post` acce
188
188
 
189
189
  | Tool | Description |
190
190
  |------|-------------|
191
- | `list_inbox_conversations` | List conversations — Instagram/Facebook DMs, comments, mentions, and LinkedIn company-page comments/mentions; filter by platform, type, or unread |
191
+ | `list_inbox_conversations` | List conversations — Instagram/Facebook DMs, comments, mentions, LinkedIn company-page comments/mentions, TikTok video comments, and X DMs (opt-in); filter by platform, type, or unread |
192
192
  | `get_inbox_conversation` | Get a conversation's full message history |
193
193
  | `mark_inbox_read` | Mark all incoming messages in a conversation as read |
194
194
  | `reply_to_inbox` | Reply to a DM, comment, or mention (existing conversations only) |
package/build/client.d.ts CHANGED
@@ -101,11 +101,16 @@ export interface MastodonPostOptionsUpdate {
101
101
  thread_parts?: MastodonThreadPartInput[] | null;
102
102
  }
103
103
  /** Non-sponsored LinkedIn poll: question + 2-4 options + duration. */
104
- export interface LinkedInPollInput {
104
+ export interface LinkedInPollFields {
105
105
  question: string;
106
106
  options: string[];
107
107
  duration: "ONE_DAY" | "THREE_DAYS" | "SEVEN_DAYS" | "FOURTEEN_DAYS";
108
108
  }
109
+ /** LinkedIn poll(s), independent per channel. A channel's poll is omitted/null when it isn't a poll. */
110
+ export interface LinkedInPollInput {
111
+ linkedin?: LinkedInPollFields | null;
112
+ linkedin_page?: LinkedInPollFields | null;
113
+ }
109
114
  /**
110
115
  * Request body for POST /posts/create. `createAndPublishPost` accepts the
111
116
  * same shape minus `scheduled_at` (publishing is immediate).
@@ -4,12 +4,13 @@ const PLATFORM_EMOJI = {
4
4
  instagram: "📸",
5
5
  facebook: "📘",
6
6
  linkedin: "💼",
7
+ tiktok: "🎵",
7
8
  x: "🐦",
8
9
  };
9
10
  export function registerInboxTools(server, getClient) {
10
- server.tool("list_inbox_conversations", "List social inbox conversations (Instagram/Facebook DMs, comments, mentions, LinkedIn company-page comments/mentions, and X DMs where the workspace has opted into X DMs), newest activity first. Cursor-paginated: pass the returned cursor to get the next page.", {
11
+ server.tool("list_inbox_conversations", "List social inbox conversations (Instagram/Facebook DMs, comments, mentions, LinkedIn company-page comments/mentions, TikTok video comments, and X DMs where the workspace has opted into X DMs), newest activity first. Cursor-paginated: pass the returned cursor to get the next page.", {
11
12
  platform: z
12
- .enum(["instagram", "facebook", "linkedin", "x"])
13
+ .enum(["instagram", "facebook", "linkedin", "tiktok", "x"])
13
14
  .optional()
14
15
  .describe("Filter to one platform"),
15
16
  type: z
@@ -137,9 +138,11 @@ export function registerInboxTools(server, getClient) {
137
138
  ],
138
139
  };
139
140
  });
140
- server.tool("reply_to_inbox", "Reply to an existing inbox conversation (DM, comment, or mention) on Instagram, Facebook, LinkedIn, or X. You can only reply to conversations that already exist. Meta direct-message replies must be within the platform's 24-hour messaging window. X replies are DM-only and use 2 prepaid credits per send (X's API fee passed through at cost) — a 402 insufficient_credits error means the organisation needs to top up at https://app.omnisocials.com/credits; relay that link to the user rather than retrying. Each workspace can send up to 1,000 replies per day.", {
141
+ server.tool("reply_to_inbox", "Reply to an existing inbox conversation (DM, comment, or mention) on Instagram, Facebook, LinkedIn, TikTok, or X. You can only reply to conversations that already exist. Meta direct-message replies must be within the platform's 24-hour messaging window. TikTok replies are comments only, text-only, and capped at 150 characters; they can take a few minutes to appear on TikTok while they pass spam review. X replies are DM-only and use 2 prepaid credits per send (X's API fee passed through at cost) — a 402 insufficient_credits error means the organisation needs to top up at https://app.omnisocials.com/credits; relay that link to the user rather than retrying. Each workspace can send up to 1,000 replies per day.", {
141
142
  conversation_id: z.string().describe("The conversation ID to reply to"),
142
- text: z.string().describe("The reply text (max 2000 characters)"),
143
+ text: z
144
+ .string()
145
+ .describe("The reply text (max 2000 characters; TikTok comments max 150)"),
143
146
  }, async ({ conversation_id, text }) => {
144
147
  const result = await getClient().replyToInboxConversation(conversation_id, {
145
148
  text,
@@ -83,8 +83,7 @@ const LINKEDIN_PAGE_OPTIONS = z
83
83
  })
84
84
  .optional()
85
85
  .describe("LinkedIn Company Page options");
86
- const LINKEDIN_POLL_OPTIONS = z
87
- .object({
86
+ const LINKEDIN_POLL_FIELDS = z.object({
88
87
  question: z.string().max(140).describe("The poll question (max 140 characters)."),
89
88
  options: z
90
89
  .array(z.string().max(30))
@@ -94,10 +93,19 @@ const LINKEDIN_POLL_OPTIONS = z
94
93
  duration: z
95
94
  .enum(["ONE_DAY", "THREE_DAYS", "SEVEN_DAYS", "FOURTEEN_DAYS"])
96
95
  .describe("How long the poll stays open for votes."),
96
+ });
97
+ const LINKEDIN_POLL_OPTIONS = z
98
+ .object({
99
+ linkedin: LINKEDIN_POLL_FIELDS.nullable()
100
+ .optional()
101
+ .describe("Poll for the personal profile post. Omit or null = not a poll."),
102
+ linkedin_page: LINKEDIN_POLL_FIELDS.nullable()
103
+ .optional()
104
+ .describe("Poll for the company page post. Omit or null = not a poll."),
97
105
  })
98
106
  .nullable()
99
107
  .optional()
100
- .describe("Non-sponsored LinkedIn poll, posted to whichever of `linkedin`/`linkedin_page` is selected in `accounts`. Mutually exclusive with media and a link share — a poll takes priority over both at publish time. Still requires `content.linkedin` (or `content.default`) as the post's caption; the poll itself only carries the question/options/duration. Pass `null` on update_post to clear a poll and revert to a normal post.");
108
+ .describe("Non-sponsored LinkedIn poll(s) independent per channel, keyed by `linkedin` (personal profile) / `linkedin_page` (company page). A poll is mutually exclusive with media and a link share on that channel's post — a poll takes priority over both at publish time. Still requires `content.linkedin` (or `content.default`) as that channel's caption; the poll itself only carries the question/options/duration. On update_post, set a channel's key to `null` to clear that channel's poll and revert it to a normal post — send the full desired state for both channels, since the whole object replaces wholesale.");
101
109
  // Per-platform option schemas shared verbatim by create_post,
102
110
  // create_and_publish_post and update_post (they were byte-identical in all
103
111
  // three). Platform blocks that genuinely differ per tool (instagram, youtube,
@@ -133,7 +141,7 @@ const USER_TAG_ENTRY = z.object({
133
141
  // z.array(...).min(2).max(25)[.nullable()].optional().describe(...) stays
134
142
  // per-tool (update_post allows null to clear the thread).
135
143
  const X_THREAD_PART = z.object({
136
- text: z.string().describe("Tweet text (≤ 280 chars). X counts every link as 23 characters (its t.co length), so a short link still uses 23 toward the limit."),
144
+ text: z.string().describe("Tweet text (≤ 280 chars by X's weighted count). X counts every link as 23 characters (its t.co length) and every emoji or non-Latin symbol (🚀, …, ₹, CJK) as 2 — so text that looks under 280 can still be over. When trimming generated copy to fit, leave headroom instead of cutting to exactly 280 raw characters."),
137
145
  media_ids: z.array(mediaIdEntry).max(4).optional().describe("Per-tweet media as Library IDs from upload_media (max 4 combined with media_urls). Each entry is a plain ID string or { id, alt } to attach alt text (X applies it to photos/GIFs). Attach your uploaded graphics to any tweet in the thread."),
138
146
  media_urls: z.array(mediaUrlEntry).max(4).optional().describe("Per-tweet media as external URLs (max 4 combined with media_ids). Each entry is a plain URL string or { url, alt } to attach alt text (X applies it to photos/GIFs)."),
139
147
  });
@@ -274,7 +282,7 @@ export function registerPostTools(server, getClient) {
274
282
  content: [{ type: "text", text: md }],
275
283
  };
276
284
  });
277
- server.tool("get_post", "Get details of a specific post by ID — content, channels, media (with URLs + any per-media alt text), 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`. A LinkedIn poll is rendered under `### LinkedIn Poll` with the question, options, and duration. 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.", {
285
+ server.tool("get_post", "Get details of a specific post by ID — content, channels, media (with URLs + any per-media alt text), 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`. A LinkedIn poll is rendered under `### LinkedIn Profile Poll` and/or `### LinkedIn Page Poll` (independent per channel) with the question, options, and duration. 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.", {
278
286
  id: z.string().describe("The post ID"),
279
287
  }, async ({ id }) => {
280
288
  const result = await getClient().getPost(id);
@@ -407,11 +415,15 @@ export function registerPostTools(server, getClient) {
407
415
  md += `\n`;
408
416
  }
409
417
  }
410
- // LinkedIn poll: mutually exclusive with media/link-share, so surface
411
- // it prominently rather than leaving callers to infer it's a poll.
412
- if (p.linkedin_poll?.question) {
413
- const poll = p.linkedin_poll;
414
- md += `\n\n### LinkedIn Poll\n\n`;
418
+ // LinkedIn poll(s): independent per channel, mutually exclusive with
419
+ // media/link-share on that channel, so surface each prominently
420
+ // rather than leaving callers to infer it's a poll.
421
+ for (const channel of ["linkedin", "linkedin_page"]) {
422
+ const poll = p.linkedin_poll?.[channel];
423
+ if (!poll?.question)
424
+ continue;
425
+ const label = channel === "linkedin_page" ? "LinkedIn Page" : "LinkedIn Profile";
426
+ md += `\n\n### ${label} Poll\n\n`;
415
427
  md += `**${poll.question}**\n\n`;
416
428
  for (const option of poll.options || []) {
417
429
  md += `- ${option}\n`;
@@ -511,10 +523,11 @@ IMPORTANT — Before calling this tool, make sure you have all required informat
511
523
  - Other platforms (LinkedIn, LinkedIn Page, X, Bluesky, etc.): Media is optional.
512
524
 
513
525
  **Per-platform max media items (enforced at submit, returns 400 validation_error if exceeded):**
514
- - Bluesky, X, Mastodon — max **4** items per post
515
- - Instagram, Threads — max **10** items per carousel
526
+ - Bluesky, X, Mastodon — max **4** items per post; cannot mix images and video
527
+ - Instagram, Threads — max **10** items per carousel; images and videos CAN be mixed in one carousel
516
528
  - TikTok — max **35** photos per photo-post; cannot mix photos and videos in one post
517
- - Pinterest carousel — 2–5 images, all same aspect ratio
529
+ - Pinterest carousel — 2–5 images, all same aspect ratio (a video splits off into its own video pin)
530
+ - Facebook, LinkedIn, LinkedIn Page — cannot mix images and video in one post (images only, or a single video)
518
531
 
519
532
  **Per-platform video caps (duration + size, checked via ffprobe at submit — exceeding either returns a 400 validation_error stating the exact cap and the file's value, e.g. "X only allows videos up to 2min 20s — yours is 2min 35s. Trim the video or deselect X."):**
520
533
  - X — **140s (2min 20s)** · **512MB** (X also can't mix a video with images in one tweet: a tweet is either 1 video OR up to 4 images)
@@ -674,7 +687,7 @@ IMPORTANT — Before calling this tool, make sure you have all required informat
674
687
  - Instagram/TikTok posts: ALWAYS need at least one image or video.
675
688
  - Pinterest: ALWAYS need an image + board_id.
676
689
  - Ask the user which media to use. Use list_media to show options.
677
- - **Max items per platform** (same caps as create_post — exceeding returns 400): Bluesky/X/Mastodon ≤4, Instagram/Threads ≤10, TikTok ≤35 photos with no photo/video mix, Pinterest carousel 2–5 same-ratio images.
690
+ - **Max items per platform** (same caps as create_post — exceeding returns 400): Bluesky/X/Mastodon ≤4, Instagram/Threads ≤10, TikTok ≤35 photos, Pinterest carousel 2–5 same-ratio images. Mixing images+video is rejected on Facebook/LinkedIn/X/Bluesky/Mastodon/TikTok; Instagram and Threads mixed carousels are allowed.
678
691
  - **Video duration/size caps** (ffprobe at submit — over either returns 400 validation_error with the exact cap + the file's value): X **140s/512MB** (no video+image mix in one tweet), Bluesky 180s, Threads 5min, Instagram 15min, TikTok 10min, YouTube Short 3min, LinkedIn 10min, Facebook Reel 90s.
679
692
  4. **Pinterest board (auto-default to first board)**: If Pinterest is in \`channels\` and \`pinterest.board_id\` is NOT provided, do NOT block on asking — and do NOT skip Pinterest. Call \`get_account\` on the Pinterest account, take the FIRST board from the returned boards list, and pass its \`id\` as \`pinterest.board_id\`. In your reply, mention which board you used (e.g. "Published to your 'Marketing' board on Pinterest — let me know if you'd prefer a different one.") so the user can redirect. If the user named a specific board in the request, match it (case-insensitive) against the list and use that one instead.
680
693
  5. **X threads vs long-form**: A chained "thread" → pass \`x.thread_parts\` as a 2–25 entry array of \`{ text }\` objects (each ≤ 280 chars). A single long-form post on a Premium / Premium+ account → put the full text (up to 25,000 chars) in \`content\`; no threading needed. On free / Basic, X caps a single post at 280 chars. Do NOT split into "1/", "2/" inside \`content\` — that produces a single tweet, not a thread.
package/build/types.d.ts CHANGED
@@ -28,7 +28,7 @@ export interface InboxParticipant {
28
28
  }
29
29
  export interface InboxConversation {
30
30
  conversation_id: string;
31
- platform: "instagram" | "facebook" | "linkedin";
31
+ platform: "instagram" | "facebook" | "linkedin" | "tiktok" | "x";
32
32
  type: "dm" | "comment" | "mention";
33
33
  participant: InboxParticipant;
34
34
  unread_count: number;
@@ -152,11 +152,18 @@ export interface ApiPost {
152
152
  facebook?: ApiPlatformOptionsOut;
153
153
  linkedin?: ApiPlatformOptionsOut;
154
154
  linkedin_page?: ApiPlatformOptionsOut;
155
- /** Non-sponsored LinkedIn poll echoed back after create/update/publish. */
155
+ /** Non-sponsored LinkedIn poll(s), independent per channel — echoed back after create/update/publish. */
156
156
  linkedin_poll?: {
157
- question: string;
158
- options: string[];
159
- duration: string;
157
+ linkedin?: {
158
+ question: string;
159
+ options: string[];
160
+ duration: string;
161
+ } | null;
162
+ linkedin_page?: {
163
+ question: string;
164
+ options: string[];
165
+ duration: string;
166
+ } | null;
160
167
  } | null;
161
168
  tiktok?: ApiPlatformOptionsOut;
162
169
  google_business?: ApiPlatformOptionsOut;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnisocials/mcp-server",
3
- "version": "1.21.0",
3
+ "version": "1.23.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",
@@ -19,11 +19,24 @@
19
19
  },
20
20
  "keywords": [
21
21
  "mcp",
22
+ "mcp-server",
23
+ "model-context-protocol",
22
24
  "omnisocials",
23
25
  "social-media",
26
+ "social-media-scheduling",
27
+ "scheduling",
24
28
  "claude",
25
- "model-context-protocol",
26
- "ai"
29
+ "ai",
30
+ "instagram",
31
+ "facebook",
32
+ "linkedin",
33
+ "tiktok",
34
+ "youtube",
35
+ "twitter",
36
+ "pinterest",
37
+ "bluesky",
38
+ "threads",
39
+ "mastodon"
27
40
  ],
28
41
  "homepage": "https://docs.omnisocials.com",
29
42
  "engines": {