@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.
package/README.md CHANGED
@@ -9,7 +9,7 @@ MCP (Model Context Protocol) server for the [OmniSocials](https://omnisocials.co
9
9
 
10
10
  ## Remote MCP URL (no install)
11
11
 
12
- For clients that support remote MCP over HTTP (claude.ai, ChatGPT, Notion, OpenClaw):
12
+ For clients that support remote MCP over HTTP (claude.ai, ChatGPT, Notion, Muse, OpenClaw):
13
13
 
14
14
  ```
15
15
  https://mcp.omnisocials.com?API_KEY=omsk_live_your_key_here
@@ -112,10 +112,12 @@ OmniSocials accepts the following channel IDs in `create_post`, `create_and_publ
112
112
  | `threads` | Threads | Posts, chained threads via `threads.thread_parts`, location tagging via `threads.location_id` (rolling out) |
113
113
  | `bluesky` | Bluesky | Posts, chained threads via `bluesky.thread_parts` |
114
114
  | `mastodon` | Mastodon | Posts, chained threads via `mastodon.thread_parts` |
115
+ | `telegram` | Telegram | A channel or group the OmniSocials bot posts to: text, photos, videos, GIFs, albums of up to 10, native polls and link buttons via `telegram` |
116
+ | `discord` | Discord | One Discord channel, posted through its webhook: text, up to 10 images, GIFs and videos, native polls, link buttons and forum post titles via `discord` |
115
117
 
116
118
  ## Available Tools
117
119
 
118
- ### Posts (13 tools)
120
+ ### Posts (15 tools)
119
121
 
120
122
  | Tool | Description |
121
123
  |------|-------------|
@@ -130,7 +132,9 @@ OmniSocials accepts the following channel IDs in `create_post`, `create_and_publ
130
132
  | `list_approval_workflows` | List the workspace's approval workflows (steps + approvers); pass an id as `approval_workflow_id` on `create_post` to hold the post for review |
131
133
  | `approve_post` | Approve the current step of a post's approval workflow (the connected user must be a listed approver for that step) |
132
134
  | `reject_post` | Reject a post's approval workflow, stopping it immediately |
135
+ | `get_post_approval` | Read the approval review of a post: steps, approvers and their decisions, the rejection reason and the comment thread |
133
136
  | `search_locations` | Find an Instagram or Threads location to tag (Instagram place IDs for `location_id`, Threads location ids for `threads.location_id`; Threads also searches by latitude/longitude) |
137
+ | `list_pinterest_products` | List the product Pins of the connected Pinterest account (catalog or own Pins), or check one Pin, to tag products with `pinterest.product_tags` |
134
138
  | `search_instagram_audio` | Search Instagram's licensed music catalog for a track to attach to a Reel |
135
139
 
136
140
  ### Media & folders (6 tools)
@@ -178,12 +182,12 @@ Apply a set when creating a post: `create_post` / `create_and_publish_post` acce
178
182
  | `list_workspaces` | List all workspaces available in this session |
179
183
  | `switch_workspace` | Switch the active workspace **by name** (id or list number also accepted) |
180
184
 
181
- ### Webhooks (5 tools)
185
+ ### Webhooks (6 tools)
182
186
 
183
187
  | Tool | Description |
184
188
  |------|-------------|
185
189
  | `list_webhooks` | List all webhooks |
186
- | `create_webhook` | Create a webhook for event notifications |
190
+ | `create_webhook` | Create a webhook for event notifications (`post.scheduled`, `post.published`, `post.failed`, `post.approved`, `post.rejected`) |
187
191
  | `get_webhook` | Get webhook details |
188
192
  | `update_webhook` | Update a webhook |
189
193
  | `delete_webhook` | Delete a webhook |
@@ -194,21 +198,34 @@ Apply a set when creating a post: `create_post` / `create_and_publish_post` acce
194
198
  | Tool | Description |
195
199
  |------|-------------|
196
200
  | `list_inbox_conversations` | List conversations: Instagram/Facebook DMs, comments, mentions, LinkedIn company-page comments/mentions, TikTok video comments, YouTube video comments, X DMs (opt-in), and Threads replies on your posts plus mentions; filter by platform, type, unread, or `unanswered` |
197
- | `get_next_unanswered` | The next item that still needs an answer (longest-waiting first) with the whole thread and the post it is on; filter by platform or type, `include_read`, `exclude` |
201
+ | `get_next_unanswered` | The next item that still needs an answer (DMs that can still be answered first, then expired Instagram/Facebook DMs, then comments and mentions oldest first) with the whole thread, the post it is on and its `reply_window`; filter by platform or type, `include_read`, `exclude` |
198
202
  | `get_inbox_conversation` | Get a conversation's full message history with the post it belongs to; every message shows its `#id` |
199
203
  | `mark_inbox_read` | Mark all incoming messages in a conversation as read (also the durable skip for `get_next_unanswered`) |
200
- | `reply_to_inbox` | Reply to a DM, comment, or mention (existing conversations only); `include_next` returns the next unanswered item in the same call |
204
+ | `reply_to_inbox` | Reply to a DM, comment, or mention (existing conversations only); `message_id` picks the comment being answered; `include_next` returns the next unanswered item in the same call |
201
205
  | `hide_inbox_comment` | Hide or unhide a comment on one of your posts (Facebook, Instagram, TikTok, YouTube, Threads) |
202
206
  | `delete_inbox_comment` | Permanently delete a comment on one of your posts, with the replies under it (Facebook, Instagram, TikTok) |
203
207
  | `hide_inbox_reply` | Older name for `hide_inbox_comment`, kept for compatibility |
204
208
 
205
209
  Requires an API key with the opt-in `inbox:read` / `inbox:write` scopes — enable "Social Inbox access" when you create the key.
206
210
 
211
+ ### Automations (6 tools)
212
+
213
+ | Tool | Description |
214
+ |------|-------------|
215
+ | `list_automations` | List the comment-to-DM automations of the workspace (Instagram and Facebook Page), optionally by `status`, `post_id` or `platform` |
216
+ | `get_automation` | Get one automation: trigger, keywords, DM text, public reply, limits and stats |
217
+ | `create_automation` | Create a comment-to-DM automation: a keyword in a comment (or a DM) gets a DM back, once per person, with an optional public reply |
218
+ | `update_automation` | Pause, resume or change the keywords, DM text, public reply, limits or post of an automation |
219
+ | `delete_automation` | Delete an automation |
220
+ | `get_automation_activity` | One row per comment or message the automation acted on: the DM sent, a skip with its reason, or the platform's error |
221
+
222
+ Requires an API key with the opt-in `automations:read` / `automations:write` scopes (Settings > API, "Automations" on the key).
223
+
207
224
  ## Environment Variables
208
225
 
209
226
  | Variable | Required | Description |
210
227
  |----------|----------|-------------|
211
- | `OMNISOCIALS_API_KEY` | Yes | Your API key (`omsk_live_*` or `omsk_test_*`). Comma-separated for multiple workspaces. |
228
+ | `OMNISOCIALS_API_KEY` | Yes | Your API key (`omsk_live_*`; there are no test keys). Comma-separated for multiple workspaces. |
212
229
  | `OMNISOCIALS_BASE_URL` | No | Custom API base URL (defaults to production) |
213
230
 
214
231
  ## Alternative: Agent Skills
@@ -227,6 +244,87 @@ Full API docs: [docs.omnisocials.com](https://docs.omnisocials.com)
227
244
 
228
245
  ## Changelog
229
246
 
247
+ ### 1.46.0 (2026-10-23)
248
+
249
+ - **Added: comment-to-DM on a Facebook Page.** The automation tools now run on the connected Facebook Page as well as on Instagram. `create_automation` takes `platform` (`instagram`, the default, or `facebook`) and `facebook_post_id` (a Page post published outside OmniSocials, `{page-id}_{post-id}` or the post id alone; it implies `facebook`). On Facebook the DM lands in Messenger, `triggers` takes `comment` and `dm` (a keyword in a Messenger message to the Page), and `story_reply` is refused (Facebook has no story replies). Meta's rules are the same on both: one private reply per comment within 7 days, a follow-up only after the person answers. `list_automations` takes a `platform` filter and shows a Platform column; `get_automation` and `create_automation` print the platform. `update_automation` takes `facebook_post_id` to move a Facebook automation to another Page post; the platform itself cannot change (`platform_immutable`). `get_automation_activity` shows a Facebook person by name, without an @. Same on the companion server (`mcp.omnisocials.com`).
250
+ - **Added: `platforms` on `comment_dm_automation`.** `create_post`, `create_and_publish_post` and `update_post` take `comment_dm_automation.platforms`: `["instagram"]`, `["facebook"]` or both, one automation per channel with the same keywords, DM, public reply and limits. Each listed channel must be a channel of the post. Without it, the post gets one automation: Instagram when the post goes there, else Facebook. On `update_post` a channel no longer listed loses its automation, and `null` removes them all. The post shows every automation id with its channel; `get_post` shows them for a post that goes to Instagram or Facebook.
251
+ - **Added: Discord.** `discord` is a new channel ID: one Discord channel the user connected in OmniSocials (with Continue with Discord, or by pasting a webhook URL). The post is sent through the channel's webhook, so it shows the webhook's name and picture. `create_post`, `create_and_publish_post` and `update_post` take a `discord` object: `silent` (no notification), `link_preview` (`false` hides the link previews), `allow_mentions` (off by default: @everyone, @here and role mentions in the text ping nobody until it is `true`), `spoiler`, `thread_name` (the post title in a forum or media channel, where Discord requires one; max 100 chars), up to 5 `buttons` (`{ text, url }`, labels up to 80 chars, not with a poll) and a native `poll` (`{ options: 2-10 of up to 55 chars, duration_hours: 1-768 (default 24), multiple }`; the post text is the question, max 300 chars; no media). Text up to 2,000 characters; up to 10 images, GIFs and videos per message, which may mix; a video or GIF over Discord's 20 MB file limit is sent as a link in the message (listed in `result.video_links`); no Stories; a Reel is sent as one video message. On `update_post` every key merges into the stored options and `null` removes it. `get_post` prints the options and, once sent, the link to the message. `list_accounts` and `get_account` show the server, the channel name and type (`platform_details.channel_name` and `channel_type`: read by the OmniSocials bot, which Continue with Discord adds with no permissions, or given by the person when connecting), the webhook name, the channel id, how it was connected and the channel link; with the bot in the server, `get_account_analytics` shows the member count as followers. Discord gives webhooks no view counts: `get_post_analytics` shows the reactions (total and per emoji) and the poll votes. Same on the companion server (`mcp.omnisocials.com`), whose version is 1.46.0 too.
252
+
253
+ ### 1.45.0 (not published; included in 1.46.0)
254
+
255
+ - **Added: a link to the post on every post tool.** `publish_post`, `retry_post`, `approve_post` and `reject_post` now end with the "Open in OmniSocials" row that `create_post`, `create_and_publish_post`, `get_post` and `update_post` already had, and `publish_post` says that `get_post` lists the live links once each platform has posted. Every app link carries `?ref=mcp`, so OmniSocials can count the visits from an agent's answer (the app ignores the parameter).
256
+ - **Added on the companion server (`mcp.omnisocials.com`): the post card.** In apps that show MCP Apps (Claude, ChatGPT, Cursor, VS Code), the results of `get_post`, `create_post`, `create_and_publish_post`, `update_post`, `publish_post`, `retry_post`, `approve_post` and `reject_post` appear as a card: the post the way each platform will show it, a channel list with each channel's state (the planned time, Posting, Posted with its live link, or Failed with the reason), and a link into OmniSocials. While a post publishes, the card follows it until every channel is done. Other clients (terminal agents, automations) get the same text answer as before. The card is built as an MCP App resource, `ui://omnisocials/post-card.html`; a new tool, `get_post_status`, is only for the card (hidden from the model in apps that support MCP Apps).
257
+
258
+ ### 1.44.0 (2026-10-17, not published; included in 1.46.0)
259
+
260
+ - **Added: Telegram.** `telegram` is a new channel ID: a Telegram channel or group the user connected in OmniSocials, where the OmniSocials bot is an admin that can post. `create_post`, `create_and_publish_post` and `update_post` take a `telegram` object: `silent`, `link_preview`, `pin`, `protect_content`, `spoiler`, `caption_above_media`, up to 3 `buttons` (`{ text, url }`, one per row, not on an album or a poll), a native `poll` (`{ options: 2-12 of up to 100 chars, open_period: 5-2628000 seconds, multiple }`; the post text is the question, max 300 chars; no media) and `first_comment` (a reply under the post). Text up to 4,096 characters; with media, text over 1,024 characters is sent as a reply under the media. Albums hold up to 10 photos and videos; a GIF goes on its own; files up to 50 MB; no Stories; a Reel is sent as a video message. On `update_post` every key merges into the stored options and `null` removes it. `get_post` prints the options and, once sent, the message count, the pin and the link. `list_accounts` and `get_account` show the chat type, the subscribers and the link. Telegram gives bots no view counts: `get_post_analytics` shows the reactions (total and per emoji) and the poll votes. Same on the companion server (`mcp.omnisocials.com`).
261
+ - **Changed: the three version strings match again.** `package.json`, the stdio server and the companion server all say 1.44.0 (the stdio server said 1.42.0 and the companion server 1.41.0 after the last releases).
262
+
263
+ ### 1.43.0 (2026-10-12, not published; included in 1.46.0)
264
+
265
+ - **Polls on X, Threads and Mastodon.** `create_post`, `create_and_publish_post` and `update_post` take `poll` under `x`, `threads` and `mastodon` (the post text is the question): X `{ options: 2-4 of up to 25 chars, duration_minutes: 5-10080 }`, Threads `{ options: 2-4 of 1-25 chars }` (closed by Threads after 24 hours), Mastodon `{ options, expires_in, multiple, hide_totals }` within the instance's limits. The API refuses a channel with a poll and media; on a thread the poll goes on the first part. `null` on `update_post` removes it. `get_post` prints the poll per channel.
266
+
267
+ ### 1.42.0 (not published; included in 1.46.0)
268
+
269
+ - **Added: comment-to-DM automations.** Six new tools manage the Instagram comment-to-DM automations of the workspace: `list_automations`, `get_automation`, `create_automation`, `update_automation` (pause, resume, new keywords, new DM text, new limits, another post), `delete_automation` and `get_automation_activity` (one row per comment or message the automation acted on: the DM sent, a skip with its reason, or Instagram's error). An automation DMs a person once when they comment a keyword on a post, and can post a public reply under the comment; `triggers` also takes `dm` (a keyword in a DM to the account) and `story_reply` (a keyword in a reply to a story). `scope` is one post (`post_id` = an OmniSocials post, scheduled or live; `instagram_media_id` = a post published outside OmniSocials, read from Instagram to check it belongs to the account), `next_post` (the next Instagram post the workspace publishes) or `any` (every post). Backed by the new `/automations` endpoints, which need the opt-in API key scopes `automations:read` and `automations:write` (Settings > API, "Automations" on the key); the tools say so on a 403. Same on the companion server (`mcp.omnisocials.com`), where the OAuth session needs the `create_schedule_post` permission for the writes.
270
+ - **Added: `comment_dm_automation` on `create_post`, `create_and_publish_post` and `update_post`.** A comment-to-DM automation bound to the new post (Instagram must be a channel): `{ trigger: { keywords, match }, actions: { dm: { text }, public_reply: { enabled, variations } }, limits: { max_sends, stop_at } }`. The automation starts listening once the post is live on Instagram. On `update_post` the object replaces the post's automation and `null` removes it. The created automation comes back as `comment_dm_automation` on the post, and `get_post` shows it for a post that goes to Instagram. Same on the companion server.
271
+
272
+ ### 1.41.0 (not published; included in 1.46.0)
273
+
274
+ - **Added: image fit per channel (`media_fit`).** A feed image whose ratio is outside what a channel shows uncropped is handled per channel at create time. Instagram shows feed images between 4:5 and 1.91:1: a 9:16 (story-style) or very wide image posted to the Instagram feed is now cropped to 4:5 / 1.91:1 around the subject by default, because Instagram cut it to that window anyway (a blind centre crop) and Meta cannot boost a feed post outside it. Pinterest keeps a Pin taller than 2:3 and reports it. `create_post`, `create_and_publish_post` and `update_post` take `media_fit: { instagram: "auto" | "crop" | "keep", pinterest: ... }`; every decision comes back as a `warnings` entry with code `image_fit` (`action` cropped / kept / crop_failed, the crop's url, and how to undo it). The crop is a separate file for that channel only; `default` and the other channels keep the original; stories, reels and videos are never cropped. On `update_post`, `media_fit` alone re-decides the stored images (`keep` gives the original back). Same on the companion server (`mcp.omnisocials.com`).
275
+ - **Changed: `check_media_compatibility` reports image fit.** For an image whose size is known (a library file of the workspace) the answer lists, per connected channel with a rule, whether its feed shows the image uncropped and what a post does by default, so an assistant can ask "crop to 4:5 or keep the original?" before it posts. Media items (`list_media`, `get_media`, uploads) now carry `width` and `height`.
276
+
277
+ ### 1.40.0 (not published; included in 1.46.0)
278
+
279
+ - **Changed: the MCP SDK dependency moves to 1.32.1.** GHSA-6qxp-vccf-f47h (CVE-2026-104850) is in the SDK's OAuth client, which let an MCP server name the authorization server that received the client's credentials. This package is a server and talks to OmniSocials with an API key, so it was not affected; the move clears the advisory for everyone who installs it.
280
+
281
+ ### 1.39.1 (2026-10-06, not published; included in 1.46.0)
282
+
283
+ - **Changed: `list_pinterest_products` names an account without a catalog.** Pinterest answers HTTP 409 ("Can't access this feature without an existing catalog") when the connection has catalog access but the account has no catalog yet. The API now returns `no_catalog: true` with empty `products` and `product_groups` instead of a `platform_error`, and the tool says where the user adds products (https://www.pinterest.com/business/catalogs/). Seen live on 2026-10-06, the first day of the feature.
284
+
285
+ ### 1.39.0 (2026-10-05)
286
+
287
+ - **Added: tag products on a Pinterest Pin.** `pinterest.product_tags` on `create_post`, `create_and_publish_post` and `update_post` takes up to 24 product Pins of the connected Pinterest account (Pin IDs or Pin links), so people can shop the items in the image. Pinterest accepts a product only when its Pin is public, belongs to the same account and links to a website that account claimed; products of other merchants cannot be tagged. The tags are added right after the Pin is published. A product that Pinterest refuses never fails the post: the other products are still tagged, and `get_post` shows the outcome (`product_tags_result`, with Pinterest's reason for each product that was not tagged).
288
+ - **Added: `list_pinterest_products`.** New tool that lists the product Pins of the connected Pinterest account, with the `pin_id` to use in `pinterest.product_tags`. `source` `catalog` reads the Pinterest catalog (price and stock included) and needs catalog access, which the user gives one time in the OmniSocials composer (Pinterest options, Add products, Connect catalog). `source` `pins` reads the account's own Pins and works on every connection. `check_pin` checks one Pin ID or Pin link instead of listing.
289
+
290
+ ### 1.38.0 (2026-10-02)
291
+
292
+ - **Added: read the approval review of a post.** New tool `get_post_approval` returns the review of a post that went through an approval workflow: every step with its approvers and their decisions, the rejection (who, when and the reason) and the comment thread, oldest first. Until now the tools only gave the status, so an assistant saw `rejected` and not who rejected the post or why. Use it on a rejected post to learn what to change before sending it again, or on a post that waits for approval to see who it waits for and what the reviewers wrote. Read-only. A post without an approval workflow returns status `none`. Backed by `GET /posts/{id}/approval` (scope `posts:read`). Same on the companion server (`mcp.omnisocials.com`).
293
+ - **Added: webhook events `post.approved` and `post.rejected`.** `create_webhook` and `update_webhook` take them next to `post.scheduled`, `post.published` and `post.failed`. `post.approved` fires when the last step of a post's approval workflow is approved (the post moves to `scheduled`, or `posting` when its publish time already passed). `post.rejected` fires when an approver rejects the post (it will not publish). Both carry one more object, `data.approval`: `{ status, decided_by, reason }` (`reason` is `null` on an approval and when the approver gave none), and their `targets` array is empty. Same on the companion server.
294
+ - **Changed: more approval emails.** A rejection, a reviewer comment, a reviewer edit and the final approval now email the person who requested the approval, and the requester's reply in the review thread emails the reviewers. A post that still waits for approval gets automatic emails near its publish time: the reviewers 24 hours before, and the reviewers plus the requester when the time passes (the post did not publish; a late approval publishes it at once). No tool input or output changed for this.
295
+ - **Changed: X link-post credits count only `http://` and `https://` links.** A post that names a website without a scheme (`brand.com`, `www.brand.com`) no longer uses credits and no longer returns the `x_url_post_credits` warning, because X does not bill it as a link post. A post with a full link still uses 20 credits. The descriptions of `create_post` and `create_and_publish_post` say which links count. Same on the companion server.
296
+
297
+ ### 1.37.1 (2026-09-30)
298
+
299
+ - **Changed: Facebook Reels longer than 90 seconds are accepted.** The API no longer refuses a Facebook Reel over 90 seconds at submit: a Reel now follows the 240 minute ceiling of a Facebook feed video. Meta removed the Reel length limit in June 2025 (every Facebook video is a reel now), while its Reels Publishing API guide still lists 3 to 90 seconds. A 3 minute Reel published through the API on 2026-09-30, and Reels of 91 to 117 seconds published before the cap existed. The `create_post` and `create_and_publish_post` descriptions no longer tell the assistant to keep a Facebook Reel under 90 seconds. If Facebook refuses a length at publish time, the Facebook target fails with "Facebook did not accept the length of this video..." and the video can go out as a regular Facebook post (`type: "post"`). Same on the companion server.
300
+ - **Changed: `video_cover` on YouTube Shorts is described as channel-dependent.** YouTube shows the cover on Shorts only on channels where it has enabled custom Shorts thumbnails (rolling out since July 2026, Partner Program channels first); on other channels it stores the cover as the default thumbnail but shows a frame from the video (checked on 15 published Shorts from 5 channels: 3 channels show it, 1 does not). The `video_cover` and `youtube` descriptions now say so, so the assistant says it depends on the channel instead of promising it. The image is now sent at up to 1080 x 1920, YouTube's Shorts thumbnail size. Same on the companion server.
301
+ - **Companion server: `video_cover` added.** The hosted server now accepts `video_cover` on `create_post`, `create_and_publish_post` and `update_post` (`null` removes it), takes `facebook.thumbnail_type` / `thumb_offset` / `cover_url`, and lists the cover under Platform Options in `get_post`. This package has had them since 1.35.0.
302
+
303
+ ### 1.37.0 (2026-09-28)
304
+
305
+ - **Added: the original PDF is kept, and LinkedIn receives it.** `upload_media` (url, base64 and the `upload_url` snippet) still splits a PDF into one image slide per page (max 20) and now also keeps the original file. When a LinkedIn post's `media_ids` are exactly that PDF's slides, in order and unchanged, LinkedIn receives the original file as the document: text stays sharp, in-document links work, viewers download the real file, and every page is included even past the 20-slide cap. The upload result names the kept file (`pdf.id`, `pdf.name`, `pdf.url`, `pdf.size_bytes`), the tool output says so, and every slide returned by `list_media` / `get_media` carries `pdf: { id, name, url, page, total_pages, rendered_pages }`. Same on the companion server (`mcp.omnisocials.com`).
306
+ - **Added: `pdf_mode` on `upload_media`.** `"slides"` (default) keeps one image media item per page, one Media ID each. `"document"` keeps the PDF as ONE media item (`type: "document"`, its page images in `pdf.pages`); pass its single Media ID in `media_ids` and the post gets every page, in order. The tool output explains which shape came back, and `list_media` shows document items as "Document (PDF, N pages)". Same on the companion server (`mcp.omnisocials.com`), where the `upload_url` snippet takes it as a form field (`-F pdf_mode=document`).
307
+ - **Added: `document_source` on the `linkedin` / `linkedin_page` options** of `create_post`, `create_and_publish_post` and `update_post`. `"original_pdf"` (default) sends the kept original when the images are its unchanged pages; `"slides"` rebuilds the document from the slide images instead (a flattened copy). On `update_post`: `"slides"` sets it, `"original_pdf"`/`null` reverts, omitted keeps the current value. `get_post` prints "document source: rebuilt from the slide images" when a channel opted out. Ignored when the images are not a PDF's pages, when `carousel_as_images` is true, and for 0-1 images, videos and polls. Same on the companion server.
308
+
309
+ ### 1.36.0 (2026-09-24)
310
+
311
+ - **Added: `reply_to_inbox` takes `message_id`.** Every comment on a post shares one inbox conversation, and the reply endpoint always answered the newest incoming comment on that post, so a reply drafted for one person could land under a different person's comment. Pass `message_id` (the "Message to answer" `#id` from `get_next_unanswered`, or a `#id` from `get_inbox_conversation`) and the reply goes under exactly that comment. Without it the old behaviour is kept for compatibility (newest comment). Ignored for DMs. A `404 not_found` means the id is not an incoming message of that conversation. The tool description and the `get_next_unanswered` output tell the assistant to always pass it for comments. Same on the companion server (`mcp.omnisocials.com`).
312
+ - **Changed: `get_next_unanswered` serves DMs first and no longer drops expired ones.** A customer's Facebook DM ("How do I buy and where please") was never served: DMs older than 24 hours were filtered out and the queue was oldest-first across everything, so a fresh DM sat behind a three-week comment backlog until Meta's reply window closed. The order is now: Instagram/Facebook DMs inside Meta's 24-hour window (the one closing soonest first) and X DMs, then Instagram/Facebook DMs whose window has closed, then comments and mentions oldest first (`order: "newest"` reverses within each group). Each item carries `reply_window: { open, closes_at }` (only Instagram/Facebook DMs have a window; everything else is `open: true, closes_at: null`). When `open` is false the tool prints a warning that OmniSocials cannot reply and that the user must answer from the Instagram or Facebook app (mirrored into the inbox) or skip with `mark_inbox_read`. `list_inbox_conversations` with `unanswered: true` now includes those expired DM conversations too. Same on the companion server.
313
+ - **Changed: `reply_to_inbox` checks the 24-hour window before sending.** A `422 outside_messaging_window` on an Instagram/Facebook DM is now raised before the platform call (it was only mapped from Meta's error afterwards). Nothing is sent, the message names when the window closed, and the tool description tells the assistant not to retry. Same on the companion server.
314
+ - **Changed: `hide_inbox_comment` explains a 502 `hide_not_applied`.** After an Instagram hide or unhide the API reads the comment back and reports Instagram's own `hidden` state. When Instagram accepted the call but the comment is still in the old state, the API answers 502 `hide_not_applied` and the inbox row is left unchanged (a hide had been reported as done while the comment stayed public). This happens with comments Instagram shows under "Comments from Facebook" on a reel that is also shared to Facebook: they live on Facebook and Instagram's hide does not reach them. The tool description tells the assistant to have the user hide it in the Instagram or Facebook app and not retry. Same on the companion server.
315
+ - **Changed: Facebook `reach` is absent, never 0, in `get_post_analytics` and `get_bulk_post_analytics`.** Meta retired Facebook post reach on 2026-06-15 and the replacement metric it documents (`post_total_media_view_unique`) returns 0 for every Page post while views and clicks keep counting, so the API had stored and returned a false 0 for every Facebook post. The key is now left out, and a row collected before 2026-09-27 that still carries `reach: 0` is dropped by the API. The tool description tells the assistant to treat a missing `reach` as "not reported" and to sum reach only over the platforms that carry it (Instagram, LinkedIn, TikTok with the Business API). Same on the companion server (`mcp.omnisocials.com`).
316
+
317
+ ### 1.35.1 (2026-09-22)
318
+
319
+ - **Changed: `reply_to_inbox` explains a 422 `reply_not_allowed`.** The API answers 422 instead of 502 when the platform refuses replies to an item for good (Facebook `#1705`: comment on a share or boosted copy of the post, or the commenter restricts replies; YouTube: the thread's `canReply` is false). The item is dropped from `get_next_unanswered` automatically; relay it, do not retry.
320
+ - **Changed: `hide_inbox_comment` explains a 422 `cannot_hide`.** Facebook does not let the Page hide some comments (the commenter blocked the Page, their account is deactivated or restricted, or the comment sits on a shared copy of the post); the API now reads the comment back and answers 422 when Facebook's `can_hide` is false, instead of a 502 on every attempt. Retrying never changes it; use `mark_inbox_read` if the item should stop coming up.
321
+
322
+ ### 1.35.0 (2026-09-17)
323
+
324
+ - **Added: video covers.** `create_post`, `create_and_publish_post` and `update_post` accept `video_cover` for a post whose media is one video: `type: "frame"` with `thumb_offset` (milliseconds into the video) or `type: "custom"` with `cover_url` (JPEG/PNG), plus per-platform `overrides` keyed by platform id (instagram, facebook, linkedin, linkedin_page, tiktok, pinterest, youtube). On `update_post`, `video_cover: null` removes the stored cover. `get_post` shows the cover in use.
325
+ - **Changed: analytics say "not reported" instead of 0.** Metrics a platform never reports at post level (reach on X, saves on Facebook, link clicks on Instagram feed posts, and so on) render as N/A with the reason, and `reach` is null when the platform did not report it, so a zero is never invented.
326
+ - **Changed: `reply_to_inbox` explains a 409 `duplicate_reply`.** The API now refuses the same reply (same text and attachment) sent twice to one comment, or to one DM conversation, within 10 minutes; the tool description tells the assistant that nothing was sent again and to continue with `get_next_unanswered` rather than resend. Guards against a re-invoked tool call after a slow response (a customer's Instagram comment got the identical reply twice, 21 seconds apart).
327
+
230
328
  ### 1.34.1 (2026-09-14)
231
329
 
232
330
  - **Changed: Threads threads and the Threads inbox are no longer "rolling out".** Meta approved the reply, read-replies and mentions permissions, so `threads.thread_parts` and the Threads inbox work for every Threads connection; accounts connected before 2026-09-14 need a one-time reconnect under Settings -> Organisation -> Workspaces (`400`/`401` errors name the reconnect). Threads location tagging stays rolling out (not approved yet). Same on the companion server.
@@ -320,7 +418,7 @@ Full API docs: [docs.omnisocials.com](https://docs.omnisocials.com)
320
418
  ### 1.16.0
321
419
 
322
420
  - **Hashtag sets:** save reusable, named groups of hashtags per workspace and apply one to a new post in a single call. Four new tools: `list_hashtag_sets`, `create_hashtag_set`, `update_hashtag_set`, `delete_hashtag_set`.
323
- - `create_post` / `create_and_publish_post` accept `hashtag_set`, `hashtag_placement` (`caption_append` default, or `first_comment`), and `hashtag_platforms`. The set expands server-side once at create time — editing a set later never changes existing posts; duplicate tags are skipped; Instagram's 30-hashtag cap fails fast with `hashtag_limit_exceeded`.
421
+ - `create_post` / `create_and_publish_post` accept `hashtag_set`, `hashtag_placement` (`caption_append` default, or `first_comment`), and `hashtag_platforms`. The set expands server-side once at create time — editing a set later never changes existing posts; duplicate tags are skipped; Instagram's 5-hashtag cap fails fast with `hashtag_limit_exceeded`.
324
422
  - Same tools on the companion server (`mcp.omnisocials.com`).
325
423
 
326
424
  ### 1.15.0
package/build/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Account, AnalyticsOverview, AccountAnalyticsEntry, ApiPost, ApiResponse, ApiVideoCover, AudioSearchResult, BestTimesResult, HashtagSet, LocationSearchResult, MediaCheckResult, MediaCompatibility, MediaFolder, MediaItem, PdfUploadResult, PostAnalytics, PublishPostResult, RecentPlatformPost, RetryPostResult, ApprovePostResult, RejectPostResult, InboxConversation, InboxDeleteResult, InboxMessage, InboxNextUnanswered, Webhook } from "./types.js";
1
+ import type { Account, AnalyticsOverview, AccountAnalyticsEntry, ApiPost, ApiResponse, ApiVideoCover, AudioSearchResult, Automation, AutomationEvent, AutomationEventsResult, AutomationInput, BestTimesResult, CommentDmAutomationInput, HashtagSet, LocationSearchResult, PinterestProductCheck, PinterestProductsResult, MediaCheckResult, MediaCompatibility, MediaFolder, MediaItem, PdfUploadResult, PostAnalytics, PublishPostResult, RecentPlatformPost, RetryPostResult, ApprovePostResult, RejectPostResult, PostApproval, InboxConversation, InboxDeleteResult, InboxMessage, InboxNextUnanswered, Webhook } from "./types.js";
2
2
  export declare function fetchImageAsBase64(url: string): Promise<{
3
3
  data: string;
4
4
  mimeType: string;
@@ -44,10 +44,27 @@ export interface XThreadPartInput {
44
44
  * Entries may be `{ url, alt }` to attach alt text (photos/GIFs only). */
45
45
  media_urls?: MediaUrlEntry[];
46
46
  }
47
+ /**
48
+ * A poll on an X, Threads or Mastodon post (the post text is the question).
49
+ * X: 2 to 4 options of up to 25 characters, duration_minutes 5 to 10080
50
+ * (default 1440). Threads: 2 to 4 options of 1 to 25 characters, closed by
51
+ * Threads after 24 hours. Mastodon: options and expires_in (seconds) within
52
+ * the instance's limits, plus multiple and hide_totals. No media together
53
+ * with a poll; on a thread the poll goes on the first part. On update,
54
+ * `null` removes the poll.
55
+ */
56
+ export interface ChannelPollInput {
57
+ options: string[];
58
+ duration_minutes?: number;
59
+ expires_in?: number;
60
+ multiple?: boolean;
61
+ hide_totals?: boolean;
62
+ }
47
63
  export interface XPostOptions {
48
64
  reply_settings?: "" | "following" | "mentionedUsers";
49
65
  paid_partnership?: boolean;
50
66
  made_with_ai?: boolean;
67
+ poll?: ChannelPollInput | null;
51
68
  /** Provide 2–25 parts to publish as a thread. Omit for a single tweet. */
52
69
  thread_parts?: XThreadPartInput[];
53
70
  }
@@ -93,6 +110,7 @@ export interface MastodonThreadPartInput {
93
110
  media_urls?: MediaUrlEntry[];
94
111
  }
95
112
  export interface MastodonPostOptions {
113
+ poll?: ChannelPollInput | null;
96
114
  /** Provide 2–25 parts to publish as a thread. Omit for a single status. */
97
115
  thread_parts?: MastodonThreadPartInput[];
98
116
  }
@@ -101,8 +119,96 @@ export interface MastodonPostOptions {
101
119
  * single status); `undefined` leaves the existing thread untouched.
102
120
  */
103
121
  export interface MastodonPostOptionsUpdate {
122
+ poll?: ChannelPollInput | null;
104
123
  thread_parts?: MastodonThreadPartInput[] | null;
105
124
  }
125
+ export interface TelegramButtonInput {
126
+ /** Button label, 1 to 64 characters. */
127
+ text: string;
128
+ /** https://, http:// or tg:// link. */
129
+ url: string;
130
+ }
131
+ export interface TelegramPollInput {
132
+ /** 2 to 12 answer options of up to 100 characters, all different. */
133
+ options: string[];
134
+ /** Seconds the poll stays open (5 to 2628000). Omit to keep it open. */
135
+ open_period?: number;
136
+ /** Allow several answers per person. */
137
+ multiple?: boolean;
138
+ }
139
+ export interface TelegramPostOptions {
140
+ silent?: boolean | null;
141
+ link_preview?: boolean | null;
142
+ pin?: boolean | null;
143
+ protect_content?: boolean | null;
144
+ spoiler?: boolean | null;
145
+ caption_above_media?: boolean | null;
146
+ /** Up to 3 link buttons, one per row. Not on an album or a poll. */
147
+ buttons?: TelegramButtonInput[] | null;
148
+ /** A native poll; the post text is the question (max 300 characters). */
149
+ poll?: TelegramPollInput | null;
150
+ /** A reply under the post, sent right after it is published. */
151
+ first_comment?: string;
152
+ }
153
+ /** What GET /posts/{id} returns under `telegram` once the post went out. */
154
+ export interface TelegramPostResult {
155
+ chat_id: string | null;
156
+ message_ids: string[];
157
+ pinned: boolean;
158
+ pin_error?: string;
159
+ text_message_id?: string;
160
+ link_public: string | null;
161
+ link_private: string | null;
162
+ sent_at: string | null;
163
+ }
164
+ export interface DiscordButtonInput {
165
+ /** Button label, 1 to 80 characters. */
166
+ text: string;
167
+ /** https:// or http:// link. */
168
+ url: string;
169
+ }
170
+ export interface DiscordPollInput {
171
+ /** 2 to 10 answer options of up to 55 characters, all different. */
172
+ options: string[];
173
+ /** Hours the poll stays open (1 to 768). Default 24. */
174
+ duration_hours?: number;
175
+ /** Allow several answers per person. */
176
+ multiple?: boolean;
177
+ }
178
+ export interface DiscordPostOptions {
179
+ /** Send without a push or desktop notification (@silent). */
180
+ silent?: boolean | null;
181
+ /** false hides the previews of the links in the text. */
182
+ link_preview?: boolean | null;
183
+ /** true lets @everyone, @here, role and user mentions ping. Off by default. */
184
+ allow_mentions?: boolean | null;
185
+ /** Blur every image and video until someone clicks it. */
186
+ spoiler?: boolean | null;
187
+ /** Post title in a forum or media channel (required there), max 100 characters. */
188
+ thread_name?: string | null;
189
+ /** Up to 5 link buttons in one row. Not with a poll. */
190
+ buttons?: DiscordButtonInput[] | null;
191
+ /** A native poll; the post text is the question (max 300 characters). */
192
+ poll?: DiscordPollInput | null;
193
+ }
194
+ /** What GET /posts/{id} returns under `discord` once the message went out. */
195
+ export interface DiscordPostResult {
196
+ webhook_id: string | null;
197
+ guild_id: string | null;
198
+ channel_id: string | null;
199
+ /** The forum post the message opened, in a forum or media channel. */
200
+ thread_id: string | null;
201
+ message_id: string | null;
202
+ /** discord.com/channels/<server>/<channel or thread>/<message>. */
203
+ url: string | null;
204
+ sent_at: string | null;
205
+ /** Media sent as a link in the text (videos and GIFs over 20 MB). */
206
+ video_links: string[];
207
+ /** A second message with the media links, when they did not fit in the text. */
208
+ link_message_id: string | null;
209
+ /** Codes such as "media_sent_as_links" or "links_sent_as_follow_up". */
210
+ warnings: string[];
211
+ }
106
212
  export interface ThreadsThreadPartInput {
107
213
  /** Part text, 1 to 500 characters. */
108
214
  text: string;
@@ -114,6 +220,7 @@ export interface ThreadsThreadPartInput {
114
220
  media_urls?: MediaUrlEntry[];
115
221
  }
116
222
  export interface ThreadsPostOptions {
223
+ poll?: ChannelPollInput | null;
117
224
  /** Provide 2 to 25 parts to publish as a thread. Omit for a single post.
118
225
  * When set, the Threads caption is taken from part 1. */
119
226
  thread_parts?: ThreadsThreadPartInput[];
@@ -128,6 +235,7 @@ export interface ThreadsPostOptions {
128
235
  * single post); `undefined` leaves the existing thread untouched.
129
236
  */
130
237
  export interface ThreadsPostOptionsUpdate {
238
+ poll?: ChannelPollInput | null;
131
239
  thread_parts?: ThreadsThreadPartInput[] | null;
132
240
  /** New Threads location id, or null to remove the tag. Omit to leave the
133
241
  * existing tag untouched. */
@@ -152,6 +260,10 @@ export interface CreatePostInput {
152
260
  content: string | Record<string, string>;
153
261
  channels?: string[];
154
262
  scheduled_at?: string;
263
+ /** With scheduled_at: keep the time but store the post as a draft (planned draft). */
264
+ draft?: boolean;
265
+ /** Image fit per channel: { instagram: "auto" | "crop" | "keep" }. */
266
+ media_fit?: Record<string, "auto" | "crop" | "keep">;
155
267
  media_ids?: MediaIdEntry[] | Record<string, MediaIdEntry[]>;
156
268
  media_urls?: MediaUrlEntry[] | Record<string, MediaUrlEntry[]>;
157
269
  type?: string;
@@ -183,11 +295,15 @@ export interface CreatePostInput {
183
295
  mastodon?: MastodonPostOptions;
184
296
  threads?: ThreadsPostOptions;
185
297
  google_business?: Record<string, unknown>;
298
+ telegram?: TelegramPostOptions | null;
299
+ discord?: DiscordPostOptions | null;
186
300
  hashtag_set?: string;
187
301
  hashtag_placement?: "caption_append" | "first_comment";
188
302
  hashtag_platforms?: string[];
189
303
  /** Route the post through a saved approval workflow (GET /approval-workflows). */
190
304
  approval_workflow_id?: string;
305
+ /** Comment-to-DM automations bound to this post (Instagram or Facebook must be a channel). `null` removes them on update. */
306
+ comment_dm_automation?: CommentDmAutomationInput | null;
191
307
  }
192
308
  export interface ApprovalWorkflowApprover {
193
309
  id: string;
@@ -234,6 +350,8 @@ export declare class OmniSocialsClient {
234
350
  updatePost(id: string, data: {
235
351
  content?: string | Record<string, string>;
236
352
  scheduled_at?: string;
353
+ /** false = schedule a planned draft at its stored time; true = unschedule, keep the time. */
354
+ draft?: boolean;
237
355
  channels?: string[];
238
356
  media_ids?: MediaIdEntry[] | Record<string, MediaIdEntry[]>;
239
357
  media_urls?: MediaUrlEntry[] | Record<string, MediaUrlEntry[]>;
@@ -258,23 +376,34 @@ export declare class OmniSocialsClient {
258
376
  linkedin?: Record<string, unknown>;
259
377
  linkedin_page?: Record<string, unknown>;
260
378
  linkedin_poll?: LinkedInPollInput | null;
379
+ comment_dm_automation?: CommentDmAutomationInput | null;
261
380
  tiktok?: Record<string, unknown>;
262
381
  x?: XPostOptionsUpdate;
263
382
  bluesky?: BlueskyPostOptionsUpdate;
264
383
  mastodon?: MastodonPostOptionsUpdate;
265
384
  threads?: ThreadsPostOptionsUpdate;
266
385
  google_business?: Record<string, unknown>;
386
+ telegram?: TelegramPostOptions | null;
387
+ discord?: DiscordPostOptions | null;
267
388
  }): Promise<ApiResponse<ApiPost>>;
268
389
  deletePost(id: string): Promise<ApiResponse<unknown>>;
269
390
  publishPost(id: string): Promise<ApiResponse<PublishPostResult>>;
270
391
  retryPost(id: string): Promise<ApiResponse<RetryPostResult>>;
271
392
  approvePost(id: string): Promise<ApiResponse<ApprovePostResult>>;
272
393
  rejectPost(id: string, comment?: string): Promise<ApiResponse<RejectPostResult>>;
394
+ getPostApproval(id: string): Promise<ApiResponse<PostApproval>>;
273
395
  searchLocations(query?: string, platform?: "instagram" | "threads", coords?: {
274
396
  latitude?: number;
275
397
  longitude?: number;
276
398
  }): Promise<LocationSearchResult>;
277
399
  validateLocation(id: string): Promise<ApiResponse<unknown>>;
400
+ listPinterestProducts(params?: {
401
+ source?: "catalog" | "pins";
402
+ product_group_id?: string;
403
+ bookmark?: string;
404
+ page_size?: string;
405
+ }): Promise<PinterestProductsResult>;
406
+ validatePinterestProduct(id: string): Promise<PinterestProductCheck>;
278
407
  searchInstagramAudio(query?: string, type?: "music" | "original_sound"): Promise<AudioSearchResult>;
279
408
  listMedia(params?: {
280
409
  limit?: string;
@@ -287,6 +416,7 @@ export declare class OmniSocialsClient {
287
416
  filename?: string;
288
417
  name?: string;
289
418
  folder?: string;
419
+ pdf_mode?: "slides" | "document";
290
420
  }): Promise<ApiResponse<MediaItem> & Partial<PdfUploadResult> & {
291
421
  compatibility?: MediaCompatibility;
292
422
  message?: string;
@@ -297,6 +427,7 @@ export declare class OmniSocialsClient {
297
427
  filename?: string;
298
428
  name?: string;
299
429
  folder?: string;
430
+ pdf_mode?: "slides" | "document";
300
431
  }): Promise<ApiResponse<MediaItem> & Partial<PdfUploadResult> & {
301
432
  compatibility?: MediaCompatibility;
302
433
  message?: string;
@@ -333,6 +464,20 @@ export declare class OmniSocialsClient {
333
464
  hashtags?: string[];
334
465
  }): Promise<ApiResponse<HashtagSet>>;
335
466
  deleteHashtagSet(id: string): Promise<ApiResponse<unknown>>;
467
+ listAutomations(params?: {
468
+ status?: string;
469
+ post_id?: string;
470
+ platform?: string;
471
+ }): Promise<ApiResponse<Automation[]>>;
472
+ getAutomation(id: string): Promise<ApiResponse<Automation>>;
473
+ createAutomation(data: AutomationInput): Promise<ApiResponse<Automation>>;
474
+ updateAutomation(id: string, data: AutomationInput): Promise<ApiResponse<Automation>>;
475
+ deleteAutomation(id: string): Promise<ApiResponse<unknown>>;
476
+ listAutomationEvents(id: string, params?: {
477
+ outcome?: string;
478
+ limit?: number;
479
+ offset?: number;
480
+ }): Promise<ApiResponse<AutomationEvent[]> & Partial<Omit<AutomationEventsResult, "data">>>;
336
481
  listAccounts(): Promise<ApiResponse<Account[]> & {
337
482
  workspace_id?: number | string | null;
338
483
  workspace_name?: string | null;
@@ -394,6 +539,7 @@ export declare class OmniSocialsClient {
394
539
  markInboxRead(conversationId: string): Promise<ApiResponse<unknown>>;
395
540
  replyToInboxConversation(conversationId: string, data: {
396
541
  text: string;
542
+ message_id?: string;
397
543
  attachment_url?: string;
398
544
  attachment_type?: string;
399
545
  include_next?: boolean;
package/build/client.js CHANGED
@@ -198,6 +198,13 @@ export function metricRows(m) {
198
198
  }
199
199
  return rows;
200
200
  }
201
+ // Query params as strings, without the undefined ones (request() takes
202
+ // Record<string, string>).
203
+ const queryString = (params) => params
204
+ ? Object.fromEntries(Object.entries(params)
205
+ .filter(([, v]) => v !== undefined && v !== null)
206
+ .map(([k, v]) => [k, String(v)]))
207
+ : undefined;
201
208
  export class OmniSocialsClient {
202
209
  baseUrl;
203
210
  apiKey;
@@ -285,6 +292,12 @@ export class OmniSocialsClient {
285
292
  ...(comment ? { comment } : {}),
286
293
  });
287
294
  }
295
+ // Reads the review of the post's approval workflow: every step with its
296
+ // approvers and their decisions, the rejection with its reason, and the
297
+ // comment thread. A post without a workflow answers 200 with status "none".
298
+ async getPostApproval(id) {
299
+ return this.request("GET", `/posts/${id}/approval`);
300
+ }
288
301
  // Locations (Instagram or Threads place tagging). NOT the standard
289
302
  // envelope; see LocationSearchResult: Instagram's degraded-path `error` is
290
303
  // a plain string next to `data`; Threads answers with `locations` and a
@@ -303,6 +316,24 @@ export class OmniSocialsClient {
303
316
  async validateLocation(id) {
304
317
  return this.request("GET", "/locations/validate", undefined, { id });
305
318
  }
319
+ // Pinterest product Pins (for `pinterest.product_tags`). NOT the standard
320
+ // envelope; see PinterestProductsResult: `products` on success, a
321
+ // { code, message } `error` object when the list could not be read.
322
+ async listPinterestProducts(params) {
323
+ const query = {};
324
+ if (params?.source)
325
+ query.source = params.source;
326
+ if (params?.product_group_id)
327
+ query.product_group_id = params.product_group_id;
328
+ if (params?.bookmark)
329
+ query.bookmark = params.bookmark;
330
+ if (params?.page_size)
331
+ query.page_size = params.page_size;
332
+ return this.request("GET", "/pinterest/products", undefined, query);
333
+ }
334
+ async validatePinterestProduct(id) {
335
+ return this.request("GET", "/pinterest/products/validate", undefined, { id });
336
+ }
306
337
  // Audio (Instagram Reel music). Same nonstandard string-`error` envelope.
307
338
  async searchInstagramAudio(query, type) {
308
339
  const params = {};
@@ -367,6 +398,26 @@ export class OmniSocialsClient {
367
398
  async deleteHashtagSet(id) {
368
399
  return this.request("DELETE", `/hashtag-sets/${id}`);
369
400
  }
401
+ // Comment-to-DM automations (opt-in scopes automations:read / automations:write)
402
+ async listAutomations(params) {
403
+ return this.request("GET", "/automations", undefined, queryString(params));
404
+ }
405
+ async getAutomation(id) {
406
+ return this.request("GET", `/automations/${id}`);
407
+ }
408
+ async createAutomation(data) {
409
+ return this.request("POST", "/automations", data);
410
+ }
411
+ async updateAutomation(id, data) {
412
+ return this.request("PATCH", `/automations/${id}`, data);
413
+ }
414
+ async deleteAutomation(id) {
415
+ return this.request("DELETE", `/automations/${id}`);
416
+ }
417
+ // The events response is not the plain list envelope: { data, counts, pagination }.
418
+ async listAutomationEvents(id, params) {
419
+ return this.request("GET", `/automations/${id}/events`, undefined, queryString(params));
420
+ }
370
421
  // Accounts. Alongside `data`, the list response carries the workspace's
371
422
  // identity top-level (backend/routes/api/v1/accounts.js) — the workspaces
372
423
  // tool reads it to label multi-workspace setups.
package/build/index.js CHANGED
@@ -9,6 +9,7 @@ import { registerAccountTools } from "./tools/accounts.js";
9
9
  import { registerAnalyticsTools } from "./tools/analytics.js";
10
10
  import { registerWebhookTools } from "./tools/webhooks.js";
11
11
  import { registerHashtagSetTools } from "./tools/hashtag-sets.js";
12
+ import { registerAutomationTools } from "./tools/automations.js";
12
13
  import { registerInboxTools } from "./tools/inbox.js";
13
14
  import { registerWorkspaceTools } from "./tools/workspaces.js";
14
15
  const apiKeyEnv = process.env.OMNISOCIALS_API_KEY;
@@ -29,7 +30,7 @@ const sessionState = { activeIndex: 0 };
29
30
  const getActiveClient = () => workspaceClients[sessionState.activeIndex].client;
30
31
  const server = new McpServer({
31
32
  name: "OmniSocials",
32
- version: "1.35.0",
33
+ version: "1.46.0",
33
34
  });
34
35
  // Register all tools - pass getter function so tools always use the active workspace's client
35
36
  registerPostTools(server, getActiveClient);
@@ -38,6 +39,7 @@ registerAccountTools(server, getActiveClient);
38
39
  registerAnalyticsTools(server, getActiveClient);
39
40
  registerWebhookTools(server, getActiveClient);
40
41
  registerHashtagSetTools(server, getActiveClient);
42
+ registerAutomationTools(server, getActiveClient);
41
43
  registerInboxTools(server, getActiveClient);
42
44
  registerWorkspaceTools(server, workspaceClients, sessionState);
43
45
  // Register prompts
@@ -20,9 +20,11 @@ export declare function getImpressions(platform: string, metrics: Metrics): numb
20
20
  export declare function getReach(platform: string, metrics: Metrics): number | null;
21
21
  /** Post-level metrics a platform can never report: platform -> metric key -> reason. */
22
22
  export declare const UNSUPPORTED_POST_METRICS: Record<string, Record<string, string>>;
23
- /** Reason `metricKey` is unavailable on `platform`, or null when it is available. */
24
- export declare function unsupportedMetricReason(platform: string, metricKey: string): string | null;
25
- /** True when the platform can never report `metricKey`; a finite stored value overrides the map. */
23
+ /** Metrics unavailable for some post types only: platform -> content_type -> metric key -> reason. Empty since Facebook reach moved to the platform-wide map. */
24
+ export declare const UNSUPPORTED_POST_METRICS_BY_CONTENT_TYPE: Record<string, Record<string, Record<string, string>>>;
25
+ /** Reason `metricKey` is unavailable on `platform` (refined by `contentType`), or null when it is available. */
26
+ export declare function unsupportedMetricReason(platform: string, metricKey: string, contentType?: string): string | null;
27
+ /** True when the platform can never report `metricKey` for this post; a finite stored value overrides the map. */
26
28
  export declare function isMetricUnsupported(platform: string, metricKey: string, metrics?: Metrics): boolean;
27
29
  /**
28
30
  * Engagement rate as a percentage (0-100), suppressed below the impression