@omnisocials/mcp-server 1.46.0 → 1.48.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 +15 -3
- package/build/client.d.ts +25 -8
- package/build/client.js +86 -6
- package/build/index.js +1 -1
- package/build/tools/analytics.js +7 -3
- package/build/tools/automations.d.ts +3 -3
- package/build/tools/automations.js +36 -19
- package/build/types.d.ts +38 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -212,12 +212,12 @@ Requires an API key with the opt-in `inbox:read` / `inbox:write` scopes — enab
|
|
|
212
212
|
|
|
213
213
|
| Tool | Description |
|
|
214
214
|
|------|-------------|
|
|
215
|
-
| `list_automations` | List the comment-to-DM automations of the workspace (Instagram
|
|
215
|
+
| `list_automations` | List the comment-to-DM automations of the workspace (Instagram, Facebook Page and X), optionally by `status`, `post_id` or `platform` |
|
|
216
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 |
|
|
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. On X (`platform: "x"`) it answers DMs only, needs X DMs on for the workspace and ends every answer with "Reply STOP to stop automatic messages." |
|
|
218
218
|
| `update_automation` | Pause, resume or change the keywords, DM text, public reply, limits or post of an automation |
|
|
219
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 |
|
|
220
|
+
| `get_automation_activity` | One row per comment or message the automation acted on: the DM sent, an answer waiting in the queue, a skip with its reason, a person who replied STOP on X, or the platform's error |
|
|
221
221
|
|
|
222
222
|
Requires an API key with the opt-in `automations:read` / `automations:write` scopes (Settings > API, "Automations" on the key).
|
|
223
223
|
|
|
@@ -244,6 +244,18 @@ Full API docs: [docs.omnisocials.com](https://docs.omnisocials.com)
|
|
|
244
244
|
|
|
245
245
|
## Changelog
|
|
246
246
|
|
|
247
|
+
### 1.48.0 (2026-10-26)
|
|
248
|
+
|
|
249
|
+
- **Added: video retention in `get_post_analytics`:** a `3-second hold (%)` row (`hold_rate_3s`: TikTok videos with TikTok comments enabled, Facebook Reels, Instagram Reels as 100 minus `skip_rate`) and a `Still watching` row that sums up the retention curve of TikTok videos and Facebook Reels and videos (1, 3, 5, 10, 15, 30 seconds and each quarter). Instagram and YouTube give no curve through their APIs.
|
|
250
|
+
- **Fixed:** Instagram Reels carry `skip_rate` again, and totals across platforms add up counters only (rates and averages show per platform).
|
|
251
|
+
|
|
252
|
+
### 1.47.0 (2026-10-25)
|
|
253
|
+
|
|
254
|
+
- **Added: automations on X.** The automation tools take `platform: "x"`: the connected X account answers a DM that contains a keyword (or any first message, with `match: "any"`) with `dm_text`, once per person. On X only `triggers: ["dm"]` exists (the default when `platform` is `x`), with no post fields and no public replies. An active X automation needs X DMs turned on for the workspace in OmniSocials (else `409 x_dms_off`; `status: "paused"` always saves). Every X answer ends with "Reply STOP to stop automatic messages.", and a person who replies with only STOP gets no automatic DM from that X account again (in every workspace, also after the automation is deleted). X sends at most 15 answers per 15 minutes and 450 a day per account, and each answer uses 2 X DM credits. Encrypted X Chat conversations get no answer: X does not give apps their messages.
|
|
255
|
+
- **Added: `status` on `create_automation`** (`paused` creates it without turning it on).
|
|
256
|
+
- **Changed: answers over the send cap wait in a queue** (`queued`, out within the hour; also when X answers 429) instead of being skipped at once. `get_automation_activity` filters on `queued` and `opted_out`; new skip reasons `dms_closed`, `no_credits`, `opted_out` and `automation_off`.
|
|
257
|
+
- Also in 1.47.0: the Ads tools listed above (see CHANGELOG.md; that text is a draft until Ads launches).
|
|
258
|
+
|
|
247
259
|
### 1.46.0 (2026-10-23)
|
|
248
260
|
|
|
249
261
|
- **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`).
|
package/build/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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";
|
|
1
|
+
import type { Account, AnalyticsOverview, AccountAnalyticsEntry, ApiPost, ApiResponse, ApiVideoCover, AudioSearchResult, Automation, AutomationEvent, AutomationEventOutcome, AutomationEventsResult, AutomationInput, AutomationPlatform, 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;
|
|
@@ -9,17 +9,34 @@ export declare function formatDate(iso: string | null): string;
|
|
|
9
9
|
export declare function formatDateTime(iso: string | null): string;
|
|
10
10
|
export declare function truncate(value: unknown, len: number): string;
|
|
11
11
|
export declare function capitalize(str: string): string;
|
|
12
|
+
/** Rates, averages and lengths: shown per platform, never added up across
|
|
13
|
+
* platforms (two 60% hold rates are not 120%). Mirrors
|
|
14
|
+
* NON_ADDITIVE_METRIC_KEYS in the backend's analyticsAggregate.js. */
|
|
15
|
+
export declare const NON_ADDITIVE_METRIC_KEYS: Set<string>;
|
|
16
|
+
/** `retention_curve` as the API returns it (TikTok, Facebook Reels and
|
|
17
|
+
* videos): the % of viewers still watching at each point of the video. */
|
|
18
|
+
export interface RetentionCurve {
|
|
19
|
+
x: "seconds" | "percent";
|
|
20
|
+
base?: "views" | "plays" | "views_3s";
|
|
21
|
+
duration?: number;
|
|
22
|
+
points: Array<[number, number]>;
|
|
23
|
+
}
|
|
24
|
+
/** One line for a retention curve: the opening seconds, then the quarters
|
|
25
|
+
* ("1s 92% · 3s 71% · 10s 48% · 50% 40% · end 22%"). Mirrors
|
|
26
|
+
* retentionSummary in the backend's utils/analytics/retentionCurve.js. */
|
|
27
|
+
export declare function retentionSummary(curve: unknown): string;
|
|
12
28
|
/** Breakdown objects (reactions by type, traffic sources, ...) rendered as
|
|
13
29
|
* "Label: a 12, b 3" lines under the metric table. */
|
|
14
30
|
export declare function metricBreakdownRows(m: Record<string, unknown> | null | undefined): Array<[string, string]>;
|
|
15
31
|
export declare function sortMetricLabels(labels: string[]): string[];
|
|
16
32
|
/**
|
|
17
|
-
* Flatten a raw per-platform metrics object into ordered [label, value]
|
|
18
|
-
* keeping every numeric counter the platform reported. Strings (note,
|
|
19
|
-
* cover_image_url, …)
|
|
20
|
-
* back-compat key so a zero
|
|
33
|
+
* Flatten a raw per-platform metrics object into ordered [label, value, key]
|
|
34
|
+
* rows, keeping every numeric counter the platform reported. Strings (note,
|
|
35
|
+
* title, cover_image_url, …) and objects (breakdowns, the retention curve) are
|
|
36
|
+
* skipped; `plays` is a legacy always-0 Instagram back-compat key so a zero
|
|
37
|
+
* there is noise, not data.
|
|
21
38
|
*/
|
|
22
|
-
export declare function metricRows(m: Record<string, unknown> | null | undefined): Array<[string, number]>;
|
|
39
|
+
export declare function metricRows(m: Record<string, unknown> | null | undefined): Array<[string, number, string]>;
|
|
23
40
|
/** A `media_urls` entry — a plain external URL, or `{ url, alt }` to attach
|
|
24
41
|
* an accessibility description (alt text, max 1500 chars) to that file. */
|
|
25
42
|
export type MediaUrlEntry = string | {
|
|
@@ -467,14 +484,14 @@ export declare class OmniSocialsClient {
|
|
|
467
484
|
listAutomations(params?: {
|
|
468
485
|
status?: string;
|
|
469
486
|
post_id?: string;
|
|
470
|
-
platform?:
|
|
487
|
+
platform?: AutomationPlatform;
|
|
471
488
|
}): Promise<ApiResponse<Automation[]>>;
|
|
472
489
|
getAutomation(id: string): Promise<ApiResponse<Automation>>;
|
|
473
490
|
createAutomation(data: AutomationInput): Promise<ApiResponse<Automation>>;
|
|
474
491
|
updateAutomation(id: string, data: AutomationInput): Promise<ApiResponse<Automation>>;
|
|
475
492
|
deleteAutomation(id: string): Promise<ApiResponse<unknown>>;
|
|
476
493
|
listAutomationEvents(id: string, params?: {
|
|
477
|
-
outcome?:
|
|
494
|
+
outcome?: AutomationEventOutcome;
|
|
478
495
|
limit?: number;
|
|
479
496
|
offset?: number;
|
|
480
497
|
}): Promise<ApiResponse<AutomationEvent[]> & Partial<Omit<AutomationEventsResult, "data">>>;
|
package/build/client.js
CHANGED
|
@@ -123,11 +123,87 @@ const METRIC_LABELS = [
|
|
|
123
123
|
["watch_time_percentage", "Watched (%)"],
|
|
124
124
|
["completion_rate", "Completion rate (%)"],
|
|
125
125
|
["skip_rate", "Skip rate (%)"],
|
|
126
|
+
["hold_rate_3s", "3-second hold (%)"],
|
|
126
127
|
["replays", "Replays"],
|
|
127
128
|
["subscribers_gained", "Subscribers gained"],
|
|
128
129
|
["subscribers_lost", "Subscribers lost"],
|
|
129
130
|
["duration", "Duration (s)"],
|
|
130
131
|
];
|
|
132
|
+
/** Rates, averages and lengths: shown per platform, never added up across
|
|
133
|
+
* platforms (two 60% hold rates are not 120%). Mirrors
|
|
134
|
+
* NON_ADDITIVE_METRIC_KEYS in the backend's analyticsAggregate.js. */
|
|
135
|
+
export const NON_ADDITIVE_METRIC_KEYS = new Set([
|
|
136
|
+
"engagement_rate",
|
|
137
|
+
"save_rate",
|
|
138
|
+
"pin_click_rate",
|
|
139
|
+
"outbound_click_rate",
|
|
140
|
+
"avg_watch_time",
|
|
141
|
+
"average_time_watched",
|
|
142
|
+
"average_view_duration",
|
|
143
|
+
"average_view_percentage",
|
|
144
|
+
"watch_time_percentage",
|
|
145
|
+
"completion_rate",
|
|
146
|
+
"full_video_watched_rate",
|
|
147
|
+
"skip_rate",
|
|
148
|
+
"hold_rate_3s",
|
|
149
|
+
"duration",
|
|
150
|
+
"upvote_ratio",
|
|
151
|
+
"story_slide_index",
|
|
152
|
+
]);
|
|
153
|
+
function interpolate(points, at) {
|
|
154
|
+
if (points.length === 0 || at < points[0][0] || at > points[points.length - 1][0])
|
|
155
|
+
return undefined;
|
|
156
|
+
for (let i = 0; i < points.length; i++) {
|
|
157
|
+
const [x1, y1] = points[i];
|
|
158
|
+
if (x1 === at)
|
|
159
|
+
return y1;
|
|
160
|
+
if (x1 > at) {
|
|
161
|
+
const [x0, y0] = points[i - 1];
|
|
162
|
+
return y0 + ((y1 - y0) * (at - x0)) / (x1 - x0);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
function curveValueAtSecond(curve, second) {
|
|
168
|
+
if (curve.x === "seconds")
|
|
169
|
+
return interpolate(curve.points, second);
|
|
170
|
+
if (curve.x === "percent" && curve.duration && curve.duration > 0) {
|
|
171
|
+
return interpolate(curve.points, (second / curve.duration) * 100);
|
|
172
|
+
}
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
function curveValueAtShare(curve, percent) {
|
|
176
|
+
if (curve.x === "percent")
|
|
177
|
+
return interpolate(curve.points, percent);
|
|
178
|
+
if (curve.duration && curve.duration > 0)
|
|
179
|
+
return interpolate(curve.points, (percent / 100) * curve.duration);
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
/** One line for a retention curve: the opening seconds, then the quarters
|
|
183
|
+
* ("1s 92% · 3s 71% · 10s 48% · 50% 40% · end 22%"). Mirrors
|
|
184
|
+
* retentionSummary in the backend's utils/analytics/retentionCurve.js. */
|
|
185
|
+
export function retentionSummary(curve) {
|
|
186
|
+
const c = curve;
|
|
187
|
+
if (!c || typeof c !== "object" || !Array.isArray(c.points) || c.points.length < 2)
|
|
188
|
+
return "";
|
|
189
|
+
const parts = [];
|
|
190
|
+
const of3sViews = c.base === "views_3s";
|
|
191
|
+
if (!of3sViews) {
|
|
192
|
+
for (const s of [1, 3, 5, 10, 15, 30]) {
|
|
193
|
+
const v = curveValueAtSecond(c, s);
|
|
194
|
+
if (v !== undefined)
|
|
195
|
+
parts.push(`${s}s ${Math.round(v)}%`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
for (const share of [25, 50, 75, 100]) {
|
|
199
|
+
const v = curveValueAtShare(c, share);
|
|
200
|
+
if (v !== undefined)
|
|
201
|
+
parts.push(`${share === 100 ? "end" : `${share}%`} ${Math.round(v)}%`);
|
|
202
|
+
}
|
|
203
|
+
if (parts.length === 0)
|
|
204
|
+
return "";
|
|
205
|
+
return of3sViews ? `${parts.join(" · ")} (of 3-second views)` : parts.join(" · ");
|
|
206
|
+
}
|
|
131
207
|
// Context keys that ride along in stored metrics, not counters.
|
|
132
208
|
const NON_COUNTER_KEYS = new Set(["create_time", "story_slide_index"]);
|
|
133
209
|
/** Breakdown objects (reactions by type, traffic sources, ...) rendered as
|
|
@@ -159,6 +235,9 @@ export function metricBreakdownRows(m) {
|
|
|
159
235
|
if (parts.length)
|
|
160
236
|
rows.push([label, parts.join(", ")]);
|
|
161
237
|
}
|
|
238
|
+
const retention = retentionSummary(m?.retention_curve);
|
|
239
|
+
if (retention)
|
|
240
|
+
rows.push(["Still watching", retention]);
|
|
162
241
|
return rows;
|
|
163
242
|
}
|
|
164
243
|
/** Canonical position of a metric label — for sorting totals rows collected
|
|
@@ -168,10 +247,11 @@ export function sortMetricLabels(labels) {
|
|
|
168
247
|
return [...labels].sort((a, b) => (LABEL_ORDER.get(a) ?? 999) - (LABEL_ORDER.get(b) ?? 999));
|
|
169
248
|
}
|
|
170
249
|
/**
|
|
171
|
-
* Flatten a raw per-platform metrics object into ordered [label, value]
|
|
172
|
-
* keeping every numeric counter the platform reported. Strings (note,
|
|
173
|
-
* cover_image_url, …)
|
|
174
|
-
* back-compat key so a zero
|
|
250
|
+
* Flatten a raw per-platform metrics object into ordered [label, value, key]
|
|
251
|
+
* rows, keeping every numeric counter the platform reported. Strings (note,
|
|
252
|
+
* title, cover_image_url, …) and objects (breakdowns, the retention curve) are
|
|
253
|
+
* skipped; `plays` is a legacy always-0 Instagram back-compat key so a zero
|
|
254
|
+
* there is noise, not data.
|
|
175
255
|
*/
|
|
176
256
|
export function metricRows(m) {
|
|
177
257
|
const rows = [];
|
|
@@ -186,7 +266,7 @@ export function metricRows(m) {
|
|
|
186
266
|
seen.add(key);
|
|
187
267
|
if (key === "plays" && n === 0)
|
|
188
268
|
continue;
|
|
189
|
-
rows.push([label, n]);
|
|
269
|
+
rows.push([label, n, key]);
|
|
190
270
|
}
|
|
191
271
|
for (const [key, v] of Object.entries(m || {})) {
|
|
192
272
|
if (seen.has(key) || NON_COUNTER_KEYS.has(key))
|
|
@@ -194,7 +274,7 @@ export function metricRows(m) {
|
|
|
194
274
|
const n = Number(v);
|
|
195
275
|
if (!Number.isFinite(n))
|
|
196
276
|
continue;
|
|
197
|
-
rows.push([capitalize(key.replace(/_/g, " ")), n]);
|
|
277
|
+
rows.push([capitalize(key.replace(/_/g, " ")), n, key]);
|
|
198
278
|
}
|
|
199
279
|
return rows;
|
|
200
280
|
}
|
package/build/index.js
CHANGED
|
@@ -30,7 +30,7 @@ const sessionState = { activeIndex: 0 };
|
|
|
30
30
|
const getActiveClient = () => workspaceClients[sessionState.activeIndex].client;
|
|
31
31
|
const server = new McpServer({
|
|
32
32
|
name: "OmniSocials",
|
|
33
|
-
version: "1.
|
|
33
|
+
version: "1.48.0",
|
|
34
34
|
});
|
|
35
35
|
// Register all tools - pass getter function so tools always use the active workspace's client
|
|
36
36
|
registerPostTools(server, getActiveClient);
|
package/build/tools/analytics.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { formatNumber, capitalize, metricRows, metricBreakdownRows, sortMetricLabels } from "../client.js";
|
|
2
|
+
import { formatNumber, capitalize, metricRows, metricBreakdownRows, sortMetricLabels, NON_ADDITIVE_METRIC_KEYS } 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, 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.", {
|
|
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). Video retention: hold_rate_3s (% of viewers still watching at second 3) on TikTok videos (with TikTok comments enabled), Facebook Reels, and Instagram Reels (there 100 minus skip_rate, Meta's 3-second skip rate); a 'Still watching' row sums up the retention curve (TikTok videos, Facebook Reels and videos) at 1, 3, 5, 10, 15 and 30 seconds and at each quarter of the video. Instagram and YouTube give no curve through their APIs. Rates and averages show per platform, never added up across platforms. 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);
|
|
@@ -47,7 +47,11 @@ export function registerAnalyticsTools(server, getClient) {
|
|
|
47
47
|
const denom = getImpressions(platform, m);
|
|
48
48
|
totalEngagements += engagements;
|
|
49
49
|
totalDenom += denom;
|
|
50
|
-
for (const [label, n] of rows) {
|
|
50
|
+
for (const [label, n, key] of rows) {
|
|
51
|
+
// Rates, averages and lengths do not add up across platforms (two
|
|
52
|
+
// 60% hold rates are not 120%); they show per platform only.
|
|
53
|
+
if (NON_ADDITIVE_METRIC_KEYS.has(key))
|
|
54
|
+
continue;
|
|
51
55
|
if (!totalsByLabel.has(label))
|
|
52
56
|
totalOrder.push(label);
|
|
53
57
|
totalsByLabel.set(label, (totalsByLabel.get(label) || 0) + n);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
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";
|
|
3
|
+
import type { Automation, AutomationInput, AutomationPlatform } from "../types.js";
|
|
4
|
+
export declare const automationChannelLabel: (platform: string | null | undefined) => "Facebook Page" | "Instagram" | "X";
|
|
5
5
|
type Fields = {
|
|
6
|
-
platform?:
|
|
6
|
+
platform?: AutomationPlatform;
|
|
7
7
|
name?: string;
|
|
8
8
|
keywords?: string[];
|
|
9
9
|
match?: "word" | "contains" | "any";
|
|
@@ -2,31 +2,34 @@ import { z } from "zod";
|
|
|
2
2
|
import { formatDateTime, truncate } from "../client.js";
|
|
3
3
|
const SCOPE_HINT = "The API key needs the opt-in Automations scopes (OmniSocials Settings > API > create a key with Automations on: automations:read and automations:write).";
|
|
4
4
|
const errorText = (error) => `Error (${error.code}): ${error.message}${error.code === "insufficient_scope" ? `\n\n${SCOPE_HINT}` : ""}`;
|
|
5
|
-
const PLATFORM = z.enum(["instagram", "facebook"]);
|
|
5
|
+
const PLATFORM = z.enum(["instagram", "facebook", "x"]);
|
|
6
6
|
const KINDS = z.enum(["comment", "dm", "story_reply"]);
|
|
7
|
+
const OUTCOME = z.enum(["sent", "skipped", "failed", "pending", "queued", "opted_out"]);
|
|
7
8
|
// How the channel reads in a table or a sentence.
|
|
8
|
-
export const automationChannelLabel = (platform) => platform === "facebook" ? "Facebook Page" : "Instagram";
|
|
9
|
+
export const automationChannelLabel = (platform) => platform === "facebook" ? "Facebook Page" : platform === "x" ? "X" : "Instagram";
|
|
9
10
|
const MATCH = z.enum(["word", "contains", "any"]);
|
|
10
11
|
const SCOPE = z.enum(["post", "next_post", "any"]);
|
|
11
12
|
// The shared fields of create_automation and update_automation, flat so an
|
|
12
13
|
// assistant fills them from one sentence ("DM everyone who comments guide").
|
|
13
14
|
const AUTOMATION_FIELDS = {
|
|
14
|
-
platform: PLATFORM.optional().describe("Where the automation runs: 'instagram' (default) = the connected Instagram account, 'facebook' = the connected Facebook Page (the DM lands in Messenger). instagram_media_id implies 'instagram', facebook_post_id implies 'facebook'. It cannot change after create."),
|
|
15
|
+
platform: PLATFORM.optional().describe("Where the automation runs: 'instagram' (default) = the connected Instagram account, 'facebook' = the connected Facebook Page (the DM lands in Messenger), 'x' = the connected X account (it answers DMs only; see create_automation). instagram_media_id implies 'instagram', facebook_post_id implies 'facebook'. It cannot change after create."),
|
|
15
16
|
name: z.string().max(120).optional().describe("A label for the automations list, e.g. 'Free guide'."),
|
|
16
17
|
keywords: z.array(z.string()).max(20).optional().describe("Keywords that start the automation (up to 20, matched case-insensitively), e.g. [\"guide\", \"link\"]. Required unless match is 'any'."),
|
|
17
18
|
match: MATCH.optional().describe("word (default): the keyword stands on its own ('guide', 'Guide!' match; 'linkedin' does not match 'link'). contains: the keyword appears anywhere. any: every comment or message with text (ask the user before using it)."),
|
|
18
|
-
dm_text: z.string().max(1000).optional().describe("The DM sent to the person (max 1000 characters), once per person. On Facebook it lands in Messenger. Required on create. Confirm the text with the user before creating the automation: it goes out in the account's name."),
|
|
19
|
-
public_replies: z.array(z.string().max(300)).max(10).optional().describe("Optional public replies posted under the comment (one is picked at random per comment), e.g. [\"Sent you a DM!\", \"Check your inbox\"]. Comment trigger only. An empty array turns the public reply off."),
|
|
20
|
-
triggers: z.array(KINDS).min(1).optional().describe("What starts the automation. 'comment' (default) = a comment on an Instagram post or reel, or on a Facebook Page post; 'dm' = a keyword in a DM to the Instagram account,
|
|
21
|
-
scope: SCOPE.optional().describe("For the comment trigger: 'post' (default) = one post (pass post_id, instagram_media_id or facebook_post_id), 'next_post' = the next post the workspace publishes on this platform, 'any' = every post of the account or Page."),
|
|
22
|
-
post_id: z.string().optional().describe("For scope 'post': the OmniSocials post id (from list_posts) that goes to the automation's platform. A scheduled post starts listening once it is live. One automation per post and platform: a post that goes to Instagram and Facebook can have one of each."),
|
|
19
|
+
dm_text: z.string().max(1000).optional().describe("The DM sent to the person (max 1000 characters), once per person. On Facebook it lands in Messenger. On X, OmniSocials adds the line 'Reply STOP to stop automatic messages.' under it. Required on create. Confirm the text with the user before creating the automation: it goes out in the account's name."),
|
|
20
|
+
public_replies: z.array(z.string().max(300)).max(10).optional().describe("Optional public replies posted under the comment (one is picked at random per comment), e.g. [\"Sent you a DM!\", \"Check your inbox\"]. Comment trigger only (Instagram and Facebook; X has none). An empty array turns the public reply off."),
|
|
21
|
+
triggers: z.array(KINDS).min(1).optional().describe("What starts the automation. 'comment' (default on Instagram and Facebook) = a comment on an Instagram post or reel, or on a Facebook Page post; 'dm' = a keyword in a DM to the Instagram account, in a Messenger message to the Facebook Page, or in a DM to the X account; 'story_reply' = a keyword in a reply to an Instagram story (Instagram only: Facebook has no story replies). On X only 'dm' exists, and it is the default there. One automation can listen to several."),
|
|
22
|
+
scope: SCOPE.optional().describe("For the comment trigger (Instagram and Facebook; an X automation has no post): 'post' (default) = one post (pass post_id, instagram_media_id or facebook_post_id), 'next_post' = the next post the workspace publishes on this platform, 'any' = every post of the account or Page."),
|
|
23
|
+
post_id: z.string().optional().describe("For scope 'post' (Instagram and Facebook): the OmniSocials post id (from list_posts) that goes to the automation's platform. A scheduled post starts listening once it is live. One automation per post and platform: a post that goes to Instagram and Facebook can have one of each."),
|
|
23
24
|
instagram_media_id: z.string().optional().describe("For scope 'post' on Instagram: the numeric Instagram media id of a post or reel that was NOT published through OmniSocials. OmniSocials reads it from Instagram to check it belongs to the connected account. Pass only one of post_id, instagram_media_id or facebook_post_id."),
|
|
24
25
|
facebook_post_id: z.string().optional().describe("For scope 'post' on Facebook: the id of a Facebook Page post that was NOT published through OmniSocials ({page-id}_{post-id}, or the post id alone). OmniSocials reads it from Facebook to check it belongs to the connected Page. Implies platform 'facebook'. Pass only one of post_id, instagram_media_id or facebook_post_id."),
|
|
25
26
|
max_sends: z.number().int().positive().nullable().optional().describe("Stop after this many DMs (the automation becomes 'completed'). null = no limit."),
|
|
26
27
|
stop_at: z.string().nullable().optional().describe("Stop at this time (ISO 8601). null = no end."),
|
|
27
28
|
};
|
|
28
29
|
// Turn the flat tool fields into the API body. Only the sections that were
|
|
29
|
-
// given are sent, so update_automation leaves the rest untouched.
|
|
30
|
+
// given are sent, so update_automation leaves the rest untouched. The API's
|
|
31
|
+
// default trigger is a comment, which X does not have: an X create without
|
|
32
|
+
// triggers sends ["dm"].
|
|
30
33
|
export function buildAutomationBody(f, { create }) {
|
|
31
34
|
const body = {};
|
|
32
35
|
if (f.name !== undefined)
|
|
@@ -43,6 +46,8 @@ export function buildAutomationBody(f, { create }) {
|
|
|
43
46
|
body.trigger = {};
|
|
44
47
|
if (f.triggers !== undefined)
|
|
45
48
|
body.trigger.kinds = f.triggers;
|
|
49
|
+
else if (create && f.platform === "x")
|
|
50
|
+
body.trigger.kinds = ["dm"];
|
|
46
51
|
if (f.scope !== undefined)
|
|
47
52
|
body.trigger.scope = f.scope;
|
|
48
53
|
if (f.keywords !== undefined)
|
|
@@ -94,7 +99,7 @@ export function describeAutomation(a) {
|
|
|
94
99
|
return md;
|
|
95
100
|
}
|
|
96
101
|
export function registerAutomationTools(server, getClient) {
|
|
97
|
-
server.tool("list_automations", "List the comment-to-DM automations of this workspace, on Instagram
|
|
102
|
+
server.tool("list_automations", "List the comment-to-DM automations of this workspace, on Instagram, on the Facebook Page and on X. An automation DMs a person once when they comment a keyword on a post (or send it in a DM, or in an Instagram story reply), with an optional public reply under the comment; on X it answers DMs only. Use it to answer 'which automations run', before creating one on a post that may have one already, or to find the id for update_automation / get_automation_activity.", {
|
|
98
103
|
status: z.enum(["active", "paused", "completed"]).optional().describe("Only automations with this status."),
|
|
99
104
|
post_id: z.string().optional().describe("Only the automations bound to this OmniSocials post (one per platform at most)."),
|
|
100
105
|
platform: PLATFORM.optional().describe("Only the automations of this platform."),
|
|
@@ -116,7 +121,7 @@ export function registerAutomationTools(server, getClient) {
|
|
|
116
121
|
md += `\nDetails with get_automation, activity with get_automation_activity, pause or edit with update_automation.`;
|
|
117
122
|
return { content: [{ type: "text", text: md }] };
|
|
118
123
|
});
|
|
119
|
-
server.tool("get_automation", "Get one comment-to-DM automation: its platform (Instagram
|
|
124
|
+
server.tool("get_automation", "Get one comment-to-DM automation: its platform (Instagram, the Facebook Page or X), its trigger, the DM text, the public replies, the limits and the counts.", { automation_id: z.string().describe("The automation id (from list_automations).") }, { title: "Get Automation", readOnlyHint: true, destructiveHint: false, openWorldHint: false }, async ({ automation_id }) => {
|
|
120
125
|
const result = await getClient().getAutomation(automation_id);
|
|
121
126
|
if (result.error)
|
|
122
127
|
return { content: [{ type: "text", text: errorText(result.error) }] };
|
|
@@ -124,10 +129,16 @@ export function registerAutomationTools(server, getClient) {
|
|
|
124
129
|
return { content: [{ type: "text", text: "The API returned no automation." }] };
|
|
125
130
|
return { content: [{ type: "text", text: describeAutomation(result.data) }] };
|
|
126
131
|
});
|
|
127
|
-
server.tool("create_automation", "Create a comment-to-DM automation on the connected Instagram account (default)
|
|
132
|
+
server.tool("create_automation", "Create a comment-to-DM automation on the connected Instagram account (default), on the connected Facebook Page (platform 'facebook') or on the connected X account (platform 'x'): when somebody comments one of the keywords on the post (or sends it in a DM, or in an Instagram story reply, see triggers), the account DMs them dm_text once, and optionally posts one of public_replies under the comment. On Facebook the DM lands in Messenger. Before creating one, show the user the keyword(s) and the exact DM text and get a yes: the DM goes out in their name. Meta rules, the same on Instagram and Facebook: one private reply per comment, within 7 days of the comment; a follow-up only after the person answers (24-hour window); OmniSocials caps sends at 180 DMs an hour per account and platform, and an answer over the cap waits in a queue and goes out within the hour. For scope 'post' pass one of post_id (an OmniSocials post, scheduled or live), instagram_media_id (an Instagram post published outside OmniSocials) or facebook_post_id (a Page post published outside OmniSocials). On X the automation answers DMs only: triggers ['dm'] (the default there; X allows an automated DM only after the person wrote first), so a DM to the X account with a keyword, or any first message with match 'any', gets dm_text once per person; no post fields and no public replies. An X automation runs only while X DMs are turned on for the workspace in OmniSocials (Social Inbox, X DMs card, or the automation editor): an active create without them answers 409 x_dms_off, so tell the user where to turn them on, or create it with status 'paused'. 409 x_not_connected means the workspace has no X account. Every X answer ends with the line 'Reply STOP to stop automatic messages.', and a person who replies STOP gets no automatic DM from that X account again (the stop list belongs to the account: it holds after the automation is deleted, for later automations and in every workspace with that account). X takes at most 15 answers per 15 minutes and 450 a day per account; more wait in the queue for up to an hour. Each X answer uses X DM credits like an inbox reply (2 credits); when the credits ran out the answer is skipped (no_credits): top up, then answer in the Social Inbox. X does not give apps the messages of an encrypted X Chat conversation, so those get no answer. To attach automations to a NEW post, pass comment_dm_automation on create_post instead (Instagram and Facebook).", {
|
|
133
|
+
...AUTOMATION_FIELDS,
|
|
134
|
+
status: z.enum(["active", "paused"]).optional().describe("'paused' creates it without turning it on (default 'active'). A paused X automation saves even while X DMs are off; turn it on later with update_automation."),
|
|
135
|
+
}, { title: "Create Automation", readOnlyHint: false, destructiveHint: false, openWorldHint: false }, async ({ status, ...params }) => {
|
|
128
136
|
if (!params.dm_text)
|
|
129
137
|
return { content: [{ type: "text", text: "dm_text is required: the DM the person receives." }] };
|
|
130
|
-
const
|
|
138
|
+
const body = buildAutomationBody(params, { create: true });
|
|
139
|
+
if (status)
|
|
140
|
+
body.status = status;
|
|
141
|
+
const result = await getClient().createAutomation(body);
|
|
131
142
|
if (result.error)
|
|
132
143
|
return { content: [{ type: "text", text: errorText(result.error) }] };
|
|
133
144
|
if (!result.data)
|
|
@@ -136,11 +147,13 @@ export function registerAutomationTools(server, getClient) {
|
|
|
136
147
|
let md = `Created automation \`${a.id}\` (${a.status}).\n\n${describeAutomation(a)}`;
|
|
137
148
|
if (a.post && a.post.live === false)
|
|
138
149
|
md += `\n_The post is not live on ${a.platform === "facebook" ? "the Facebook Page" : "Instagram"} yet; the automation starts listening at publish._`;
|
|
150
|
+
if (a.platform === "x" && a.status === "paused")
|
|
151
|
+
md += `\n_Paused: it answers nothing until it is turned on (update_automation with status 'active'), which needs X DMs on for the workspace._`;
|
|
139
152
|
return { content: [{ type: "text", text: md }] };
|
|
140
153
|
});
|
|
141
|
-
server.tool("update_automation", "Change a comment-to-DM automation: pause or resume it (status), new keywords or match, a new DM text or public replies, new limits, or move it to another post (post_id, instagram_media_id or facebook_post_id). Only the fields you pass change. The platform cannot change: to move
|
|
154
|
+
server.tool("update_automation", "Change a comment-to-DM automation: pause or resume it (status), new keywords or match, a new DM text or public replies, new limits, or move it to another post (post_id, instagram_media_id or facebook_post_id). Only the fields you pass change. The platform cannot change: to move to another channel (Instagram, the Facebook Page or X), create a new automation. Turning an X automation on (status 'active') needs X DMs on for the workspace, else 409 x_dms_off. A 'completed' automation starts again when its limits or trigger change.", {
|
|
142
155
|
automation_id: z.string().describe("The automation id (from list_automations)."),
|
|
143
|
-
status: z.enum(["active", "paused"]).optional().describe("'paused' stops it without deleting it; 'active' resumes it."),
|
|
156
|
+
status: z.enum(["active", "paused"]).optional().describe("'paused' stops it without deleting it; 'active' resumes it (on X only while X DMs are on for the workspace)."),
|
|
144
157
|
...AUTOMATION_FIELDS,
|
|
145
158
|
}, { title: "Update Automation", readOnlyHint: false, destructiveHint: false, openWorldHint: false }, async ({ automation_id, status, ...fields }) => {
|
|
146
159
|
const body = buildAutomationBody(fields, { create: false });
|
|
@@ -155,15 +168,15 @@ export function registerAutomationTools(server, getClient) {
|
|
|
155
168
|
return { content: [{ type: "text", text: "Automation updated, but the API returned no details. Read it with get_automation." }] };
|
|
156
169
|
return { content: [{ type: "text", text: `Updated automation \`${automation_id}\`.\n\n${describeAutomation(result.data)}` }] };
|
|
157
170
|
});
|
|
158
|
-
server.tool("delete_automation", "Delete a comment-to-DM automation (Instagram
|
|
171
|
+
server.tool("delete_automation", "Delete a comment-to-DM automation (Instagram, Facebook Page or X) and its activity log. DMs that were already sent stay sent. To stop it for a while, use update_automation with status 'paused' instead. Confirm with the user first.", { automation_id: z.string().describe("The automation id (from list_automations).") }, { title: "Delete Automation", readOnlyHint: false, destructiveHint: true, openWorldHint: false }, async ({ automation_id }) => {
|
|
159
172
|
const result = await getClient().deleteAutomation(automation_id);
|
|
160
173
|
if (result.error)
|
|
161
174
|
return { content: [{ type: "text", text: errorText(result.error) }] };
|
|
162
175
|
return { content: [{ type: "text", text: `Deleted automation \`${automation_id}\`.` }] };
|
|
163
176
|
});
|
|
164
|
-
server.tool("get_automation_activity", "The activity of a comment-to-DM automation, newest first: one row per comment or message it acted on, with the DM sent, a skip and its reason (already got the DM, limit reached, comment older than 7 days,
|
|
177
|
+
server.tool("get_automation_activity", "The activity of a comment-to-DM automation, newest first: one row per comment or message it acted on, with the DM sent, an answer that waits in the queue (queued: the send cap is full or X asked to wait; it goes out within the hour), a skip and its reason (already got the DM, limit reached, comment older than 7 days, the send cap stayed full for an hour, the person does not accept DMs from the X account, credits ran out before the answer, the person replied STOP earlier, the automation was paused while the answer waited), a person who replied STOP on X (opted_out: no automatic DMs to them from that X account again, in any workspace or automation), or a failure with the platform's error. On the Facebook Page the person shows by name (Facebook has no @handle). Use it to answer 'how many people got the DM' or 'why did this person not get it'.", {
|
|
165
178
|
automation_id: z.string().describe("The automation id (from list_automations)."),
|
|
166
|
-
outcome:
|
|
179
|
+
outcome: OUTCOME.optional().describe("Only rows with this outcome."),
|
|
167
180
|
limit: z.number().int().min(1).max(100).optional().describe("Rows per page (default 25, max 100)."),
|
|
168
181
|
offset: z.number().int().min(0).optional().describe("Rows to skip (for the next page)."),
|
|
169
182
|
}, { title: "Get Automation Activity", readOnlyHint: true, destructiveHint: false, openWorldHint: false }, async ({ automation_id, ...params }) => {
|
|
@@ -187,7 +200,11 @@ export function registerAutomationTools(server, getClient) {
|
|
|
187
200
|
for (const e of rows) {
|
|
188
201
|
const who = e.person.username ? (onFacebook ? e.person.username : `@${e.person.username}`) : e.person.id;
|
|
189
202
|
const did = e.kind === "comment" ? "commented" : e.kind === "dm" ? (onFacebook ? "sent a Messenger message" : "sent a DM") : "replied to a story";
|
|
190
|
-
const outcome = e.outcome === "skipped" ? `skipped: ${e.reason_label || e.reason || ""}`
|
|
203
|
+
const outcome = e.outcome === "skipped" ? `skipped: ${e.reason_label || e.reason || ""}`
|
|
204
|
+
: e.outcome === "failed" ? `failed: ${truncate(e.reason || "", 60)}`
|
|
205
|
+
: e.outcome === "queued" ? `queued: ${e.reason_label || "waits for room under the send limit"}`
|
|
206
|
+
: e.outcome === "opted_out" ? `opted out: ${e.reason_label || "replied STOP"}`
|
|
207
|
+
: e.outcome;
|
|
191
208
|
const text = e.trigger_text ? `"${truncate(e.trigger_text, 50)}"` : "";
|
|
192
209
|
md += `| ${formatDateTime(e.sent_at || e.created_at)} | ${who} | ${did} | ${outcome} | ${text} |\n`;
|
|
193
210
|
}
|
package/build/types.d.ts
CHANGED
|
@@ -631,10 +631,20 @@ export interface HashtagSet {
|
|
|
631
631
|
created_at: string;
|
|
632
632
|
updated_at: string;
|
|
633
633
|
}
|
|
634
|
-
/** The channel an automation runs on: the Instagram account or the
|
|
635
|
-
export type AutomationPlatform = "instagram" | "facebook";
|
|
636
|
-
/** `
|
|
634
|
+
/** The channel an automation runs on: the Instagram account, the Facebook Page or the X account. */
|
|
635
|
+
export type AutomationPlatform = "instagram" | "facebook" | "x";
|
|
636
|
+
/** The channels a post-bound automation (`comment_dm_automation`) can run on: X has no comment trigger. */
|
|
637
|
+
export type CommentDmPlatform = "instagram" | "facebook";
|
|
638
|
+
/** `story_reply` is Instagram only: Facebook has no story replies. X has `dm` only. */
|
|
637
639
|
export type AutomationTriggerKind = "comment" | "dm" | "story_reply";
|
|
640
|
+
/**
|
|
641
|
+
* `queued` = the answer waits for a free slot under the send cap (X 15 per 15
|
|
642
|
+
* minutes and 450 a day, Instagram and Facebook 180 an hour), or for X's
|
|
643
|
+
* reset time after X answered 429, and goes out within the hour, else it
|
|
644
|
+
* becomes `skipped` with `rate_limited`. `opted_out` = the person replied
|
|
645
|
+
* STOP on X (the stop list of that X account).
|
|
646
|
+
*/
|
|
647
|
+
export type AutomationEventOutcome = "sent" | "skipped" | "failed" | "pending" | "queued" | "opted_out";
|
|
638
648
|
export type AutomationScope = "post" | "next_post" | "any";
|
|
639
649
|
export type AutomationMatch = "word" | "contains" | "any";
|
|
640
650
|
export interface AutomationPost {
|
|
@@ -696,7 +706,11 @@ export interface Automation {
|
|
|
696
706
|
/** The body of POST /automations; every section is optional on PATCH. */
|
|
697
707
|
export interface AutomationInput {
|
|
698
708
|
name?: string | null;
|
|
699
|
-
/**
|
|
709
|
+
/**
|
|
710
|
+
* Default instagram; cannot change on PATCH (400 platform_immutable). `x`
|
|
711
|
+
* takes trigger.kinds ["dm"] only, needs X DMs on for an active automation
|
|
712
|
+
* (409 x_dms_off; `status: "paused"` always saves) and has no post fields.
|
|
713
|
+
*/
|
|
700
714
|
platform?: AutomationPlatform;
|
|
701
715
|
post_id?: string;
|
|
702
716
|
instagram_media_id?: string;
|
|
@@ -726,7 +740,7 @@ export interface AutomationInput {
|
|
|
726
740
|
/** `comment_dm_automation` on a post: the automations bound to that post. */
|
|
727
741
|
export interface CommentDmAutomationInput {
|
|
728
742
|
/** One automation per listed channel; default Instagram when the post goes there, else Facebook */
|
|
729
|
-
platforms?:
|
|
743
|
+
platforms?: CommentDmPlatform[];
|
|
730
744
|
trigger?: {
|
|
731
745
|
keywords?: string[];
|
|
732
746
|
match?: AutomationMatch;
|
|
@@ -748,10 +762,10 @@ export interface CommentDmAutomationInput {
|
|
|
748
762
|
/** `comment_dm_automation` on a post: the first automation's fields (Instagram
|
|
749
763
|
* first), plus the channels and the id of each automation. */
|
|
750
764
|
export interface PostCommentDmAutomation extends Automation {
|
|
751
|
-
platforms?:
|
|
765
|
+
platforms?: CommentDmPlatform[];
|
|
752
766
|
automations?: Array<{
|
|
753
767
|
id: string;
|
|
754
|
-
platform:
|
|
768
|
+
platform: CommentDmPlatform;
|
|
755
769
|
status: Automation["status"];
|
|
756
770
|
}>;
|
|
757
771
|
}
|
|
@@ -759,10 +773,20 @@ export interface AutomationEvent {
|
|
|
759
773
|
id: string;
|
|
760
774
|
automation_id: string;
|
|
761
775
|
kind: AutomationTriggerKind;
|
|
762
|
-
outcome:
|
|
776
|
+
outcome: AutomationEventOutcome;
|
|
777
|
+
/**
|
|
778
|
+
* `skipped`: already_sent, limit_reached, stopped, window_passed,
|
|
779
|
+
* rate_limited (the send cap stayed full for an hour), cannot_message,
|
|
780
|
+
* already_replied, dm_window_passed, dms_closed (X: the person does not
|
|
781
|
+
* accept DMs from the account), no_credits (credits ran out before the
|
|
782
|
+
* answer), opted_out (the person replied STOP before, also while the
|
|
783
|
+
* answer waited), automation_off (paused or finished while the answer
|
|
784
|
+
* waited). `failed`: the platform's error text. A `queued` row carries
|
|
785
|
+
* the reason it waits, an `opted_out` row the reason `opted_out`.
|
|
786
|
+
*/
|
|
763
787
|
reason: string | null;
|
|
764
788
|
reason_label: string | null;
|
|
765
|
-
/** `username` = the Instagram @handle, or the person's name on Facebook */
|
|
789
|
+
/** `username` = the Instagram or X @handle, or the person's name on Facebook */
|
|
766
790
|
person: {
|
|
767
791
|
id: string;
|
|
768
792
|
username: string | null;
|
|
@@ -933,6 +957,11 @@ export interface PostAnalyticsStorySlide {
|
|
|
933
957
|
export interface PostAnalyticsPlatformEntry {
|
|
934
958
|
platform: string;
|
|
935
959
|
platform_post_id: string;
|
|
960
|
+
/** Numbers and strings, plus objects this type leaves out: breakdowns
|
|
961
|
+
* (`reactions`, `impression_sources`, ...) and, on TikTok and Facebook
|
|
962
|
+
* videos, `retention_curve` (RetentionCurve in client.ts). `hold_rate_3s`
|
|
963
|
+
* is the % of viewers still watching at second 3 (TikTok, Facebook Reels,
|
|
964
|
+
* Instagram Reels as 100 - skip_rate). */
|
|
936
965
|
metrics: Record<string, number | string>;
|
|
937
966
|
thread_parts: number;
|
|
938
967
|
collected_at: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omnisocials/mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.48.0",
|
|
4
4
|
"mcpName": "io.github.omnisocials/mcp-server",
|
|
5
5
|
"description": "MCP server for OmniSocials API - manage social media posts, media, accounts, analytics, and webhooks",
|
|
6
6
|
"type": "module",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"typescript": "^5.8.2"
|
|
53
53
|
},
|
|
54
54
|
"overrides": {
|
|
55
|
-
"zod": "4.
|
|
55
|
+
"zod": "4.6.7",
|
|
56
56
|
"fast-uri": "^3.1.8",
|
|
57
57
|
"hono": "^4.13.7",
|
|
58
58
|
"qs": "^6.16.0"
|