@omnisocials/mcp-server 1.48.0 → 1.50.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
@@ -109,7 +109,7 @@ OmniSocials accepts the following channel IDs in `create_post`, `create_and_publ
109
109
  | `tiktok` | TikTok | Posts and reels |
110
110
  | `pinterest` | Pinterest | Pins (requires `board_id`) |
111
111
  | `x` | X (Twitter) | Posts, chained threads via `x.thread_parts` |
112
- | `threads` | Threads | Posts, chained threads via `threads.thread_parts`, location tagging via `threads.location_id` (rolling out) |
112
+ | `threads` | Threads | Posts, chained threads via `threads.thread_parts`, a topic via `threads.topic_tag`, polls via `threads.poll`, location tagging via `threads.location_id` (rolling out) |
113
113
  | `bluesky` | Bluesky | Posts, chained threads via `bluesky.thread_parts` |
114
114
  | `mastodon` | Mastodon | Posts, chained threads via `mastodon.thread_parts` |
115
115
  | `telegram` | Telegram | A channel or group the OmniSocials bot posts to: text, photos, videos, GIFs, albums of up to 10, native polls and link buttons via `telegram` |
@@ -202,6 +202,7 @@ Apply a set when creating a post: `create_post` / `create_and_publish_post` acce
202
202
  | `get_inbox_conversation` | Get a conversation's full message history with the post it belongs to; every message shows its `#id` |
203
203
  | `mark_inbox_read` | Mark all incoming messages in a conversation as read (also the durable skip for `get_next_unanswered`) |
204
204
  | `reply_to_inbox` | Reply to a DM, comment, or mention (existing conversations only); `message_id` picks the comment being answered; `include_next` returns the next unanswered item in the same call |
205
+ | `like_inbox_comment` | Like or unlike a comment on one of your posts as the account (Facebook Page, LinkedIn Page, TikTok; Facebook allows a Like only) |
205
206
  | `hide_inbox_comment` | Hide or unhide a comment on one of your posts (Facebook, Instagram, TikTok, YouTube, Threads) |
206
207
  | `delete_inbox_comment` | Permanently delete a comment on one of your posts, with the replies under it (Facebook, Instagram, TikTok) |
207
208
  | `hide_inbox_reply` | Older name for `hide_inbox_comment`, kept for compatibility |
@@ -244,6 +245,14 @@ Full API docs: [docs.omnisocials.com](https://docs.omnisocials.com)
244
245
 
245
246
  ## Changelog
246
247
 
248
+ ### 1.50.0 (2026-10-27)
249
+
250
+ - **Added: `like_inbox_comment`.** Like (or unlike with `like: false`) a Facebook, LinkedIn Page or TikTok comment as the connected account. Facebook allows a Like only. Inbox messages carry `liked`.
251
+
252
+ ### 1.49.0 (2026-10-27)
253
+
254
+ - **Added: the Threads topic.** `threads.topic_tag` on `create_post`, `create_and_publish_post` and `update_post` (one topic per post, 1 to 50 characters, spaces allowed, no period or ampersand; `null` removes it on update). `get_post` shows it.
255
+
247
256
  ### 1.48.0 (2026-10-26)
248
257
 
249
258
  - **Added: video retention in `get_post_analytics`:** a `3-second hold (%)` row (`hold_rate_3s`: TikTok videos with TikTok comments enabled, Facebook Reels, Instagram Reels as 100 minus `skip_rate`) and a `Still watching` row that sums up the retention curve of TikTok videos and Facebook Reels and videos (1, 3, 5, 10, 15, 30 seconds and each quarter). Instagram and YouTube give no curve through their APIs.
package/build/client.d.ts CHANGED
@@ -246,6 +246,9 @@ export interface ThreadsPostOptions {
246
246
  * the tag goes on the first post. Needs the threads_location_tagging
247
247
  * permission on the Threads connection. */
248
248
  location_id?: string | null;
249
+ /** The topic of the post on Threads (1 to 50 characters, no period and
250
+ * no ampersand; spaces allowed). On a thread it goes on the first post. */
251
+ topic_tag?: string | null;
249
252
  }
250
253
  /**
251
254
  * Update-side variant: `thread_parts: null` clears the thread (revert to a
@@ -257,6 +260,8 @@ export interface ThreadsPostOptionsUpdate {
257
260
  /** New Threads location id, or null to remove the tag. Omit to leave the
258
261
  * existing tag untouched. */
259
262
  location_id?: string | null;
263
+ /** New Threads topic, or null to remove it. Omit to leave it untouched. */
264
+ topic_tag?: string | null;
260
265
  }
