@omnisocials/mcp-server 1.11.0 → 1.13.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/build/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ApiResponse, MediaItem, PdfUploadResult } from "./types.js";
1
+ import type { ApiResponse, MediaItem, PdfUploadResult, InboxConversation, InboxMessage } from "./types.js";
2
2
  export declare function fetchImageAsBase64(url: string): Promise<{
3
3
  data: string;
4
4
  mimeType: string;
@@ -187,6 +187,7 @@ export declare class OmniSocialsClient {
187
187
  publishPost(id: string): Promise<ApiResponse<unknown>>;
188
188
  searchLocations(query: string): Promise<ApiResponse<unknown>>;
189
189
  validateLocation(id: string): Promise<ApiResponse<unknown>>;
190
+ searchInstagramAudio(query?: string, type?: "music" | "original_sound"): Promise<ApiResponse<unknown>>;
190
191
  listMedia(params?: {
191
192
  limit?: string;
192
193
  offset?: string;
@@ -263,4 +264,21 @@ export declare class OmniSocialsClient {
263
264
  is_active?: boolean;
264
265
  }): Promise<ApiResponse<unknown>>;
265
266
  rotateWebhookSecret(id: string): Promise<ApiResponse<unknown>>;
267
+ listInboxConversations(params?: {
268
+ platform?: string;
269
+ type?: string;
270
+ unread?: string;
271
+ limit?: string;
272
+ cursor?: string;
273
+ }): Promise<ApiResponse<InboxConversation[]>>;
274
+ getInboxMessages(conversationId: string, params?: {
275
+ limit?: string;
276
+ cursor?: string;
277
+ }): Promise<ApiResponse<InboxMessage[]>>;
278
+ markInboxRead(conversationId: string): Promise<ApiResponse<unknown>>;
279
+ replyToInboxConversation(conversationId: string, data: {
280
+ text: string;
281
+ attachment_url?: string;
282
+ attachment_type?: string;
283
+ }): Promise<ApiResponse<InboxMessage>>;
266
284
  }
package/build/client.js CHANGED
@@ -226,6 +226,15 @@ export class OmniSocialsClient {
226
226
  async validateLocation(id) {
227
227
  return this.request("GET", "/locations/validate", undefined, { id });
228
228
  }
229
+ // Audio (Instagram Reel music)
230
+ async searchInstagramAudio(query, type) {
231
+ const params = {};
232
+ if (query)
233
+ params.q = query;
234
+ if (type)
235
+ params.type = type;
236
+ return this.request("GET", "/audio/search", undefined, params);
237
+ }
229
238
  // Media
230
239
  async listMedia(params) {
231
240
  return this.request("GET", "/media", undefined, params);
@@ -309,4 +318,18 @@ export class OmniSocialsClient {
309
318
  async rotateWebhookSecret(id) {
310
319
  return this.request("POST", `/webhooks/${id}/rotate-secret`);
311
320
  }
321
+ // Inbox — conversation ids can contain ":" and "()" (LinkedIn URNs), so any
322
+ // path segment built from one must be URL-encoded.
323
+ async listInboxConversations(params) {
324
+ return this.request("GET", "/inbox/conversations", undefined, params);
325
+ }
326
+ async getInboxMessages(conversationId, params) {
327
+ return this.request("GET", `/inbox/conversations/${encodeURIComponent(conversationId)}/messages`, undefined, params);
328
+ }
329
+ async markInboxRead(conversationId) {
330
+ return this.request("POST", `/inbox/conversations/${encodeURIComponent(conversationId)}/read`);
331
+ }
332
+ async replyToInboxConversation(conversationId, data) {
333
+ return this.request("POST", `/inbox/conversations/${encodeURIComponent(conversationId)}/reply`, data);
334
+ }
312
335
  }
package/build/index.js CHANGED
@@ -8,6 +8,7 @@ import { registerMediaTools } from "./tools/media.js";
8
8
  import { registerAccountTools } from "./tools/accounts.js";
9
9
  import { registerAnalyticsTools } from "./tools/analytics.js";
10
10
  import { registerWebhookTools } from "./tools/webhooks.js";
11
+ import { registerInboxTools } from "./tools/inbox.js";
11
12
  import { registerWorkspaceTools } from "./tools/workspaces.js";
12
13
  const apiKeyEnv = process.env.OMNISOCIALS_API_KEY;
13
14
  if (!apiKeyEnv) {
@@ -27,7 +28,7 @@ const sessionState = { activeIndex: 0 };
27
28
  const getActiveClient = () => workspaceClients[sessionState.activeIndex].client;
28
29
  const server = new McpServer({
29
30
  name: "OmniSocials",
30
- version: "1.11.0",
31
+ version: "1.13.0",
31
32
  });
32
33
  // Register all tools - pass getter function so tools always use the active workspace's client
33
34
  registerPostTools(server, getActiveClient);
@@ -35,6 +36,7 @@ registerMediaTools(server, getActiveClient);
35
36
  registerAccountTools(server, getActiveClient);
36
37
  registerAnalyticsTools(server, getActiveClient);
37
38
  registerWebhookTools(server, getActiveClient);
39
+ registerInboxTools(server, getActiveClient);
38
40
  registerWorkspaceTools(server, workspaceClients, sessionState);
39
41
  // Register prompts
40
42
  server.prompt("weekly-report", "Generate a weekly social media performance report", {}, async () => ({
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { OmniSocialsClient } from "../client.js";
3
+ export declare function registerInboxTools(server: McpServer, getClient: () => OmniSocialsClient): void;
@@ -0,0 +1,166 @@
1
+ import { z } from "zod";
2
+ import { formatDateTime } from "../client.js";
3
+ const PLATFORM_EMOJI = {
4
+ instagram: "📸",
5
+ facebook: "📘",
6
+ linkedin: "💼",
7
+ };
8
+ export function registerInboxTools(server, getClient) {
9
+ server.tool("list_inbox_conversations", "List social inbox conversations (Instagram/Facebook DMs, comments, mentions, and LinkedIn company-page comments/mentions), newest activity first. Cursor-paginated: pass the returned cursor to get the next page.", {
10
+ platform: z
11
+ .enum(["instagram", "facebook", "linkedin"])
12
+ .optional()
13
+ .describe("Filter to one platform"),
14
+ type: z
15
+ .enum(["dm", "comment", "mention"])
16
+ .optional()
17
+ .describe("Filter to one conversation type"),
18
+ unread: z
19
+ .boolean()
20
+ .optional()
21
+ .describe("Only conversations with unread incoming messages"),
22
+ limit: z
23
+ .number()
24
+ .optional()
25
+ .describe("Max conversations per page (1-100, default 25)"),
26
+ cursor: z
27
+ .string()
28
+ .optional()
29
+ .describe("Pagination cursor from a previous response"),
30
+ }, async (params) => {
31
+ const query = {};
32
+ if (params.platform)
33
+ query.platform = params.platform;
34
+ if (params.type)
35
+ query.type = params.type;
36
+ if (params.unread !== undefined)
37
+ query.unread = String(params.unread);
38
+ if (params.limit !== undefined)
39
+ query.limit = String(params.limit);
40
+ if (params.cursor)
41
+ query.cursor = params.cursor;
42
+ const result = await getClient().listInboxConversations(query);
43
+ if (result.error) {
44
+ return {
45
+ content: [
46
+ {
47
+ type: "text",
48
+ text: `Error (${result.error.code}): ${result.error.message}`,
49
+ },
50
+ ],
51
+ };
52
+ }
53
+ const convos = result.data || [];
54
+ if (!convos.length) {
55
+ return {
56
+ content: [{ type: "text", text: "No conversations found." }],
57
+ };
58
+ }
59
+ let md = `## Inbox Conversations (${convos.length})\n\n`;
60
+ md += `| Platform | Type | From | Unread | Latest message | Conversation ID |\n`;
61
+ md += `|----------|------|------|--------|----------------|----------------|\n`;
62
+ for (const c of convos) {
63
+ const who = c.participant?.name || c.participant?.username || "Unknown";
64
+ const text = (c.last_message?.text || "").replace(/\n/g, " ").slice(0, 40);
65
+ md += `| ${PLATFORM_EMOJI[c.platform] || ""} ${c.platform} | ${c.type} | ${who} | ${c.unread_count || 0} | ${text} | \`${c.conversation_id}\` |\n`;
66
+ }
67
+ if (result.pagination?.next_cursor) {
68
+ md += `\n_More available — call list_inbox_conversations again with cursor "${result.pagination.next_cursor}"._`;
69
+ }
70
+ return { content: [{ type: "text", text: md }] };
71
+ });
72
+ server.tool("get_inbox_conversation", "Get the full message history of one inbox conversation, oldest first. Use the conversation_id from list_inbox_conversations.", {
73
+ conversation_id: z.string().describe("The conversation ID to fetch"),
74
+ limit: z
75
+ .number()
76
+ .optional()
77
+ .describe("Max messages per page (1-100, default 50)"),
78
+ cursor: z.string().optional().describe("Pagination cursor"),
79
+ }, async ({ conversation_id, limit, cursor }) => {
80
+ const query = {};
81
+ if (limit !== undefined)
82
+ query.limit = String(limit);
83
+ if (cursor)
84
+ query.cursor = cursor;
85
+ const result = await getClient().getInboxMessages(conversation_id, query);
86
+ if (result.error) {
87
+ return {
88
+ content: [
89
+ {
90
+ type: "text",
91
+ text: `Error (${result.error.code}): ${result.error.message}`,
92
+ },
93
+ ],
94
+ };
95
+ }
96
+ const msgs = result.data || [];
97
+ if (!msgs.length) {
98
+ return {
99
+ content: [
100
+ { type: "text", text: "No messages in this conversation." },
101
+ ],
102
+ };
103
+ }
104
+ let md = `## Conversation \`${conversation_id}\`\n\n`;
105
+ for (const m of msgs) {
106
+ const who = m.direction === "outgoing"
107
+ ? "→ You"
108
+ : `← ${m.sender?.name || m.sender?.username || "Them"}`;
109
+ md += `**${who}** _(${formatDateTime(m.timestamp)})_\n${m.text || ""}\n\n`;
110
+ }
111
+ if (result.pagination?.next_cursor) {
112
+ md += `_More messages — call again with cursor "${result.pagination.next_cursor}"._`;
113
+ }
114
+ return { content: [{ type: "text", text: md }] };
115
+ });
116
+ server.tool("mark_inbox_read", "Mark all incoming messages in a conversation as read.", {
117
+ conversation_id: z.string().describe("The conversation ID to mark read"),
118
+ }, async ({ conversation_id }) => {
119
+ const result = await getClient().markInboxRead(conversation_id);
120
+ if (result.error) {
121
+ return {
122
+ content: [
123
+ {
124
+ type: "text",
125
+ text: `Error (${result.error.code}): ${result.error.message}`,
126
+ },
127
+ ],
128
+ };
129
+ }
130
+ return {
131
+ content: [
132
+ {
133
+ type: "text",
134
+ text: `Marked ${result.marked_read ?? 0} message(s) as read in \`${conversation_id}\`.`,
135
+ },
136
+ ],
137
+ };
138
+ });
139
+ server.tool("reply_to_inbox", "Reply to an existing inbox conversation (DM, comment, or mention) on Instagram, Facebook, or LinkedIn. You can only reply to conversations that already exist. Direct-message replies must be within the platform's 24-hour messaging window. Each workspace can send up to 1,000 replies per day.", {
140
+ conversation_id: z.string().describe("The conversation ID to reply to"),
141
+ text: z.string().describe("The reply text (max 2000 characters)"),
142
+ }, async ({ conversation_id, text }) => {
143
+ const result = await getClient().replyToInboxConversation(conversation_id, {
144
+ text,
145
+ });
146
+ if (result.error) {
147
+ return {
148
+ content: [
149
+ {
150
+ type: "text",
151
+ text: `Error (${result.error.code}): ${result.error.message}`,
152
+ },
153
+ ],
154
+ };
155
+ }
156
+ const m = result.data;
157
+ return {
158
+ content: [
159
+ {
160
+ type: "text",
161
+ text: `Reply sent to \`${conversation_id}\`${m?.platform ? ` on ${m.platform}` : ""}.`,
162
+ },
163
+ ],
164
+ };
165
+ });
166
+ }
@@ -140,7 +140,7 @@ export function registerPostTools(server, getClient) {
140
140
  content: [{ type: "text", text: md }],
141
141
  };
142
142
  });
143
- server.tool("get_post", "Get details of a specific post by ID — content, channels, media (with URLs), first comment, Instagram collaborators/user tags/location, 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.", {
143
+ server.tool("get_post", "Get details of a specific post by ID — content, channels, media (with URLs), first comment, Instagram collaborators/user tags/location/Trial Reel state, 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.", {
144
144
  id: z.string().describe("The post ID"),
145
145
  }, async ({ id }) => {
146
146
  const result = await getClient().getPost(id);
@@ -290,6 +290,16 @@ export function registerPostTools(server, getClient) {
290
290
  if (firstCommentRows.length) {
291
291
  md += `\n\n### First Comments\n\n${firstCommentRows.join("\n")}\n`;
292
292
  }
293
+ // Trial Reel state: nested under the instagram options object by the
294
+ // API. Only rendered when enabled, so batch-scheduled Trial Reels can be
295
+ // verified without opening the dashboard.
296
+ const igOpts = p.instagram;
297
+ if (igOpts && typeof igOpts === "object" && igOpts.is_trial_reel === true) {
298
+ const strategy = igOpts.trial_graduation_strategy === "SS_PERFORMANCE"
299
+ ? "automatic (Instagram shares it with followers if it performs well)"
300
+ : "manual (graduate it yourself in the Instagram app)";
301
+ md += `\n\n**Instagram Trial Reel:** enabled — shown to non-followers first; graduation: ${strategy}\n`;
302
+ }
293
303
  return {
294
304
  content: [{ type: "text", text: md }],
295
305
  };
@@ -385,7 +395,12 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
385
395
  thumbnail_type: z.enum(["from-video", "from-library"]).optional(),
386
396
  thumb_offset: z.number().optional(),
387
397
  cover_url: z.string().optional(),
398
+ audio_id: z.string().optional().describe("Reels only. Licensed music track to attach — a numeric audio ID from search_instagram_audio. Requires a Facebook account connected to the workspace whose Page links this Instagram account."),
399
+ audio_volume: z.number().int().min(0).max(100).optional().describe("Volume of the attached music track, 0-100 (default 100). Only used with audio_id."),
400
+ video_volume: z.number().int().min(0).max(100).optional().describe("Volume of the video's own audio, 0-100 (default 100). Set 0 for a music-only Reel. Only used with audio_id."),
388
401
  first_comment: z.string().max(2200).optional().describe("Text auto-posted as the first comment on the post/reel right after it publishes. Common for keeping hashtags out of the caption. Not posted for Stories."),
402
+ is_trial_reel: z.boolean().optional().describe("Publish the reel as an Instagram Trial Reel — shown to non-followers first to test performance before (optionally) graduating to everyone. ONLY set this when the user explicitly asks for a Trial Reel; never enable it by default. NOT available on every account: Instagram requires roughly 1,000+ followers and enables the feature per account (the user sees a 'Trial' toggle when creating a reel in the Instagram app). Ineligible accounts fail at publish time with a clear per-platform error. Reels only."),
403
+ trial_graduation_strategy: z.enum(["MANUAL", "SS_PERFORMANCE"]).optional().describe("How a Trial Reel graduates to all followers. MANUAL (default): the user decides in the Instagram app. SS_PERFORMANCE: Instagram shares it with followers automatically if it performs well. Only used with is_trial_reel."),
389
404
  }).optional().describe("Instagram options"),
390
405
  facebook: FACEBOOK_OPTIONS,
391
406
  linkedin: LINKEDIN_OPTIONS,
@@ -393,10 +408,12 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
393
408
  tiktok: z.object({
394
409
  privacy_level: z.enum(["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR", "SELF_ONLY"]).optional(),
395
410
  disable_comment: z.boolean().optional(),
396
- disable_duet: z.boolean().optional(),
397
- disable_stitch: z.boolean().optional(),
398
- is_aigc: z.boolean().optional(),
399
- brand_content_toggle: z.boolean().optional(),
411
+ disable_duet: z.boolean().optional().describe("Reels only. Disable Duet on the video."),
412
+ disable_stitch: z.boolean().optional().describe("Reels only. Disable Stitch on the video."),
413
+ video_cover_timestamp_ms: z.number().optional().describe("Reels only. Timestamp (ms) of the video frame to use as the cover."),
414
+ is_aigc: z.boolean().optional().describe("Mark as AI-generated content."),
415
+ brand_content_toggle: z.boolean().optional().describe("Paid partnership disclosure (promotes a third-party brand)."),
416
+ brand_organic_toggle: z.boolean().optional().describe("Your own brand disclosure (promotes your own business)."),
400
417
  auto_add_music: z.boolean().optional().describe("Photo carousels only. When true, TikTok auto-selects a soundtrack. Defaults to false to avoid unsuitable tracks."),
401
418
  }).optional().describe("TikTok options"),
402
419
  x: z.object({
@@ -544,13 +561,26 @@ Do NOT call without required media — it will fail.`, {
544
561
  first_comment: z.string().max(10000).optional().describe("Text auto-posted as the first comment on the video right after it publishes. The video must have comments enabled."),
545
562
  }).optional().describe("YouTube Shorts options. Only applies when type is 'reel' and youtube is among the selected channels."),
546
563
  instagram: z.object({
564
+ audio_id: z.string().optional().describe("Reels only. Licensed music track to attach — a numeric audio ID from search_instagram_audio. Requires a Facebook account connected to the workspace whose Page links this Instagram account."),
565
+ audio_volume: z.number().int().min(0).max(100).optional().describe("Volume of the attached music track, 0-100 (default 100). Only used with audio_id."),
566
+ video_volume: z.number().int().min(0).max(100).optional().describe("Volume of the video's own audio, 0-100 (default 100). Set 0 for a music-only Reel. Only used with audio_id."),
547
567
  first_comment: z.string().max(2200).optional().describe("Text auto-posted as the first comment on the post/reel right after it publishes. Common for keeping hashtags out of the caption. Not posted for Stories."),
568
+ is_trial_reel: z.boolean().optional().describe("Publish the reel as an Instagram Trial Reel — shown to non-followers first to test performance before (optionally) graduating to everyone. ONLY set this when the user explicitly asks for a Trial Reel; never enable it by default. NOT available on every account: Instagram requires roughly 1,000+ followers and enables the feature per account (the user sees a 'Trial' toggle when creating a reel in the Instagram app). Ineligible accounts fail at publish time with a clear per-platform error. Reels only."),
569
+ trial_graduation_strategy: z.enum(["MANUAL", "SS_PERFORMANCE"]).optional().describe("How a Trial Reel graduates to all followers. MANUAL (default): the user decides in the Instagram app. SS_PERFORMANCE: Instagram shares it with followers automatically if it performs well. Only used with is_trial_reel."),
548
570
  }).optional().describe("Instagram options"),
549
571
  facebook: FACEBOOK_OPTIONS,
550
572
  linkedin: LINKEDIN_OPTIONS,
551
573
  linkedin_page: LINKEDIN_PAGE_OPTIONS,
552
574
  tiktok: z.object({
553
575
  privacy_level: z.enum(["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR", "SELF_ONLY"]).optional(),
576
+ disable_comment: z.boolean().optional(),
577
+ disable_duet: z.boolean().optional().describe("Reels only. Disable Duet on the video."),
578
+ disable_stitch: z.boolean().optional().describe("Reels only. Disable Stitch on the video."),
579
+ video_cover_timestamp_ms: z.number().optional().describe("Reels only. Timestamp (ms) of the video frame to use as the cover."),
580
+ is_aigc: z.boolean().optional().describe("Mark as AI-generated content."),
581
+ brand_content_toggle: z.boolean().optional().describe("Paid partnership disclosure (promotes a third-party brand)."),
582
+ brand_organic_toggle: z.boolean().optional().describe("Your own brand disclosure (promotes your own business)."),
583
+ auto_add_music: z.boolean().optional().describe("Photo carousels only. When true, TikTok auto-selects a soundtrack. Defaults to false to avoid unsuitable tracks."),
554
584
  }).optional().describe("TikTok options"),
555
585
  x: z.object({
556
586
  reply_settings: z.enum(["", "following", "mentionedUsers"]).optional(),
@@ -675,18 +705,25 @@ Do NOT call without required media — it will fail.`, {
675
705
  thumbnail_type: z.enum(["from-video", "from-library"]).optional(),
676
706
  thumb_offset: z.number().optional(),
677
707
  cover_url: z.string().optional(),
708
+ audio_id: z.string().optional().describe("Reels only. Licensed music track to attach — a numeric audio ID from search_instagram_audio. Requires a Facebook account connected to the workspace whose Page links this Instagram account."),
709
+ audio_volume: z.number().int().min(0).max(100).optional().describe("Volume of the attached music track, 0-100 (default 100). Only used with audio_id."),
710
+ video_volume: z.number().int().min(0).max(100).optional().describe("Volume of the video's own audio, 0-100 (default 100). Set 0 for a music-only Reel. Only used with audio_id."),
678
711
  first_comment: z.string().max(2200).optional().describe("Text auto-posted as the first comment on the post/reel right after it publishes. Pass an empty string to clear a previously set first comment."),
679
- }).optional().describe("Instagram options"),
712
+ is_trial_reel: z.boolean().optional().describe("Publish the reel as an Instagram Trial Reel — shown to non-followers first to test performance before (optionally) graduating to everyone. ONLY set this when the user explicitly asks for a Trial Reel; never enable it by default. NOT available on every account: Instagram requires roughly 1,000+ followers and enables the feature per account. Ineligible accounts fail at publish time with a clear per-platform error. Reels only. Pass false to turn a Trial Reel back into a regular reel."),
713
+ trial_graduation_strategy: z.enum(["MANUAL", "SS_PERFORMANCE"]).optional().describe("How a Trial Reel graduates to all followers. MANUAL (default): the user decides in the Instagram app. SS_PERFORMANCE: Instagram shares it with followers automatically if it performs well. Only used with is_trial_reel."),
714
+ }).optional().describe("Instagram options. MERGED into the post's stored options: omitted keys keep their current value (updating one option cannot clear a Trial Reel flag or reel music configured elsewhere)."),
680
715
  facebook: FACEBOOK_OPTIONS,
681
716
  linkedin: LINKEDIN_OPTIONS,
682
717
  linkedin_page: LINKEDIN_PAGE_OPTIONS,
683
718
  tiktok: z.object({
684
719
  privacy_level: z.enum(["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR", "SELF_ONLY"]).optional(),
685
720
  disable_comment: z.boolean().optional(),
686
- disable_duet: z.boolean().optional(),
687
- disable_stitch: z.boolean().optional(),
688
- is_aigc: z.boolean().optional(),
689
- brand_content_toggle: z.boolean().optional(),
721
+ disable_duet: z.boolean().optional().describe("Reels only. Disable Duet on the video."),
722
+ disable_stitch: z.boolean().optional().describe("Reels only. Disable Stitch on the video."),
723
+ video_cover_timestamp_ms: z.number().optional().describe("Reels only. Timestamp (ms) of the video frame to use as the cover."),
724
+ is_aigc: z.boolean().optional().describe("Mark as AI-generated content."),
725
+ brand_content_toggle: z.boolean().optional().describe("Paid partnership disclosure (promotes a third-party brand)."),
726
+ brand_organic_toggle: z.boolean().optional().describe("Your own brand disclosure (promotes your own business)."),
690
727
  auto_add_music: z.boolean().optional().describe("Photo carousels only. When true, TikTok auto-selects a soundtrack. Defaults to false to avoid unsuitable tracks."),
691
728
  }).optional().describe("TikTok options"),
692
729
  x: z.object({
@@ -812,6 +849,42 @@ Notes:
812
849
  });
813
850
  return { content: [{ type: "text", text: md }] };
814
851
  });
852
+ server.tool("search_instagram_audio", `Search Instagram's licensed music catalog for a track to attach to a REEL. Use this whenever the user wants to post a reel WITH music/a song/a sound (e.g. "post this reel with trending audio", "add that song to it").
853
+
854
+ Flow: call with a song/artist/keyword (or NO query for currently trending audio) → present the options → once the user picks, pass that result's \`audio_id\` as \`instagram.audio_id\` on create_post / create_and_publish_post / update_post. Optionally mix with \`instagram.audio_volume\` / \`instagram.video_volume\` (0-100; set video_volume 0 to fully replace the video's own sound). Instagram Reels only — feed posts, carousels, and Stories can't take music via the API (Meta limitation).
855
+
856
+ Notes: only tracks Meta licenses for third-party publishing appear, so the selection can differ from the Instagram app. Requires the workspace to have a Facebook account connected whose Page is linked to this Instagram account — if results say Facebook is needed, tell the user to connect it under Settings → Organisation → Workspaces.`, {
857
+ query: z
858
+ .string()
859
+ .optional()
860
+ .describe("Song title, artist, or keyword. OMIT for currently trending audio."),
861
+ type: z
862
+ .enum(["music", "original_sound"])
863
+ .optional()
864
+ .describe("Catalog to search: licensed music (default) or original sounds created by Instagram users."),
865
+ }, async ({ query, type }) => {
866
+ const result = await getClient().searchInstagramAudio(query, type);
867
+ const tracks = result?.data || [];
868
+ if (!tracks.length) {
869
+ const why = result?.error ||
870
+ (query
871
+ ? `No tracks found for "${query}". Try another title or artist — only music licensed for third-party publishing appears here.`
872
+ : "No trending audio available right now.");
873
+ return { content: [{ type: "text", text: why }] };
874
+ }
875
+ const fmtDuration = (ms) => {
876
+ if (!ms)
877
+ return "—";
878
+ const s = Math.round(ms / 1000);
879
+ return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
880
+ };
881
+ let md = `## ${query ? `Audio matching "${query}"` : "Trending audio"}\n\nAsk the user which track, then pass its ID as \`instagram.audio_id\` (reels only):\n\n`;
882
+ md += `| # | Title | Artist | Length | audio_id |\n|---|-------|--------|--------|----------|\n`;
883
+ tracks.forEach((t, i) => {
884
+ md += `| ${i + 1} | ${t.title || "—"} | ${t.artist || t.ig_username || "—"} | ${fmtDuration(t.duration_ms)} | \`${t.audio_id}\` |\n`;
885
+ });
886
+ return { content: [{ type: "text", text: md }] };
887
+ });
815
888
  server.tool("get_recent_platform_posts", "Fetch the user's most recent posts straight from their connected platform APIs (Instagram, TikTok, X, YouTube, Facebook, LinkedIn, and more), INCLUDING content published outside OmniSocials. Use this when list_posts is empty — e.g. a brand-new workspace that has not published through OmniSocials yet — so you can still analyze the user's real content. Each post includes normalized `engagement` plus every raw metric the platform reported (Instagram: reach/views/saves/shares from per-post insights). Metrics only appear where the platform exposes them for historical posts (X, TikTok, Bluesky, Mastodon, Instagram, Facebook, YouTube); Threads, Pinterest, and Google Business return captions only. LinkedIn personal profiles can't be listed live (LinkedIn grants apps no such permission), so their results are posts published through OmniSocials with their latest collected stats. Fetched live, so expect a few seconds of latency. Requires the analytics:read scope.", {
816
889
  limit: z
817
890
  .string()
package/build/types.d.ts CHANGED
@@ -4,6 +4,57 @@ export interface ApiResponse<T = unknown> {
4
4
  code: string;
5
5
  message: string;
6
6
  };
7
+ pagination?: {
8
+ next_cursor: string | null;
9
+ has_more: boolean;
10
+ limit: number;
11
+ };
12
+ conversation_id?: string;
13
+ marked_read?: number;
14
+ }
15
+ export interface InboxParticipant {
16
+ id: string;
17
+ name: string;
18
+ username: string;
19
+ profile_picture: string | null;
20
+ }
21
+ export interface InboxConversation {
22
+ conversation_id: string;
23
+ platform: "instagram" | "facebook" | "linkedin";
24
+ type: "dm" | "comment" | "mention";
25
+ participant: InboxParticipant;
26
+ unread_count: number;
27
+ last_message: {
28
+ id: string;
29
+ direction: "incoming" | "outgoing";
30
+ text: string;
31
+ timestamp: string;
32
+ is_read: boolean;
33
+ };
34
+ post: {
35
+ id: string | null;
36
+ caption: string | null;
37
+ thumbnail: string | null;
38
+ } | null;
39
+ }
40
+ export interface InboxMessage {
41
+ id: string;
42
+ conversation_id: string;
43
+ platform: string;
44
+ type: "dm" | "comment" | "mention";
45
+ direction: "incoming" | "outgoing";
46
+ text: string;
47
+ timestamp: string;
48
+ is_read: boolean;
49
+ is_replied: boolean;
50
+ reaction: string | null;
51
+ parent_comment_id: string | null;
52
+ sender: InboxParticipant;
53
+ post: {
54
+ id: string | null;
55
+ caption: string | null;
56
+ thumbnail: string | null;
57
+ } | null;
7
58
  }
8
59
  export interface Post {
9
60
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnisocials/mcp-server",
3
- "version": "1.11.0",
3
+ "version": "1.13.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",