@omnisocials/mcp-server 1.20.1 → 1.22.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
@@ -100,6 +100,17 @@ export interface MastodonPostOptions {
100
100
  export interface MastodonPostOptionsUpdate {
101
101
  thread_parts?: MastodonThreadPartInput[] | null;
102
102
  }
103
+ /** Non-sponsored LinkedIn poll: question + 2-4 options + duration. */
104
+ export interface LinkedInPollFields {
105
+ question: string;
106
+ options: string[];
107
+ duration: "ONE_DAY" | "THREE_DAYS" | "SEVEN_DAYS" | "FOURTEEN_DAYS";
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
+ }
103
114
  /**
104
115
  * Request body for POST /posts/create. `createAndPublishPost` accepts the
105
116
  * same shape minus `scheduled_at` (publishing is immediate).
@@ -130,6 +141,7 @@ export interface CreatePostInput {
130
141
  facebook?: Record<string, unknown>;
131
142
  linkedin?: Record<string, unknown>;
132
143
  linkedin_page?: Record<string, unknown>;
144
+ linkedin_poll?: LinkedInPollInput | null;
133
145
  tiktok?: Record<string, unknown>;
134
146
  x?: XPostOptions;
135
147
  bluesky?: BlueskyPostOptions;
@@ -183,6 +195,7 @@ export declare class OmniSocialsClient {
183
195
  facebook?: Record<string, unknown>;
184
196
  linkedin?: Record<string, unknown>;
185
197
  linkedin_page?: Record<string, unknown>;
198
+ linkedin_poll?: LinkedInPollInput | null;
186
199
  tiktok?: Record<string, unknown>;
187
200
  x?: XPostOptionsUpdate;
188
201
  bluesky?: BlueskyPostOptionsUpdate;
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.20.0",
32
+ version: "1.21.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);
@@ -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,6 +83,29 @@ const LINKEDIN_PAGE_OPTIONS = z
83
83
  })
84
84
  .optional()
85
85
  .describe("LinkedIn Company Page options");
86
+ const LINKEDIN_POLL_FIELDS = z.object({
87
+ question: z.string().max(140).describe("The poll question (max 140 characters)."),
88
+ options: z
89
+ .array(z.string().max(30))
90
+ .min(2)
91
+ .max(4)
92
+ .describe("2-4 answer options (max 30 characters each)."),
93
+ duration: z
94
+ .enum(["ONE_DAY", "THREE_DAYS", "SEVEN_DAYS", "FOURTEEN_DAYS"])
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."),
105
+ })
106
+ .nullable()
107
+ .optional()
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.");
86
109
  // Per-platform option schemas shared verbatim by create_post,
87
110
  // create_and_publish_post and update_post (they were byte-identical in all
88
111
  // three). Platform blocks that genuinely differ per tool (instagram, youtube,
@@ -259,7 +282,7 @@ export function registerPostTools(server, getClient) {
259
282
  content: [{ type: "text", text: md }],
260
283
  };
261
284
  });
262
- 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`. 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.", {
263
286
  id: z.string().describe("The post ID"),
264
287
  }, async ({ id }) => {
265
288
  const result = await getClient().getPost(id);
@@ -392,6 +415,21 @@ export function registerPostTools(server, getClient) {
392
415
  md += `\n`;
393
416
  }
394
417
  }
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`;
427
+ md += `**${poll.question}**\n\n`;
428
+ for (const option of poll.options || []) {
429
+ md += `- ${option}\n`;
430
+ }
431
+ md += `\n_Duration: ${poll.duration}_\n`;
432
+ }
395
433
  const publishedUrls = (p.published_urls && typeof p.published_urls === "object" && !Array.isArray(p.published_urls))
396
434
  ? p.published_urls
397
435
  : {};
@@ -559,6 +597,7 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
559
597
  facebook: FACEBOOK_OPTIONS,
560
598
  linkedin: LINKEDIN_OPTIONS,
561
599
  linkedin_page: LINKEDIN_PAGE_OPTIONS,
600
+ linkedin_poll: LINKEDIN_POLL_OPTIONS,
562
601
  tiktok: TIKTOK_OPTIONS,
563
602
  x: z.object({
564
603
  reply_settings: z.enum(["", "following", "mentionedUsers"]).optional(),
@@ -698,6 +737,7 @@ Do NOT call without required media — it will fail.`, {
698
737
  facebook: FACEBOOK_OPTIONS,
699
738
  linkedin: LINKEDIN_OPTIONS,
700
739
  linkedin_page: LINKEDIN_PAGE_OPTIONS,
740
+ linkedin_poll: LINKEDIN_POLL_OPTIONS,
701
741
  tiktok: TIKTOK_OPTIONS,
702
742
  x: z.object({
703
743
  reply_settings: z.enum(["", "following", "mentionedUsers"]).optional(),
@@ -803,6 +843,7 @@ Do NOT call without required media — it will fail.`, {
803
843
  facebook: FACEBOOK_OPTIONS,
804
844
  linkedin: LINKEDIN_OPTIONS,
805
845
  linkedin_page: LINKEDIN_PAGE_OPTIONS,
846
+ linkedin_poll: LINKEDIN_POLL_OPTIONS,
806
847
  tiktok: TIKTOK_OPTIONS,
807
848
  x: z.object({
808
849
  reply_settings: z.enum(["", "following", "mentionedUsers"]).optional(),
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,6 +152,19 @@ export interface ApiPost {
152
152
  facebook?: ApiPlatformOptionsOut;
153
153
  linkedin?: ApiPlatformOptionsOut;
154
154
  linkedin_page?: ApiPlatformOptionsOut;
155
+ /** Non-sponsored LinkedIn poll(s), independent per channel — echoed back after create/update/publish. */
156
+ linkedin_poll?: {
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;
167
+ } | null;
155
168
  tiktok?: ApiPlatformOptionsOut;
156
169
  google_business?: ApiPlatformOptionsOut;
157
170
  x?: ApiPlatformOptionsOut;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnisocials/mcp-server",
3
- "version": "1.20.1",
3
+ "version": "1.22.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",