@omnisocials/mcp-server 1.39.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
@@ -112,6 +112,8 @@ 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
 
@@ -206,11 +208,24 @@ Apply a set when creating a post: `create_post` / `create_and_publish_post` acce
206
208
 
207
209
  Requires an API key with the opt-in `inbox:read` / `inbox:write` scopes — enable "Social Inbox access" when you create the key.
208
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
+
209
224
  ## Environment Variables
210
225
 
211
226
  | Variable | Required | Description |
212
227
  |----------|----------|-------------|
213
- | `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. |
214
229
  | `OMNISOCIALS_BASE_URL` | No | Custom API base URL (defaults to production) |
215
230
 
216
231
  ## Alternative: Agent Skills
@@ -229,6 +244,44 @@ Full API docs: [docs.omnisocials.com](https://docs.omnisocials.com)
229
244
 
230
245
  ## Changelog
231
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
+
232
285
  ### 1.39.0 (2026-10-05)
233
286
 
234
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).
@@ -365,7 +418,7 @@ Full API docs: [docs.omnisocials.com](https://docs.omnisocials.com)
365
418
  ### 1.16.0
366
419
 
367
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`.
368
- - `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`.
369
422
  - Same tools on the companion server (`mcp.omnisocials.com`).
370
423
 
371
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, PinterestProductCheck, PinterestProductsResult, MediaCheckResult, MediaCompatibility, MediaFolder, MediaItem, PdfUploadResult, PostAnalytics, PublishPostResult, RecentPlatformPost, RetryPostResult, ApprovePostResult, RejectPostResult, PostApproval, 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,12 +376,15 @@ 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>>;
@@ -343,6 +464,20 @@ export declare class OmniSocialsClient {
343
464
  hashtags?: string[];
344
465
  }): Promise<ApiResponse<HashtagSet>>;
