@omnisocials/mcp-server 1.35.0 → 1.46.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.
@@ -28,9 +28,9 @@ export function registerWebhookTools(server, getClient) {
28
28
  content: [{ type: "text", text: md }],
29
29
  };
30
30
  });
31
- server.tool("create_webhook", "Create a new webhook to receive event notifications. Available events: post.scheduled, post.published, post.failed", {
31
+ server.tool("create_webhook", "Create a new webhook to receive event notifications. Available events: post.scheduled, post.published, post.failed, post.approved (the last approval of a post's approval workflow is in), post.rejected (an approver rejected the post)", {
32
32
  url: z.string().describe("The HTTPS URL to send webhook events to"),
33
- events: z.array(z.string()).describe("Events to subscribe to: post.scheduled, post.published, post.failed"),
33
+ events: z.array(z.string()).describe("Events to subscribe to: post.scheduled, post.published, post.failed, post.approved, post.rejected"),
34
34
  }, { title: "Create Webhook", readOnlyHint: false, destructiveHint: false, openWorldHint: false }, async (params) => {
35
35
  const result = await getClient().createWebhook(params);
36
36
  if (result.error) {
@@ -95,7 +95,7 @@ export function registerWebhookTools(server, getClient) {
95
95
  server.tool("update_webhook", "Update a webhook's URL, events, or active status.", {
96
96
  id: z.string().describe("The webhook ID to update"),
97
97
  url: z.string().optional().describe("Updated HTTPS URL"),
98
- events: z.array(z.string()).optional().describe("Updated event list"),
98
+ events: z.array(z.string()).optional().describe("Updated event list: post.scheduled, post.published, post.failed, post.approved, post.rejected"),
99
99
  is_active: z.boolean().optional().describe("Enable or disable the webhook"),
100
100
  }, { title: "Update Webhook", readOnlyHint: false, destructiveHint: false, openWorldHint: false }, async ({ id, ...data }) => {
101
101
  const result = await getClient().updateWebhook(id, data);
package/build/types.d.ts CHANGED
@@ -11,6 +11,12 @@ export interface ApiResponse<T = unknown> {
11
11
  credits_balance?: number | null;
12
12
  enforced?: boolean;
13
13
  enforce_from?: string;
14
+ platform?: string;
15
+ media_url?: string;
16
+ ratio?: string;
17
+ target?: string;
18
+ action?: "cropped" | "kept" | "crop_failed";
19
+ crop_url?: string;
14
20
  }>;
15
21
  pagination?: {
16
22
  next_cursor: string | null;
@@ -84,13 +90,23 @@ export interface InboxMessage {
84
90
  }
85
91
  /** What GET /inbox/next serves (and what `reply_to_inbox` returns as `next`
86
92
  * 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.
93
+ * `id` is what hide/delete take and the `message_id` to pass to reply for a
94
+ * comment thread, its `conversation_id` is what reply takes as the path.
88
95
  * `messages` is the thread oldest first (the most recent 50, plus the target
89
96
  * and its parent when they fall outside that window). */
90
97
  export interface InboxNextUnanswered {
91
98
  conversation: InboxConversation;
92
99
  message: InboxMessage;
93
100
  messages: InboxMessage[];
101
+ /** Only Instagram and Facebook DMs have a reply window (Meta's 24 hours
102
+ * after the customer's last message): `closes_at` is that deadline and
103
+ * `open` is false once it has passed, in which case reply_to_inbox answers
104
+ * 422 outside_messaging_window. Every other item: open true, closes_at
105
+ * null. */
106
+ reply_window: {
107
+ open: boolean;
108
+ closes_at: string | null;
109
+ };
94
110
  }
95
111
  /** Response of DELETE /inbox/messages/:id. Replies under the comment are
96
112
  * removed with it; their inbox ids are listed in `removed_reply_ids`. */
@@ -105,6 +121,17 @@ export type ApiPostMediaEntry = string | {
105
121
  id?: string | number;
106
122
  url?: string;
107
123
  alt?: string;
124
+ /** Image fit of this channel's copy: 'crop' = url is a crop of crop.of. */
125
+ fit?: "crop" | "keep";
126
+ crop?: {
127
+ of: string;
128
+ id?: string;
129
+ ratio: string;
130
+ focus: {
131
+ x: number;
132
+ y: number;
133
+ } | null;
134
+ };
108
135
  [k: string]: unknown;
109
136
  };
110
137
  /** A thread part as echoed back by the API (X / Bluesky / Mastodon / Threads). */
@@ -170,6 +197,10 @@ export interface ApiPost {
170
197
  status: string;
171
198
  /** "post" | "story" | "reel" */
172
199
  type?: string;
200
+ /** The comment-to-DM automations bound to this post (get_post on a post
201
+ * that goes to Instagram or Facebook, create/update when the request
202
+ * carried it); null = none. */
203
+ comment_dm_automation?: PostCommentDmAutomation | null;
173
204
  /** Per-platform captions keyed by platform id plus "default". Legacy
174
205
  * servers returned a plain string. */
175
206
  content?: Record<string, string> | string;
@@ -237,6 +268,12 @@ export interface ApiPost {
237
268
  bluesky?: ApiPlatformOptionsOut;
238
269
  mastodon?: ApiPlatformOptionsOut;
239
270
  threads?: ApiPlatformOptionsOut;
271
+ /** Telegram options (silent, pin, buttons, poll ...) and, once sent,
272
+ * `result` with the message ids and the links. */
273
+ telegram?: ApiPlatformOptionsOut;
274
+ /** Discord options (silent, thread_name, buttons, poll ...) and, once
275
+ * sent, `result` with the message id and the link. */
276
+ discord?: ApiPlatformOptionsOut;
240
277
  /** platform → user-friendly error message; only failed platforms appear.
241
278
  * null while draft/scheduled/processing or when every platform succeeded. */
242
279
  errors?: Record<string, string> | null;
@@ -274,6 +311,68 @@ export interface RejectPostResult {
274
311
  approval_status: "rejected";
275
312
  message?: string;
276
313
  }
314
+ /** One approver of a step in GET /posts/:id/approval. */
315
+ export interface PostApprovalApprover {
316
+ id: string;
317
+ name: string | null;
318
+ email: string | null;
319
+ status: "pending" | "approved" | "rejected";
320
+ decided_at: string | null;
321
+ /** The reason this approver gave with a rejection. */
322
+ comment: string | null;
323
+ }
324
+ export interface PostApprovalStep {
325
+ order: number;
326
+ name: string;
327
+ /** `all` = every approver of the step must approve. `any` = one approval is enough. */
328
+ require_mode: "all" | "any";
329
+ status: "pending" | "approved" | "rejected";
330
+ approvers: PostApprovalApprover[];
331
+ }
332
+ export interface PostApprovalComment {
333
+ id: string;
334
+ author: {
335
+ id: string;
336
+ name: string | null;
337
+ } | null;
338
+ message: string;
339
+ /** The channel the comment is about (`instagram`, `linkedin_page`, ...). Null = the whole post. */
340
+ account: string | null;
341
+ created_at: string;
342
+ }
343
+ /** `data` of GET /posts/:id/approval (backend/routes/api/v1/posts.js).
344
+ * `status: "none"` = the post has no approval workflow: `workflow`,
345
+ * `requested_by`, `requested_at`, `current_step` and `rejection` are null and
346
+ * `steps` and `comments` are empty. */
347
+ export interface PostApproval {
348
+ post_id: string;
349
+ status: "none" | "pending" | "approved" | "rejected";
350
+ /** `id` is null for a one-off approval that was not made from a saved workflow. */
351
+ workflow: {
352
+ id: string | null;
353
+ name: string;
354
+ } | null;
355
+ requested_by: {
356
+ id: string;
357
+ name: string | null;
358
+ } | null;
359
+ requested_at: string | null;
360
+ /** Order of the step the post waits on. Null when the review is finished. */
361
+ current_step: number | null;
362
+ steps: PostApprovalStep[];
363
+ rejection: {
364
+ by: {
365
+ id: string;
366
+ name: string | null;
367
+ };
368
+ reason: string | null;
369
+ at: string | null;
370
+ step: number | null;
371
+ } | null;
372
+ /** The review thread, oldest first. Includes the entries OmniSocials
373
+ * writes when a reviewer edits the post. */
374
+ comments: PostApprovalComment[];
375
+ }
277
376
  /** One place from GET /locations/search
278
377
  * (@openapi block in backend/routes/api/v1/locations.js). */
279
378
  export interface LocationPlace {
@@ -314,6 +413,59 @@ export interface LocationSearchResult {
314
413
  };
315
414
  needsPermission?: boolean;
316
415
  }
416
+ /** One product Pin from GET /pinterest/products
417
+ * (@openapi block in backend/routes/api/v1/pinterest.js). */
418
+ export interface PinterestProduct {
419
+ /** Use in `pinterest.product_tags`. */
420
+ pin_id: string;
421
+ title?: string | null;
422
+ description?: string | null;
423
+ link?: string | null;
424
+ image_url?: string | null;
425
+ /** Catalog source only. */
426
+ price?: number | null;
427
+ currency?: string | null;
428
+ availability?: string | null;
429
+ item_id?: string | null;
430
+ }
431
+ /** GET /pinterest/products envelope: NOT the standard data/error envelope.
432
+ * Success: `products` + `bookmark` + `source` + `catalog_access` (and, for
433
+ * the catalog source, `product_groups` + `product_group_id`). When the list
434
+ * could not be read, `error` is a { code, message } object with code one of
435
+ * pinterest_not_connected | pinterest_catalog_access_required |
436
+ * platform_error, and `products` is absent. */
437
+ export interface PinterestProductsResult {
438
+ products?: PinterestProduct[];
439
+ bookmark?: string | null;
440
+ source?: "catalog" | "pins";
441
+ catalog_access?: boolean;
442
+ /** The connection has catalog access but the account has no catalog yet. */
443
+ no_catalog?: boolean;
444
+ product_groups?: Array<{
445
+ id: string;
446
+ name?: string | null;
447
+ }>;
448
+ product_group_id?: string | null;
449
+ error?: string | {
450
+ code: string;
451
+ message: string;
452
+ };
453
+ }
454
+ /** GET /pinterest/products/validate result. `unverified` = the check could
455
+ * not run; the publish step is then the final check. */
456
+ export interface PinterestProductCheck {
457
+ valid?: boolean;
458
+ pin_id?: string | null;
459
+ title?: string | null;
460
+ link?: string | null;
461
+ image_url?: string | null;
462
+ unverified?: boolean;
463
+ reason?: string;
464
+ error?: string | {
465
+ code: string;
466
+ message: string;
467
+ };
468
+ }
317
469
  /** One track from GET /audio/search
318
470
  * (@openapi block in backend/routes/api/v1/audio.js). */
319
471
  export interface InstagramAudioTrack {
@@ -364,16 +516,33 @@ export interface MediaItem {
364
516
  filename: string;
365
517
  folder_id?: string | null;
366
518
  size: string;
519
+ width?: number | null;
520
+ height?: number | null;
367
521
  status?: string;
368
522
  created_at: string;
523
+ pdf?: {
524
+ id: string;
525
+ name: string;
526
+ url: string;
527
+ page?: number;
528
+ total_pages: number;
529
+ rendered_pages: number;
530
+ truncated?: boolean;
531
+ pages?: Array<{
532
+ url: string;
533
+ width?: number | null;
534
+ height?: number | null;
535
+ }>;
536
+ };
369
537
  }
370
538
  /**
371
539
  * Extra top-level fields returned by /media/upload* when the uploaded file is a
372
- * PDF. A PDF is rasterized into one image slide per page (max 20): `data`
373
- * mirrors the first slide (back-compat), while `slides` + `media_ids` carry the
374
- * whole carousel in page order. Pass ALL of `media_ids` to create_post. On
375
- * LinkedIn the slides post as a native swipeable document; elsewhere as an
376
- * image carousel.
540
+ * PDF. A PDF is rasterized into one image slide per page (max 20) and the
541
+ * original file is kept: `data` mirrors the first slide (back-compat), while
542
+ * `slides` + `media_ids` carry the whole carousel in page order. Pass ALL of
543
+ * `media_ids` to create_post. On LinkedIn the slides post as a native
544
+ * swipeable document made from the original PDF (unless the post's
545
+ * linkedin.document_source is "slides"); elsewhere as an image carousel.
377
546
  */
378
547
  export interface PdfUploadResult {
379
548
  slides: MediaItem[];
@@ -382,6 +551,10 @@ export interface PdfUploadResult {
382
551
  total_pages: number;
383
552
  rendered_pages: number;
384
553
  truncated: boolean;
554
+ id?: string;
555
+ name?: string;
556
+ url?: string;
557
+ size_bytes?: number;
385
558
  };
386
559
  }
387
560
  export interface Account {
@@ -407,6 +580,29 @@ export interface Account {
407
580
  }[];
408
581
  platform_details?: {
409
582
  subscription_type?: string;
583
+ /** Telegram: "channel", "supergroup" or "group". */
584
+ chat_type?: string | null;
585
+ /** Telegram chat id (a negative number as a string). */
586
+ chat_id?: string | null;
587
+ /** Telegram: subscribers of a channel, members of a group. */
588
+ subscribers?: number | null;
589
+ /** Telegram: t.me link of a public chat, else the chat's private link.
590
+ * Discord: discord.com/channels link to the channel. */
591
+ url?: string | null;
592
+ /** Discord: the server id. */
593
+ guild_id?: string | null;
594
+ /** Discord: the server name (Continue with Discord, or a pasted URL in a server the OmniSocials bot is in). */
595
+ guild_name?: string | null;
596
+ /** Discord: the channel id. */
597
+ channel_id?: string | null;
598
+ /** Discord: the channel name without the # (read by the OmniSocials bot, or given by the person); null when unknown. */
599
+ channel_name?: string | null;
600
+ /** Discord: text, voice, announcement, stage, forum or media, as the bot read it; null when it cannot see the channel. */
601
+ channel_type?: "text" | "voice" | "announcement" | "stage" | "forum" | "media" | null;
602
+ /** Discord: the webhook's name, shown as the sender of every post. */
603
+ webhook_name?: string | null;
604
+ /** Discord: "oauth" (Continue with Discord) or "url" (a pasted webhook URL). */
605
+ connect_method?: string | null;
410
606
  };
411
607
  }
412
608
  export interface Webhook {
@@ -435,6 +631,162 @@ export interface HashtagSet {
435
631
  created_at: string;
436
632
  updated_at: string;
437
633
  }
634
+ /** The channel an automation runs on: the Instagram account or the Facebook Page. */
635
+ export type AutomationPlatform = "instagram" | "facebook";
636
+ /** `story_reply` is Instagram only: Facebook has no story replies. */
637
+ export type AutomationTriggerKind = "comment" | "dm" | "story_reply";
638
+ export type AutomationScope = "post" | "next_post" | "any";
639
+ export type AutomationMatch = "word" | "contains" | "any";
640
+ export interface AutomationPost {
641
+ /** `omnisocials` = a post of this workspace, `instagram` / `facebook` = read from that platform */
642
+ source: "omnisocials" | "instagram" | "facebook";
643
+ id: string;
644
+ caption: string;
645
+ thumbnail_url: string | null;
646
+ permalink: string | null;
647
+ media_type: string | null;
648
+ status?: string;
649
+ post_date_time?: string | null;
650
+ /** omnisocials source only: the post is live on the automation's platform */
651
+ live?: boolean;
652
+ timestamp?: string | null;
653
+ }
654
+ export interface Automation {
655
+ id: string;
656
+ type: "comment_to_dm";
657
+ platform: AutomationPlatform;
658
+ status: "active" | "paused" | "completed";
659
+ name: string | null;
660
+ /** The OmniSocials post the automation listens on (scope post), else null */
661
+ post_id: string | null;
662
+ /** The Instagram media id, set once the post is live (null on Facebook) */
663
+ instagram_media_id: string | null;
664
+ /** The Facebook Page post id ({page-id}_{post-id}), set once the post is live (null on Instagram) */
665
+ facebook_post_id: string | null;
666
+ post: AutomationPost | null;
667
+ trigger: {
668
+ kinds: AutomationTriggerKind[];
669
+ scope: AutomationScope | null;
670
+ keywords: string[];
671
+ match: AutomationMatch;
672
+ };
673
+ actions: {
674
+ dm: {
675
+ text: string;
676
+ };
677
+ public_reply: {
678
+ enabled: boolean;
679
+ variations: string[];
680
+ };
681
+ };
682
+ limits: {
683
+ max_sends: number | null;
684
+ stop_at: string | null;
685
+ };
686
+ stats: {
687
+ matched: number;
688
+ sent: number;
689
+ skipped: number;
690
+ failed: number;
691
+ last_event_at: string | null;
692
+ };
693
+ created_at: string;
694
+ updated_at: string;
695
+ }
696
+ /** The body of POST /automations; every section is optional on PATCH. */
697
+ export interface AutomationInput {
698
+ name?: string | null;
699
+ /** Default instagram; cannot change on PATCH (400 platform_immutable) */
700
+ platform?: AutomationPlatform;
701
+ post_id?: string;
702
+ instagram_media_id?: string;
703
+ /** A Page post published outside OmniSocials; implies platform facebook */
704
+ facebook_post_id?: string;
705
+ trigger?: {
706
+ kinds?: AutomationTriggerKind[];
707
+ scope?: AutomationScope;
708
+ keywords?: string[];
709
+ match?: AutomationMatch;
710
+ };
711
+ actions?: {
712
+ dm: {
713
+ text: string;
714
+ };
715
+ public_reply?: {
716
+ enabled?: boolean;
717
+ variations?: string[];
718
+ };
719
+ };
720
+ limits?: {
721
+ max_sends?: number | null;
722
+ stop_at?: string | null;
723
+ };
724
+ status?: "active" | "paused";
725
+ }
726
+ /** `comment_dm_automation` on a post: the automations bound to that post. */
727
+ export interface CommentDmAutomationInput {
728
+ /** One automation per listed channel; default Instagram when the post goes there, else Facebook */
729
+ platforms?: AutomationPlatform[];
730
+ trigger?: {
731
+ keywords?: string[];
732
+ match?: AutomationMatch;
733
+ };
734
+ actions: {
735
+ dm: {
736
+ text: string;
737
+ };
738
+ public_reply?: {
739
+ enabled?: boolean;
740
+ variations?: string[];
741
+ };
742
+ };
743
+ limits?: {
744
+ max_sends?: number | null;
745
+ stop_at?: string | null;
746
+ };
747
+ }
748
+ /** `comment_dm_automation` on a post: the first automation's fields (Instagram
749
+ * first), plus the channels and the id of each automation. */
750
+ export interface PostCommentDmAutomation extends Automation {
751
+ platforms?: AutomationPlatform[];
752
+ automations?: Array<{
753
+ id: string;
754
+ platform: AutomationPlatform;
755
+ status: Automation["status"];
756
+ }>;
757
+ }
758
+ export interface AutomationEvent {
759
+ id: string;
760
+ automation_id: string;
761
+ kind: AutomationTriggerKind;
762
+ outcome: "sent" | "skipped" | "failed" | "pending";
763
+ reason: string | null;
764
+ reason_label: string | null;
765
+ /** `username` = the Instagram @handle, or the person's name on Facebook */
766
+ person: {
767
+ id: string;
768
+ username: string | null;
769
+ };
770
+ trigger_id: string | null;
771
+ /** The comment; a DM's text only with the inbox:read scope */
772
+ trigger_text: string | null;
773
+ /** The DM that was sent */
774
+ message: string | null;
775
+ public_reply: string | null;
776
+ platform_message_id: string | null;
777
+ created_at: string;
778
+ sent_at: string | null;
779
+ }
780
+ export interface AutomationEventsResult {
781
+ data: AutomationEvent[];
782
+ counts: Record<string, number>;
783
+ pagination: {
784
+ total: number;
785
+ limit: number;
786
+ offset: number;
787
+ has_more: boolean;
788
+ };
789
+ }
438
790
  /** Per-platform stats block inside AnalyticsOverview.platform_breakdown.
439
791
  * Shape from backend/routes/api/v1/analytics.js (overview route). */
440
792
  export interface AnalyticsPlatformBreakdown {
@@ -476,6 +828,16 @@ export interface MediaCompatibility {
476
828
  /** POST /media/check response — the compatibility block is spread top-level
477
829
  * next to `file` and `connected_platforms`; there is no `data` key
478
830
  * (backend/routes/api/v1/media.js check route). */
831
+ export interface MediaFitRow {
832
+ platform: string;
833
+ display_name: string;
834
+ fits: boolean;
835
+ ratio: string;
836
+ shown: "whole" | "cut";
837
+ target: string | null;
838
+ default_action: "crop" | "keep" | null;
839
+ message: string | null;
840
+ }
479
841
  export interface MediaCheckResult {
480
842
  error?: {
481
843
  code: string;
@@ -485,11 +847,15 @@ export interface MediaCheckResult {
485
847
  size_bytes: number | null;
486
848
  mime: string | null;
487
849
  size_known: boolean;
850
+ width?: number | null;
851
+ height?: number | null;
488
852
  };
489
853
  connected_platforms?: string[];
490
854
  compatible?: boolean;
491
855
  warnings?: MediaCompatWarning[];
492
856
  summary?: string | null;
857
+ /** How each connected channel with a fit rule shows this image as a feed post. */
858
+ fit?: MediaFitRow[];
493
859
  }
494
860
  /** One folder from GET /folders — formatFolder in
495
861
  * backend/routes/api/v1/folders.js (:15-23). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnisocials/mcp-server",
3
- "version": "1.35.0",
3
+ "version": "1.46.0",
4
4
  "mcpName": "io.github.omnisocials/mcp-server",
5
5
  "description": "MCP server for OmniSocials API - manage social media posts, media, accounts, analytics, and webhooks",
6
6
  "type": "module",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "license": "MIT",
47
47
  "dependencies": {
48
- "@modelcontextprotocol/sdk": "^1.12.1"
48
+ "@modelcontextprotocol/sdk": "^1.32.1"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "^25.3.3",
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "overrides": {
55
55
  "zod": "4.4.0",
56
- "fast-uri": "^3.1.7",
56
+ "fast-uri": "^3.1.8",
57
57
  "hono": "^4.13.7",
58
58
  "qs": "^6.16.0"
59
59
  }