posterly-mcp-server 0.4.0 → 0.5.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/dist/index.js CHANGED
@@ -12,9 +12,11 @@ import { updatePostTool } from './tools/update-post.js';
12
12
  import { deletePostTool } from './tools/delete-post.js';
13
13
  import { whoamiTool } from './tools/whoami.js';
14
14
  import { generateImageTool } from './tools/generate-image.js';
15
+ import { getAccountAnalyticsTool } from './tools/get-account-analytics.js';
16
+ import { getPostAnalyticsTool } from './tools/get-post-analytics.js';
15
17
  const server = new McpServer({
16
18
  name: 'posterly',
17
- version: '0.4.0',
19
+ version: '0.5.0',
18
20
  });
19
21
  let client;
20
22
  try {
@@ -115,6 +117,24 @@ server.tool(deletePostTool.name, deletePostTool.description, deletePostTool.inpu
115
117
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
116
118
  }
117
119
  });
120
+ server.tool(getAccountAnalyticsTool.name, getAccountAnalyticsTool.description, getAccountAnalyticsTool.inputSchema.shape, async (input) => {
121
+ try {
122
+ const text = await getAccountAnalyticsTool.execute(client, input);
123
+ return { content: [{ type: 'text', text }] };
124
+ }
125
+ catch (err) {
126
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
127
+ }
128
+ });
129
+ server.tool(getPostAnalyticsTool.name, getPostAnalyticsTool.description, getPostAnalyticsTool.inputSchema.shape, async (input) => {
130
+ try {
131
+ const text = await getPostAnalyticsTool.execute(client, input);
132
+ return { content: [{ type: 'text', text }] };
133
+ }
134
+ catch (err) {
135
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
136
+ }
137
+ });
118
138
  // Start the server
