@omnisocials/mcp-server 1.33.0 → 1.34.1

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
@@ -189,15 +189,18 @@ Apply a set when creating a post: `create_post` / `create_and_publish_post` acce
189
189
  | `delete_webhook` | Delete a webhook |
190
190
  | `rotate_webhook_secret` | Rotate a webhook's signing secret |
191
191
 
192
- ### Social Inbox (5 tools)
192
+ ### Social Inbox (8 tools)
193
193
 
194
194
  | Tool | Description |
195
195
  |------|-------------|
196
- | `list_inbox_conversations` | List conversations — Instagram/Facebook DMs, comments, mentions, LinkedIn company-page comments/mentions, TikTok video comments, YouTube video comments, X DMs (opt-in), and Threads replies on your posts plus mentions (rolling out); filter by platform, type, or unread |
197
- | `get_inbox_conversation` | Get a conversation's full message history |
198
- | `mark_inbox_read` | Mark all incoming messages in a conversation as read |
199
- | `reply_to_inbox` | Reply to a DM, comment, or mention (existing conversations only) |
200
- | `hide_inbox_reply` | Hide or unhide a reply on one of your Threads posts (Threads only for now) |
196
+ | `list_inbox_conversations` | List conversations: Instagram/Facebook DMs, comments, mentions, LinkedIn company-page comments/mentions, TikTok video comments, YouTube video comments, X DMs (opt-in), and Threads replies on your posts plus mentions; filter by platform, type, unread, or `unanswered` |
197
+ | `get_next_unanswered` | The next item that still needs an answer (longest-waiting first) with the whole thread and the post it is on; filter by platform or type, `include_read`, `exclude` |
198
+ | `get_inbox_conversation` | Get a conversation's full message history with the post it belongs to; every message shows its `#id` |
199
+ | `mark_inbox_read` | Mark all incoming messages in a conversation as read (also the durable skip for `get_next_unanswered`) |
200
+ | `reply_to_inbox` | Reply to a DM, comment, or mention (existing conversations only); `include_next` returns the next unanswered item in the same call |
201
+ | `hide_inbox_comment` | Hide or unhide a comment on one of your posts (Facebook, Instagram, TikTok, YouTube, Threads) |
202
+ | `delete_inbox_comment` | Permanently delete a comment on one of your posts, with the replies under it (Facebook, Instagram, TikTok) |
203
+ | `hide_inbox_reply` | Older name for `hide_inbox_comment`, kept for compatibility |
201
204
 
202
205
  Requires an API key with the opt-in `inbox:read` / `inbox:write` scopes — enable "Social Inbox access" when you create the key.
203
206
 
