@omnisocials/mcp-server 1.20.0 → 1.21.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, InboxConversation, InboxMessage } from "./types.js";
1
+ import type { Account, AnalyticsOverview, AccountAnalyticsEntry, ApiPost, ApiResponse, AudioSearchResult, BestTimesResult, HashtagSet, LocationSearchResult, MediaCheckResult, MediaCompatibility, MediaFolder, MediaItem, PdfUploadResult, PostAnalytics, PublishPostResult, RecentPlatformPost, RetryPostResult, InboxConversation, InboxMessage, Webhook } from "./types.js";
2
2
  export declare function fetchImageAsBase64(url: string): Promise<{
3
3
  data: string;
4
4
  mimeType: string;
@@ -100,6 +100,52 @@ export interface MastodonPostOptions {
100
100
  export interface MastodonPostOptionsUpdate {
101
101
  thread_parts?: MastodonThreadPartInput[] | null;
102
102
  }
103
+ /** Non-sponsored LinkedIn poll: question + 2-4 options + duration. */
104
+ export interface LinkedInPollInput {
105
+ question: string;
106
+ options: string[];
107
+ duration: "ONE_DAY" | "THREE_DAYS" | "SEVEN_DAYS" | "FOURTEEN_DAYS";
108
+ }
109
+ /**
110
+ * Request body for POST /posts/create. `createAndPublishPost` accepts the
111
+ * same shape minus `scheduled_at` (publishing is immediate).
112
+ */
113
+ export interface CreatePostInput {
114
+ content: string | Record<string, string>;
115
+ channels?: string[];
116
+ scheduled_at?: string;
117
+ media_ids?: MediaIdEntry[] | Record<string, MediaIdEntry[]>;
118
+ media_urls?: MediaUrlEntry[] | Record<string, MediaUrlEntry[]>;
119
+ type?: string;
120
+ source?: string;
121
+ link_url?: string;
122
+ link_title?: string;
123
+ link_description?: string;
124
+ link_thumbnail_url?: string;
125
+ location_id?: string;
126
+ collaborators?: string[];
127
+ user_tags?: Array<{
128
+ username: string;
129
+ x: number;
130
+ y: number;
131
+ image_index?: number;
132
+ }>;
133
+ pinterest?: Record<string, unknown>;
134
+ youtube?: Record<string, unknown>;
135
+ instagram?: Record<string, unknown>;
136
+ facebook?: Record<string, unknown>;
137
+ linkedin?: Record<string, unknown>;
138
+ linkedin_page?: Record<string, unknown>;
139
+ linkedin_poll?: LinkedInPollInput | null;
140
+ tiktok?: Record<string, unknown>;
141
+ x?: XPostOptions;
142
+ bluesky?: BlueskyPostOptions;
143
+ mastodon?: MastodonPostOptions;
144
+ google_business?: Record<string, unknown>;
145
+ hashtag_set?: string;
146
+ hashtag_placement?: "caption_append" | "first_comment";
147
+ hashtag_platforms?: string[];
148
+ }
103
149
  export declare class OmniSocialsClient {
104
150
  private baseUrl;
105
151
  private apiKey;
@@ -109,81 +155,20 @@ export declare class OmniSocialsClient {
109
155
  status?: string;
110
156
  limit?: string;
111
157
  offset?: string;
112
- }): Promise<ApiResponse<unknown>>;
113
- getPost(id: string): Promise<ApiResponse<unknown>>;
158
+ }): Promise<ApiResponse<ApiPost[]>>;
159
+ getPost(id: string): Promise<ApiResponse<ApiPost>>;
114
160
  getRecentPlatformPosts(params?: {
115
161
  limit?: string;
116
162
  platforms?: string;
117
- }): Promise<ApiResponse<unknown>>;
118
- createPost(data: {
119
- content: string | Record<string, string>;
120
- channels?: string[];
121
- scheduled_at?: string;
122
- media_ids?: MediaIdEntry[] | Record<string, MediaIdEntry[]>;
123
- media_urls?: MediaUrlEntry[] | Record<string, MediaUrlEntry[]>;
124
- type?: string;
125
- source?: string;
126
- link_url?: string;
127
- link_title?: string;
128
- link_description?: string;
129
- link_thumbnail_url?: string;
130
- location_id?: string;
131
- collaborators?: string[];
132
- user_tags?: Array<{
133
- username: string;
134
- x: number;
135
- y: number;
136
- image_index?: number;
137
- }>;
138
- pinterest?: Record<string, unknown>;
139
- youtube?: Record<string, unknown>;
140
- instagram?: Record<string, unknown>;
141
- facebook?: Record<string, unknown>;
142
- linkedin?: Record<string, unknown>;
143
- linkedin_page?: Record<string, unknown>;
144
- tiktok?: Record<string, unknown>;
145
- x?: XPostOptions;
146
- bluesky?: BlueskyPostOptions;
147
- mastodon?: MastodonPostOptions;
148
- google_business?: Record<string, unknown>;
149
- hashtag_set?: string;
150
- hashtag_placement?: "caption_append" | "first_comment";
151
- hashtag_platforms?: string[];
152
- }): Promise<ApiResponse<unknown>>;
153
- createAndPublishPost(data: {
154
- content: string | Record<string, string>;
155
- channels?: string[];
156
- media_ids?: MediaIdEntry[] | Record<string, MediaIdEntry[]>;
157
- media_urls?: MediaUrlEntry[] | Record<string, MediaUrlEntry[]>;
158
- type?: string;
159
- source?: string;
160
- link_url?: string;
161
- link_title?: string;
162
- link_description?: string;
163
- link_thumbnail_url?: string;
164
- location_id?: string;
165
- collaborators?: string[];
166
- user_tags?: Array<{
167
- username: string;
168
- x: number;
169
- y: number;
170
- image_index?: number;
171
- }>;
172
- pinterest?: Record<string, unknown>;
173
- youtube?: Record<string, unknown>;
174
- instagram?: Record<string, unknown>;
175
- facebook?: Record<string, unknown>;
176
- linkedin?: Record<string, unknown>;
177
- linkedin_page?: Record<string, unknown>;
178
- tiktok?: Record<string, unknown>;
179
- x?: XPostOptions;
180
- bluesky?: BlueskyPostOptions;
181
- mastodon?: MastodonPostOptions;
182
- google_business?: Record<string, unknown>;
183
- hashtag_set?: string;
184
- hashtag_placement?: "caption_append" | "first_comment";
185
- hashtag_platforms?: string[];
186
- }): Promise<ApiResponse<unknown>>;
163
+ }): Promise<ApiResponse<RecentPlatformPost[]> & {
164
+ count?: number;
165
+ connected_platforms?: string[];
166
+ errors?: Record<string, string>;
167
+ note?: string;
168
+ current_date?: string;
169
+ }>;
170
+ createPost(data: CreatePostInput): Promise<ApiResponse<ApiPost>>;
171
+ createAndPublishPost(data: Omit<CreatePostInput, "scheduled_at">): Promise<ApiResponse<ApiPost>>;
187
172
  updatePost(id: string, data: {
188
173
  content?: string | Record<string, string>;
189
174
  scheduled_at?: string;
@@ -205,31 +190,32 @@ export declare class OmniSocialsClient {
205
190
  facebook?: Record<string, unknown>;
206
191
  linkedin?: Record<string, unknown>;
207
192
  linkedin_page?: Record<string, unknown>;
193
+ linkedin_poll?: LinkedInPollInput | null;
208
194
  tiktok?: Record<string, unknown>;
209
195
  x?: XPostOptionsUpdate;
210
196
  bluesky?: BlueskyPostOptionsUpdate;
211
197
  mastodon?: MastodonPostOptionsUpdate;
212
198
  google_business?: Record<string, unknown>;
213
- }): Promise<ApiResponse<unknown>>;
199
+ }): Promise<ApiResponse<ApiPost>>;
214
200
  deletePost(id: string): Promise<ApiResponse<unknown>>;
