posterly-mcp-server 0.3.1 → 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 +31 -1
- package/dist/lib/api-client.d.ts +117 -0
- package/dist/lib/api-client.js +25 -0
- package/dist/tools/generate-image.d.ts +36 -0
- package/dist/tools/generate-image.js +70 -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 +46 -1
- package/src/lib/api-client.ts +123 -0
- package/src/tools/generate-image.ts +86 -0
- package/src/tools/get-account-analytics.ts +72 -0
- package/src/tools/get-post-analytics.ts +78 -0
package/dist/index.js
CHANGED
|
@@ -11,9 +11,12 @@ import { getPostTool } from './tools/get-post.js';
|
|
|
11
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
|
+
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';
|
|
14
17
|
const server = new McpServer({
|
|
15
18
|
name: 'posterly',
|
|
16
|
-
version: '0.
|
|
19
|
+
version: '0.5.0',
|
|
17
20
|
});
|
|
18
21
|
let client;
|
|
19
22
|
try {
|
|
@@ -78,6 +81,15 @@ server.tool(uploadMediaTool.name, uploadMediaTool.description, uploadMediaTool.i
|
|
|
78
81
|
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
79
82
|
}
|
|
80
83
|
});
|
|
84
|
+
server.tool(generateImageTool.name, generateImageTool.description, generateImageTool.inputSchema.shape, async (input) => {
|
|
85
|
+
try {
|
|
86
|
+
const text = await generateImageTool.execute(client, input);
|
|
87
|
+
return { content: [{ type: 'text', text }] };
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
91
|
+
}
|
|
92
|
+
});
|
|
81
93
|
server.tool(getPostTool.name, getPostTool.description, getPostTool.inputSchema.shape, async (input) => {
|
|
82
94
|
try {
|
|
83
95
|
const text = await getPostTool.execute(client, input);
|
|
@@ -105,6 +117,24 @@ server.tool(deletePostTool.name, deletePostTool.description, deletePostTool.inpu
|
|
|
105
117
|
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
106
118
|
}
|
|
107
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
|
+
});
|
|
108
138
|
// Start the server
|
|
109
139
|
async function main() {
|
|
110
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;
|
|
@@ -99,12 +179,49 @@ export declare class PosterlyClient {
|
|
|
99
179
|
deleted: boolean;
|
|
100
180
|
id: number;
|
|
101
181
|
}>;
|
|
182
|
+
generateImage(data: {
|
|
183
|
+
prompt: string;
|
|
184
|
+
aspect_ratio?: string;
|
|
185
|
+
style?: string;
|
|
186
|
+
variations?: number;
|
|
187
|
+
model?: 'flash' | 'pro';
|
|
188
|
+
resolution?: '512' | '1K' | '2K' | '4K';
|
|
189
|
+
}): Promise<{
|
|
190
|
+
urls: string[];
|
|
191
|
+
images: Array<{
|
|
192
|
+
url: string;
|
|
193
|
+
filename: string;
|
|
194
|
+
path: string;
|
|
195
|
+
}>;
|
|
196
|
+
model: string;
|
|
197
|
+
credits_used: number;
|
|
198
|
+
warnings?: string[];
|
|
199
|
+
usage: {
|
|
200
|
+
used: number | null;
|
|
201
|
+
limit: number | null;
|
|
202
|
+
period: string | null;
|
|
203
|
+
tier: string | null;
|
|
204
|
+
billed_from: 'plan_quota' | 'credits';
|
|
205
|
+
};
|
|
206
|
+
}>;
|
|
102
207
|
findAvailableSlots(params?: {
|
|
103
208
|
account_ids?: string[];
|
|
104
209
|
timezone?: string;
|
|
105
210
|
count?: number;
|
|
106
211
|
workspace_id?: string;
|
|
107
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>;
|
|
108
225
|
getSignedUploadUrl(filename: string, contentType: string, size: number): Promise<{
|
|
109
226
|
upload_url: string;
|
|
110
227
|
token: string;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -68,6 +68,9 @@ export class PosterlyClient {
|
|
|
68
68
|
async deletePost(id) {
|
|
69
69
|
return this.request('DELETE', `/posts/${id}`);
|
|
70
70
|
}
|
|
71
|
+
async generateImage(data) {
|
|
72
|
+
return this.request('POST', '/ai/generate-image', data);
|
|
73
|
+
}
|
|
71
74
|
async findAvailableSlots(params) {
|
|
72
75
|
const searchParams = new URLSearchParams();
|
|
73
76
|
if (params?.account_ids?.length)
|
|
@@ -82,6 +85,28 @@ export class PosterlyClient {
|
|
|
82
85
|
const data = await this.request('GET', `/slots/next${qs ? `?${qs}` : ''}`);
|
|
83
86
|
return data.slots;
|
|
84
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
|
+
}
|
|
85
110
|
async getSignedUploadUrl(filename, contentType, size) {
|
|
86
111
|
return this.request('POST', '/media/signed-upload', {
|
|
87
112
|
filename,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
export declare const generateImageTool: {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: z.ZodObject<{
|
|
7
|
+
prompt: z.ZodString;
|
|
8
|
+
aspect_ratio: z.ZodOptional<z.ZodEnum<["1:1", "9:16", "4:5", "3:4", "2:3", "1:4", "1:8", "21:9", "16:9", "4:3", "3:2", "4:1", "8:1", "5:4"]>>;
|
|
9
|
+
style: z.ZodOptional<z.ZodEnum<["photographic", "illustration", "minimal", "vibrant", "professional", "youtube_thumbnail", "reel_cover", "review_background"]>>;
|
|
10
|
+
variations: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
model: z.ZodOptional<z.ZodEnum<["flash", "pro"]>>;
|
|
12
|
+
resolution: z.ZodOptional<z.ZodEnum<["512", "1K", "2K", "4K"]>>;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
prompt: string;
|
|
15
|
+
aspect_ratio?: "1:1" | "9:16" | "4:5" | "3:4" | "2:3" | "1:4" | "1:8" | "21:9" | "16:9" | "4:3" | "3:2" | "4:1" | "8:1" | "5:4" | undefined;
|
|
16
|
+
style?: "photographic" | "illustration" | "minimal" | "vibrant" | "professional" | "youtube_thumbnail" | "reel_cover" | "review_background" | undefined;
|
|
17
|
+
variations?: number | undefined;
|
|
18
|
+
model?: "flash" | "pro" | undefined;
|
|
19
|
+
resolution?: "512" | "1K" | "2K" | "4K" | undefined;
|
|
20
|
+
}, {
|
|
21
|
+
prompt: string;
|
|
22
|
+
aspect_ratio?: "1:1" | "9:16" | "4:5" | "3:4" | "2:3" | "1:4" | "1:8" | "21:9" | "16:9" | "4:3" | "3:2" | "4:1" | "8:1" | "5:4" | undefined;
|
|
23
|
+
style?: "photographic" | "illustration" | "minimal" | "vibrant" | "professional" | "youtube_thumbnail" | "reel_cover" | "review_background" | undefined;
|
|
24
|
+
variations?: number | undefined;
|
|
25
|
+
model?: "flash" | "pro" | undefined;
|
|
26
|
+
resolution?: "512" | "1K" | "2K" | "4K" | undefined;
|
|
27
|
+
}>;
|
|
28
|
+
execute(client: PosterlyClient, input: {
|
|
29
|
+
prompt: string;
|
|
30
|
+
aspect_ratio?: string;
|
|
31
|
+
style?: string;
|
|
32
|
+
variations?: number;
|
|
33
|
+
model?: "flash" | "pro";
|
|
34
|
+
resolution?: "512" | "1K" | "2K" | "4K";
|
|
35
|
+
}): Promise<string>;
|
|
36
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const generateImageTool = {
|
|
3
|
+
name: 'generate_image',
|
|
4
|
+
description: 'Generate an AI image via Posterly\'s Nano Banana (Gemini) integration. The image is saved to the user\'s media storage and the returned URL can be passed to `create_post` as `media_url`.\n\n' +
|
|
5
|
+
'COSTS CREDITS. Every call consumes part of the user\'s monthly AI image quota (or purchased credits once the quota is exhausted). Do NOT call speculatively — always describe the image you\'re about to generate (subject, style, aspect ratio) to the user and get confirmation before calling.\n\n' +
|
|
6
|
+
'If the user is over their plan limit and has no credits, this tool returns a 402 with upgrade info. Surface that message verbatim — do not retry.\n\n' +
|
|
7
|
+
'Common aspect ratios by platform:\n' +
|
|
8
|
+
' • Instagram feed / LinkedIn: 1:1 (square) or 4:5 (portrait)\n' +
|
|
9
|
+
' • Instagram Story/Reel, TikTok, YouTube Shorts: 9:16\n' +
|
|
10
|
+
' • YouTube thumbnail, Twitter cards, landscape feed: 16:9\n' +
|
|
11
|
+
' • Pinterest: 2:3',
|
|
12
|
+
inputSchema: z.object({
|
|
13
|
+
prompt: z
|
|
14
|
+
.string()
|
|
15
|
+
.min(5)
|
|
16
|
+
.max(4000)
|
|
17
|
+
.describe('Detailed image description. 5–4000 characters. Describe subject, scene, lighting, mood, and style.'),
|
|
18
|
+
aspect_ratio: z
|
|
19
|
+
.enum([
|
|
20
|
+
'1:1', '9:16', '4:5', '3:4', '2:3', '1:4', '1:8',
|
|
21
|
+
'21:9', '16:9', '4:3', '3:2', '4:1', '8:1', '5:4',
|
|
22
|
+
])
|
|
23
|
+
.optional()
|
|
24
|
+
.describe('Aspect ratio. Default 1:1. Pick based on target platform (see tool description).'),
|
|
25
|
+
style: z
|
|
26
|
+
.enum([
|
|
27
|
+
'photographic', 'illustration', 'minimal', 'vibrant', 'professional',
|
|
28
|
+
'youtube_thumbnail', 'reel_cover', 'review_background',
|
|
29
|
+
])
|
|
30
|
+
.optional()
|
|
31
|
+
.describe('Preset style. Default photographic. Use youtube_thumbnail / reel_cover for platform-optimized covers; review_background for testimonial backgrounds.'),
|
|
32
|
+
variations: z
|
|
33
|
+
.number()
|
|
34
|
+
.int()
|
|
35
|
+
.min(1)
|
|
36
|
+
.max(4)
|
|
37
|
+
.optional()
|
|
38
|
+
.describe('How many variations to generate. Default 1. Each variation costs credits separately — prefer 1 unless the user explicitly wants options.'),
|
|
39
|
+
model: z
|
|
40
|
+
.enum(['flash', 'pro'])
|
|
41
|
+
.optional()
|
|
42
|
+
.describe('flash (default, 1 credit per 1K image) is fast and cost-effective. pro (2 credits per 1K) is higher quality for hero imagery. Start with flash unless quality is critical.'),
|
|
43
|
+
resolution: z
|
|
44
|
+
.enum(['512', '1K', '2K', '4K'])
|
|
45
|
+
.optional()
|
|
46
|
+
.describe('Output resolution. Default 1K. Higher resolutions cost more credits. 512 is flash-only.'),
|
|
47
|
+
}),
|
|
48
|
+
async execute(client, input) {
|
|
49
|
+
const result = await client.generateImage(input);
|
|
50
|
+
const lines = [];
|
|
51
|
+
lines.push(`Generated ${result.images.length} image${result.images.length === 1 ? '' : 's'} via ${result.model}.`);
|
|
52
|
+
lines.push('');
|
|
53
|
+
result.images.forEach((img, i) => {
|
|
54
|
+
lines.push(`${i + 1}. ${img.url}`);
|
|
55
|
+
});
|
|
56
|
+
lines.push('');
|
|
57
|
+
lines.push(`Billed from: ${result.usage.billed_from}${result.credits_used > 0 ? ` (${result.credits_used} credits)` : ''}`);
|
|
58
|
+
if (result.usage.limit != null) {
|
|
59
|
+
lines.push(`Plan usage: ${result.usage.used ?? 0}/${result.usage.limit} this ${result.usage.period || 'period'}${result.usage.tier ? ` (${result.usage.tier})` : ''}`);
|
|
60
|
+
}
|
|
61
|
+
if (result.warnings?.length) {
|
|
62
|
+
lines.push('');
|
|
63
|
+
lines.push('Warnings:');
|
|
64
|
+
result.warnings.forEach((w) => lines.push(`• ${w}`));
|
|
65
|
+
}
|
|
66
|
+
lines.push('');
|
|
67
|
+
lines.push('Pass any of these URLs to create_post as media_url (or media_urls for a carousel).');
|
|
68
|
+
return lines.join('\n');
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -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
|
@@ -12,10 +12,13 @@ import { getPostTool } from './tools/get-post.js';
|
|
|
12
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
|
+
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';
|
|
15
18
|
|
|
16
19
|
const server = new McpServer({
|
|
17
20
|
name: 'posterly',
|
|
18
|
-
version: '0.
|
|
21
|
+
version: '0.5.0',
|
|
19
22
|
});
|
|
20
23
|
|
|
21
24
|
let client: PosterlyClient;
|
|
@@ -112,6 +115,20 @@ server.tool(
|
|
|
112
115
|
}
|
|
113
116
|
);
|
|
114
117
|
|
|
118
|
+
server.tool(
|
|
119
|
+
generateImageTool.name,
|
|
120
|
+
generateImageTool.description,
|
|
121
|
+
generateImageTool.inputSchema.shape,
|
|
122
|
+
async (input) => {
|
|
123
|
+
try {
|
|
124
|
+
const text = await generateImageTool.execute(client, input as any);
|
|
125
|
+
return { content: [{ type: 'text' as const, text }] };
|
|
126
|
+
} catch (err: any) {
|
|
127
|
+
return { content: [{ type: 'text' as const, text: `Error: ${err.message}` }], isError: true };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
|
|
115
132
|
server.tool(
|
|
116
133
|
getPostTool.name,
|
|
117
134
|
getPostTool.description,
|
|
@@ -154,6 +171,34 @@ server.tool(
|
|
|
154
171
|
}
|
|
155
172
|
);
|
|
156
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
|
+
|
|
157
202
|
// Start the server
|
|
158
203
|
async function main() {
|
|
159
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;
|
|
@@ -157,6 +228,30 @@ export class PosterlyClient {
|
|
|
157
228
|
return this.request('DELETE', `/posts/${id}`);
|
|
158
229
|
}
|
|
159
230
|
|
|
231
|
+
async generateImage(data: {
|
|
232
|
+
prompt: string;
|
|
233
|
+
aspect_ratio?: string;
|
|
234
|
+
style?: string;
|
|
235
|
+
variations?: number;
|
|
236
|
+
model?: 'flash' | 'pro';
|
|
237
|
+
resolution?: '512' | '1K' | '2K' | '4K';
|
|
238
|
+
}): Promise<{
|
|
239
|
+
urls: string[];
|
|
240
|
+
images: Array<{ url: string; filename: string; path: string }>;
|
|
241
|
+
model: string;
|
|
242
|
+
credits_used: number;
|
|
243
|
+
warnings?: string[];
|
|
244
|
+
usage: {
|
|
245
|
+
used: number | null;
|
|
246
|
+
limit: number | null;
|
|
247
|
+
period: string | null;
|
|
248
|
+
tier: string | null;
|
|
249
|
+
billed_from: 'plan_quota' | 'credits';
|
|
250
|
+
};
|
|
251
|
+
}> {
|
|
252
|
+
return this.request('POST', '/ai/generate-image', data);
|
|
253
|
+
}
|
|
254
|
+
|
|
160
255
|
async findAvailableSlots(params?: {
|
|
161
256
|
account_ids?: string[];
|
|
162
257
|
timezone?: string;
|
|
@@ -174,6 +269,34 @@ export class PosterlyClient {
|
|
|
174
269
|
return data.slots;
|
|
175
270
|
}
|
|
176
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
|
+
|
|
177
300
|
async getSignedUploadUrl(
|
|
178
301
|
filename: string,
|
|
179
302
|
contentType: string,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PosterlyClient } from '../lib/api-client.js';
|
|
3
|
+
|
|
4
|
+
export const generateImageTool = {
|
|
5
|
+
name: 'generate_image',
|
|
6
|
+
description:
|
|
7
|
+
'Generate an AI image via Posterly\'s Nano Banana (Gemini) integration. The image is saved to the user\'s media storage and the returned URL can be passed to `create_post` as `media_url`.\n\n' +
|
|
8
|
+
'COSTS CREDITS. Every call consumes part of the user\'s monthly AI image quota (or purchased credits once the quota is exhausted). Do NOT call speculatively — always describe the image you\'re about to generate (subject, style, aspect ratio) to the user and get confirmation before calling.\n\n' +
|
|
9
|
+
'If the user is over their plan limit and has no credits, this tool returns a 402 with upgrade info. Surface that message verbatim — do not retry.\n\n' +
|
|
10
|
+
'Common aspect ratios by platform:\n' +
|
|
11
|
+
' • Instagram feed / LinkedIn: 1:1 (square) or 4:5 (portrait)\n' +
|
|
12
|
+
' • Instagram Story/Reel, TikTok, YouTube Shorts: 9:16\n' +
|
|
13
|
+
' • YouTube thumbnail, Twitter cards, landscape feed: 16:9\n' +
|
|
14
|
+
' • Pinterest: 2:3',
|
|
15
|
+
inputSchema: z.object({
|
|
16
|
+
prompt: z
|
|
17
|
+
.string()
|
|
18
|
+
.min(5)
|
|
19
|
+
.max(4000)
|
|
20
|
+
.describe('Detailed image description. 5–4000 characters. Describe subject, scene, lighting, mood, and style.'),
|
|
21
|
+
aspect_ratio: z
|
|
22
|
+
.enum([
|
|
23
|
+
'1:1', '9:16', '4:5', '3:4', '2:3', '1:4', '1:8',
|
|
24
|
+
'21:9', '16:9', '4:3', '3:2', '4:1', '8:1', '5:4',
|
|
25
|
+
])
|
|
26
|
+
.optional()
|
|
27
|
+
.describe('Aspect ratio. Default 1:1. Pick based on target platform (see tool description).'),
|
|
28
|
+
style: z
|
|
29
|
+
.enum([
|
|
30
|
+
'photographic', 'illustration', 'minimal', 'vibrant', 'professional',
|
|
31
|
+
'youtube_thumbnail', 'reel_cover', 'review_background',
|
|
32
|
+
])
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('Preset style. Default photographic. Use youtube_thumbnail / reel_cover for platform-optimized covers; review_background for testimonial backgrounds.'),
|
|
35
|
+
variations: z
|
|
36
|
+
.number()
|
|
37
|
+
.int()
|
|
38
|
+
.min(1)
|
|
39
|
+
.max(4)
|
|
40
|
+
.optional()
|
|
41
|
+
.describe('How many variations to generate. Default 1. Each variation costs credits separately — prefer 1 unless the user explicitly wants options.'),
|
|
42
|
+
model: z
|
|
43
|
+
.enum(['flash', 'pro'])
|
|
44
|
+
.optional()
|
|
45
|
+
.describe('flash (default, 1 credit per 1K image) is fast and cost-effective. pro (2 credits per 1K) is higher quality for hero imagery. Start with flash unless quality is critical.'),
|
|
46
|
+
resolution: z
|
|
47
|
+
.enum(['512', '1K', '2K', '4K'])
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('Output resolution. Default 1K. Higher resolutions cost more credits. 512 is flash-only.'),
|
|
50
|
+
}),
|
|
51
|
+
|
|
52
|
+
async execute(
|
|
53
|
+
client: PosterlyClient,
|
|
54
|
+
input: {
|
|
55
|
+
prompt: string;
|
|
56
|
+
aspect_ratio?: string;
|
|
57
|
+
style?: string;
|
|
58
|
+
variations?: number;
|
|
59
|
+
model?: 'flash' | 'pro';
|
|
60
|
+
resolution?: '512' | '1K' | '2K' | '4K';
|
|
61
|
+
}
|
|
62
|
+
) {
|
|
63
|
+
const result = await client.generateImage(input);
|
|
64
|
+
|
|
65
|
+
const lines: string[] = [];
|
|
66
|
+
lines.push(`Generated ${result.images.length} image${result.images.length === 1 ? '' : 's'} via ${result.model}.`);
|
|
67
|
+
lines.push('');
|
|
68
|
+
result.images.forEach((img, i) => {
|
|
69
|
+
lines.push(`${i + 1}. ${img.url}`);
|
|
70
|
+
});
|
|
71
|
+
lines.push('');
|
|
72
|
+
lines.push(`Billed from: ${result.usage.billed_from}${result.credits_used > 0 ? ` (${result.credits_used} credits)` : ''}`);
|
|
73
|
+
if (result.usage.limit != null) {
|
|
74
|
+
lines.push(`Plan usage: ${result.usage.used ?? 0}/${result.usage.limit} this ${result.usage.period || 'period'}${result.usage.tier ? ` (${result.usage.tier})` : ''}`);
|
|
75
|
+
}
|
|
76
|
+
if (result.warnings?.length) {
|
|
77
|
+
lines.push('');
|
|
78
|
+
lines.push('Warnings:');
|
|
79
|
+
result.warnings.forEach((w) => lines.push(`• ${w}`));
|
|
80
|
+
}
|
|
81
|
+
lines.push('');
|
|
82
|
+
lines.push('Pass any of these URLs to create_post as media_url (or media_urls for a carousel).');
|
|
83
|
+
|
|
84
|
+
return lines.join('\n');
|
|
85
|
+
},
|
|
86
|
+
};
|
|
@@ -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
|
+
};
|