@@ -224,6 +227,22 @@ Full API docs: [docs.omnisocials.com](https://docs.omnisocials.com)
224
227
 
225
228
  ## Changelog
226
229
 
230
+ ### 1.34.1 (2026-09-14)
231
+
232
+ - **Changed: Threads threads and the Threads inbox are no longer "rolling out".** Meta approved the reply, read-replies and mentions permissions, so `threads.thread_parts` and the Threads inbox work for every Threads connection; accounts connected before 2026-09-14 need a one-time reconnect under Settings -> Organisation -> Workspaces (`400`/`401` errors name the reconnect). Threads location tagging stays rolling out (not approved yet). Same on the companion server.
233
+
234
+ ### 1.34.0 (2026-09-06)
235
+
236
+ - **Added: work through the inbox one item at a time.** New tool `get_next_unanswered` returns the next inbox item that still needs an answer, with the whole conversation and the post it is on, in one call: the customer's latest DM with no reply after it (Instagram/Facebook DMs within the 24-hour reply window), or a comment/mention nobody replied to and that is not hidden. Longest-waiting first; filter with `platform` and `type`; only unread items by default (`mark_inbox_read` is the durable skip, `include_read: true` widens it, `exclude` skips up to 100 conversation ids for one call); looks back 30 days and reports `remaining`. `reply_to_inbox` accepts `include_next: true` and returns the next item in the same response. `list_inbox_conversations` gains an `unanswered` filter. Replies typed in the native apps now appear as outgoing messages and count as answers. Same on the companion server (`mcp.omnisocials.com`).
237
+ - **Added: hide and delete comments on every comment platform.** New tool `hide_inbox_comment` hides (or unhides with `hide: false`) a comment on one of your posts on Facebook, Instagram, TikTok, YouTube and Threads (on YouTube, hide sets the comment to `rejected` and unhide publishes it again); `hide_inbox_reply` stays as an alias. New tool `delete_inbox_comment` permanently deletes a comment on Facebook, Instagram and TikTok, on the platform and from the inbox, with the replies under it (YouTube cannot delete other people's comments: hide instead). Both take the message id (`#id` in the thread). Errors: `403 reconnect_required`, `429 quota_exceeded` (YouTube hide), `502 platform_error`, `404 account_not_connected`, `401 reauth_required` (TikTok/Threads). `hidden` is now a boolean for every comment platform, `null` for DMs. Same on the companion server.
238
+ - **Added: post context in inbox threads.** The `post` object on conversations and messages carries `url` and `media_type` next to `id`, `caption` and `thumbnail`. `get_inbox_conversation` and `get_next_unanswered` render the post above the thread, and every message shows its `#id` plus `hidden` and reply-to-a-comment markers. Same on the companion server.
239
+
240
+ ### 1.33.0 (2026-09-06)
241
+
242
+ - **Added: submit posts for approval.** New tool `list_approval_workflows` lists the workspace's approval workflows (id, steps, named approvers; workflows are created in the dashboard under Approvals). `create_post` accepts `approval_workflow_id`: the post is created as `in_approval` instead of `scheduled`, the approvers are notified (dashboard badge, email, Slack), and it publishes at `scheduled_at` once the workflow's last step approves it (Approvals page, or `approve_post`). This is the flow for "the agent prepares everything, a human only approves". Requires `scheduled_at`; not allowed with `publish_now`. Errors: `404 workflow_not_found`, `400 validation_error` (no `scheduled_at`, or no approvers on the first step). Same on the companion server (`mcp.omnisocials.com`).
243
+ - **Added: platform options in `get_post`.** A new "Platform Options" section renders the TikTok options that were set (title, privacy level, cover frame `video_cover_timestamp_ms`, comment/duet/stitch toggles, AI-generated and brand disclosures, auto-add music), the YouTube options (title, privacy, tags, made for kids) and the Pinterest options (board, title, link). Previously only `first_comment` was rendered, so a pinned TikTok cover could not be verified without opening the post. Same on the companion server.
244
+ - **Changed: inbox attachment types.** Inbox `attachment.type` now reflects the fetched bytes rather than the platform's label (a generic photo DM is `image`, an MP4 story mention is `video`; `file` only when we could not classify the media), and a new `link` type carries the platform's own URL when it did not point at media (for example a shared post). `reply_to_inbox` now returns clear errors instead of `platform_error` for Instagram DM text over 1,000 characters (`validation_error`), replies outside the 24-hour messaging window (`outside_messaging_window`), and replies to a comment or message that no longer exists on the platform (`not_found`).
245
+
227
246
  ### 1.32.0 (2026-09-02)
228
247
 
229
248
  - **Added: TikTok Photo Mode title.** The `tiktok` option object on `create_post`, `create_and_publish_post`, and `update_post` accepts `title` (max 90 characters): the title TikTok shows above the caption on photo carousels. TikTok's photo endpoint takes the title and the caption as two separate fields, and until now only the caption (`content`) could be set here, so photo posts created through the MCP always published with an empty title. Video posts have a single caption field and ignore `title`. `get_post` returns it as `tiktok.title`. Same on the companion server (`mcp.omnisocials.com`).
package/build/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Account, AnalyticsOverview, AccountAnalyticsEntry, ApiPost, ApiResponse, AudioSearchResult, BestTimesResult, HashtagSet, LocationSearchResult, MediaCheckResult, MediaCompatibility, MediaFolder, MediaItem, PdfUploadResult, PostAnalytics, PublishPostResult, RecentPlatformPost, RetryPostResult, ApprovePostResult, RejectPostResult, InboxConversation, InboxMessage, Webhook } from "./types.js";
1
+ import type { Account, AnalyticsOverview, AccountAnalyticsEntry, ApiPost, ApiResponse, AudioSearchResult, BestTimesResult, HashtagSet, LocationSearchResult, MediaCheckResult, MediaCompatibility, MediaFolder, MediaItem, PdfUploadResult, PostAnalytics, PublishPostResult, RecentPlatformPost, RetryPostResult, ApprovePostResult, RejectPostResult, InboxConversation, InboxDeleteResult, InboxMessage, InboxNextUnanswered, Webhook } from "./types.js";
2
2
  export declare function fetchImageAsBase64(url: string): Promise<{
3
3
  data: string;
4
4
  mimeType: string;
@@ -380,6 +380,7 @@ export declare class OmniSocialsClient {
380
380
  platform?: string;
381
381
  type?: string;
382
382
  unread?: string;
383
+ unanswered?: string;
383
384
  limit?: string;
384
385
  cursor?: string;
385
386
  }): Promise<ApiResponse<InboxConversation[]>>;
@@ -392,6 +393,15 @@ export declare class OmniSocialsClient {
392
393
  text: string;
393
394
  attachment_url?: string;
394
395
  attachment_type?: string;
396
+ include_next?: boolean;
395
397
  }): Promise<ApiResponse<InboxMessage>>;
398
+ getNextUnanswered(params?: {
399
+ platform?: string;
400
+ type?: string;
401
+ order?: string;
402
+ include_read?: string;
403
+ exclude?: string;
404
+ }): Promise<ApiResponse<InboxNextUnanswered | null>>;
396
405
  hideInboxMessage(messageId: string, hide?: boolean): Promise<ApiResponse<InboxMessage>>;
406
+ deleteInboxMessage(messageId: string): Promise<ApiResponse<InboxDeleteResult>>;
397
407
  }
package/build/client.js CHANGED
@@ -435,12 +435,29 @@ export class OmniSocialsClient {
435
435
  async markInboxRead(conversationId) {
436
436
  return this.request("POST", `/inbox/conversations/${encodeURIComponent(conversationId)}/read`);
437
437
  }
438
+ // `include_next: true` makes the response also carry `next` (the next
439
+ // conversation that needs an answer, same shape as getNextUnanswered data)
440
+ // and `remaining` at the top level.
438
441
  async replyToInboxConversation(conversationId, data) {
439
442
  return this.request("POST", `/inbox/conversations/${encodeURIComponent(conversationId)}/reply`, data);
440
443
  }
441
- // Hide/unhide a reply on one of the workspace's Threads posts (Threads
442
- // only for now). `hide: false` unhides. Needs the inbox:write scope.
444
+ // The next inbox item that needs an answer (GET /inbox/next): `data` is
445
+ // { conversation, message, messages } or null, `remaining` sits next to it.
446
+ // Only unread items by default (include_read: "true" widens it); `exclude`
447
+ // is a comma-separated list of conversation ids to skip for this call.
448
+ async getNextUnanswered(params) {
449
+ return this.request("GET", "/inbox/next", undefined, params);
450
+ }
451
+ // Hide/unhide a comment on one of the workspace's posts (Facebook,
452
+ // Instagram, TikTok, YouTube, Threads). `hide: false` unhides. Needs the
453
+ // inbox:write scope.
443
454
  async hideInboxMessage(messageId, hide = true) {
444
455
  return this.request("POST", `/inbox/messages/${encodeURIComponent(messageId)}/hide`, { hide });
445
456
  }
457
+ // Delete a comment on the platform and from the inbox (Facebook, Instagram,
458
+ // TikTok; YouTube cannot delete other people's comments, hide instead).
459
+ // Replies under it go too. Needs the inbox:write scope.
460
+ async deleteInboxMessage(messageId) {
461
+ return this.request("DELETE", `/inbox/messages/${encodeURIComponent(messageId)}`);
462
+ }
446
463
  }
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.32.0",
32
+ version: "1.34.1",
33
33
  });