261
266
  /** Non-sponsored LinkedIn poll: question + 2-4 options + duration. */
262
267
  export interface LinkedInPollFields {
@@ -569,5 +574,6 @@ export declare class OmniSocialsClient {
569
574
  exclude?: string;
570
575
  }): Promise<ApiResponse<InboxNextUnanswered | null>>;
571
576
  hideInboxMessage(messageId: string, hide?: boolean): Promise<ApiResponse<InboxMessage>>;
577
+ likeInboxMessage(messageId: string, like?: boolean): Promise<ApiResponse<InboxMessage>>;
572
578
  deleteInboxMessage(messageId: string): Promise<ApiResponse<InboxDeleteResult>>;
573
579
  }
package/build/client.js CHANGED
@@ -585,6 +585,12 @@ export class OmniSocialsClient {
585
585
  async hideInboxMessage(messageId, hide = true) {
586
586
  return this.request("POST", `/inbox/messages/${encodeURIComponent(messageId)}/hide`, { hide });
587
587
  }
588
+ // Like/unlike a comment as the connected account (Facebook Page, LinkedIn
589
+ // Page, TikTok). `like: false` removes the like. Needs the inbox:write
590
+ // scope.
591
+ async likeInboxMessage(messageId, like = true) {
592
+ return this.request("POST", `/inbox/messages/${encodeURIComponent(messageId)}/like`, { like });
593
+ }
588
594
  // Delete a comment on the platform and from the inbox (Facebook, Instagram,
589
595
  // TikTok; YouTube cannot delete other people's comments, hide instead).
590
596
  // Replies under it go too. Needs the inbox:write scope.
package/build/index.js CHANGED
@@ -30,7 +30,7 @@ const sessionState = { activeIndex: 0 };
30
30
  const getActiveClient = () => workspaceClients[sessionState.activeIndex].client;
31
31
  const server = new McpServer({
32
32
  name: "OmniSocials",
33
- version: "1.48.0",
33
+ version: "1.50.0",
34
34
  });
35
35
  // Register all tools - pass getter function so tools always use the active workspace's client
36
36
  registerPostTools(server, getActiveClient);
@@ -61,6 +61,8 @@ function renderInboxMessages(msgs, highlightId) {
61
61
  }
62
62
  if (m.hidden)
63
63
  flags.push("hidden");
64
+ if (m.liked)
65
+ flags.push("liked");
64
66
  if (m.parent_comment_id)
65
67
  flags.push("reply to a comment");
66
68
  md += `**${who}** _(${formatDateTime(m.timestamp)})_ \`#${m.id}\`${flags.length ? " · " + flags.join(" · ") : ""}\n${m.text || ""}\n`;
@@ -88,7 +90,7 @@ function renderNextUnanswered(result) {
88
90
  md += renderInboxPost(item.message.post || c.post);
89
91
  md += renderInboxMessages(item.messages, item.message.id);
90
92
  md += `_${result.remaining || 0} more waiting._\n\n`;
91
- md += `_Reply: reply_to_inbox with the conversation ID and message_id: "${item.message.id}" (add include_next: true to get the next one in the same call). Skip for good: mark_inbox_read. Comments: hide_inbox_comment / delete_inbox_comment with the message id._`;
93
+ md += `_Reply: reply_to_inbox with the conversation ID and message_id: "${item.message.id}" (add include_next: true to get the next one in the same call). Skip for good: mark_inbox_read. Comments: like_inbox_comment / hide_inbox_comment / delete_inbox_comment with the message id._`;
92
94
  return md;
93
95
  }
94
96
  export function registerInboxTools(server, getClient) {
@@ -154,7 +156,7 @@ export function registerInboxTools(server, getClient) {
154
156
  }
155
157
  return { content: [{ type: "text", text: md }] };
156
158
  });