215
- publishPost(id: string): Promise<ApiResponse<unknown>>;
216
- retryPost(id: string): Promise<ApiResponse<unknown>>;
217
- searchLocations(query: string): Promise<ApiResponse<unknown>>;
201
+ publishPost(id: string): Promise<ApiResponse<PublishPostResult>>;
202
+ retryPost(id: string): Promise<ApiResponse<RetryPostResult>>;
203
+ searchLocations(query: string): Promise<LocationSearchResult>;
218
204
  validateLocation(id: string): Promise<ApiResponse<unknown>>;
219
- searchInstagramAudio(query?: string, type?: "music" | "original_sound"): Promise<ApiResponse<unknown>>;
205
+ searchInstagramAudio(query?: string, type?: "music" | "original_sound"): Promise<AudioSearchResult>;
220
206
  listMedia(params?: {
221
207
  limit?: string;
222
208
  offset?: string;
223
209
  search?: string;
224
210
  folder_id?: string;
225
- }): Promise<ApiResponse<unknown>>;
211
+ }): Promise<ApiResponse<MediaItem[]>>;
226
212
  uploadMedia(data: {
227
213
  url: string;
228
214
  filename?: string;
229
215
  name?: string;
230
216
  folder?: string;
231
217
  }): Promise<ApiResponse<MediaItem> & Partial<PdfUploadResult> & {
232
- compatibility?: unknown;
218
+ compatibility?: MediaCompatibility;
233
219
  message?: string;
234
220
  }>;
