@omnisocials/mcp-server 1.19.0 → 1.20.1
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 +5 -0
- package/build/client.d.ts +94 -100
- package/build/client.js +21 -4
- package/build/index.js +1 -1
- package/build/metrics.d.ts +24 -0
- package/build/metrics.js +111 -0
- package/build/tools/accounts.js +2 -2
- package/build/tools/analytics.js +49 -27
- package/build/tools/hashtag-sets.js +11 -1
- package/build/tools/inbox.js +4 -3
- package/build/tools/media.js +19 -3
- package/build/tools/posts.js +196 -206
- package/build/tools/webhooks.js +21 -1
- package/build/types.d.ts +295 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -218,6 +218,11 @@ Full API docs: [docs.omnisocials.com](https://docs.omnisocials.com)
|
|
|
218
218
|
|
|
219
219
|
## Changelog
|
|
220
220
|
|
|
221
|
+
### 1.20.0 (2026-08-04)
|
|
222
|
+
|
|
223
|
+
- **Engagement rate now matches the dashboard:** `get_post_analytics` / `get_posts_analytics` previously summed likes + comments + shares only, dropping LinkedIn link clicks and X quotes/bookmarks from engagement. Both tools now use the same normalization as the dashboard and `get_analytics_overview`; rates are suppressed below 10 impressions and capped at 100%.
|
|
224
|
+
- **Account metric semantics clarified:** LinkedIn account-level `impressions` on `get_account_analytics` are lifetime totals across all of the account's content (not a windowed count) — the tool now says so and renders each platform's scope `note`.
|
|
225
|
+
|
|
221
226
|
### 1.19.0 (2026-08-02)
|
|
222
227
|
|
|
223
228
|
- **Alt text now reaches Instagram and LinkedIn:** per-media alt text (`{ url, alt }` / `{ id, alt }` entries) is also delivered to Instagram (image posts and carousel image slides — not Reels/Stories) and LinkedIn (images only — not video or documents), alongside Mastodon, Bluesky, X, and Pinterest.
|
package/build/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ApiResponse, MediaItem, PdfUploadResult, InboxConversation, InboxMessage } from "./types.js";
|
|
1
|
+
import type { Account, AnalyticsOverview, AccountAnalyticsEntry, ApiPost, ApiResponse, AudioSearchResult, BestTimesResult, HashtagSet, LocationSearchResult, MediaCheckResult, MediaCompatibility, MediaFolder, MediaItem, PdfUploadResult, PostAnalytics, PublishPostResult, RecentPlatformPost, RetryPostResult, InboxConversation, InboxMessage, Webhook } from "./types.js";
|
|
2
2
|
export declare function fetchImageAsBase64(url: string): Promise<{
|
|
3
3
|
data: string;
|
|
4
4
|
mimeType: string;
|
|
@@ -100,6 +100,45 @@ export interface MastodonPostOptions {
|
|
|
100
100
|
export interface MastodonPostOptionsUpdate {
|
|
101
101
|
thread_parts?: MastodonThreadPartInput[] | null;
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Request body for POST /posts/create. `createAndPublishPost` accepts the
|
|
105
|
+
* same shape minus `scheduled_at` (publishing is immediate).
|
|
106
|
+
*/
|
|
107
|
+
export interface CreatePostInput {
|
|
108
|
+
content: string | Record<string, string>;
|
|
109
|
+
channels?: string[];
|
|
110
|
+
scheduled_at?: string;
|
|
111
|
+
media_ids?: MediaIdEntry[] | Record<string, MediaIdEntry[]>;
|
|
112
|
+
media_urls?: MediaUrlEntry[] | Record<string, MediaUrlEntry[]>;
|
|
113
|
+
type?: string;
|
|
114
|
+
source?: string;
|
|
115
|
+
link_url?: string;
|
|
116
|
+
link_title?: string;
|
|
117
|
+
link_description?: string;
|
|
118
|
+
link_thumbnail_url?: string;
|
|
119
|
+
location_id?: string;
|
|
120
|
+
collaborators?: string[];
|
|
121
|
+
user_tags?: Array<{
|
|
122
|
+
username: string;
|
|
123
|
+
x: number;
|
|
124
|
+
y: number;
|
|
125
|
+
image_index?: number;
|
|
126
|
+
}>;
|
|
127
|
+
pinterest?: Record<string, unknown>;
|
|
128
|
+
youtube?: Record<string, unknown>;
|
|
129
|
+
instagram?: Record<string, unknown>;
|
|
130
|
+
facebook?: Record<string, unknown>;
|
|
131
|
+
linkedin?: Record<string, unknown>;
|
|
132
|
+
linkedin_page?: Record<string, unknown>;
|
|
133
|
+
tiktok?: Record<string, unknown>;
|
|
134
|
+
x?: XPostOptions;
|
|
135
|
+
bluesky?: BlueskyPostOptions;
|
|
136
|
+
mastodon?: MastodonPostOptions;
|
|
137
|
+
google_business?: Record<string, unknown>;
|
|
138
|
+
hashtag_set?: string;
|
|
139
|
+
hashtag_placement?: "caption_append" | "first_comment";
|
|
140
|
+
hashtag_platforms?: string[];
|
|
141
|
+
}
|
|
103
142
|
export declare class OmniSocialsClient {
|
|
104
143
|
private baseUrl;
|
|
105
144
|
private apiKey;
|
|
@@ -109,81 +148,20 @@ export declare class OmniSocialsClient {
|
|
|
109
148
|
status?: string;
|
|
110
149
|
limit?: string;
|
|
111
150
|
offset?: string;
|
|
112
|
-
}): Promise<ApiResponse<
|
|
113
|
-
getPost(id: string): Promise<ApiResponse<
|
|
151
|
+
}): Promise<ApiResponse<ApiPost[]>>;
|
|
152
|
+
getPost(id: string): Promise<ApiResponse<ApiPost>>;
|
|
114
153
|
getRecentPlatformPosts(params?: {
|
|
115
154
|
limit?: string;
|
|
116
155
|
platforms?: string;
|
|
117
|
-
}): Promise<ApiResponse<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
link_url?: string;
|
|
127
|
-
link_title?: string;
|
|
128
|
-
link_description?: string;
|
|
129
|
-
link_thumbnail_url?: string;
|
|
130
|
-
location_id?: string;
|
|
131
|
-
collaborators?: string[];
|
|
132
|
-
user_tags?: Array<{
|
|
133
|
-
username: string;
|
|
134
|
-
x: number;
|
|
135
|
-
y: number;
|
|
136
|
-
image_index?: number;
|
|
137
|
-
}>;
|
|
138
|
-
pinterest?: Record<string, unknown>;
|
|
139
|
-
youtube?: Record<string, unknown>;
|
|
140
|
-
instagram?: Record<string, unknown>;
|
|
141
|
-
facebook?: Record<string, unknown>;
|
|
142
|
-
linkedin?: Record<string, unknown>;
|
|
143
|
-
linkedin_page?: Record<string, unknown>;
|
|
144
|
-
tiktok?: Record<string, unknown>;
|
|
145
|
-
x?: XPostOptions;
|
|
146
|
-
bluesky?: BlueskyPostOptions;
|
|
147
|
-
mastodon?: MastodonPostOptions;
|
|
148
|
-
google_business?: Record<string, unknown>;
|
|
149
|
-
hashtag_set?: string;
|
|
150
|
-
hashtag_placement?: "caption_append" | "first_comment";
|
|
151
|
-
hashtag_platforms?: string[];
|
|
152
|
-
}): Promise<ApiResponse<unknown>>;
|
|
153
|
-
createAndPublishPost(data: {
|
|
154
|
-
content: string | Record<string, string>;
|
|
155
|
-
channels?: string[];
|
|
156
|
-
media_ids?: MediaIdEntry[] | Record<string, MediaIdEntry[]>;
|
|
157
|
-
media_urls?: MediaUrlEntry[] | Record<string, MediaUrlEntry[]>;
|
|
158
|
-
type?: string;
|
|
159
|
-
source?: string;
|
|
160
|
-
link_url?: string;
|
|
161
|
-
link_title?: string;
|
|
162
|
-
link_description?: string;
|
|
163
|
-
link_thumbnail_url?: string;
|
|
164
|
-
location_id?: string;
|
|
165
|
-
collaborators?: string[];
|
|
166
|
-
user_tags?: Array<{
|
|
167
|
-
username: string;
|
|
168
|
-
x: number;
|
|
169
|
-
y: number;
|
|
170
|
-
image_index?: number;
|
|
171
|
-
}>;
|
|
172
|
-
pinterest?: Record<string, unknown>;
|
|
173
|
-
youtube?: Record<string, unknown>;
|
|
174
|
-
instagram?: Record<string, unknown>;
|
|
175
|
-
facebook?: Record<string, unknown>;
|
|
176
|
-
linkedin?: Record<string, unknown>;
|
|
177
|
-
linkedin_page?: Record<string, unknown>;
|
|
178
|
-
tiktok?: Record<string, unknown>;
|
|
179
|
-
x?: XPostOptions;
|
|
180
|
-
bluesky?: BlueskyPostOptions;
|
|
181
|
-
mastodon?: MastodonPostOptions;
|
|
182
|
-
google_business?: Record<string, unknown>;
|
|
183
|
-
hashtag_set?: string;
|
|
184
|
-
hashtag_placement?: "caption_append" | "first_comment";
|
|
185
|
-
hashtag_platforms?: string[];
|
|
186
|
-
}): Promise<ApiResponse<unknown>>;
|
|
156
|
+
}): Promise<ApiResponse<RecentPlatformPost[]> & {
|
|
157
|
+
count?: number;
|
|
158
|
+
connected_platforms?: string[];
|
|
159
|
+
errors?: Record<string, string>;
|
|
160
|
+
note?: string;
|
|
161
|
+
current_date?: string;
|
|
162
|
+
}>;
|
|
163
|
+
createPost(data: CreatePostInput): Promise<ApiResponse<ApiPost>>;
|
|
164
|
+
createAndPublishPost(data: Omit<CreatePostInput, "scheduled_at">): Promise<ApiResponse<ApiPost>>;
|
|
187
165
|
updatePost(id: string, data: {
|
|
188
166
|
content?: string | Record<string, string>;
|
|
189
167
|
scheduled_at?: string;
|
|
@@ -210,26 +188,26 @@ export declare class OmniSocialsClient {
|
|
|
210
188
|
bluesky?: BlueskyPostOptionsUpdate;
|
|
211
189
|
mastodon?: MastodonPostOptionsUpdate;
|
|
212
190
|
google_business?: Record<string, unknown>;
|
|
213
|
-
}): Promise<ApiResponse<
|
|
191
|
+
}): Promise<ApiResponse<ApiPost>>;
|
|
214
192
|
deletePost(id: string): Promise<ApiResponse<unknown>>;
|
|
215
|
-
publishPost(id: string): Promise<ApiResponse<
|
|
216
|
-
retryPost(id: string): Promise<ApiResponse<
|
|
217
|
-
searchLocations(query: string): Promise<
|
|
193
|
+
publishPost(id: string): Promise<ApiResponse<PublishPostResult>>;
|
|
194
|
+
retryPost(id: string): Promise<ApiResponse<RetryPostResult>>;
|
|
195
|
+
searchLocations(query: string): Promise<LocationSearchResult>;
|
|
218
196
|
validateLocation(id: string): Promise<ApiResponse<unknown>>;
|
|
219
|
-
searchInstagramAudio(query?: string, type?: "music" | "original_sound"): Promise<
|
|
197
|
+
searchInstagramAudio(query?: string, type?: "music" | "original_sound"): Promise<AudioSearchResult>;
|
|
220
198
|
listMedia(params?: {
|
|
221
199
|
limit?: string;
|
|
222
200
|
offset?: string;
|
|
223
201
|
search?: string;
|
|
224
202
|
folder_id?: string;
|
|
225
|
-
}): Promise<ApiResponse<
|
|
203
|
+
}): Promise<ApiResponse<MediaItem[]>>;
|
|
226
204
|
uploadMedia(data: {
|
|
227
205
|
url: string;
|
|
228
206
|
filename?: string;
|
|
229
207
|
name?: string;
|
|
230
208
|
folder?: string;
|
|
231
209
|
}): Promise<ApiResponse<MediaItem> & Partial<PdfUploadResult> & {
|
|
232
|
-
compatibility?:
|
|
210
|
+
compatibility?: MediaCompatibility;
|
|
233
211
|
message?: string;
|
|
234
212
|
}>;
|
|
235
213
|
uploadMediaFromBase64(data: {
|
|
@@ -239,7 +217,7 @@ export declare class OmniSocialsClient {
|
|
|
239
217
|
name?: string;
|
|
240
218
|
folder?: string;
|
|
241
219
|
}): Promise<ApiResponse<MediaItem> & Partial<PdfUploadResult> & {
|
|
242
|
-
compatibility?:
|
|
220
|
+
compatibility?: MediaCompatibility;
|
|
243
221
|
message?: string;
|
|
244
222
|
}>;
|
|
245
223
|
/**
|
|
@@ -252,57 +230,73 @@ export declare class OmniSocialsClient {
|
|
|
252
230
|
media_id?: string;
|
|
253
231
|
size_bytes?: number;
|
|
254
232
|
mime?: string;
|
|
255
|
-
}): Promise<
|
|
233
|
+
}): Promise<MediaCheckResult>;
|
|
256
234
|
deleteMedia(id: string): Promise<ApiResponse<unknown>>;
|
|
257
235
|
updateMedia(id: string, data: {
|
|
258
236
|
name?: string;
|
|
259
237
|
folder_id?: string | null;
|
|
260
|
-
}): Promise<ApiResponse<
|
|
261
|
-
listFolders(): Promise<ApiResponse<
|
|
238
|
+
}): Promise<ApiResponse<MediaItem>>;
|
|
239
|
+
listFolders(): Promise<ApiResponse<MediaFolder[]>>;
|
|
262
240
|
createFolder(data: {
|
|
263
241
|
name: string;
|
|
264
242
|
parent_id?: string;
|
|
265
|
-
}): Promise<ApiResponse<
|
|
266
|
-
listHashtagSets(): Promise<ApiResponse<
|
|
243
|
+
}): Promise<ApiResponse<MediaFolder>>;
|
|
244
|
+
listHashtagSets(): Promise<ApiResponse<HashtagSet[]>>;
|
|
267
245
|
createHashtagSet(data: {
|
|
268
246
|
name: string;
|
|
269
247
|
hashtags: string[];
|
|
270
|
-
}): Promise<ApiResponse<
|
|
248
|
+
}): Promise<ApiResponse<HashtagSet>>;
|
|
271
249
|
updateHashtagSet(id: string, data: {
|
|
272
250
|
name?: string;
|
|
273
251
|
hashtags?: string[];
|
|
274
|
-
}): Promise<ApiResponse<
|
|
252
|
+
}): Promise<ApiResponse<HashtagSet>>;
|
|
275
253
|
deleteHashtagSet(id: string): Promise<ApiResponse<unknown>>;
|
|
276
|
-
listAccounts(): Promise<ApiResponse<
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
254
|
+
listAccounts(): Promise<ApiResponse<Account[]> & {
|
|
255
|
+
workspace_id?: number | string | null;
|
|
256
|
+
workspace_name?: string | null;
|
|
257
|
+
workspace_icon?: string | null;
|
|
258
|
+
}>;
|
|
259
|
+
getAccount(id: string): Promise<ApiResponse<Account>>;
|
|
260
|
+
getPostAnalytics(postId: string): Promise<ApiResponse<PostAnalytics>>;
|
|
261
|
+
getPostsAnalytics(postIds: string[]): Promise<ApiResponse<PostAnalytics[]>>;
|
|
280
262
|
getAnalyticsOverview(params?: {
|
|
281
263
|
period?: string;
|
|
282
264
|
start_date?: string;
|
|
283
265
|
end_date?: string;
|
|
284
|
-
}): Promise<ApiResponse<
|
|
266
|
+
}): Promise<ApiResponse<AnalyticsOverview> & {
|
|
267
|
+
period?: string;
|
|
268
|
+
start_date?: string;
|
|
269
|
+
end_date?: string;
|
|
270
|
+
current_date?: string;
|
|
271
|
+
}>;
|
|
285
272
|
getAccountAnalytics(params?: {
|
|
286
273
|
platform?: string;
|
|
287
274
|
date?: string;
|
|
288
|
-
}): Promise<ApiResponse<
|
|
275
|
+
}): Promise<ApiResponse<AccountAnalyticsEntry[]> & {
|
|
276
|
+
date?: string;
|
|
277
|
+
}>;
|
|
289
278
|
getBestTimes(params: {
|
|
290
279
|
platform: string;
|
|
291
280
|
timezone?: string;
|
|
292
|
-
}): Promise<
|
|
293
|
-
listWebhooks(): Promise<ApiResponse<
|
|
281
|
+
}): Promise<BestTimesResult>;
|
|
282
|
+
listWebhooks(): Promise<ApiResponse<Webhook[]>>;
|
|
294
283
|
createWebhook(data: {
|
|
295
284
|
url: string;
|
|
296
285
|
events: string[];
|
|
297
|
-
}): Promise<ApiResponse<
|
|
286
|
+
}): Promise<ApiResponse<Webhook & {
|
|
287
|
+
secret: string;
|
|
288
|
+
}>>;
|
|
298
289
|
deleteWebhook(id: string): Promise<ApiResponse<unknown>>;
|
|
299
|
-
getWebhook(id: string): Promise<ApiResponse<
|
|
290
|
+
getWebhook(id: string): Promise<ApiResponse<Webhook>>;
|
|
300
291
|
updateWebhook(id: string, data: {
|
|
301
292
|
url?: string;
|
|
302
293
|
events?: string[];
|
|
303
294
|
is_active?: boolean;
|
|
304
|
-
}): Promise<ApiResponse<
|
|
305
|
-
rotateWebhookSecret(id: string): Promise<ApiResponse<
|
|
295
|
+
}): Promise<ApiResponse<Webhook>>;
|
|
296
|
+
rotateWebhookSecret(id: string): Promise<ApiResponse<{
|
|
297
|
+
id: string;
|
|
298
|
+
secret: string;
|
|
299
|
+
}>>;
|
|
306
300
|
listInboxConversations(params?: {
|
|
307
301
|
platform?: string;
|
|
308
302
|
type?: string;
|
package/build/client.js
CHANGED
|
@@ -201,6 +201,8 @@ export class OmniSocialsClient {
|
|
|
201
201
|
// Recent posts fetched live from the connected platform APIs (including
|
|
202
202
|
// content published outside OmniSocials). The fallback for brand-new
|
|
203
203
|
// workspaces where listPosts is empty. Requires the analytics:read scope.
|
|
204
|
+
// `connected_platforms`/`errors`/`note`/`current_date` ride top-level on the
|
|
205
|
+
// envelope (backend/routes/api/v1/posts.js recent-platform route).
|
|
204
206
|
async getRecentPlatformPosts(params) {
|
|
205
207
|
return this.request("GET", "/posts/recent-platform", undefined, params);
|
|
206
208
|
}
|
|
@@ -224,14 +226,17 @@ export class OmniSocialsClient {
|
|
|
224
226
|
async retryPost(id) {
|
|
225
227
|
return this.request("POST", `/posts/${id}/retry`);
|
|
226
228
|
}
|
|
227
|
-
// Locations (Instagram place tagging)
|
|
229
|
+
// Locations (Instagram place tagging). NOT the standard envelope — see
|
|
230
|
+
// LocationSearchResult (degraded-path `error` is a plain string).
|
|
228
231
|
async searchLocations(query) {
|
|
229
|
-
return this.request("GET", "/locations/search", undefined, {
|
|
232
|
+
return this.request("GET", "/locations/search", undefined, {
|
|
233
|
+
q: query,
|
|
234
|
+
});
|
|
230
235
|
}
|
|
231
236
|
async validateLocation(id) {
|
|
232
237
|
return this.request("GET", "/locations/validate", undefined, { id });
|
|
233
238
|
}
|
|
234
|
-
// Audio (Instagram Reel music)
|
|
239
|
+
// Audio (Instagram Reel music). Same nonstandard string-`error` envelope.
|
|
235
240
|
async searchInstagramAudio(query, type) {
|
|
236
241
|
const params = {};
|
|
237
242
|
if (query)
|
|
@@ -262,6 +267,7 @@ export class OmniSocialsClient {
|
|
|
262
267
|
* or `size_bytes` + `mime`.
|
|
263
268
|
*/
|
|
264
269
|
async checkMediaCompatibility(data) {
|
|
270
|
+
// Nonstandard envelope: compatibility fields ride top-level, no `data`.
|
|
265
271
|
return this.request("POST", "/media/check", data);
|
|
266
272
|
}
|
|
267
273
|
async deleteMedia(id) {
|
|
@@ -290,7 +296,9 @@ export class OmniSocialsClient {
|
|
|
290
296
|
async deleteHashtagSet(id) {
|
|
291
297
|
return this.request("DELETE", `/hashtag-sets/${id}`);
|
|
292
298
|
}
|
|
293
|
-
// Accounts
|
|
299
|
+
// Accounts. Alongside `data`, the list response carries the workspace's
|
|
300
|
+
// identity top-level (backend/routes/api/v1/accounts.js) — the workspaces
|
|
301
|
+
// tool reads it to label multi-workspace setups.
|
|
294
302
|
async listAccounts() {
|
|
295
303
|
return this.request("GET", "/accounts");
|
|
296
304
|
}
|
|
@@ -308,12 +316,18 @@ export class OmniSocialsClient {
|
|
|
308
316
|
ids: postIds.join(","),
|
|
309
317
|
});
|
|
310
318
|
}
|
|
319
|
+
// The resolved range and today's date ride top-level on the envelope
|
|
320
|
+
// (backend/routes/api/v1/analytics.js overview route) so clients can render
|
|
321
|
+
// an accurate "as of <today>" line.
|
|
311
322
|
async getAnalyticsOverview(params) {
|
|
312
323
|
return this.request("GET", "/analytics/overview", undefined, params);
|
|
313
324
|
}
|
|
325
|
+
// The resolved `date` rides top-level on the envelope.
|
|
314
326
|
async getAccountAnalytics(params) {
|
|
315
327
|
return this.request("GET", "/analytics/accounts", undefined, params);
|
|
316
328
|
}
|
|
329
|
+
// Nonstandard envelope: the result object is returned top-level, no `data`
|
|
330
|
+
// (backend/routes/api/v1/analytics.js best-times route).
|
|
317
331
|
async getBestTimes(params) {
|
|
318
332
|
return this.request("GET", "/analytics/best-times", undefined, params);
|
|
319
333
|
}
|
|
@@ -321,6 +335,7 @@ export class OmniSocialsClient {
|
|
|
321
335
|
async listWebhooks() {
|
|
322
336
|
return this.request("GET", "/webhooks");
|
|
323
337
|
}
|
|
338
|
+
// The signing secret is only ever returned at creation time.
|
|
324
339
|
async createWebhook(data) {
|
|
325
340
|
return this.request("POST", "/webhooks", data);
|
|
326
341
|
}
|
|
@@ -333,6 +348,8 @@ export class OmniSocialsClient {
|
|
|
333
348
|
async updateWebhook(id, data) {
|
|
334
349
|
return this.request("PATCH", `/webhooks/${id}`, data);
|
|
335
350
|
}
|
|
351
|
+
// Returns only { id, secret } — not the full webhook
|
|
352
|
+
// (backend/routes/api/v1/webhooks.js rotate-secret route).
|
|
336
353
|
async rotateWebhookSecret(id) {
|
|
337
354
|
return this.request("POST", `/webhooks/${id}/rotate-secret`);
|
|
338
355
|
}
|
package/build/index.js
CHANGED
|
@@ -29,7 +29,7 @@ const sessionState = { activeIndex: 0 };
|
|
|
29
29
|
const getActiveClient = () => workspaceClients[sessionState.activeIndex].client;
|
|
30
30
|
const server = new McpServer({
|
|
31
31
|
name: "OmniSocials",
|
|
32
|
-
version: "1.
|
|
32
|
+
version: "1.20.0",
|
|
33
33
|
});
|
|
34
34
|
// Register all tools - pass getter function so tools always use the active workspace's client
|
|
35
35
|
registerPostTools(server, getActiveClient);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engagement / impressions normalizers — VENDORED COPY of the repo's
|
|
3
|
+
* `shared/analytics-metrics/index.js` (the single source of truth used by the
|
|
4
|
+
* backend, public API, remote MCP, and dashboard). This package publishes to
|
|
5
|
+
* npm standalone, so it can't require() across the monorepo — keep this file
|
|
6
|
+
* in sync with shared/analytics-metrics when the rules change there.
|
|
7
|
+
*
|
|
8
|
+
* WHY: the MCP tools used to compute engagement inline as
|
|
9
|
+
* likes+comments+shares, which silently dropped LinkedIn clicks and X
|
|
10
|
+
* quotes/bookmarks — so the same post showed a different engagement rate in
|
|
11
|
+
* MCP output than on the dashboard or /analytics/overview.
|
|
12
|
+
*/
|
|
13
|
+
export declare const MIN_IMPRESSIONS_FOR_ENGAGEMENT_RATE = 10;
|
|
14
|
+
type Metrics = Record<string, unknown> | null | undefined;
|
|
15
|
+
/** Total engagement (interaction count) for a post's metrics on a platform. */
|
|
16
|
+
export declare function getEngagement(platform: string, metrics: Metrics): number;
|
|
17
|
+
/** Best-available impressions ("how many times seen") for a post's metrics. */
|
|
18
|
+
export declare function getImpressions(platform: string, metrics: Metrics): number;
|
|
19
|
+
/**
|
|
20
|
+
* Engagement rate as a percentage (0-100), suppressed below the impression
|
|
21
|
+
* floor and capped at 100%.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getEngagementRate(engagement: number, impressions: number): number;
|
|
24
|
+
export {};
|
package/build/metrics.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engagement / impressions normalizers — VENDORED COPY of the repo's
|
|
3
|
+
* `shared/analytics-metrics/index.js` (the single source of truth used by the
|
|
4
|
+
* backend, public API, remote MCP, and dashboard). This package publishes to
|
|
5
|
+
* npm standalone, so it can't require() across the monorepo — keep this file
|
|
6
|
+
* in sync with shared/analytics-metrics when the rules change there.
|
|
7
|
+
*
|
|
8
|
+
* WHY: the MCP tools used to compute engagement inline as
|
|
9
|
+
* likes+comments+shares, which silently dropped LinkedIn clicks and X
|
|
10
|
+
* quotes/bookmarks — so the same post showed a different engagement rate in
|
|
11
|
+
* MCP output than on the dashboard or /analytics/overview.
|
|
12
|
+
*/
|
|
13
|
+
// Minimum impressions before an engagement-rate percentage is meaningful.
|
|
14
|
+
// Below this we suppress the rate rather than render a noisy ratio.
|
|
15
|
+
export const MIN_IMPRESSIONS_FOR_ENGAGEMENT_RATE = 10;
|
|
16
|
+
// Coerce anything (number, numeric string, null, undefined) to a finite number.
|
|
17
|
+
const num = (v) => {
|
|
18
|
+
if (typeof v === "number")
|
|
19
|
+
return Number.isFinite(v) ? v : 0;
|
|
20
|
+
const n = Number(v);
|
|
21
|
+
return Number.isFinite(n) ? n : 0;
|
|
22
|
+
};
|
|
23
|
+
/** Total engagement (interaction count) for a post's metrics on a platform. */
|
|
24
|
+
export function getEngagement(platform, metrics) {
|
|
25
|
+
const m = metrics || {};
|
|
26
|
+
// Trust a precomputed engagement count when a fetcher already supplied one
|
|
27
|
+
// (Instagram total_interactions, TikTok/Threads/Pinterest sums, Google
|
|
28
|
+
// Business account engagement). Only when it's a real positive number —
|
|
29
|
+
// a stored 0 means "not computed" for the platforms that don't set it.
|
|
30
|
+
const pre = num(m.engagement);
|
|
31
|
+
if (pre > 0)
|
|
32
|
+
return pre;
|
|
33
|
+
const sum = (...keys) => keys.reduce((t, k) => t + num(m[k]), 0);
|
|
34
|
+
switch (platform) {
|
|
35
|
+
case "facebook":
|
|
36
|
+
// total_reactions already aggregates like/love/wow/...; fall back to the
|
|
37
|
+
// bare like count only when the aggregate is absent (never add both).
|
|
38
|
+
return (num(m.total_reactions) || num(m.likes)) + num(m.comments) + num(m.shares);
|
|
39
|
+
case "linkedin":
|
|
40
|
+
case "linkedin_page":
|
|
41
|
+
// LinkedIn's native engagement includes link clicks.
|
|
42
|
+
return sum("likes", "comments", "shares", "clicks");
|
|
43
|
+
case "youtube":
|
|
44
|
+
return sum("likes", "comments", "shares");
|
|
45
|
+
case "x":
|
|
46
|
+
// X's engagement total includes bookmarks (saves).
|
|
47
|
+
return sum("likes", "comments", "reposts", "quotes", "bookmarks");
|
|
48
|
+
case "threads":
|
|
49
|
+
// Threads stores replies (not comments) as the comment-equivalent.
|
|
50
|
+
return num(m.likes) + (num(m.comments) || num(m.replies)) + num(m.reposts) + num(m.quotes);
|
|
51
|
+
case "bluesky":
|
|
52
|
+
return sum("likes", "comments", "reposts", "quotes");
|
|
53
|
+
case "mastodon":
|
|
54
|
+
return sum("likes", "comments", "reposts");
|
|
55
|
+
case "reddit":
|
|
56
|
+
// likes == upvote score; shares == crossposts.
|
|
57
|
+
return sum("likes", "comments", "shares");
|
|
58
|
+
case "instagram":
|
|
59
|
+
return sum("likes", "comments", "saves", "shares");
|
|
60
|
+
case "tiktok":
|
|
61
|
+
return sum("likes", "comments", "shares");
|
|
62
|
+
case "pinterest":
|
|
63
|
+
return sum("saves", "pin_clicks", "outbound_clicks");
|
|
64
|
+
case "google_business":
|
|
65
|
+
// engagement is precomputed at account level; nothing to sum per post.
|
|
66
|
+
return pre;
|
|
67
|
+
default:
|
|
68
|
+
// Generic safe sum of common interaction fields for any future platform.
|
|
69
|
+
return sum("likes", "comments", "replies", "shares", "reposts", "quotes", "saves");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Best-available impressions ("how many times seen") for a post's metrics. */
|
|
73
|
+
export function getImpressions(platform, metrics) {
|
|
74
|
+
const m = metrics || {};
|
|
75
|
+
switch (platform) {
|
|
76
|
+
case "instagram":
|
|
77
|
+
// Reels/video report `views` (play count) which is the true total-seen
|
|
78
|
+
// number and exceeds unique `reach`. Prefer it; fall back for old rows.
|
|
79
|
+
return num(m.views) || num(m.impressions) || num(m.reach);
|
|
80
|
+
case "facebook":
|
|
81
|
+
// Facebook exposes reach/impressions; video posts also report video_views.
|
|
82
|
+
return num(m.reach) || num(m.impressions) || num(m.video_views) || num(m.views);
|
|
83
|
+
case "linkedin":
|
|
84
|
+
case "linkedin_page":
|
|
85
|
+
case "pinterest":
|
|
86
|
+
case "google_business":
|
|
87
|
+
return num(m.impressions) || num(m.views) || num(m.reach);
|
|
88
|
+
case "youtube":
|
|
89
|
+
case "tiktok":
|
|
90
|
+
case "x":
|
|
91
|
+
case "threads":
|
|
92
|
+
case "bluesky":
|
|
93
|
+
case "mastodon":
|
|
94
|
+
case "reddit":
|
|
95
|
+
default:
|
|
96
|
+
return num(m.views) || num(m.impressions) || num(m.reach);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Engagement rate as a percentage (0-100), suppressed below the impression
|
|
101
|
+
* floor and capped at 100%.
|
|
102
|
+
*/
|
|
103
|
+
export function getEngagementRate(engagement, impressions) {
|
|
104
|
+
const imp = num(impressions);
|
|
105
|
+
if (imp < MIN_IMPRESSIONS_FOR_ENGAGEMENT_RATE)
|
|
106
|
+
return 0;
|
|
107
|
+
const rate = (num(engagement) / imp) * 100;
|
|
108
|
+
if (!Number.isFinite(rate) || rate < 0)
|
|
109
|
+
return 0;
|
|
110
|
+
return Math.min(rate, 100);
|
|
111
|
+
}
|
package/build/tools/accounts.js
CHANGED
|
@@ -8,7 +8,7 @@ export function registerAccountTools(server, getClient) {
|
|
|
8
8
|
content: [{ type: "text", text: `Error (${result.error.code}): ${result.error.message}` }],
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
|
-
const accounts = Array.isArray(result.data) ? result.data :
|
|
11
|
+
const accounts = Array.isArray(result.data) ? result.data : [];
|
|
12
12
|
if (!accounts.length) {
|
|
13
13
|
return {
|
|
14
14
|
content: [{ type: "text", text: "No connected accounts found." }],
|
|
@@ -42,7 +42,7 @@ export function registerAccountTools(server, getClient) {
|
|
|
42
42
|
// Fetch profile pictures
|
|
43
43
|
const content = [];
|
|
44
44
|
const imagePromises = accounts
|
|
45
|
-
.filter((a) => a.profile_picture)
|
|
45
|
+
.filter((a) => !!a.profile_picture)
|
|
46
46
|
.slice(0, 6)
|
|
47
47
|
.map(async (a) => {
|
|
48
48
|
const img = await fetchImageAsBase64(a.profile_picture);
|