157
- server.tool("get_inbox_conversation", "Get the full message history of one inbox conversation, oldest first, with the post it belongs to (caption, link, image) for comment/mention threads. Each message shows its inbox id (#id): that id is what hide_inbox_comment and delete_inbox_comment take. Use the conversation_id from list_inbox_conversations. Replies sent from the native apps appear as outgoing messages too.", {
159
+ server.tool("get_inbox_conversation", "Get the full message history of one inbox conversation, oldest first, with the post it belongs to (caption, link, image) for comment/mention threads. Each message shows its inbox id (#id): that id is what like_inbox_comment, hide_inbox_comment and delete_inbox_comment take. Use the conversation_id from list_inbox_conversations. Replies sent from the native apps appear as outgoing messages too.", {
158
160
  conversation_id: z.string().describe("The conversation ID to fetch"),
159
161
  limit: z
160
162
  .number()
@@ -320,6 +322,28 @@ export function registerInboxTools(server, getClient) {
320
322
  // Older name from 1.28.0 (when hiding was Threads-only). Kept so existing
321
323
  // setups keep working; same call, same platforms as hide_inbox_comment.
322
324
  server.tool("hide_inbox_reply", "Older name for hide_inbox_comment, kept so existing setups keep working; prefer hide_inbox_comment. Same behaviour: hide (or unhide with hide: false) a comment someone left on one of the workspace's posts, as the post owner, on Facebook, Instagram, TikTok, YouTube or Threads. Takes the message id (the #id in the thread), not the conversation id. A 403 reconnect_required means the account must be reconnected in the dashboard to grant the comment-moderation permission.", hideCommentSchema, { title: "Hide Inbox Comment (older name)", readOnlyHint: false, destructiveHint: false, openWorldHint: true }, hideComment);
325
+ server.tool("like_inbox_comment", "Like (or remove the like from) a comment someone left on one of the workspace's posts, as the connected account: the Facebook Page, the LinkedIn Page or the TikTok account. Incoming Facebook, LinkedIn Page and TikTok comments only. Facebook allows a Like only: no app can add Love or another reaction, so say so when the user asks for one. Takes the message id (the #id shown by get_inbox_conversation and get_next_unanswered), not the conversation id. Liking an already liked comment succeeds and changes nothing. A like does not mark the comment as read or answered: to move past it in get_next_unanswered without a reply, use mark_inbox_read or exclude. Like only comments the user asked you to like, or that match a rule the user gave (for example 'like the positive comments'); never like in bulk on your own. A 403 reconnect_required or 401 reauth_required means the account must be reconnected in the dashboard; relay that to the user rather than retrying.", {
326
+ message_id: z
327
+ .string()
328
+ .describe("The inbox message id of the comment (the #id in the thread)"),
329
+ like: z
330
+ .boolean()
331
+ .optional()
332
+ .describe("true (default) to like, false to remove the like"),
333
+ }, { title: "Like Inbox Comment", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ message_id, like }) => {
334
+ const result = await getClient().likeInboxMessage(message_id, like !== false);
335
+ if (result.error)
336
+ return errorResult(result.error);
337
+ const m = result.data;
338
+ return {
339
+ content: [
340
+ {
341
+ type: "text",
342
+ text: `Comment \`#${m?.id ?? message_id}\` on ${m?.platform || "the platform"} is now ${m?.liked ? "liked" : "not liked"}.`,
343
+ },
344
+ ],
345
+ };
346
+ });
323
347
  server.tool("delete_inbox_comment", "Permanently delete a comment someone left on one of the workspace's posts, on the platform and from the inbox. Facebook, Instagram and TikTok comments only (YouTube's API cannot delete other people's comments: use hide_inbox_comment there). Replies under the comment are deleted with it. This cannot be undone: confirm with the user before calling. Takes the message id (the #id in the thread), not the conversation id. A 403 reconnect_required means the account must be reconnected in the dashboard to grant the moderation permission.", {
324
348
  message_id: z
325
349
  .string()
@@ -207,6 +207,13 @@ const THREADS_POLL_OPTIONS = z
207
207
  .nullable()
208
208
  .optional()
209
209
  .describe("Attach a poll to the post; the post text is the question. Threads closes the poll 24 hours after it is published (no duration). Polls go on text-only posts: no media on Threads together with a poll. On a thread the poll goes on the first post. On update_post, null removes it.");
210
+ // The Threads topic (2026-10-26): `threads.topic_tag`, one per post.
211
+ const THREADS_TOPIC_TAG = z
212
+ .string()
213
+ .max(50)
214
+ .nullable()
215
+ .optional()
216
+ .describe("The topic of the post on Threads, one per post, e.g. \"Build in public\". Unlike a #tag in the text it can hold spaces. 1 to 50 characters, no period (.) and no ampersand (&); a leading # is dropped. On a multi-post thread the topic goes on the first post. On update_post, pass null to remove it.");
210
217
  const MASTODON_POLL_OPTIONS = z
211
218
  .object({
212
219
  options: z.array(z.string().min(1)).min(2).describe("The answer options, all different; count and length within the instance's limits (Mastodon's defaults: 4 options of 50 characters)."),
@@ -568,6 +575,10 @@ export function registerPostTools(server, getClient) {
568
575
  const locBits = [threadsLoc.name, threadsLoc.city, threadsLoc.country].filter(Boolean).join(", ");
569
576
  md += `| **Threads location** | ${locBits ? `${locBits} ` : ""}\`${threadsLoc.id}\` |\n`;
570
577
  }
578
+ const threadsTopic = p.threads?.topic_tag;
579
+ if (typeof threadsTopic === "string" && threadsTopic) {
580
+ md += `| **Threads topic** | ${threadsTopic} |\n`;
581
+ }
571
582
  if (Array.isArray(p.collaborators) && p.collaborators.length) {
572
583
  md += `| **IG collaborators** | ${p.collaborators.map((c) => `@${c}`).join(", ")} |\n`;
573
584
  }
@@ -1073,9 +1084,10 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
1073
1084
  }).optional().describe("Mastodon options"),
1074
1085
  threads: z.object({
1075
1086
  poll: THREADS_POLL_OPTIONS,
1087
+ topic_tag: THREADS_TOPIC_TAG,
1076
1088
  location_id: z.string().nullable().optional().describe("Tag a location on the Threads post. Use an `id` from search_locations with platform \"threads\" (Threads location ids are NOT Facebook Place IDs, so never reuse the Instagram location_id here). On a multi-post thread the tag goes on the first post. Needs the workspace's Threads connection to have the threads_location_tagging permission; the API returns a clear 400 asking to reconnect Threads when it is missing, and a clear 400 while location tagging is still rolling out (Meta has not approved the permission yet). On update_post, pass null to remove the tag."),
1077
1089
  thread_parts: z.array(THREADS_THREAD_PART).min(2).max(25).optional().describe("Publish as a chained Threads (Meta) thread instead of a single post. Provide 2 to 25 parts; parts after the first are posted as replies to the previous part. Each part holds up to 500 characters and up to 10 media (images and videos can be mixed) via media_ids (from upload_media) or media_urls. When set, the Threads caption is taken from part 1 and content is ignored for Threads. For a single post, omit thread_parts and use content."),
1078
- }).optional().describe("Threads (Meta) options: thread mode via `thread_parts`, location tag via `location_id`."),
1090
+ }).optional().describe("Threads (Meta) options: thread mode via `thread_parts`, location tag via `location_id`, a poll via `poll`, the topic via `topic_tag`."),
1079
1091
  google_business: GOOGLE_BUSINESS_OPTIONS,
1080
1092
  telegram: TELEGRAM_OPTIONS,
1081
1093
  discord: DISCORD_OPTIONS,
@@ -1249,9 +1261,10 @@ Do NOT call without required media — it will fail.`, {
1249
1261
  }).optional().describe("Mastodon options"),
1250
1262
  threads: z.object({
1251
1263
  poll: THREADS_POLL_OPTIONS,
1264
+ topic_tag: THREADS_TOPIC_TAG,
1252
1265
  location_id: z.string().nullable().optional().describe("Tag a location on the Threads post. Use an `id` from search_locations with platform \"threads\" (Threads location ids are NOT Facebook Place IDs, so never reuse the Instagram location_id here). On a multi-post thread the tag goes on the first post. Needs the workspace's Threads connection to have the threads_location_tagging permission; the API returns a clear 400 asking to reconnect Threads when it is missing, and a clear 400 while location tagging is still rolling out (Meta has not approved the permission yet). On update_post, pass null to remove the tag."),
1253
1266
  thread_parts: z.array(THREADS_THREAD_PART).min(2).max(25).optional().describe("Publish as a chained Threads (Meta) thread (2 to 25 parts). Parts after the first are posted as replies to the previous part. Each part holds up to 500 characters and up to 10 media (images and videos can be mixed) via media_ids or media_urls. The Threads caption is taken from part 1."),
1254
- }).optional().describe("Threads (Meta) options: thread mode via `thread_parts`, location tag via `location_id`."),
1267
+ }).optional().describe("Threads (Meta) options: thread mode via `thread_parts`, location tag via `location_id`, a poll via `poll`, the topic via `topic_tag`."),
1255
1268
  google_business: GOOGLE_BUSINESS_OPTIONS,
1256
1269
  telegram: TELEGRAM_OPTIONS,
1257
1270
  discord: DISCORD_OPTIONS,
@@ -1372,9 +1385,10 @@ Do NOT call without required media — it will fail.`, {
1372
1385
  }).optional().describe("Mastodon options"),
1373
1386
  threads: z.object({
1374
1387
  poll: THREADS_POLL_OPTIONS,
1388
+ topic_tag: THREADS_TOPIC_TAG,
1375
1389
  location_id: z.string().nullable().optional().describe("Tag a location on the Threads post. Use an `id` from search_locations with platform \"threads\" (Threads location ids are NOT Facebook Place IDs, so never reuse the Instagram location_id here). On a multi-post thread the tag goes on the first post. Needs the workspace's Threads connection to have the threads_location_tagging permission; the API returns a clear 400 asking to reconnect Threads when it is missing, and a clear 400 while location tagging is still rolling out (Meta has not approved the permission yet). On update_post, pass null to remove the tag."),
1376
1390
  thread_parts: z.array(THREADS_THREAD_PART).min(2).max(25).nullable().optional().describe("Replace the Threads (Meta) thread shape on this post. Pass an array (2 to 25 parts, 500 characters per part) to update/create the thread (attach up to 10 media to any part via media_ids or media_urls; images and videos can be mixed), or `null` to revert to single-post mode."),
1377
- }).optional().describe("Threads (Meta) options: thread mode via `thread_parts`, location tag via `location_id`."),
1391
+ }).optional().describe("Threads (Meta) options: thread mode via `thread_parts`, location tag via `location_id`, a poll via `poll`, the topic via `topic_tag`."),
1378
1392
  google_business: GOOGLE_BUSINESS_OPTIONS,
1379
1393
  telegram: TELEGRAM_OPTIONS,
1380
1394
  discord: DISCORD_OPTIONS,
package/build/types.d.ts CHANGED
@@ -76,6 +76,10 @@ export interface InboxMessage {
76
76
  * YouTube, Threads): true when the comment is hidden on the platform (flip
77
77
  * it with hide_inbox_comment). null for DMs. */
78
78
  hidden?: boolean | null;
79
+ /** Incoming Facebook, LinkedIn Page and TikTok comments: true when the
80
+ * connected account liked the comment (flip it with like_inbox_comment).
81
+ * null for DMs, outgoing messages and the other platforms. */
82
+ liked?: boolean | null;
79
83
  /** Link to the reply or mentioning post on the platform, when known. */
80
84
  permalink?: string | null;
81
85
  /** Media sent with this message, when present. Incoming Instagram/Facebook
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnisocials/mcp-server",
3
- "version": "1.48.0",
3
+ "version": "1.50.0",
4
4
  "mcpName": "io.github.omnisocials/mcp-server",
5
5
  "description": "MCP server for OmniSocials API - manage social media posts, media, accounts, analytics, and webhooks",
6
6
  "type": "module",