119
139
  async function main() {
120
140
  const transport = new StdioServerTransport();
@@ -43,6 +43,86 @@ export interface Whoami {
43
43
  default_workspace: Workspace;
44
44
  workspaces: Workspace[];
45
45
  }
46
+ export interface AccountAnalyticsSummary {
47
+ current_followers: number;
48
+ followers_change: number;
49
+ total_reach: number | null;
50
+ total_views: number | null;
51
+ total_accounts_engaged: number | null;
52
+ total_follows_gained: number;
53
+ total_follows_lost: number;
54
+ engagement_rate: number;
55
+ engagement_rate_by_followers: number;
56
+ total_website_clicks: number | null;
57
+ total_call_clicks: number | null;
58
+ total_direction_requests: number | null;
59
+ total_conversations: number | null;
60
+ total_bookings: number | null;
61
+ }
62
+ export interface AccountAnalyticsSnapshot {
63
+ date: string;
64
+ followers: number | null;
65
+ following: number | null;
66
+ media_count: number | null;
67
+ reach: number | null;
68
+ views: number | null;
69
+ profile_views: number | null;
70
+ accounts_engaged: number | null;
71
+ follows_gained: number | null;
72
+ follows_lost: number | null;
73
+ website_clicks: number | null;
74
+ call_clicks: number | null;
75
+ direction_requests: number | null;
76
+ conversations: number | null;
77
+ bookings: number | null;
78
+ }
79
+ export interface AccountAnalyticsResponse {
80
+ account: {
81
+ id: number;
82
+ platform: string;
83
+ username: string;
84
+ };
85
+ range: {
86
+ from: string;
87
+ to: string;
88
+ };
89
+ summary: AccountAnalyticsSummary;
90
+ snapshots: AccountAnalyticsSnapshot[];
91
+ }
92
+ export interface PostAnalyticsRow {
93
+ id: number | null;
94
+ platform_media_id: string;
95
+ platform: string;
96
+ posted_at: string | null;
97
+ likes: number;
98
+ comments: number;
99
+ impressions: number;
100
+ reach: number;
101
+ saved: number;
102
+ shares: number;
103
+ plays: number;
104
+ total_interactions: number;
105
+ media_type: string | null;
106
+ media_url: string | null;
107
+ permalink: string | null;
108
+ caption_snippet: string | null;
109
+ synced_at: string;
110
+ }
111
+ export interface PostAnalyticsResponse {
112
+ account: {
113
+ id: number;
114
+ platform: string;
115
+ username: string;
116
+ };
117
+ range: {
118
+ from: string;
119
+ to: string;
120
+ };
121
+ posts: PostAnalyticsRow[];
122
+ total: number;
123
+ limit: number;
124
+ offset: number;
125
+ }
46
126
  export declare class PosterlyClient {
47
127
  private baseUrl;
48
128
  private apiKey;
@@ -130,6 +210,18 @@ export declare class PosterlyClient {
130
210
  count?: number;
131
211
  workspace_id?: string;
132
212
  }): Promise<Slot[]>;
213
+ getAccountAnalytics(params: {
214
+ account_id: number;
215
+ from?: string;
216
+ to?: string;
217
+ }): Promise<AccountAnalyticsResponse>;
218
+ getPostAnalytics(params: {
219
+ account_id: number;
220
+ from?: string;
221
+ to?: string;
222
+ limit?: number;
223
+ offset?: number;
224
+ }): Promise<PostAnalyticsResponse>;
133
225
  getSignedUploadUrl(filename: string, contentType: string, size: number): Promise<{
134
226
  upload_url: string;
135
227
  token: string;
@@ -85,6 +85,28 @@ export class PosterlyClient {
85
85
  const data = await this.request('GET', `/slots/next${qs ? `?${qs}` : ''}`);
86
86
  return data.slots;
87
87
  }
88
+ async getAccountAnalytics(params) {
89
+ const searchParams = new URLSearchParams();
90
+ searchParams.set('account_id', String(params.account_id));
91
+ if (params.from)
92
+ searchParams.set('from', params.from);
93
+ if (params.to)
94
+ searchParams.set('to', params.to);
95
+ return this.request('GET', `/analytics/accounts?${searchParams.toString()}`);
96
+ }
97
+ async getPostAnalytics(params) {
98
+ const searchParams = new URLSearchParams();
99
+ searchParams.set('account_id', String(params.account_id));
100
+ if (params.from)
101
+ searchParams.set('from', params.from);
102
+ if (params.to)
103
+ searchParams.set('to', params.to);
104
+ if (params.limit)
105
+ searchParams.set('limit', String(params.limit));
106
+ if (params.offset)
107
+ searchParams.set('offset', String(params.offset));
108
+ return this.request('GET', `/analytics/posts?${searchParams.toString()}`);
109
+ }
88
110
  async getSignedUploadUrl(filename, contentType, size) {
89
111
  return this.request('POST', '/media/signed-upload', {
90
112
  filename,
@@ -0,0 +1,24 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+ export declare const getAccountAnalyticsTool: {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: z.ZodObject<{
7
+ account_id: z.ZodNumber;
8
+ from: z.ZodOptional<z.ZodString>;
9
+ to: z.ZodOptional<z.ZodString>;
10
+ }, "strip", z.ZodTypeAny, {
11
+ account_id: number;
12
+ from?: string | undefined;
13
+ to?: string | undefined;
14
+ }, {
15
+ account_id: number;
16
+ from?: string | undefined;
17
+ to?: string | undefined;
18
+ }>;
19
+ execute(client: PosterlyClient, input: {
20
+ account_id: number;
21
+ from?: string;
22
+ to?: string;
23
+ }): Promise<string>;
24
+ };
@@ -0,0 +1,59 @@
1
+ import { z } from 'zod';
2
+ export const getAccountAnalyticsTool = {
3
+ name: 'get_account_analytics',
4
+ description: 'Get daily analytics snapshots and a period summary for a connected social account. Supports Instagram, LinkedIn, and Google Business Profile. Returns follower growth, reach, views, engagement rate, and platform-specific metrics (e.g. website clicks, direction requests for GBP).',
5
+ inputSchema: z.object({
6
+ account_id: z
7
+ .number()
8
+ .describe('The social account ID (from list_accounts)'),
9
+ from: z
10
+ .string()
11
+ .optional()
12
+ .describe('Start date (ISO date, e.g. 2026-03-19). Defaults to 30 days ago.'),
13
+ to: z
14
+ .string()
15
+ .optional()
16
+ .describe('End date (ISO date). Defaults to today.'),
17
+ }),
18
+ async execute(client, input) {
19
+ const result = await client.getAccountAnalytics(input);
20
+ const { account, range, summary, snapshots } = result;
21
+ const lines = [
22
+ `Analytics for @${account.username} (${account.platform}, id ${account.id})`,
23
+ `Range: ${range.from} → ${range.to} (${snapshots.length} daily snapshots)`,
24
+ '',
25
+ 'Summary:',
26
+ `• Followers: ${summary.current_followers.toLocaleString()} (${formatDelta(summary.followers_change)} in range)`,
27
+ `• Follows gained / lost: +${summary.total_follows_gained} / -${summary.total_follows_lost}`,
28
+ ];
29
+ if (summary.total_reach !== null)
30
+ lines.push(`• Total reach: ${summary.total_reach.toLocaleString()}`);
31
+ if (summary.total_views !== null)
32
+ lines.push(`• Total views: ${summary.total_views.toLocaleString()}`);
33
+ if (summary.total_accounts_engaged !== null) {
34
+ lines.push(`• Total accounts engaged: ${summary.total_accounts_engaged.toLocaleString()}`);
35
+ }
36
+ lines.push(`• Engagement rate (by reach): ${summary.engagement_rate}%`, `• Engagement rate (by followers): ${summary.engagement_rate_by_followers}%`);
37
+ if (account.platform === 'google_business') {
38
+ if (summary.total_website_clicks !== null) {
39
+ lines.push(`• Website clicks: ${summary.total_website_clicks.toLocaleString()}`);
40
+ }
41
+ if (summary.total_call_clicks !== null) {
42
+ lines.push(`• Call clicks: ${summary.total_call_clicks.toLocaleString()}`);
43
+ }
44
+ if (summary.total_direction_requests !== null) {
45
+ lines.push(`• Direction requests: ${summary.total_direction_requests.toLocaleString()}`);
46
+ }
47
+ if (summary.total_conversations !== null) {
48
+ lines.push(`• Conversations: ${summary.total_conversations.toLocaleString()}`);
49
+ }
50
+ if (summary.total_bookings !== null) {
51
+ lines.push(`• Bookings: ${summary.total_bookings.toLocaleString()}`);
52
+ }
53
+ }
54
+ return lines.join('\n');
55
+ },
56
+ };
57
+ function formatDelta(n) {
58
+ return n >= 0 ? `+${n.toLocaleString()}` : n.toLocaleString();
59
+ }
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+ export declare const getPostAnalyticsTool: {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: z.ZodObject<{
7
+ account_id: z.ZodNumber;
8
+ from: z.ZodOptional<z.ZodString>;
9
+ to: z.ZodOptional<z.ZodString>;
10
+ limit: z.ZodOptional<z.ZodNumber>;
11
+ offset: z.ZodOptional<z.ZodNumber>;
12
+ }, "strip", z.ZodTypeAny, {
13
+ account_id: number;
14
+ limit?: number | undefined;
15
+ offset?: number | undefined;
16
+ from?: string | undefined;
17
+ to?: string | undefined;
18
+ }, {
19
+ account_id: number;
20
+ limit?: number | undefined;
21
+ offset?: number | undefined;
22
+ from?: string | undefined;
23
+ to?: string | undefined;
24
+ }>;
25
+ execute(client: PosterlyClient, input: {
26
+ account_id: number;
27
+ from?: string;
28
+ to?: string;
29
+ limit?: number;
30
+ offset?: number;
31
+ }): Promise<string>;
32
+ };
@@ -0,0 +1,65 @@
1
+ import { z } from 'zod';
2
+ export const getPostAnalyticsTool = {
3
+ name: 'get_post_analytics',
4
+ description: 'Get per-post engagement metrics (likes, comments, reach, impressions, saves, shares, plays) for a connected social account. Supports Instagram, LinkedIn, and Google Business Profile. Returns the most recent posts first.',
5
+ inputSchema: z.object({
6
+ account_id: z
7
+ .number()
8
+ .describe('The social account ID (from list_accounts)'),
9
+ from: z
10
+ .string()
11
+ .optional()
12
+ .describe('Start date (ISO date, e.g. 2026-03-19). Defaults to 30 days ago.'),
13
+ to: z
14
+ .string()
15
+ .optional()
16
+ .describe('End date (ISO date). Defaults to today.'),
17
+ limit: z
18
+ .number()
19
+ .min(1)
20
+ .max(200)
21
+ .optional()
22
+ .describe('Number of posts to return (default 50, max 200)'),
23
+ offset: z.number().min(0).optional().describe('Pagination offset'),
24
+ }),
25
+ async execute(client, input) {
26
+ const result = await client.getPostAnalytics(input);
27
+ const { account, range, posts, total } = result;
28
+ if (posts.length === 0) {
29
+ return `No analytics found for @${account.username} (${account.platform}) between ${range.from} and ${range.to}.`;
30
+ }
31
+ const lines = [
32
+ `Post analytics for @${account.username} (${account.platform})`,
33
+ `Range: ${range.from} → ${range.to} • Showing ${posts.length} of ${total}`,
34
+ '',
35
+ ];
36
+ for (const p of posts) {
37
+ const postedAt = p.posted_at
38
+ ? new Date(p.posted_at).toLocaleString()
39
+ : 'unknown date';
40
+ const caption = p.caption_snippet
41
+ ? p.caption_snippet.length > 60
42
+ ? `${p.caption_snippet.slice(0, 60)}…`
43
+ : p.caption_snippet
44
+ : '(no caption)';
45
+ const metrics = [
46
+ `${p.likes} likes`,
47
+ `${p.comments} comments`,
48
+ `${p.reach.toLocaleString()} reach`,
49
+ ];
50
+ if (p.impressions)
51
+ metrics.push(`${p.impressions.toLocaleString()} impressions`);
52
+ if (p.saved)
53
+ metrics.push(`${p.saved} saved`);
54
+ if (p.shares)
55
+ metrics.push(`${p.shares} shares`);
56
+ if (p.plays)
57
+ metrics.push(`${p.plays.toLocaleString()} plays`);
58
+ lines.push(`• [${postedAt}] ${caption}`);
59
+ lines.push(` ${metrics.join(' • ')}`);
60
+ if (p.permalink)
61
+ lines.push(` ${p.permalink}`);
62
+ }
63
+ return lines.join('\n');
64
+ },
65
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posterly-mcp-server",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "MCP server for posterly — schedule social media posts from Claude Desktop",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/index.ts CHANGED
@@ -13,10 +13,12 @@ import { updatePostTool } from './tools/update-post.js';
13
13
  import { deletePostTool } from './tools/delete-post.js';
14
14
  import { whoamiTool } from './tools/whoami.js';
15
15
  import { generateImageTool } from './tools/generate-image.js';
16
+ import { getAccountAnalyticsTool } from './tools/get-account-analytics.js';
17
+ import { getPostAnalyticsTool } from './tools/get-post-analytics.js';
16
18
 
17
19
  const server = new McpServer({
18
20
  name: 'posterly',
19
- version: '0.4.0',
21
+ version: '0.5.0',
20
22
  });
21
23
 
22
24
  let client: PosterlyClient;
@@ -169,6 +171,34 @@ server.tool(
169
171
  }
170
172
  );
171
173
 
174
+ server.tool(
175
+ getAccountAnalyticsTool.name,
176
+ getAccountAnalyticsTool.description,
177
+ getAccountAnalyticsTool.inputSchema.shape,
178
+ async (input) => {
179
+ try {
180
+ const text = await getAccountAnalyticsTool.execute(client, input as any);
181
+ return { content: [{ type: 'text' as const, text }] };
182
+ } catch (err: any) {
183
+ return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
184
+ }
185
+ }
186
+ );
187
+
188
+ server.tool(
189
+ getPostAnalyticsTool.name,
190
+ getPostAnalyticsTool.description,
191
+ getPostAnalyticsTool.inputSchema.shape,
192
+ async (input) => {
193
+ try {
194
+ const text = await getPostAnalyticsTool.execute(client, input as any);
195
+ return { content: [{ type: 'text' as const, text }] };
196
+ } catch (err: any) {
197
+ return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
198
+ }
199
+ }
200
+ );
201
+
172
202
  // Start the server
173
203
  async function main() {
174
204
  const transport = new StdioServerTransport();
@@ -45,6 +45,77 @@ export interface Whoami {
45
45
  workspaces: Workspace[];
46
46
  }
47
47
 
48
+ export interface AccountAnalyticsSummary {
49
+ current_followers: number;
50
+ followers_change: number;
51
+ total_reach: number | null;
52
+ total_views: number | null;
53
+ total_accounts_engaged: number | null;
54
+ total_follows_gained: number;
55
+ total_follows_lost: number;
56
+ engagement_rate: number;
57
+ engagement_rate_by_followers: number;
58
+ total_website_clicks: number | null;
59
+ total_call_clicks: number | null;
60
+ total_direction_requests: number | null;
61
+ total_conversations: number | null;
62
+ total_bookings: number | null;
63
+ }
64
+
65
+ export interface AccountAnalyticsSnapshot {
66
+ date: string;
67
+ followers: number | null;
68
+ following: number | null;
69
+ media_count: number | null;
70
+ reach: number | null;
71
+ views: number | null;
72
+ profile_views: number | null;
73
+ accounts_engaged: number | null;
74
+ follows_gained: number | null;
75
+ follows_lost: number | null;
76
+ website_clicks: number | null;
77
+ call_clicks: number | null;
78
+ direction_requests: number | null;
79
+ conversations: number | null;
80
+ bookings: number | null;
81
+ }
82
+
83
+ export interface AccountAnalyticsResponse {
84
+ account: { id: number; platform: string; username: string };
85
+ range: { from: string; to: string };
86
+ summary: AccountAnalyticsSummary;
87
+ snapshots: AccountAnalyticsSnapshot[];
88
+ }
89
+
90
+ export interface PostAnalyticsRow {
91
+ id: number | null;
92
+ platform_media_id: string;
93
+ platform: string;
94
+ posted_at: string | null;
95
+ likes: number;
96
+ comments: number;
97
+ impressions: number;
98
+ reach: number;
99
+ saved: number;
100
+ shares: number;
101
+ plays: number;
102
+ total_interactions: number;
103
+ media_type: string | null;
104
+ media_url: string | null;
105
+ permalink: string | null;
106
+ caption_snippet: string | null;
107
+ synced_at: string;
108
+ }
109
+
110
+ export interface PostAnalyticsResponse {
111
+ account: { id: number; platform: string; username: string };
112
+ range: { from: string; to: string };
113
+ posts: PostAnalyticsRow[];
114
+ total: number;
115
+ limit: number;
116
+ offset: number;
117
+ }
118
+
48
119
  export class PosterlyClient {
49
120
  private baseUrl: string;
50
121
  private apiKey: string;
@@ -198,6 +269,34 @@ export class PosterlyClient {
198
269
  return data.slots;
199
270
  }
200
271
 
272
+ async getAccountAnalytics(params: {
273
+ account_id: number;
274
+ from?: string;
275
+ to?: string;
276
+ }): Promise<AccountAnalyticsResponse> {
277
+ const searchParams = new URLSearchParams();
278
+ searchParams.set('account_id', String(params.account_id));
279
+ if (params.from) searchParams.set('from', params.from);
280
+ if (params.to) searchParams.set('to', params.to);
281
+ return this.request('GET', `/analytics/accounts?${searchParams.toString()}`);
282
+ }
283
+
284
+ async getPostAnalytics(params: {
285
+ account_id: number;
286
+ from?: string;
287
+ to?: string;
288
+ limit?: number;
289
+ offset?: number;
290
+ }): Promise<PostAnalyticsResponse> {
291
+ const searchParams = new URLSearchParams();
292
+ searchParams.set('account_id', String(params.account_id));
293
+ if (params.from) searchParams.set('from', params.from);
294
+ if (params.to) searchParams.set('to', params.to);
295
+ if (params.limit) searchParams.set('limit', String(params.limit));
296
+ if (params.offset) searchParams.set('offset', String(params.offset));
297
+ return this.request('GET', `/analytics/posts?${searchParams.toString()}`);
298
+ }
299
+
201
300
  async getSignedUploadUrl(
202
301
  filename: string,
203
302
  contentType: string,
@@ -0,0 +1,72 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+
4
+ export const getAccountAnalyticsTool = {
5
+ name: 'get_account_analytics',
6
+ description:
7
+ 'Get daily analytics snapshots and a period summary for a connected social account. Supports Instagram, LinkedIn, and Google Business Profile. Returns follower growth, reach, views, engagement rate, and platform-specific metrics (e.g. website clicks, direction requests for GBP).',
8
+ inputSchema: z.object({
9
+ account_id: z
10
+ .number()
11
+ .describe('The social account ID (from list_accounts)'),
12
+ from: z
13
+ .string()
14
+ .optional()
15
+ .describe('Start date (ISO date, e.g. 2026-03-19). Defaults to 30 days ago.'),
16
+ to: z
17
+ .string()
18
+ .optional()
19
+ .describe('End date (ISO date). Defaults to today.'),
20
+ }),
21
+
22
+ async execute(
23
+ client: PosterlyClient,
24
+ input: { account_id: number; from?: string; to?: string }
25
+ ) {
26
+ const result = await client.getAccountAnalytics(input);
27
+ const { account, range, summary, snapshots } = result;
28
+
29
+ const lines: string[] = [
30
+ `Analytics for @${account.username} (${account.platform}, id ${account.id})`,
31
+ `Range: ${range.from} → ${range.to} (${snapshots.length} daily snapshots)`,
32
+ '',
33
+ 'Summary:',
34
+ `• Followers: ${summary.current_followers.toLocaleString()} (${formatDelta(summary.followers_change)} in range)`,
35
+ `• Follows gained / lost: +${summary.total_follows_gained} / -${summary.total_follows_lost}`,
36
+ ];
37
+
38
+ if (summary.total_reach !== null) lines.push(`• Total reach: ${summary.total_reach.toLocaleString()}`);
39
+ if (summary.total_views !== null) lines.push(`• Total views: ${summary.total_views.toLocaleString()}`);
40
+ if (summary.total_accounts_engaged !== null) {
41
+ lines.push(`• Total accounts engaged: ${summary.total_accounts_engaged.toLocaleString()}`);
42
+ }
43
+ lines.push(
44
+ `• Engagement rate (by reach): ${summary.engagement_rate}%`,
45
+ `• Engagement rate (by followers): ${summary.engagement_rate_by_followers}%`
46
+ );
47
+
48
+ if (account.platform === 'google_business') {
49
+ if (summary.total_website_clicks !== null) {
50
+ lines.push(`• Website clicks: ${summary.total_website_clicks.toLocaleString()}`);
51
+ }
52
+ if (summary.total_call_clicks !== null) {
53
+ lines.push(`• Call clicks: ${summary.total_call_clicks.toLocaleString()}`);
54
+ }
55
+ if (summary.total_direction_requests !== null) {
56
+ lines.push(`• Direction requests: ${summary.total_direction_requests.toLocaleString()}`);
57
+ }
58
+ if (summary.total_conversations !== null) {
59
+ lines.push(`• Conversations: ${summary.total_conversations.toLocaleString()}`);
60
+ }
61
+ if (summary.total_bookings !== null) {
62
+ lines.push(`• Bookings: ${summary.total_bookings.toLocaleString()}`);
63
+ }
64
+ }
65
+
66
+ return lines.join('\n');
67
+ },
68
+ };
69
+
70
+ function formatDelta(n: number): string {
71
+ return n >= 0 ? `+${n.toLocaleString()}` : n.toLocaleString();
72
+ }
@@ -0,0 +1,78 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+
4
+ export const getPostAnalyticsTool = {
5
+ name: 'get_post_analytics',
6
+ description:
7
+ 'Get per-post engagement metrics (likes, comments, reach, impressions, saves, shares, plays) for a connected social account. Supports Instagram, LinkedIn, and Google Business Profile. Returns the most recent posts first.',
8
+ inputSchema: z.object({
9
+ account_id: z
10
+ .number()
11
+ .describe('The social account ID (from list_accounts)'),
12
+ from: z
13
+ .string()
14
+ .optional()
15
+ .describe('Start date (ISO date, e.g. 2026-03-19). Defaults to 30 days ago.'),
16
+ to: z
17
+ .string()
18
+ .optional()
19
+ .describe('End date (ISO date). Defaults to today.'),
20
+ limit: z
21
+ .number()
22
+ .min(1)
23
+ .max(200)
24
+ .optional()
25
+ .describe('Number of posts to return (default 50, max 200)'),
26
+ offset: z.number().min(0).optional().describe('Pagination offset'),
27
+ }),
28
+
29
+ async execute(
30
+ client: PosterlyClient,
31
+ input: {
32
+ account_id: number;
33
+ from?: string;
34
+ to?: string;
35
+ limit?: number;
36
+ offset?: number;
37
+ }
38
+ ) {
39
+ const result = await client.getPostAnalytics(input);
40
+ const { account, range, posts, total } = result;
41
+
42
+ if (posts.length === 0) {
43
+ return `No analytics found for @${account.username} (${account.platform}) between ${range.from} and ${range.to}.`;
44
+ }
45
+
46
+ const lines: string[] = [
47
+ `Post analytics for @${account.username} (${account.platform})`,
48
+ `Range: ${range.from} → ${range.to} • Showing ${posts.length} of ${total}`,
49
+ '',
50
+ ];
51
+
52
+ for (const p of posts) {
53
+ const postedAt = p.posted_at
54
+ ? new Date(p.posted_at).toLocaleString()
55
+ : 'unknown date';
56
+ const caption = p.caption_snippet
57
+ ? p.caption_snippet.length > 60
58
+ ? `${p.caption_snippet.slice(0, 60)}…`
59
+ : p.caption_snippet
60
+ : '(no caption)';
61
+ const metrics = [
62
+ `${p.likes} likes`,
63
+ `${p.comments} comments`,
64
+ `${p.reach.toLocaleString()} reach`,
65
+ ];
66
+ if (p.impressions) metrics.push(`${p.impressions.toLocaleString()} impressions`);
67
+ if (p.saved) metrics.push(`${p.saved} saved`);
68
+ if (p.shares) metrics.push(`${p.shares} shares`);
69
+ if (p.plays) metrics.push(`${p.plays.toLocaleString()} plays`);
70
+
71
+ lines.push(`• [${postedAt}] ${caption}`);
72
+ lines.push(` ${metrics.join(' • ')}`);
73
+ if (p.permalink) lines.push(` ${p.permalink}`);
74
+ }
75
+
76
+ return lines.join('\n');
77
+ },
78
+ };