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 +21 -1
- package/dist/lib/api-client.d.ts +92 -0
- package/dist/lib/api-client.js +22 -0
- package/dist/tools/get-account-analytics.d.ts +24 -0
- package/dist/tools/get-account-analytics.js +59 -0
- package/dist/tools/get-post-analytics.d.ts +32 -0
- package/dist/tools/get-post-analytics.js +65 -0
- package/package.json +1 -1
- package/src/index.ts +31 -1
- package/src/lib/api-client.ts +99 -0
- package/src/tools/get-account-analytics.ts +72 -0
- package/src/tools/get-post-analytics.ts +78 -0
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.
|
|
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();
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -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
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.
|
|
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();
|
package/src/lib/api-client.ts
CHANGED
|
@@ -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
|
+
};
|