@omnisocials/mcp-server 1.12.0 → 1.13.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
@@ -173,6 +173,17 @@ OmniSocials accepts the following channel IDs in `create_post`, `create_and_publ
173
173
  | `delete_webhook` | Delete a webhook |
174
174
  | `rotate_webhook_secret` | Rotate a webhook's signing secret |
175
175
 
176
+ ### Social Inbox (4 tools)
177
+
178
+ | Tool | Description |
179
+ |------|-------------|
180
+ | `list_inbox_conversations` | List conversations — Instagram/Facebook DMs, comments, mentions, and LinkedIn company-page comments/mentions; filter by platform, type, or unread |
181
+ | `get_inbox_conversation` | Get a conversation's full message history |
182
+ | `mark_inbox_read` | Mark all incoming messages in a conversation as read |
183
+ | `reply_to_inbox` | Reply to a DM, comment, or mention (existing conversations only) |
184
+
185
+ Requires an API key with the opt-in `inbox:read` / `inbox:write` scopes — enable "Social Inbox access" when you create the key.
186
+
176
187
  ## Environment Variables
177
188
 
178
189
  | Variable | Required | Description |
@@ -196,6 +207,50 @@ Full API docs: [docs.omnisocials.com](https://docs.omnisocials.com)
196
207
 
197
208
  ## Changelog
198
209
 
210
+ ### 1.13.1
211
+
212
+ - Docs-only republish to refresh the README on npmjs.com — the changelog below had drifted (it stopped at 1.5.0) and the Social Inbox tools were missing from the tool list. No code changes from 1.13.0.
213
+
214
+ ### 1.13.0
215
+
216
+ - **Added: Social Inbox tools** — `list_inbox_conversations`, `get_inbox_conversation`, `mark_inbox_read`, `reply_to_inbox`. Read and reply to Instagram and Facebook DMs, comments, and mentions, plus LinkedIn company-page comments and mentions (LinkedIn has no API DMs). Cursor-paginated with platform/type/unread filters. Replies work only on existing conversations (no cold outreach); direct-message replies must be within the platform's 24-hour window, and each workspace can send up to 1,000 replies per day. Requires the opt-in `inbox:read` / `inbox:write` scopes (enable "Social Inbox access" when creating the key). Same tools on the companion server (`mcp.omnisocials.com`).
217
+
218
+ ### 1.12.0
219
+
220
+ - **Added: Instagram Trial Reels** — `instagram.is_trial_reel` (plus `trial_graduation_strategy`) on `create_post` / `create_and_publish_post` / `update_post`; the reel is shown to non-followers first. Requires roughly 1,000+ followers and per-account eligibility.
221
+ - **Added: reel music** — `instagram.audio_id` attaches licensed audio to a reel; the new `search_instagram_audio` tool finds tracks and returns their `audio_id`.
222
+ - **Changed:** `update_post` Instagram options now MERGE into the stored options instead of replacing them, so updating one option no longer clears others.
223
+
224
+ ### 1.11.0
225
+
226
+ - **Fixed:** `get_post_analytics` and `get_posts_analytics` now render every metric the platform reports (reach, saves, views, and more) instead of a fixed column set that silently dropped some.
227
+
228
+ ### 1.10.0
229
+
230
+ - **Added:** `get_best_times` — best times to post for the workspace, per platform, from historical engagement.
231
+ - **Added:** YouTube cover support plus media order and thumbnail fixes in `create_post` / `update_post`.
232
+
233
+ ### 1.9.0
234
+
235
+ - Maintenance release aligned with a backend upload-media fix; existing clients benefit automatically once the backend deploys. No MCP tool changes.
236
+
237
+ ### 1.8.0
238
+
239
+ - **Added:** `get_posts_analytics` — the latest per-platform metrics for up to 100 posts in a single (bulk) call.
240
+ - **Added:** upload a PDF as a carousel via `upload_media` — rasterized into one slide per page (max 20); posts as a native swipeable document on LinkedIn and an image carousel elsewhere.
241
+
242
+ ### 1.7.1
243
+
244
+ - **Changed:** clearer guidance for files over 100 MB — base64/direct uploads cap at 100 MB; for up to 1 GB pass a public `url` or use the Library UI. Also fixes a stray " B" in `list_media` size output.
245
+
246
+ ### 1.7.0
247
+
248
+ - **Changed:** `switch_workspace` now switches by workspace **name** (id or list position also accepted); `list_workspaces` leads with names. Fixes posts landing in the wrong workspace when the list order changed.
249
+
250
+ ### 1.6.0
251
+
252
+ - **Added:** media library names, search, and folders — `name` / `folder` on `upload_media`, `search` / `folder_id` on `list_media`, plus `update_media`, `list_folders`, and `create_folder`.
253
+
199
254
  ### 1.5.0
200
255
 
201
256
  - **Added:** Attach uploaded media to any tweet in an X thread. `x.thread_parts[]` now accepts `media_ids` (the same numeric Library IDs returned by `upload_media`) on any part, first tweet or reply, alongside the existing `media_urls`. Combined cap is 4 media per part. This makes graphic-led threads possible without self-hosting image URLs: for example a graphic on the first tweet and the signup link alone in a reply (which keeps the first tweet's reach). Companion server change: media attached to the parent post was previously dropped in thread mode and is now folded into the first tweet.
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;
@@ -264,4 +264,21 @@ export declare class OmniSocialsClient {
264
264
  is_active?: boolean;
265
265
  }): Promise<ApiResponse<unknown>>;
266
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>>;
267
284
  }
package/build/client.js CHANGED
@@ -318,4 +318,18 @@ export class OmniSocialsClient {
318
318
  async rotateWebhookSecret(id) {
319
319
  return this.request("POST", `/webhooks/${id}/rotate-secret`);
320
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
+ }
321
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.12.0",
31
+ version: "1.13.1",
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
+ }
@@ -885,7 +885,7 @@ Notes: only tracks Meta licenses for third-party publishing appear, so the selec
885
885
  });
886
886
  return { content: [{ type: "text", text: md }] };
887
887
  });
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.", {
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. Output is a human-readable summary table PLUS a 'Structured data' JSON block carrying, for every post, the platform's own post id (the stable dedupe key), a permalink, the FULL untruncated caption, and exact-integer metrics — use that block when ingesting or storing native posts rather than the rounded/truncated table. Requires the analytics:read scope.", {
889
889
  limit: z
890
890
  .string()
891
891
  .optional()
@@ -938,6 +938,25 @@ Notes: only tracks Meta licenses for third-party publishing appear, so the selec
938
938
  });
939
939
  }
940
940
  md += `\nPosts showing "—" for engagement have no metrics available from that platform for historical posts; analyze their captions and formats instead.\n`;
941
+ // The table above is rounded + truncated for a human skim. Anything
942
+ // storing/ingesting these posts needs the raw fields: the platform's own
943
+ // post id (dedupe key), permalink, the FULL caption, and exact-integer
944
+ // metrics. Emit them as a machine-readable JSON block.
945
+ const records = posts.map((p) => ({
946
+ platform: p.platform,
947
+ id: p.id ?? null,
948
+ permalink: p.permalink ?? null,
949
+ text: p.text ?? "",
950
+ format: p.format ?? "post",
951
+ media_count: p.media_count ?? 1,
952
+ timestamp: p.timestamp ?? null,
953
+ image_url: p.image_url ?? null,
954
+ engagement: p.engagement ?? null,
955
+ impressions: p.impressions ?? null,
956
+ engagement_rate: p.engagement_rate ?? null,
957
+ metrics: p.metrics || {},
958
+ }));
959
+ md += `\n### Structured data\nFull records for storing/ingesting each post — exact-integer metrics, the platform's own post \`id\` (dedupe key), \`permalink\`, and the complete untruncated caption:\n\n\`\`\`json\n${JSON.stringify(records, null, 2)}\n\`\`\`\n`;
941
960
  return {
942
961
  content: [{ type: "text", text: md }],
943
962
  };
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.12.0",
3
+ "version": "1.13.1",
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",