345
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">>>;
346
481
  listAccounts(): Promise<ApiResponse<Account[]> & {
347
482
  workspace_id?: number | string | null;
348
483
  workspace_name?: string | null;
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;
@@ -391,6 +398,26 @@ export class OmniSocialsClient {
391
398
  async deleteHashtagSet(id) {
392
399
  return this.request("DELETE", `/hashtag-sets/${id}`);
393
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
+ }
394
421
  // Accounts. Alongside `data`, the list response carries the workspace's
395
422
  // identity top-level (backend/routes/api/v1/accounts.js) — the workspaces
396
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.39.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
package/build/metrics.js CHANGED
@@ -57,6 +57,14 @@ export function getEngagement(platform, metrics) {
57
57
  return sum("likes", "comments", "reposts", "quotes", "bookmarks");
58
58
  case "mastodon":
59
59
  return sum("likes", "comments", "reposts", "quotes");
60
+ case "telegram":
61
+ // Reactions are the only post number a bot gets (message_reaction_count),
62
+ // stored as `likes` (total) next to `reactions` (per emoji).
63
+ return sum("likes");
64
+ case "discord":
65
+ // A webhook reads back its own message: reactions only (2026-10-22),
66
+ // stored the Telegram way, `likes` (total) next to `reactions` (per emoji).
67
+ return sum("likes");
60
68
  case "reddit":
61
69
  // likes == upvote score; shares == crossposts.
62
70
  return sum("likes", "comments", "shares");
@@ -177,6 +185,28 @@ export const UNSUPPORTED_POST_METRICS = {
177
185
  video_views: "Mastodon does not report video views.",
178
186
  engagement_rate: "Engagement rate needs impressions, which Mastodon does not report.",
179
187
  },
188
+ telegram: {
189
+ impressions: "Telegram does not give view counts to bots. Open the post in Telegram to see its views.",
190
+ reach: "Telegram does not report reach to bots.",
191
+ comments: "Telegram does not report comment counts to bots.",
192
+ shares: "Telegram does not report forwards to bots.",
193
+ saves: "Telegram does not report saves.",
194
+ clicks: "Telegram does not report clicks.",
195
+ link_clicks: "Telegram does not report clicks.",
196
+ video_views: "Telegram does not give view counts to bots.",
197
+ engagement_rate: "Engagement rate needs views, which Telegram does not give to bots.",
198
+ },
199
+ discord: {
200
+ impressions: "Discord does not report views. Open the message in Discord to see who reacted.",
201
+ reach: "Discord does not report reach.",
202
+ comments: "Discord does not report replies to a webhook message.",
203
+ shares: "Discord does not report shares.",
204
+ saves: "Discord does not report saves.",
205
+ clicks: "Discord does not report clicks.",
206
+ link_clicks: "Discord does not report clicks.",
207
+ video_views: "Discord does not report video views.",
208
+ engagement_rate: "Engagement rate needs views, which Discord does not report.",
209
+ },
180
210
  google_business: {
181
211
  impressions: "Google discontinued per-post metrics for Business Profiles in 2023.",
182
212
  reach: "Google discontinued per-post metrics for Business Profiles in 2023.",
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { fetchImageAsBase64, capitalize } from "../client.js";
3
3
  export function registerAccountTools(server, getClient) {
4
- server.tool("list_accounts", "List all connected social media accounts in the workspace. Each account includes its platform, display name, channel ID (used for create_post), supported content_types (post, story, reel), and `needs_reconnect` (true when the OAuth token has been revoked/expired and the user must reconnect before posts can succeed; also reflected in `status` as `needs_reconnect` instead of `active`). Pinterest accounts include a `boards` array with `{id, name}` — use the board `id` as `board_id` when creating Pinterest posts. X accounts with Premium include `platform_details` with `subscription_type` (e.g. \"Premium\", \"PremiumPlus\"). LinkedIn appears as two independent platforms: `linkedin` for a personal profile and `linkedin_page` for a company page. A workspace can have both connected at the same time and post to each separately. Call this to help users pick which platforms to post to.", {}, { title: "List Connected Accounts", readOnlyHint: true, destructiveHint: false, openWorldHint: false }, async () => {
4
+ server.tool("list_accounts", "List all connected social media accounts in the workspace. Each account includes its platform, display name, channel ID (used for create_post), supported content_types (post, story, reel), and `needs_reconnect` (true when the OAuth token has been revoked/expired and the user must reconnect before posts can succeed; also reflected in `status` as `needs_reconnect` instead of `active`). Pinterest accounts include a `boards` array with `{id, name}` — use the board `id` as `board_id` when creating Pinterest posts. X accounts with Premium include `platform_details` with `subscription_type` (e.g. \"Premium\", \"PremiumPlus\"). Telegram accounts are a channel or group the OmniSocials bot posts to; `platform_details` carries `chat_type` (channel, supergroup or group), `subscribers` and `url`. Discord accounts are one Discord channel the workspace posts to through its webhook (posts show the webhook's name and picture); `platform_details` carries `guild_id`, `guild_name` (the server, when connected with Continue with Discord), `channel_id`, `channel_name` (read by the OmniSocials bot or given by the person, else null), `channel_type` (text, voice, announcement, stage, forum or media; a forum or media channel needs `discord.thread_name` on every post), `webhook_name`, `connect_method` (oauth or url) and `url`. LinkedIn appears as two independent platforms: `linkedin` for a personal profile and `linkedin_page` for a company page. A workspace can have both connected at the same time and post to each separately. Call this to help users pick which platforms to post to.", {}, { title: "List Connected Accounts", readOnlyHint: true, destructiveHint: false, openWorldHint: false }, async () => {
5
5
  const result = await getClient().listAccounts();
6
6
  if (result.error) {
7
7
  return {
@@ -20,13 +20,21 @@ export function registerAccountTools(server, getClient) {
20
20
  md += `|---|----------|---------|---------------|------------|--------|\n`;
21
21
  for (let i = 0; i < accounts.length; i++) {
22
22
  const a = accounts[i];
23
- const name = a.username ? `@${a.username}` : a.display_name || "—";
23
+ // Discord: the server, then the channel the bot read or the person gave.
24
+ const channel = a.platform === "discord" && a.platform_details?.channel_name ? ` · #${a.platform_details.channel_name}` : "";
25
+ const name = `${a.username ? `@${a.username}` : a.display_name || "—"}${channel}`;
24
26
  const types = (a.content_types || []).join(", ");
25
27
  const platform = capitalize(a.platform);
26
28
  const subType = a.platform_details?.subscription_type;
29
+ const chatType = a.platform === "telegram" ? a.platform_details?.chat_type : null;
30
+ const guildName = a.platform === "discord" ? a.platform_details?.guild_name : null;
27
31
  const sub = subType === "Premium" || subType === "PremiumPlus"
28
32
  ? ` (${subType})`
29
- : "";
33
+ : chatType
34
+ ? ` (${chatType === "channel" ? "channel" : "group"})`
35
+ : guildName && guildName !== a.display_name
36
+ ? ` (${guildName})`
37
+ : "";
30
38
  const statusCell = a.needs_reconnect
31
39
  ? `⚠️ needs_reconnect`
32
40
  : "active";
@@ -93,6 +101,30 @@ export function registerAccountTools(server, getClient) {
93
101
  if (getSubType === "Premium" || getSubType === "PremiumPlus") {
94
102
  md += `| **Subscription** | ${getSubType} |\n`;
95
103
  }
104
+ if (a.platform === "telegram" && a.platform_details) {
105
+ const d = a.platform_details;
106
+ if (d.chat_type)
107
+ md += `| **Chat type** | ${d.chat_type} |\n`;
108
+ if (typeof d.subscribers === "number")
109
+ md += `| **Subscribers** | ${d.subscribers.toLocaleString("en-US")} |\n`;
110
+ if (d.url)
111
+ md += `| **Link** | ${d.url} |\n`;
112
+ }
113
+ if (a.platform === "discord" && a.platform_details) {
114
+ const d = a.platform_details;
115
+ if (d.guild_name)
116
+ md += `| **Server** | ${d.guild_name} |\n`;
117
+ if (d.channel_name)
118
+ md += `| **Channel** | #${d.channel_name}${d.channel_type ? ` (${d.channel_type})` : ""} |\n`;
119
+ if (d.webhook_name)
120
+ md += `| **Posts as** | ${d.webhook_name} (the webhook's name) |\n`;
121
+ if (d.channel_id)
122
+ md += `| **Channel ID** | ${d.channel_id} |\n`;
123
+ if (d.connect_method)
124
+ md += `| **Connected with** | ${d.connect_method === "oauth" ? "Continue with Discord" : "a webhook URL"} |\n`;
125
+ if (d.url)
126
+ md += `| **Link** | ${d.url} |\n`;
127
+ }
96
128
  if (a.boards?.length) {
97
129
  md += `\n### Pinterest Boards\n\n`;
98
130
  md += `| Board Name | Board ID |\n`;
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { formatNumber, capitalize, metricRows, metricBreakdownRows, sortMetricLabels } from "../client.js";
3
3
  import { getEngagement, getImpressions, getEngagementRate, MIN_IMPRESSIONS_FOR_ENGAGEMENT_RATE, } from "../metrics.js";
4
4
  export function registerAnalyticsTools(server, getClient) {
5
- server.tool("get_post_analytics", "Get analytics/statistics for a specific published post. Renders every metric the platform reported per platform: impressions, reach, views, saves, likes, comments, shares, clicks, link_clicks, engagements, and more. A missing key means the platform does not report it: reach is absent (not 0) on X, Pinterest, YouTube, Threads, Bluesky, Mastodon and Facebook (Meta retired Facebook post reach in June 2026 and its replacement metric returns no data; a Facebook row collected before 2026-09-27 may still carry reach: 0, treat it as absent); link_clicks (clicks on links in the post) exists for X posts under 30 days, LinkedIn profile, Facebook feed posts, Pinterest and Instagram stories, while clicks is the platform's broadest click count. TikTok videos also carry average_time_watched, full_video_watched_rate, total_time_watched, favorites and reach when the workspace enabled TikTok comments (Business API authorization).", {
5
+ server.tool("get_post_analytics", "Get analytics/statistics for a specific published post. Renders every metric the platform reported per platform: impressions, reach, views, saves, likes, comments, shares, clicks, link_clicks, engagements, and more. A missing key means the platform does not report it: reach is absent (not 0) on X, Pinterest, YouTube, Threads, Bluesky, Mastodon, Telegram, Discord and Facebook (Meta retired Facebook post reach in June 2026 and its replacement metric returns no data; a Facebook row collected before 2026-09-27 may still carry reach: 0, treat it as absent); link_clicks (clicks on links in the post) exists for X posts under 30 days, LinkedIn profile, Facebook feed posts, Pinterest and Instagram stories, while clicks is the platform's broadest click count. TikTok videos also carry average_time_watched, full_video_watched_rate, total_time_watched, favorites and reach when the workspace enabled TikTok comments (Business API authorization). Telegram gives bots no view counts: a Telegram post carries likes (all reactions), reactions (per emoji) and, for a poll, the votes. Discord reports the same for a webhook message: likes (all reactions), reactions (per emoji) and the poll votes, no views.", {
6
6
  post_id: z.string().describe("The numeric OmniSocials post id (the `id` field from list_posts). NOT a platform post id such as a YouTube video id or tweet id."),
7
7
  }, { title: "Get Post Analytics", readOnlyHint: true, destructiveHint: false, openWorldHint: false }, async ({ post_id }) => {
8
8
  const result = await getClient().getPostAnalytics(post_id);
@@ -310,7 +310,7 @@ export function registerAnalyticsTools(server, getClient) {
310
310
  };
311
311
  });
312
312
  server.tool("get_best_times", "Get recommended posting times (day + hour) for one platform, computed from the workspace's own posting history: publish time × engagement of every post published there, recency-weighted and outlier-damped, bucketed in the user's timezone, and blended with when the account's followers are online when the platform provides that (Instagram, TikTok Business; `basis: own_data_and_audience`, response carries `audience_online`). Returns the top 3 recommended slots plus a per-day breakdown. When the workspace has fewer than 15 analyzed posts on the platform, the audience-online profile alone is used (`basis: audience`) or industry-average defaults are returned (`basis: defaults`) with how many more posts unlock personalized recommendations — tell the user that so the numbers aren't mistaken for their own audience data. Use this before scheduling when the user asks 'when should I post?' or hasn't specified a time. Requires the analytics:read scope.", {
313
- platform: z.string().describe("Platform identifier, e.g. instagram, tiktok, linkedin, linkedin_page, x, facebook, youtube, pinterest, threads, bluesky, mastodon, google_business"),
313
+ platform: z.string().describe("Platform identifier, e.g. instagram, tiktok, linkedin, linkedin_page, x, facebook, youtube, pinterest, threads, bluesky, mastodon, google_business, telegram, discord"),
314
314
  timezone: z.string().optional().describe("IANA timezone for the buckets (e.g. Europe/Amsterdam). Defaults to the account's timezone."),
315
315
  }, { title: "Get Best Times to Post", readOnlyHint: true, destructiveHint: false, openWorldHint: false }, async (params) => {
316
316
  const result = await getClient().getBestTimes(params);
@@ -0,0 +1,25 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { OmniSocialsClient } from "../client.js";
3
+ import type { Automation, AutomationInput } from "../types.js";
4
+ export declare const automationChannelLabel: (platform: string | null | undefined) => "Facebook Page" | "Instagram";
5
+ type Fields = {
6
+ platform?: "instagram" | "facebook";
7
+ name?: string;
8
+ keywords?: string[];
9
+ match?: "word" | "contains" | "any";
10
+ dm_text?: string;
11
+ public_replies?: string[];
12
+ triggers?: Array<"comment" | "dm" | "story_reply">;
13
+ scope?: "post" | "next_post" | "any";
14
+ post_id?: string;
15
+ instagram_media_id?: string;
16
+ facebook_post_id?: string;
17
+ max_sends?: number | null;
18
+ stop_at?: string | null;
19
+ };
20
+ export declare function buildAutomationBody(f: Fields, { create }: {
21
+ create: boolean;
22
+ }): AutomationInput;
23
+ export declare function describeAutomation(a: Automation): string;
24
+ export declare function registerAutomationTools(server: McpServer, getClient: () => OmniSocialsClient): void;
25
+ export {};