34
34
  // Register all tools - pass getter function so tools always use the active workspace's client
35
35
  registerPostTools(server, getActiveClient);
@@ -9,10 +9,88 @@ const PLATFORM_EMOJI = {
9
9
  x: "🐦",
10
10
  threads: "🧵",
11
11
  };
12
+ const INBOX_PLATFORMS = [
13
+ "instagram",
14
+ "facebook",
15
+ "linkedin",
16
+ "tiktok",
17
+ "youtube",
18
+ "x",
19
+ "threads",
20
+ ];
21
+ function errorResult(error) {
22
+ return {
23
+ content: [
24
+ {
25
+ type: "text",
26
+ text: `Error (${error.code}): ${error.message}`,
27
+ },
28
+ ],
29
+ };
30
+ }
31
+ // Inbox thread rendering shared by get_inbox_conversation, get_next_unanswered
32
+ // and reply_to_inbox (include_next). The post comes first (a comment like
33
+ // "what size is this?" only makes sense with the post in view), then the
34
+ // messages oldest first, each with its inbox id so hide/delete can target it.
35
+ function renderInboxPost(post) {
36
+ if (!post || (!post.caption && !post.url && !post.thumbnail))
37
+ return "";
38
+ let md = `**Post**`;
39
+ if (post.media_type) {
40
+ md += ` _(${String(post.media_type).toLowerCase().replace(/_/g, " ")})_`;
41
+ }
42
+ md += `\n`;
43
+ if (post.caption) {
44
+ md += `> ${post.caption.replace(/\s*\n+\s*/g, " ").slice(0, 600)}\n`;
45
+ }
46
+ if (post.url)
47
+ md += `🔗 ${post.url}\n`;
48
+ if (post.thumbnail)
49
+ md += `🖼️ ${post.thumbnail}\n`;
50
+ return md + `\n`;
51
+ }
52
+ function renderInboxMessages(msgs, highlightId) {
53
+ let md = "";
54
+ for (const m of msgs) {
55
+ const who = m.direction === "outgoing"
56
+ ? "→ You"
57
+ : `← ${m.sender?.name || m.sender?.username || "Them"}`;
58
+ const flags = [];
59
+ if (highlightId && String(m.id) === String(highlightId)) {
60
+ flags.push("**needs a reply**");
61
+ }
62
+ if (m.hidden)
63
+ flags.push("hidden");
64
+ if (m.parent_comment_id)
65
+ flags.push("reply to a comment");
66
+ md += `**${who}** _(${formatDateTime(m.timestamp)})_ \`#${m.id}\`${flags.length ? " · " + flags.join(" · ") : ""}\n${m.text || ""}\n`;
67
+ if (m.attachment?.url) {
68
+ md += `📎 [${m.attachment.type}] ${m.attachment.url}\n`;
69
+ }
70
+ if (m.permalink)
71
+ md += `${m.permalink}\n`;
72
+ md += `\n`;
73
+ }
74
+ return md;
75
+ }
76
+ function renderNextUnanswered(result) {
77
+ const item = result.data;
78
+ if (!item)
79
+ return "Nothing is waiting for an answer.";
80
+ const c = item.conversation;
81
+ const who = c.participant?.name || c.participant?.username || "Unknown";
82
+ let md = `## Next: ${PLATFORM_EMOJI[c.platform] || ""} ${c.platform} ${c.type} from ${who}\n\n`;
83
+ md += `Conversation ID: \`${c.conversation_id}\`\nMessage to answer: \`#${item.message.id}\`\n\n`;
84
+ md += renderInboxPost(item.message.post || c.post);
85
+ md += renderInboxMessages(item.messages, item.message.id);
86
+ md += `_${result.remaining || 0} more waiting._\n\n`;
87
+ md += `_Reply: reply_to_inbox with the conversation 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._`;
88
+ return md;
89
+ }
12
90
  export function registerInboxTools(server, getClient) {
13
- server.tool("list_inbox_conversations", "List social inbox conversations (Instagram/Facebook DMs, comments, mentions, LinkedIn company-page comments/mentions, TikTok video comments, YouTube video comments, X DMs where the workspace has opted into X DMs, and Threads replies on the workspace's posts plus mentions), newest activity first. Threads conversations are comment (replies on the workspace's posts) and mention only, need a Threads connection with the reply-reading permission, and are currently rolling out (disabled on production until Meta approves the permission). Cursor-paginated: pass the returned cursor to get the next page.", {
91
+ server.tool("list_inbox_conversations", "List social inbox conversations (Instagram/Facebook DMs, comments, mentions, LinkedIn company-page comments/mentions, TikTok video comments, YouTube video comments, X DMs where the workspace has opted into X DMs, and Threads replies on the workspace's posts plus mentions), newest activity first. Cursor-paginated: pass the returned cursor to get the next page. To work through what needs answering one item at a time, use get_next_unanswered instead.", {
14
92
  platform: z
15
- .enum(["instagram", "facebook", "linkedin", "tiktok", "x", "youtube", "threads"])
93
+ .enum(INBOX_PLATFORMS)
16
94
  .optional()
17
95
  .describe("Filter to one platform"),
18
96
  type: z
@@ -23,6 +101,10 @@ export function registerInboxTools(server, getClient) {
23
101
  .boolean()
24
102
  .optional()
25
103
  .describe("Only conversations with unread incoming messages"),
104
+ unanswered: z
105
+ .boolean()
106
+ .optional()
107
+ .describe("Only conversations that still need an answer: the customer's latest DM has no reply after it (Instagram/Facebook DMs within the 24-hour reply window), or a comment/mention nobody replied to and that is not hidden. Replies typed in the native apps count as answers."),
26
108
  limit: z
27
109
  .number()
28
110
  .optional()
@@ -39,21 +121,16 @@ export function registerInboxTools(server, getClient) {
39
121
  query.type = params.type;
40
122
  if (params.unread !== undefined)
41
123
  query.unread = String(params.unread);
124
+ if (params.unanswered !== undefined) {
125
+ query.unanswered = String(params.unanswered);
126
+ }
42
127
  if (params.limit !== undefined)
43
128
  query.limit = String(params.limit);
44
129
  if (params.cursor)
45
130
  query.cursor = params.cursor;
46
131
  const result = await getClient().listInboxConversations(query);
47
- if (result.error) {
48
- return {
49
- content: [
50
- {
51
- type: "text",
52
- text: `Error (${result.error.code}): ${result.error.message}`,
53
- },
54
- ],
55
- };
56
- }
132
+ if (result.error)
133
+ return errorResult(result.error);
57
134
  const convos = result.data || [];
58
135
  if (!convos.length) {
59
136
  return {
@@ -69,17 +146,20 @@ export function registerInboxTools(server, getClient) {
69
146
  md += `| ${PLATFORM_EMOJI[c.platform] || ""} ${c.platform} | ${c.type} | ${who} | ${c.unread_count || 0} | ${text} | \`${c.conversation_id}\` |\n`;
70
147
  }
71
148
  if (result.pagination?.next_cursor) {
72
- md += `\n_More available — call list_inbox_conversations again with cursor "${result.pagination.next_cursor}"._`;
149
+ md += `\n_More available: pass \`cursor: "${result.pagination.next_cursor}"\` to list_inbox_conversations for the next page._`;
73
150
  }
74
151
  return { content: [{ type: "text", text: md }] };
75
152
  });
76
- server.tool("get_inbox_conversation", "Get the full message history of one inbox conversation, oldest first. Use the conversation_id from list_inbox_conversations.", {
153
+ 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.", {
77
154
  conversation_id: z.string().describe("The conversation ID to fetch"),
78
155
  limit: z
79
156
  .number()
80
157
  .optional()
81
158
  .describe("Max messages per page (1-100, default 50)"),
82
- cursor: z.string().optional().describe("Pagination cursor"),
159
+ cursor: z
160
+ .string()
161
+ .optional()
162
+ .describe("Pagination cursor from a previous response"),
83
163
  }, { title: "Get Inbox Conversation", readOnlyHint: true, destructiveHint: false, openWorldHint: false }, async ({ conversation_id, limit, cursor }) => {
84
164
  const query = {};
85
165
  if (limit !== undefined)
@@ -87,16 +167,8 @@ export function registerInboxTools(server, getClient) {
87
167
  if (cursor)
88
168
  query.cursor = cursor;
89
169
  const result = await getClient().getInboxMessages(conversation_id, query);
90
- if (result.error) {
91
- return {
92
- content: [
93
- {
94
- type: "text",
95
- text: `Error (${result.error.code}): ${result.error.message}`,
96
- },
97
- ],
98
- };
99
- }
170
+ if (result.error)
171
+ return errorResult(result.error);
100
172
  const msgs = result.data || [];
101
173
  if (!msgs.length) {
102
174
  return {
@@ -106,24 +178,11 @@ export function registerInboxTools(server, getClient) {
106
178
  };
107
179
  }
108
180
  let md = `## Conversation \`${conversation_id}\`\n\n`;
109
- for (const m of msgs) {
110
- const who = m.direction === "outgoing"
111
- ? "→ You"
112
- : `← ${m.sender?.name || m.sender?.username || "Them"}`;
113
- // The message id is what hide_inbox_reply takes, so always render it.
114
- const flags = [`id ${m.id}`];
115
- if (m.hidden === true)
116
- flags.push("hidden on Threads");
117
- md += `**${who}** _(${formatDateTime(m.timestamp)})_ \`${flags.join(" · ")}\`\n${m.text || ""}\n`;
118
- if (m.attachment?.url) {
119
- md += `📎 [${m.attachment.type}] ${m.attachment.url}\n`;
120
- }
121
- if (m.permalink)
122
- md += `${m.permalink}\n`;
123
- md += `\n`;
124
- }
181
+ const post = msgs.find((m) => m.post && (m.post.caption || m.post.url))?.post;
182
+ md += renderInboxPost(post);
183
+ md += renderInboxMessages(msgs);
125
184
  if (result.pagination?.next_cursor) {
126
- md += `_More messages — call again with cursor "${result.pagination.next_cursor}"._`;
185
+ md += `_More messages: pass \`cursor: "${result.pagination.next_cursor}"\` for the next page._`;
127
186
  }
128
187
  return { content: [{ type: "text", text: md }] };
129
188
  });
@@ -131,16 +190,8 @@ export function registerInboxTools(server, getClient) {
131
190
  conversation_id: z.string().describe("The conversation ID to mark read"),
132
191
  }, { title: "Mark Inbox Message Read", readOnlyHint: false, destructiveHint: false, openWorldHint: false }, async ({ conversation_id }) => {
133
192
  const result = await getClient().markInboxRead(conversation_id);
134
- if (result.error) {
135
- return {
136
- content: [
137
- {
138
- type: "text",
139
- text: `Error (${result.error.code}): ${result.error.message}`,
140
- },
141
- ],
142
- };
143
- }
193
+ if (result.error)
194
+ return errorResult(result.error);
144
195
  return {
145
196
  content: [
146
197
  {
@@ -150,7 +201,7 @@ export function registerInboxTools(server, getClient) {
150
201
  ],
151
202
  };
152
203
  });
153
- server.tool("reply_to_inbox", "Reply to an existing inbox conversation (DM, comment, or mention) on Instagram, Facebook, LinkedIn, TikTok, X, YouTube, or Threads (Threads replies are on the workspace's posts and mentions; a 401 reauth_required means the Threads connection must be reconnected to grant the reply permission; the reply publishes as a native Threads reply). 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. YouTube replies are comments only. 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. attachment_url/attachment_type let you send media on a Facebook or Instagram DM (text is optional when an attachment is set — an attachment-only DM is allowed); other platforms are text-only.", {
204
+ server.tool("reply_to_inbox", "Reply to an existing inbox conversation (DM, comment, or mention) on Instagram, Facebook, LinkedIn, TikTok, YouTube, X, or Threads. 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. YouTube replies are comments only. Threads replies are on the workspace's posts and mentions; a 401 reauth_required means the Threads connection must be reconnected to grant the reply permission. 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. attachment_url/attachment_type let you send media on a Facebook or Instagram DM (text is optional when an attachment is set); other platforms are text-only. Set include_next: true to get the next conversation that needs an answer back in the same call (same as get_next_unanswered), so the inbox can be worked through as: get_next_unanswered → draft → reply_to_inbox(include_next) → draft → repeat.", {
154
205
  conversation_id: z.string().describe("The conversation ID to reply to"),
155
206
  text: z
156
207
  .string()
@@ -164,60 +215,121 @@ export function registerInboxTools(server, getClient) {
164
215
  .enum(["image", "video", "audio", "file"])
165
216
  .optional()
166
217
  .describe("Required if attachment_url is set"),
167
- }, { title: "Reply to Inbox Message", readOnlyHint: false, destructiveHint: false, openWorldHint: true }, async ({ conversation_id, text, attachment_url, attachment_type }) => {
218
+ include_next: z
219
+ .boolean()
220
+ .optional()
221
+ .describe("After sending, also return the next conversation that needs an answer (full thread + post context), or 'nothing waiting'"),
222
+ }, { title: "Reply to Inbox Message", readOnlyHint: false, destructiveHint: true, openWorldHint: true }, async ({ conversation_id, text, attachment_url, attachment_type, include_next }) => {
168
223
  const result = await getClient().replyToInboxConversation(conversation_id, {
169
224
  text: text ?? "",
170
225
  attachment_url,
171
226
  attachment_type,
227
+ ...(include_next ? { include_next: true } : {}),
172
228
  });
173
- if (result.error) {
174
- return {
175
- content: [
176
- {
177
- type: "text",
178
- text: `Error (${result.error.code}): ${result.error.message}`,
179
- },
180
- ],
181
- };
229
+ if (result.error)
230
+ return errorResult(result.error);
231
+ const m = result.data;
232
+ let md = `Reply sent to \`${conversation_id}\` on ${m?.platform || "the platform"}.`;
233
+ if (include_next) {
234
+ md +=
235
+ `\n\n` +
236
+ renderNextUnanswered({
237
+ data: result.next || null,
238
+ remaining: result.remaining || 0,
239
+ });
182
240
  }
241
+ return { content: [{ type: "text", text: md }] };
242
+ });
243
+ server.tool("get_next_unanswered", "Get the next inbox item that needs an answer, with the whole conversation and the post it is on, in one call. Serves the longest-waiting item first: the customer's latest DM with no reply after it (Instagram/Facebook DMs within the 24-hour reply window only, since Meta refuses later replies), or a comment/mention nobody replied to and that is not hidden. Replies typed in the native apps count as answered (they are mirrored into the inbox), so a thread a colleague answered on their phone is not served again. Instagram mentions are skipped (no reply path). Only unread items are served by default: to skip one for good, mark_inbox_read it; pass include_read to also see read-but-unanswered items. Looks back 30 days. Loop: get_next_unanswered → draft a reply → reply_to_inbox with include_next: true → repeat until nothing is waiting.", {
244
+ platform: z
245
+ .enum(INBOX_PLATFORMS)
246
+ .optional()
247
+ .describe("Only items from one platform"),
248
+ type: z
249
+ .enum(["dm", "comment", "mention"])
250
+ .optional()
251
+ .describe("Only items of one type"),
252
+ order: z
253
+ .enum(["oldest", "newest"])
254
+ .optional()
255
+ .describe("oldest (default): the item that has waited longest first; newest: the most recent first"),
256
+ include_read: z
257
+ .boolean()
258
+ .optional()
259
+ .describe("Also serve items that were marked read but never answered"),
260
+ exclude: z
261
+ .string()
262
+ .optional()
263
+ .describe("Comma-separated conversation ids to leave out of this call (a temporary skip; up to 100)"),
264
+ }, { title: "Get Next Unanswered", readOnlyHint: true, destructiveHint: false, openWorldHint: false }, async (params) => {
265
+ const query = {};
266
+ if (params.platform)
267
+ query.platform = params.platform;
268
+ if (params.type)
269
+ query.type = params.type;
270
+ if (params.order)
271
+ query.order = params.order;
272
+ if (params.include_read !== undefined) {
273
+ query.include_read = String(params.include_read);
274
+ }
275
+ if (params.exclude)
276
+ query.exclude = params.exclude;
277
+ const result = await getClient().getNextUnanswered(query);
278
+ if (result.error)
279
+ return errorResult(result.error);
280
+ return {
281
+ content: [{ type: "text", text: renderNextUnanswered(result) }],
282
+ };
283
+ });
284
+ const hideCommentSchema = {
285
+ message_id: z
286
+ .string()
287
+ .describe("The inbox message id of the comment (the #id in the thread)"),
288
+ hide: z
289
+ .boolean()
290
+ .optional()
291
+ .describe("true (default) to hide, false to unhide"),
292
+ };
293
+ const hideComment = async ({ message_id, hide, }) => {
294
+ const result = await getClient().hideInboxMessage(message_id, hide !== false);
295
+ if (result.error)
296
+ return errorResult(result.error);
183
297
  const m = result.data;
298
+ const text = result.note
299
+ ? `Comment \`#${m?.id ?? message_id}\` on ${m?.platform || "the platform"}: ${result.note} Marked as handled — no need to retry it.`
300
+ : `Comment \`#${m?.id ?? message_id}\` on ${m?.platform || "the platform"} is now ${m?.hidden ? "hidden" : "visible"}.`;
184
301
  return {
185
302
  content: [
186
303
  {
187
304
  type: "text",
188
- text: `Reply sent to \`${conversation_id}\`${m?.platform ? ` on ${m.platform}` : ""}.`,
305
+ text,
189
306
  },
190
307
  ],
191
308
  };
192
- });
193
- server.tool("hide_inbox_reply", "Hide (or unhide) a reply someone left on one of the workspace's Threads posts, as the post owner. Threads only for now, and only incoming top-level replies can be hidden (Threads does not allow hiding nested replies). The message keeps its place in the conversation; its `hidden` flag flips. Use the message id shown by get_inbox_conversation. Errors: 400 unsupported_platform (not an incoming Threads reply), 400 not_hideable (nested reply, or Threads refused), 401 reauth_required (the Threads connection lacks the reply permission; reconnect Threads under Settings → Organisation → Workspaces). Threads inbox features are currently rolling out (disabled on production until Meta approves the permission). Requires the inbox:write scope.", {
309
+ };
310
+ server.tool("hide_inbox_comment", "Hide (or unhide) a comment someone left on one of the workspace's posts, on the platform, as the post owner. Facebook, Instagram, TikTok, YouTube and Threads comments. On YouTube, hide sets the comment to 'rejected' (removed from public view together with its replies) and unhide publishes it again. Takes the message id (the #id shown by get_inbox_conversation and get_next_unanswered), not the conversation id. A hidden comment no longer counts as unanswered. If the comment was already deleted on the platform (its author removed it, etc.), this still succeeds — the response says so — and the item is marked handled; do not retry it. A 403 reconnect_required means the account was connected without the comment-moderation permission and must be reconnected in the dashboard; relay that to the user rather than retrying.", hideCommentSchema, { title: "Hide Inbox Comment", readOnlyHint: false, destructiveHint: false, openWorldHint: true }, hideComment);
311
+ // Older name from 1.28.0 (when hiding was Threads-only). Kept so existing
312
+ // setups keep working; same call, same platforms as hide_inbox_comment.
313
+ 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);
314
+ 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.", {
194
315
  message_id: z
195
316
  .string()
196
- .describe("The inbox message id of the reply (from get_inbox_conversation)"),
197
- hide: z
198
- .boolean()
199
- .optional()
200
- .describe("true (default) hides the reply; false unhides it"),
201
- }, async ({ message_id, hide }) => {
202
- const result = await getClient().hideInboxMessage(message_id, hide ?? true);
203
- if (result.error) {
204
- return {
205
- content: [
206
- {
207
- type: "text",
208
- text: `Error (${result.error.code}): ${result.error.message}`,
209
- },
210
- ],
211
- };
212
- }
213
- const hidden = result.data?.hidden === true;
317
+ .describe("The inbox message id of the comment (the #id in the thread)"),
318
+ }, { title: "Delete Inbox Comment", readOnlyHint: false, destructiveHint: true, openWorldHint: true }, async ({ message_id }) => {
319
+ const result = await getClient().deleteInboxMessage(message_id);
320
+ if (result.error)
321
+ return errorResult(result.error);
322
+ const d = result.data;
323
+ const replies = Array.isArray(d?.removed_reply_ids)
324
+ ? d.removed_reply_ids.length
325
+ : 0;
214
326
  return {
215
327
  content: [
216
328
  {
217
329
  type: "text",
218
- text: hidden
219
- ? `Reply \`${message_id}\` is now hidden on Threads.`
220
- : `Reply \`${message_id}\` is now visible on Threads again.`,
330
+ text: `Comment \`#${d?.id ?? message_id}\` deleted${replies
331
+ ? ` together with ${replies} repl${replies === 1 ? "y" : "ies"}`
332
+ : ""}.`,
221
333
  },
222
334
  ],
223
335
  };
@@ -72,7 +72,7 @@ When the user provides an image in the conversation (not a URL), use base64_data
72
72
  filename: z.string().optional().describe("Optional filename for the uploaded media"),
73
73
  name: z.string().optional().describe('Human-readable label so you can find this asset by name later instead of re-uploading it, e.g. "pp-play5get50". Strongly recommended on every upload.'),
74
74
  folder: z.string().optional().describe('Optional folder name to file this asset under (created at the top level if it does not exist), e.g. "win-graphics". See list_folders.'),
75
- }, { title: "Upload Media", readOnlyHint: false, destructiveHint: false, openWorldHint: false }, async (params) => {
75
+ }, { title: "Upload Media", readOnlyHint: false, destructiveHint: false, openWorldHint: true }, async (params) => {
76
76
  let result;
77
77
  if (params.base64_data && params.mime_type) {
78
78
  result = await getClient().uploadMediaFromBase64({
@@ -159,7 +159,7 @@ Provide ONE of: a public 'url' (its size/type is read via a HEAD request), an ex
159
159
  media_id: z.string().optional().describe("Id of an already-uploaded library item to check."),
160
160
  size_bytes: z.number().optional().describe("File size in bytes (use with mime when you already know them)."),
161
161
  mime: z.string().optional().describe("MIME type, e.g. 'video/mp4' (use with size_bytes)."),
162
- }, { title: "Check Media Compatibility", readOnlyHint: true, destructiveHint: false, openWorldHint: false }, async (params) => {
162
+ }, { title: "Check Media Compatibility", readOnlyHint: true, destructiveHint: false, openWorldHint: true }, async (params) => {
163
163
  if (!params.url && !params.media_id && !(params.size_bytes && params.mime)) {
164
164
  return {
165
165
  content: [{ type: "text", text: "Error: Provide one of 'url', 'media_id', or 'size_bytes' + 'mime'." }],
@@ -756,7 +756,7 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
756
756
  thread_parts: z.array(MASTODON_THREAD_PART).min(2).max(25).optional().describe("Publish as a chained Mastodon thread instead of a single status. Provide 2–25 parts; each is posted in order as a native reply to the previous status (in_reply_to_id). Attach media to any part via media_ids (from upload_media) or media_urls — max 4 per part. For a single status, omit thread_parts and use content."),
757
757
  }).optional().describe("Mastodon options"),
758
758
  threads: z.object({
759
- 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 (disabled in this environment). On update_post, pass null to remove the tag."),
759
+ 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."),
760
760
  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."),
761
761
  }).optional().describe("Threads (Meta) options: thread mode via `thread_parts`, location tag via `location_id`."),
762
762
  google_business: GOOGLE_BUSINESS_OPTIONS,
@@ -913,11 +913,11 @@ Do NOT call without required media — it will fail.`, {
913
913
  thread_parts: z.array(MASTODON_THREAD_PART).min(2).max(25).optional().describe("Publish as a chained Mastodon thread (2–25 parts). Each is posted in order as a native reply (in_reply_to_id). Attach media to any part via media_ids or media_urls — max 4 per part."),
914
914
  }).optional().describe("Mastodon options"),
915
915
  threads: z.object({
916
- 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 (disabled in this environment). On update_post, pass null to remove the tag."),
916
+ 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."),
917
917
  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."),
918
918
  }).optional().describe("Threads (Meta) options: thread mode via `thread_parts`, location tag via `location_id`."),
919
919
  google_business: GOOGLE_BUSINESS_OPTIONS,
920
- }, { title: "Create and Publish Post", readOnlyHint: false, destructiveHint: false, openWorldHint: true }, async (params) => {
920
+ }, { title: "Create and Publish Post", readOnlyHint: false, destructiveHint: true, openWorldHint: true }, async (params) => {
921
921
  const result = await getClient().createAndPublishPost({ ...params, source: "mcp" });
922
922
  if (result.error) {
923
923
  return {
@@ -1027,7 +1027,7 @@ Do NOT call without required media — it will fail.`, {
1027
1027
  thread_parts: z.array(MASTODON_THREAD_PART).min(2).max(25).nullable().optional().describe("Replace the Mastodon thread shape on this post. Pass an array (2–25 parts) to update/create the thread (attach media to any part via media_ids or media_urls; max 4 per part), or `null` to revert to single-status mode."),
1028
1028
  }).optional().describe("Mastodon options"),
1029
1029
  threads: z.object({
1030
- 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 (disabled in this environment). On update_post, pass null to remove the tag."),
1030
+ 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."),
1031
1031
  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."),
1032
1032
  }).optional().describe("Threads (Meta) options: thread mode via `thread_parts`, location tag via `location_id`."),
1033
1033
  google_business: GOOGLE_BUSINESS_OPTIONS,
@@ -1059,7 +1059,7 @@ Do NOT call without required media — it will fail.`, {
1059
1059
  content: [{ type: "text", text: md }],
1060
1060
  };
1061
1061
  });
1062
- server.tool("delete_post", "Delete a post by ID. This action cannot be undone.", {
1062
+ server.tool("delete_post", "Delete a post by ID from OmniSocials only: it leaves the calendar, the posts list and the analytics. This never deletes the live post on a platform, so a published post stays live everywhere it went; the user must delete it in the platform's own app or via \"Delete from [platform]\" in the OmniSocials dashboard. This action cannot be undone.", {
1063
1063
  id: z.string().describe("The post ID to delete"),
1064
1064
  }, { title: "Delete Post", readOnlyHint: false, destructiveHint: true, openWorldHint: false }, async ({ id }) => {
1065
1065
  const result = await getClient().deletePost(id);
@@ -1071,7 +1071,7 @@ Do NOT call without required media — it will fail.`, {
1071
1071
 
1072
1072
  IMPORTANT: Before publishing, verify the post has all required media. If publishing a story or reel, ensure media was attached when the post was created/updated. If it's missing, use update_post to add media first, or inform the user.`, {
1073
1073
  id: z.string().describe("The post ID to publish"),
1074
- }, { title: "Publish Post", readOnlyHint: false, destructiveHint: false, openWorldHint: true }, async ({ id }) => {
1074
+ }, { title: "Publish Post", readOnlyHint: false, destructiveHint: true, openWorldHint: true }, async ({ id }) => {
1075
1075
  const result = await getClient().publishPost(id);
1076
1076
  if (result.error) {
1077
1077
  return {
@@ -1097,7 +1097,7 @@ IMPORTANT: Before publishing, verify the post has all required media. If publish
1097
1097
 
1098
1098
  The retry runs asynchronously (usually within a few minutes). Poll \`get_post\` afterwards: on success the status becomes \`published\` and \`published_urls\` gains the platform's live URL; on another failure the platform's entry reappears under errors. Each platform can be retried at most 3 times — after that, recreate the post. Note \`publish_post\` refuses failed posts; this is the tool for them.`, {
1099
1099
  id: z.string().describe("The failed or partially failed post ID to retry"),
1100
- }, { title: "Retry Failed Post", readOnlyHint: false, destructiveHint: false, openWorldHint: true }, async ({ id }) => {
1100
+ }, { title: "Retry Failed Post", readOnlyHint: false, destructiveHint: true, openWorldHint: true }, async ({ id }) => {
1101
1101
  const result = await getClient().retryPost(id);
1102
1102
  if (result.error) {
1103
1103
  return {
@@ -1161,7 +1161,7 @@ IMPORTANT: this only succeeds if the connected user is a listed approver for the
1161
1161
 
1162
1162
  If this is the workflow's last step, the post is finalized immediately: it moves to \`scheduled\` (future time) or \`posting\` (due now/in the past). If steps remain, the post stays \`in_approval\` and the next step's approvers are notified — call this again once they've approved, or use \`reject_post\` to stop the workflow instead.`, {
1163
1163
  id: z.string().describe("The post ID to approve (must have approval_status 'pending')"),
1164
- }, { title: "Approve Post", readOnlyHint: false, destructiveHint: false, openWorldHint: true }, async ({ id }) => {
1164
+ }, { title: "Approve Post", readOnlyHint: false, destructiveHint: true, openWorldHint: true }, async ({ id }) => {
1165
1165
  const result = await getClient().approvePost(id);
1166
1166
  if (result.error) {
1167
1167
  return {
@@ -1195,7 +1195,7 @@ IMPORTANT: this only succeeds if the connected user is a listed approver for the
1195
1195
  .max(2000)
1196
1196
  .optional()
1197
1197
  .describe("Optional reason for the rejection, shown to the requester and other approvers."),
1198
- }, { title: "Reject Post", readOnlyHint: false, destructiveHint: false, openWorldHint: true }, async ({ id, comment }) => {
1198
+ }, { title: "Reject Post", readOnlyHint: false, destructiveHint: false, openWorldHint: false }, async ({ id, comment }) => {
1199
1199
  const result = await getClient().rejectPost(id, comment);
1200
1200
  if (result.error) {
1201
1201
  return {
@@ -1223,7 +1223,7 @@ Flow (Threads): same, but pass the \`id\` as \`threads.location_id\`. Threads ca
1223
1223
  Notes:
1224
1224
  - Use a SPECIFIC venue name. Searching a broad brand ("Starbucks") returns individual store locations, not the brand page.
1225
1225
  - Instagram: if the user already gives you a numeric Facebook Place ID, you can pass it straight to create_post as \`location_id\`; it's validated at publish time and an invalid one returns a clear error. If results come back empty with a permission note, the workspace's Facebook app can't search arbitrary places; tell the user to use their own business Page's ID.
1226
- - Threads: needs the workspace's Threads connection to have the location permission; if the result says to reconnect Threads, tell the user to reconnect it under Settings → Organisation → Workspaces. Threads location tagging is currently rolling out, so the search can also answer that it is not available yet.`, {
1226
+ - Threads: needs the workspace's Threads connection to have the location permission; if the result says to reconnect Threads, tell the user to reconnect it under Settings → Organisation → Workspaces. Threads location tagging is currently rolling out (Meta has not approved the permission yet), so the search can also answer that it is not available yet.`, {
1227
1227
  query: z
1228
1228
  .string()
1229
1229
  .optional()
package/build/types.d.ts CHANGED
@@ -19,6 +19,9 @@ export interface ApiResponse<T = unknown> {
19
19
  };
20
20
  conversation_id?: string;
21
21
  marked_read?: number;
22
+ next?: InboxNextUnanswered | null;
23
+ remaining?: number;
24
+ note?: string;
22
25
  }
23
26
  export interface InboxParticipant {
24
27
  id: string;
@@ -26,6 +29,16 @@ export interface InboxParticipant {
26
29
  username: string;
27
30
  profile_picture: string | null;
28
31
  }
32
+ /** The post a comment/mention thread belongs to. `url` is the public link to
33
+ * the post and `media_type` the platform's own label (IMAGE, VIDEO,
34
+ * CAROUSEL_ALBUM, ...); both nullable. null on DM conversations. */
35
+ export interface InboxPostContext {
36
+ id: string | null;
37
+ caption: string | null;
38
+ thumbnail: string | null;
39
+ url: string | null;
40
+ media_type: string | null;
41
+ }
29
42
  export interface InboxConversation {
30
43
  conversation_id: string;
31
44
  platform: "instagram" | "facebook" | "linkedin" | "tiktok" | "x" | "youtube" | "threads";
@@ -39,11 +52,7 @@ export interface InboxConversation {
39
52
  timestamp: string;
40
53
  is_read: boolean;
41
54
  };
42
- post: {
43
- id: string | null;
44
- caption: string | null;
45
- thumbnail: string | null;
46
- } | null;
55
+ post: InboxPostContext | null;
47
56
  }
48
57
  export interface InboxMessage {
49
58
  id: string;
@@ -57,8 +66,9 @@ export interface InboxMessage {
57
66
  is_replied: boolean;
58
67
  reaction: string | null;
59
68
  parent_comment_id: string | null;
60
- /** Threads replies only: true when the reply is hidden on Threads (flip it
61
- * with hide_inbox_reply). null for every other platform. */
69
+ /** Comments on every comment platform (Facebook, Instagram, TikTok,
70
+ * YouTube, Threads): true when the comment is hidden on the platform (flip
71
+ * it with hide_inbox_comment). null for DMs. */
62
72
  hidden?: boolean | null;
63
73
  /** Link to the reply or mentioning post on the platform, when known. */
64
74
  permalink?: string | null;
@@ -70,11 +80,24 @@ export interface InboxMessage {
70
80
  type: "image" | "video" | "audio" | "file";
71
81
  } | null;
72
82
  sender: InboxParticipant;
73
- post: {
74
- id: string | null;
75
- caption: string | null;
76
- thumbnail: string | null;
77
- } | null;
83
+ post: InboxPostContext | null;
84
+ }
85
+ /** What GET /inbox/next serves (and what `reply_to_inbox` returns as `next`
86
+ * when `include_next` is set). `message` is the unanswered incoming item: its
87
+ * `id` is what hide/delete take, its `conversation_id` is what reply takes.
88
+ * `messages` is the thread oldest first (the most recent 50, plus the target
89
+ * and its parent when they fall outside that window). */
90
+ export interface InboxNextUnanswered {
91
+ conversation: InboxConversation;
92
+ message: InboxMessage;
93
+ messages: InboxMessage[];
94
+ }
95
+ /** Response of DELETE /inbox/messages/:id. Replies under the comment are
96
+ * removed with it; their inbox ids are listed in `removed_reply_ids`. */
97
+ export interface InboxDeleteResult {
98
+ id: string;
99
+ conversation_id: string;
100
+ removed_reply_ids: string[];
78
101
  }
79
102
  /** One media entry on a post — a plain URL string, or an object carrying the
80
103
  * url plus an optional accessibility description (alt text). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnisocials/mcp-server",
3
- "version": "1.33.0",
3
+ "version": "1.34.1",
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",