235
221
  uploadMediaFromBase64(data: {
@@ -239,7 +225,7 @@ export declare class OmniSocialsClient {
239
225
  name?: string;
240
226
  folder?: string;
241
227
  }): Promise<ApiResponse<MediaItem> & Partial<PdfUploadResult> & {
242
- compatibility?: unknown;
228
+ compatibility?: MediaCompatibility;
243
229
  message?: string;
244
230
  }>;
245
231
  /**
@@ -252,57 +238,73 @@ export declare class OmniSocialsClient {
252
238
  media_id?: string;
253
239
  size_bytes?: number;
254
240
  mime?: string;
255
- }): Promise<ApiResponse<unknown>>;
241
+ }): Promise<MediaCheckResult>;
256
242
  deleteMedia(id: string): Promise<ApiResponse<unknown>>;
257
243
  updateMedia(id: string, data: {
258
244
  name?: string;
259
245
  folder_id?: string | null;
260
- }): Promise<ApiResponse<unknown>>;
261
- listFolders(): Promise<ApiResponse<unknown>>;
246
+ }): Promise<ApiResponse<MediaItem>>;
247
+ listFolders(): Promise<ApiResponse<MediaFolder[]>>;
262
248
  createFolder(data: {
263
249
  name: string;
264
250
  parent_id?: string;
265
- }): Promise<ApiResponse<unknown>>;
266
- listHashtagSets(): Promise<ApiResponse<unknown>>;
251
+ }): Promise<ApiResponse<MediaFolder>>;
252
+ listHashtagSets(): Promise<ApiResponse<HashtagSet[]>>;
267
253
  createHashtagSet(data: {
268
254
  name: string;
269
255
  hashtags: string[];
270
- }): Promise<ApiResponse<unknown>>;
256
+ }): Promise<ApiResponse<HashtagSet>>;
271
257
  updateHashtagSet(id: string, data: {
272
258
  name?: string;
273
259
  hashtags?: string[];
274
- }): Promise<ApiResponse<unknown>>;
260
+ }): Promise<ApiResponse<HashtagSet>>;
275
261
  deleteHashtagSet(id: string): Promise<ApiResponse<unknown>>;
276
- listAccounts(): Promise<ApiResponse<unknown>>;
277
- getAccount(id: string): Promise<ApiResponse<unknown>>;
278
- getPostAnalytics(postId: string): Promise<ApiResponse<unknown>>;
279
- getPostsAnalytics(postIds: string[]): Promise<ApiResponse<unknown>>;
262
+ listAccounts(): Promise<ApiResponse<Account[]> & {
263
+ workspace_id?: number | string | null;
264
+ workspace_name?: string | null;
265
+ workspace_icon?: string | null;
266
+ }>;
267
+ getAccount(id: string): Promise<ApiResponse<Account>>;
268
+ getPostAnalytics(postId: string): Promise<ApiResponse<PostAnalytics>>;
269
+ getPostsAnalytics(postIds: string[]): Promise<ApiResponse<PostAnalytics[]>>;
280
270
  getAnalyticsOverview(params?: {
281
271
  period?: string;
282
272
  start_date?: string;
283
273
  end_date?: string;
284
- }): Promise<ApiResponse<unknown>>;
274
+ }): Promise<ApiResponse<AnalyticsOverview> & {
275
+ period?: string;
276
+ start_date?: string;
277
+ end_date?: string;
278
+ current_date?: string;
279
+ }>;
285
280
  getAccountAnalytics(params?: {
286
281
  platform?: string;
287
282
  date?: string;
288
- }): Promise<ApiResponse<unknown>>;
283
+ }): Promise<ApiResponse<AccountAnalyticsEntry[]> & {
284
+ date?: string;
285
+ }>;
289
286
  getBestTimes(params: {
290
287
  platform: string;
291
288
  timezone?: string;
292
- }): Promise<ApiResponse<unknown>>;
293
- listWebhooks(): Promise<ApiResponse<unknown>>;
289
+ }): Promise<BestTimesResult>;
290
+ listWebhooks(): Promise<ApiResponse<Webhook[]>>;
294
291
  createWebhook(data: {
295
292
  url: string;
296
293
  events: string[];
297
- }): Promise<ApiResponse<unknown>>;
294
+ }): Promise<ApiResponse<Webhook & {
295
+ secret: string;
296
+ }>>;
298
297
  deleteWebhook(id: string): Promise<ApiResponse<unknown>>;
299
- getWebhook(id: string): Promise<ApiResponse<unknown>>;
298
+ getWebhook(id: string): Promise<ApiResponse<Webhook>>;
300
299
  updateWebhook(id: string, data: {
301
300
  url?: string;
302
301
  events?: string[];
303
302
  is_active?: boolean;
304
- }): Promise<ApiResponse<unknown>>;
305
- rotateWebhookSecret(id: string): Promise<ApiResponse<unknown>>;
303
+ }): Promise<ApiResponse<Webhook>>;
304
+ rotateWebhookSecret(id: string): Promise<ApiResponse<{
305
+ id: string;
306
+ secret: string;
307
+ }>>;
306
308
  listInboxConversations(params?: {
307
309
  platform?: string;
308
310
  type?: string;
package/build/client.js CHANGED
@@ -201,6 +201,8 @@ export class OmniSocialsClient {
201
201
  // Recent posts fetched live from the connected platform APIs (including
202
202
  // content published outside OmniSocials). The fallback for brand-new
203
203
  // workspaces where listPosts is empty. Requires the analytics:read scope.
204
+ // `connected_platforms`/`errors`/`note`/`current_date` ride top-level on the
205
+ // envelope (backend/routes/api/v1/posts.js recent-platform route).
204
206
  async getRecentPlatformPosts(params) {
205
207
  return this.request("GET", "/posts/recent-platform", undefined, params);
206
208
  }
@@ -224,14 +226,17 @@ export class OmniSocialsClient {
224
226
  async retryPost(id) {
225
227
  return this.request("POST", `/posts/${id}/retry`);
226
228
  }
227
- // Locations (Instagram place tagging)
229
+ // Locations (Instagram place tagging). NOT the standard envelope — see
230
+ // LocationSearchResult (degraded-path `error` is a plain string).
228
231
  async searchLocations(query) {
229
- return this.request("GET", "/locations/search", undefined, { q: query });
232
+ return this.request("GET", "/locations/search", undefined, {
233
+ q: query,
234
+ });
230
235
  }
231
236
  async validateLocation(id) {
232
237
  return this.request("GET", "/locations/validate", undefined, { id });
233
238
  }
234
- // Audio (Instagram Reel music)
239
+ // Audio (Instagram Reel music). Same nonstandard string-`error` envelope.
235
240
  async searchInstagramAudio(query, type) {
236
241
  const params = {};
237
242
  if (query)
@@ -262,6 +267,7 @@ export class OmniSocialsClient {
262
267
  * or `size_bytes` + `mime`.
263
268
  */
264
269
  async checkMediaCompatibility(data) {
270
+ // Nonstandard envelope: compatibility fields ride top-level, no `data`.
265
271
  return this.request("POST", "/media/check", data);
266
272
  }
267
273
  async deleteMedia(id) {
@@ -290,7 +296,9 @@ export class OmniSocialsClient {
290
296
  async deleteHashtagSet(id) {
291
297
  return this.request("DELETE", `/hashtag-sets/${id}`);
292
298
  }
293
- // Accounts
299
+ // Accounts. Alongside `data`, the list response carries the workspace's
300
+ // identity top-level (backend/routes/api/v1/accounts.js) — the workspaces
301
+ // tool reads it to label multi-workspace setups.
294
302
  async listAccounts() {
295
303
  return this.request("GET", "/accounts");
296
304
  }
@@ -308,12 +316,18 @@ export class OmniSocialsClient {
308
316
  ids: postIds.join(","),
309
317
  });
310
318
  }
319
+ // The resolved range and today's date ride top-level on the envelope
320
+ // (backend/routes/api/v1/analytics.js overview route) so clients can render
321
+ // an accurate "as of <today>" line.
311
322
  async getAnalyticsOverview(params) {
312
323
  return this.request("GET", "/analytics/overview", undefined, params);
313
324
  }
325
+ // The resolved `date` rides top-level on the envelope.
314
326
  async getAccountAnalytics(params) {
315
327
  return this.request("GET", "/analytics/accounts", undefined, params);
316
328
  }
329
+ // Nonstandard envelope: the result object is returned top-level, no `data`
330
+ // (backend/routes/api/v1/analytics.js best-times route).
317
331
  async getBestTimes(params) {
318
332
  return this.request("GET", "/analytics/best-times", undefined, params);
319
333
  }
@@ -321,6 +335,7 @@ export class OmniSocialsClient {
321
335
  async listWebhooks() {
322
336
  return this.request("GET", "/webhooks");
323
337
  }
338
+ // The signing secret is only ever returned at creation time.
324
339
  async createWebhook(data) {
325
340
  return this.request("POST", "/webhooks", data);
326
341
  }
@@ -333,6 +348,8 @@ export class OmniSocialsClient {
333
348
  async updateWebhook(id, data) {
334
349
  return this.request("PATCH", `/webhooks/${id}`, data);
335
350
  }
351
+ // Returns only { id, secret } — not the full webhook
352
+ // (backend/routes/api/v1/webhooks.js rotate-secret route).
336
353
  async rotateWebhookSecret(id) {
337
354
  return this.request("POST", `/webhooks/${id}/rotate-secret`);
338
355
  }
package/build/index.js CHANGED
@@ -29,7 +29,7 @@ const sessionState = { activeIndex: 0 };
29
29
  const getActiveClient = () => workspaceClients[sessionState.activeIndex].client;
30
30
  const server = new McpServer({
31
31
  name: "OmniSocials",
32
- version: "1.20.0",
32
+ version: "1.21.0",
33
33
  });
34
34
  // Register all tools - pass getter function so tools always use the active workspace's client
35
35
  registerPostTools(server, getActiveClient);
@@ -8,7 +8,7 @@ export function registerAccountTools(server, getClient) {
8
8
  content: [{ type: "text", text: `Error (${result.error.code}): ${result.error.message}` }],
9
9
  };
10
10
  }
11
- const accounts = Array.isArray(result.data) ? result.data : result.data?.accounts || [];
11
+ const accounts = Array.isArray(result.data) ? result.data : [];
12
12
  if (!accounts.length) {
13
13
  return {
14
14
  content: [{ type: "text", text: "No connected accounts found." }],
@@ -42,7 +42,7 @@ export function registerAccountTools(server, getClient) {
42
42
  // Fetch profile pictures
43
43
  const content = [];
44
44
  const imagePromises = accounts
45
- .filter((a) => a.profile_picture)
45
+ .filter((a) => !!a.profile_picture)
46
46
  .slice(0, 6)
47
47
  .map(async (a) => {
48
48
  const img = await fetchImageAsBase64(a.profile_picture);
@@ -17,7 +17,7 @@ export function registerAnalyticsTools(server, getClient) {
17
17
  // (thread posts publish as a chain), so we just total across platforms here.
18
18
  // The legacy flat shape (d.impressions / d.platform_stats) doesn't exist on
19
19
  // this endpoint and reading those keys returned "—" for every field.
20
- const platforms = d?.platforms || {};
20
+ const platforms = d?.platforms ?? {};
21
21
  const platformEntries = Object.entries(platforms);
22
22
  if (platformEntries.length === 0) {
23
23
  return {
@@ -110,9 +110,9 @@ export function registerAnalyticsTools(server, getClient) {
110
110
  // API returns { data: [{ post_id, platforms: { <platform>: { metrics } } }] }.
111
111
  // Per-platform metrics are already thread-summed server-side; collapse each
112
112
  // post's per-platform metrics into totals, mirroring get_post_analytics.
113
- const entries = result.data || [];
113
+ const entries = result.data ?? [];
114
114
  const rows = entries.map((entry) => {
115
- const platforms = entry?.platforms || {};
115
+ const platforms = entry?.platforms ?? {};
116
116
  const platformEntries = Object.entries(platforms);
117
117
  let impressions = 0;
118
118
  let engagements = 0;
@@ -167,6 +167,11 @@ export function registerAnalyticsTools(server, getClient) {
167
167
  };
168
168
  }
169
169
  const d = result.data;
170
+ if (!d) {
171
+ return {
172
+ content: [{ type: "text", text: "No analytics overview data returned." }],
173
+ };
174
+ }
170
175
  const resolvedStart = result.start_date;
171
176
  const resolvedEnd = result.end_date;
172
177
  const currentDate = result.current_date;
@@ -189,8 +194,7 @@ export function registerAnalyticsTools(server, getClient) {
189
194
  md += `|----------|-------|-------------|-------------|----------|\n`;
190
195
  // Sort by impressions descending
191
196
  const sorted = Object.entries(d.platform_breakdown).sort((a, b) => (b[1].total_impressions || 0) - (a[1].total_impressions || 0));
192
- for (const [platform, stats] of sorted) {
193
- const s = stats;
197
+ for (const [platform, s] of sorted) {
194
198
  const rate = s.engagement_rate !== undefined
195
199
  ? `${s.engagement_rate.toFixed(2)}%`
196
200
  : "—";
@@ -276,7 +280,10 @@ export function registerAnalyticsTools(server, getClient) {
276
280
  };
277
281
  }
278
282
  const d = result;
279
- let md = `## Best times to post ${capitalize(d.platform)} (${d.timezone})\n\n`;
283
+ // On success the service always returns platform/timezone; params is the
284
+ // typed fallback so the label never renders "undefined".
285
+ const platformLabel = capitalize(d.platform || params.platform);
286
+ let md = `## Best times to post — ${platformLabel} (${d.timezone})\n\n`;
280
287
  md += `**Top slots:**\n`;
281
288
  (d.recommendations || []).forEach((r, i) => {
282
289
  const extras = [];
@@ -309,10 +316,10 @@ export function registerAnalyticsTools(server, getClient) {
309
316
  md += `\n### Best hours per day (score 0-100)\n\n| Day | Best hours |\n|-----|------------|\n${dayRows.join("\n")}\n`;
310
317
  }
311
318
  if (d.basis === "defaults") {
312
- md += `\n_Not enough posting history on ${capitalize(d.platform)} yet (${d.sample_size} analyzed post${d.sample_size === 1 ? "" : "s"}). These are cross-industry averages — publish ${d.posts_needed} more post${d.posts_needed === 1 ? "" : "s"} to unlock recommendations from this audience's own data._\n`;
319
+ md += `\n_Not enough posting history on ${platformLabel} yet (${d.sample_size} analyzed post${d.sample_size === 1 ? "" : "s"}). These are cross-industry averages — publish ${d.posts_needed} more post${d.posts_needed === 1 ? "" : "s"} to unlock recommendations from this audience's own data._\n`;
313
320
  }
314
321
  else {
315
- md += `\n_Based on ${d.sample_size} ${capitalize(d.platform)} posts from the last ${d.window_days} days (metric: ${d.metric}, times in ${d.timezone})._\n`;
322
+ md += `\n_Based on ${d.sample_size} ${platformLabel} posts from the last ${d.window_days} days (metric: ${d.metric}, times in ${d.timezone})._\n`;
316
323
  }
317
324
  return {
318
325
  content: [{ type: "text", text: md }],
@@ -7,7 +7,7 @@ export function registerHashtagSetTools(server, getClient) {
7
7
  content: [{ type: "text", text: `Error (${result.error.code}): ${result.error.message}` }],
8
8
  };
9
9
  }
10
- const sets = Array.isArray(result.data) ? result.data : result.data?.data || [];
10
+ const sets = Array.isArray(result.data) ? result.data : [];
11
11
  if (!sets.length) {
12
12
  return {
13
13
  content: [{ type: "text", text: "No hashtag sets yet. Create one with create_hashtag_set." }],
@@ -36,6 +36,11 @@ export function registerHashtagSetTools(server, getClient) {
36
36
  };
37
37
  }
38
38
  const s = result.data;
39
+ if (!s) {
40
+ return {
41
+ content: [{ type: "text", text: "Hashtag set created, but the API returned no details. List it with list_hashtag_sets." }],
42
+ };
43
+ }
39
44
  return {
40
45
  content: [{
41
46
  type: "text",
@@ -55,6 +60,11 @@ export function registerHashtagSetTools(server, getClient) {
55
60
  };
56
61
  }
57
62
  const s = result.data;
63
+ if (!s) {
64
+ return {
65
+ content: [{ type: "text", text: "Hashtag set updated, but the API returned no details. List it with list_hashtag_sets." }],
66
+ };
67
+ }
58
68
  return {
59
69
  content: [{
60
70
  type: "text",
@@ -4,11 +4,12 @@ const PLATFORM_EMOJI = {
4
4
  instagram: "📸",
5
5
  facebook: "📘",
6
6
  linkedin: "💼",
7
+ x: "🐦",
7
8
  };
8
9
  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
+ server.tool("list_inbox_conversations", "List social inbox conversations (Instagram/Facebook DMs, comments, mentions, LinkedIn company-page comments/mentions, and X DMs where the workspace has opted into X DMs), newest activity first. Cursor-paginated: pass the returned cursor to get the next page.", {
10
11
  platform: z
11
- .enum(["instagram", "facebook", "linkedin"])
12
+ .enum(["instagram", "facebook", "linkedin", "x"])
12
13
  .optional()
13
14
  .describe("Filter to one platform"),
14
15
  type: z
@@ -136,7 +137,7 @@ export function registerInboxTools(server, getClient) {
136
137
  ],
137
138
  };
138
139
  });
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
+ server.tool("reply_to_inbox", "Reply to an existing inbox conversation (DM, comment, or mention) on Instagram, Facebook, LinkedIn, or X. You can only reply to conversations that already exist. Meta direct-message replies must be within the platform's 24-hour messaging window. X replies are DM-only and use 2 prepaid credits per send (X's API fee passed through at cost) — a 402 insufficient_credits error means the organisation needs to top up at https://app.omnisocials.com/credits; relay that link to the user rather than retrying. Each workspace can send up to 1,000 replies per day.", {
140
141
  conversation_id: z.string().describe("The conversation ID to reply to"),
141
142
  text: z.string().describe("The reply text (max 2000 characters)"),
142
143
  }, async ({ conversation_id, text }) => {
@@ -13,7 +13,7 @@ export function registerMediaTools(server, getClient) {
13
13
  content: [{ type: "text", text: `Error (${result.error.code}): ${result.error.message}` }],
14
14
  };
15
15
  }
16
- const items = Array.isArray(result.data) ? result.data : result.data?.media || [];
16
+ const items = Array.isArray(result.data) ? result.data : [];
17
17
  if (!items.length) {
18
18
  const hint = params?.search
19
19
  ? `No media found matching "${params.search}". It hasn't been uploaded yet — upload it (and pass a \`name\`) so it's findable next time.`
@@ -122,6 +122,11 @@ When the user provides an image in the conversation (not a URL), use base64_data
122
122
  return { content: [{ type: "text", text: md }] };
123
123
  }
124
124
  const m = result.data;
125
+ if (!m) {
126
+ return {
127
+ content: [{ type: "text", text: "Upload succeeded, but the API returned no media details. Find the file with list_media." }],
128
+ };
129
+ }
125
130
  const isProcessing = m.status === "processing";
126
131
  let md = isProcessing ? `## Media Processing\n\n` : `## Media Uploaded\n\n`;
127
132
  md += `| Field | Value |\n`;
@@ -170,7 +175,8 @@ Provide ONE of: a public 'url' (its size/type is read via a HEAD request), an ex
170
175
  const connected = r.connected_platforms || [];
171
176
  let md = `## Compatibility Check\n\n`;
172
177
  if (r.file) {
173
- const sz = r.file.size_known ? formatBytes(r.file.size_bytes) : "unknown size";
178
+ // size_known implies size_bytes is a finite number (media.js check route).
179
+ const sz = r.file.size_known ? formatBytes(r.file.size_bytes ?? 0) : "unknown size";
174
180
  md += `File: ${sz}${r.file.mime ? ` · ${r.file.mime}` : ""}\n`;
175
181
  }
176
182
  md += `Connected platforms: ${connected.length ? connected.join(", ") : "none"}\n\n`;
@@ -204,6 +210,11 @@ Provide ONE of: a public 'url' (its size/type is read via a HEAD request), an ex
204
210
  };
205
211
  }
206
212
  const m = result.data;
213
+ if (!m) {
214
+ return {
215
+ content: [{ type: "text", text: "Media updated, but the API returned no details. Verify with list_media." }],
216
+ };
217
+ }
207
218
  return {
208
219
  content: [
209
220
  {
@@ -218,7 +229,7 @@ Provide ONE of: a public 'url' (its size/type is read via a HEAD request), an ex
218
229
  if (result.error) {
219
230
  return { content: [{ type: "text", text: `Error (${result.error.code}): ${result.error.message}` }] };
220
231
  }
221
- const items = Array.isArray(result.data) ? result.data : result.data?.data || [];
232
+ const items = Array.isArray(result.data) ? result.data : [];
222
233
  if (!items.length) {
223
234
  return { content: [{ type: "text", text: "No folders yet. Create one with create_folder." }] };
224
235
  }
@@ -238,6 +249,11 @@ Provide ONE of: a public 'url' (its size/type is read via a HEAD request), an ex
238
249
  return { content: [{ type: "text", text: `Error (${result.error.code}): ${result.error.message}` }] };
239
250
  }
240
251
  const f = result.data;
252
+ if (!f) {
253
+ return {
254
+ content: [{ type: "text", text: "Folder created, but the API returned no details. Verify with list_folders." }],
255
+ };
256
+ }
241
257
  return {
242
258
  content: [
243
259
  {