@omnisocials/mcp-server 1.7.1 → 1.8.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
@@ -146,11 +146,12 @@ OmniSocials accepts the following channel IDs in `create_post`, `create_and_publ
146
146
  | `list_accounts` | List connected social media accounts |
147
147
  | `get_account` | Get account details |
148
148
 
149
- ### Analytics (3 tools)
149
+ ### Analytics (4 tools)
150
150
 
151
151
  | Tool | Description |
152
152
  |------|-------------|
153
153
  | `get_post_analytics` | Get stats for a published post |
154
+ | `get_posts_analytics` | Get stats for up to 100 posts in one call (bulk) |
154
155
  | `get_analytics_overview` | Overview analytics for a time period |
155
156
  | `get_account_analytics` | Account-level analytics (followers, etc.) |
156
157
 
package/build/client.d.ts CHANGED
@@ -165,6 +165,7 @@ export declare class OmniSocialsClient {
165
165
  listAccounts(): Promise<ApiResponse<unknown>>;
166
166
  getAccount(id: string): Promise<ApiResponse<unknown>>;
167
167
  getPostAnalytics(postId: string): Promise<ApiResponse<unknown>>;
168
+ getPostsAnalytics(postIds: string[]): Promise<ApiResponse<unknown>>;
168
169
  getAnalyticsOverview(params?: {
169
170
  period?: string;
170
171
  start_date?: string;
package/build/client.js CHANGED
@@ -202,6 +202,13 @@ export class OmniSocialsClient {
202
202
  async getPostAnalytics(postId) {
203
203
  return this.request("GET", `/analytics/posts/${postId}`);
204
204
  }
205
+ // Batch version of getPostAnalytics — fetch up to 100 posts' latest
206
+ // per-platform metrics in one call instead of one request per post.
207
+ async getPostsAnalytics(postIds) {
208
+ return this.request("GET", "/analytics/posts", undefined, {
209
+ ids: postIds.join(","),
210
+ });
211
+ }
205
212
  async getAnalyticsOverview(params) {
206
213
  return this.request("GET", "/analytics/overview", undefined, params);
207
214
  }
package/build/index.js CHANGED
@@ -27,7 +27,7 @@ const sessionState = { activeIndex: 0 };
27
27
  const getActiveClient = () => workspaceClients[sessionState.activeIndex].client;
28
28
  const server = new McpServer({
29
29
  name: "OmniSocials",
30
- version: "1.7.1",
30
+ version: "1.8.0",
31
31
  });
32
32
  // Register all tools - pass getter function so tools always use the active workspace's client
33
33
  registerPostTools(server, getActiveClient);
@@ -65,6 +65,60 @@ export function registerAnalyticsTools(server, getClient) {
65
65
  content: [{ type: "text", text: md }],
66
66
  };
67
67
  });
68
+ server.tool("get_posts_analytics", "Get analytics for several published posts at once (up to 100). Returns each post's totalled impressions and engagements. Use this instead of calling get_post_analytics in a loop — it is one request rather than one per post.", {
69
+ post_ids: z
70
+ .array(z.string())
71
+ .min(1)
72
+ .max(100)
73
+ .describe("Numeric OmniSocials post ids (the `id` field from list_posts), up to 100. NOT platform post ids such as YouTube video ids or tweet ids."),
74
+ }, async ({ post_ids }) => {
75
+ const result = await getClient().getPostsAnalytics(post_ids);
76
+ if (result.error) {
77
+ return {
78
+ content: [{ type: "text", text: `Error (${result.error.code}): ${result.error.message}` }],
79
+ };
80
+ }
81
+ // API returns { data: [{ post_id, platforms: { <platform>: { metrics } } }] }.
82
+ // Collapse each post's per-platform metrics into totals, mirroring the
83
+ // aggregation used by get_post_analytics.
84
+ const entries = result.data || [];
85
+ const rows = entries.map((entry) => {
86
+ const platforms = entry?.platforms || {};
87
+ const platformEntries = Object.entries(platforms);
88
+ let impressions = 0;
89
+ let engagements = 0;
90
+ for (const [platform, p] of platformEntries) {
91
+ const m = p?.metrics || {};
92
+ const imp = platform === "instagram"
93
+ ? Number(m.reach ?? m.impressions ?? 0)
94
+ : Number(m.views ?? m.impressions ?? 0);
95
+ const likes = Number(m.likes ?? m.favorites ?? m.reactions ?? 0);
96
+ const comments = Number(m.comments ?? m.replies ?? 0);
97
+ const shares = Number(m.shares ?? m.retweets ?? m.reposts ?? 0);
98
+ const eng = m.engagement != null ? Number(m.engagement) : likes + comments + shares;
99
+ impressions += imp;
100
+ engagements += eng;
101
+ }
102
+ return {
103
+ post_id: entry.post_id,
104
+ platformCount: platformEntries.length,
105
+ platformNames: platformEntries.map(([name]) => capitalize(name)).join(", "),
106
+ impressions,
107
+ engagements,
108
+ };
109
+ });
110
+ const withData = rows.filter((r) => r.platformCount > 0).length;
111
+ let md = `## Posts Analytics (${rows.length} posts, ${withData} with collected stats)\n\n`;
112
+ md += `| Post ID | Platforms | Impressions | Engagements |\n`;
113
+ md += `|---------|-----------|-------------|-------------|\n`;
114
+ for (const r of rows) {
115
+ md += `| ${r.post_id} | ${r.platformNames || "—"} | ${r.platformCount ? formatNumber(r.impressions) : "—"} | ${r.platformCount ? formatNumber(r.engagements) : "—"} |\n`;
116
+ }
117
+ md += `\nPosts showing "—" have no analytics collected yet. Stats are fetched periodically after publishing; check back in a few hours.\n`;
118
+ return {
119
+ content: [{ type: "text", text: md }],
120
+ };
121
+ });
68
122
  server.tool("get_analytics_overview", "Get analytics overview with total posts, impressions, engagements, engagement rate, and per-platform breakdown. Use `period` for a rolling window (7d / 30d / 90d) or `start_date` + `end_date` for a custom range. The response echoes the resolved range and today's date — read those before assuming a year from training data (e.g. when the user says 'April', use the most recent April, not April from your training cutoff).", {
69
123
  period: z.string().optional().describe("Rolling window: 7d, 30d, 90d (default: 30d). Ignored if start_date/end_date are provided."),
70
124
  start_date: z.string().optional().describe('Custom start date. Accepts "YYYY-MM-DD" (e.g. "2026-04-01") or "YYYY-MM" month-shorthand (e.g. "2026-04" → first of the month).'),
@@ -271,7 +271,7 @@ Do NOT call this tool without media when creating stories, reels, Instagram post
271
271
  paid_partnership: z.boolean().optional().describe("Mark as paid partnership disclosure"),
272
272
  made_with_ai: z.boolean().optional().describe("Mark as AI-generated content"),
273
273
  thread_parts: z.array(z.object({
274
- text: z.string().describe("Tweet text (≤ 280 chars)"),
274
+ text: z.string().describe("Tweet text (≤ 280 chars). X counts every link as 23 characters (its t.co length), so a short link still uses 23 toward the limit."),
275
275
  media_ids: z.array(z.string()).max(4).optional().describe("Per-tweet media as Library IDs from upload_media (max 4 combined with media_urls). Attach your uploaded graphics to any tweet in the thread."),
276
276
  media_urls: z.array(z.string()).max(4).optional().describe("Per-tweet media as external URLs (max 4 combined with media_ids)"),
277
277
  })).min(2).max(25).optional().describe("Publish as a chained X thread instead of a single tweet. Provide 2–25 parts; each is posted in order via in_reply_to_tweet_id. Attach media to any part (first tweet or reply) via media_ids (from upload_media) or media_urls — max 4 per part. For a single tweet, omit thread_parts and use content."),
@@ -403,7 +403,7 @@ Do NOT call without required media — it will fail.`, {
403
403
  paid_partnership: z.boolean().optional().describe("Mark as paid partnership disclosure"),
404
404
  made_with_ai: z.boolean().optional().describe("Mark as AI-generated content"),
405
405
  thread_parts: z.array(z.object({
406
- text: z.string().describe("Tweet text (≤ 280 chars)"),
406
+ text: z.string().describe("Tweet text (≤ 280 chars). X counts every link as 23 characters (its t.co length), so a short link still uses 23 toward the limit."),
407
407
  media_ids: z.array(z.string()).max(4).optional().describe("Per-tweet media as Library IDs from upload_media (max 4 combined with media_urls). Attach your uploaded graphics to any tweet in the thread."),
408
408
  media_urls: z.array(z.string()).max(4).optional().describe("Per-tweet media as external URLs (max 4 combined with media_ids)"),
409
409
  })).min(2).max(25).optional().describe("Publish as a chained X thread (2–25 parts). Each is posted in order via in_reply_to_tweet_id. Attach media to any part via media_ids (from upload_media) or media_urls — max 4 per part."),
@@ -521,7 +521,7 @@ Do NOT call without required media — it will fail.`, {
521
521
  paid_partnership: z.boolean().optional(),
522
522
  made_with_ai: z.boolean().optional(),
523
523
  thread_parts: z.array(z.object({
524
- text: z.string().describe("Tweet text (≤ 280 chars)"),
524
+ text: z.string().describe("Tweet text (≤ 280 chars). X counts every link as 23 characters (its t.co length), so a short link still uses 23 toward the limit."),
525
525
  media_ids: z.array(z.string()).max(4).optional().describe("Per-tweet media as Library IDs from upload_media (max 4 combined with media_urls). Attach your uploaded graphics to any tweet in the thread."),
526
526
  media_urls: z.array(z.string()).max(4).optional().describe("Per-tweet media as external URLs (max 4 combined with media_ids)"),
527
527
  })).min(2).max(25).nullable().optional().describe("Replace the X thread shape on this post. Pass an array (2–25 parts) to update/create the thread (attach media to any part via media_ids or media_urls, max 4 per part), or `null` to revert to single-tweet mode."),
@@ -73,6 +73,10 @@ export function registerWebhookTools(server, getClient) {
73
73
  md += `| **Status** | ${w.is_active ? "Active" : "Inactive"} |\n`;
74
74
  md += `| **Last Triggered** | ${w.last_triggered_at ? formatDateTime(w.last_triggered_at) : "Never"} |\n`;
75
75
  md += `| **Failure Count** | ${w.failure_count ?? 0} |\n`;
76
+ if (w.last_failure) {
77
+ const lf = w.last_failure;
78
+ md += `| **Last Failure** | ${formatDateTime(lf.at)} — ${lf.error} |\n`;
79
+ }
76
80
  md += `| **Created** | ${formatDateTime(w.created_at)} |\n`;
77
81
  return {
78
82
  content: [{ type: "text", text: md }],
package/build/types.d.ts CHANGED
@@ -74,6 +74,12 @@ export interface Webhook {
74
74
  is_active: boolean;
75
75
  last_triggered_at: string | null;
76
76
  failure_count: number;
77
+ last_failure: {
78
+ at: string;
79
+ status: number | null;
80
+ error: string;
81
+ attempts: number;
82
+ } | null;
77
83
  created_at: string;
78
84
  }
79
85
  export interface AnalyticsOverview {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnisocials/mcp-server",
3
- "version": "1.7.1",
3
+ "version": "1.8.0",
4
4
  "description": "MCP server for OmniSocials API - manage social media posts, media, accounts, analytics, and webhooks",
5
5
  "type": "module",
6
6
  "main": "build/index.js",