posterly-mcp-server 0.4.0 → 0.6.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.
@@ -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,79 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+
4
+ export const getBrandProfileTool = {
5
+ name: 'get_brand_profile',
6
+ description:
7
+ 'Get the extended brand profile for a brand/client. Returns voice/tone guidance, audience, keywords, dos and don’ts, visual notes, and other saved brand context.',
8
+ inputSchema: z.object({
9
+ brand_id: z.string().describe('The brand ID to inspect (from list_brands).'),
10
+ }),
11
+
12
+ async execute(
13
+ client: PosterlyClient,
14
+ input: { brand_id: string },
15
+ ) {
16
+ const result = await client.getBrandProfile(input.brand_id);
17
+ const profile = result.brand_profile as Record<string, any>;
18
+
19
+ const lines = [
20
+ `Brand profile: ${profile.brand_name}`,
21
+ `• Source: ${profile.source}`,
22
+ ];
23
+
24
+ if (profile.workspace_client_id) lines.push(`• Workspace client ID: ${profile.workspace_client_id}`);
25
+ if (profile.brand_group_id) lines.push(`• Legacy brand group ID: ${profile.brand_group_id}`);
26
+ if (profile.tone_of_voice) lines.push(`• Tone of voice: ${profile.tone_of_voice}`);
27
+ if (profile.audience) lines.push(`• Audience: ${profile.audience}`);
28
+ if (profile.brand_values) lines.push(`• Brand values: ${profile.brand_values}`);
29
+ if (profile.custom_instructions) lines.push(`• Custom instructions: ${profile.custom_instructions}`);
30
+ if (profile.logo_url) lines.push(`• Logo URL: ${profile.logo_url}`);
31
+
32
+ addStructuredLine(lines, 'Keywords', profile.keywords);
33
+ addStructuredLine(lines, 'Competitors', profile.competitors);
34
+ addStructuredLine(lines, 'Do / Don’ts', profile.do_donts);
35
+ addStructuredLine(lines, 'Visual guidelines', profile.visual_guidelines);
36
+ addStructuredLine(lines, 'Brand story', profile.brand_story);
37
+ addStructuredLine(lines, 'Example posts', profile.example_posts);
38
+ addStructuredLine(lines, 'Topics to cover', profile.topics_to_cover);
39
+ addStructuredLine(lines, 'Topics to avoid', profile.topics_to_avoid);
40
+ addStructuredLine(lines, 'Voice examples', profile.voice_examples);
41
+
42
+ if (profile.last_context_refresh_at) {
43
+ lines.push(`• Last refreshed: ${profile.last_context_refresh_at}`);
44
+ }
45
+
46
+ if (lines.length === 2) {
47
+ lines.push('• No extended brand profile fields have been saved yet.');
48
+ }
49
+
50
+ return lines.join('\n');
51
+ },
52
+ };
53
+
54
+ function addStructuredLine(lines: string[], label: string, value: unknown) {
55
+ const formatted = formatStructuredValue(value);
56
+ if (formatted) {
57
+ lines.push(`• ${label}: ${formatted}`);
58
+ }
59
+ }
60
+
61
+ function formatStructuredValue(value: unknown): string | null {
62
+ if (value == null) return null;
63
+ if (typeof value === 'string') {
64
+ const trimmed = value.trim();
65
+ return trimmed.length > 0 ? trimmed : null;
66
+ }
67
+ if (Array.isArray(value)) {
68
+ if (value.length === 0) return null;
69
+ return value
70
+ .map((item) => formatStructuredValue(item) || JSON.stringify(item))
71
+ .filter(Boolean)
72
+ .join(' | ');
73
+ }
74
+ if (typeof value === 'object') {
75
+ const json = JSON.stringify(value);
76
+ return json === '{}' ? null : json;
77
+ }
78
+ return String(value);
79
+ }
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+
4
+ export const getBrandTool = {
5
+ name: 'get_brand',
6
+ description:
7
+ 'Get one brand/client by ID. Returns its workspace, source, linked legacy brand group if present, and the number of social accounts assigned to it.',
8
+ inputSchema: z.object({
9
+ brand_id: z.string().describe('The brand ID to look up (from list_brands).'),
10
+ }),
11
+
12
+ async execute(
13
+ client: PosterlyClient,
14
+ input: { brand_id: string },
15
+ ) {
16
+ const result = await client.getBrand(input.brand_id);
17
+ const brand = result.brand as Record<string, any>;
18
+
19
+ const lines = [
20
+ `Brand: ${brand.name}`,
21
+ `• ID: ${brand.id}`,
22
+ `• Source: ${brand.source}`,
23
+ `• Workspace: ${brand.workspace_id || 'N/A'}`,
24
+ `• Accounts assigned: ${brand.account_count ?? 0}`,
25
+ ];
26
+
27
+ if (brand.workspace_client_id) lines.push(`• Workspace client ID: ${brand.workspace_client_id}`);
28
+ if (brand.legacy_brand_group_id) lines.push(`• Legacy brand group ID: ${brand.legacy_brand_group_id}`);
29
+ if (brand.created_at) lines.push(`• Created: ${brand.created_at}`);
30
+ if (brand.updated_at) lines.push(`• Updated: ${brand.updated_at}`);
31
+
32
+ return lines.join('\n');
33
+ },
34
+ };
@@ -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
+ };
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+
4
+ export const listBrandAccountsTool = {
5
+ name: 'list_brand_accounts',
6
+ description:
7
+ 'List the connected social accounts assigned to a brand/client. Use this when a user refers to a brand name rather than a raw account handle.',
8
+ inputSchema: z.object({
9
+ brand_id: z.string().describe('The brand ID to inspect (from list_brands).'),
10
+ }),
11
+
12
+ async execute(
13
+ client: PosterlyClient,
14
+ input: { brand_id: string },
15
+ ) {
16
+ const accounts = await client.listBrandAccounts(input.brand_id);
17
+
18
+ if (accounts.length === 0) {
19
+ return 'No social accounts are currently assigned to this brand.';
20
+ }
21
+
22
+ const lines = accounts.map(
23
+ (account) => `• ${account.platform} — @${account.username} (ID: ${account.id}${account.workspace_id ? `, ws: ${account.workspace_id}` : ''})`
24
+ );
25
+
26
+ return `Brand accounts (${accounts.length}):\n${lines.join('\n')}`;
27
+ },
28
+ };
@@ -0,0 +1,36 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+
4
+ export const listBrandsTool = {
5
+ name: 'list_brands',
6
+ description:
7
+ 'List brands/clients the caller can access. Returns each brand ID, name, workspace ID, source, and how many social accounts are currently assigned to it.',
8
+ inputSchema: z.object({
9
+ workspace_id: z
10
+ .string()
11
+ .optional()
12
+ .describe('Filter to brands in a specific workspace (get IDs via whoami).'),
13
+ }),
14
+
15
+ async execute(client: PosterlyClient, input: { workspace_id?: string }) {
16
+ const brands = await client.listBrands({ workspace_id: input.workspace_id });
17
+
18
+ if (brands.length === 0) {
19
+ return input.workspace_id
20
+ ? 'No brands found in this workspace.'
21
+ : 'No brands found. Create brands in the posterly dashboard first.';
22
+ }
23
+
24
+ const lines = brands.map((brand) => {
25
+ const suffix = [
26
+ `ID: ${brand.id}`,
27
+ brand.workspace_id ? `ws: ${brand.workspace_id}` : null,
28
+ `accounts: ${brand.account_count}`,
29
+ brand.source,
30
+ ].filter(Boolean).join(', ');
31
+ return `• ${brand.name} (${suffix})`;
32
+ });
33
+
34
+ return `Brands (${brands.length}):\n${lines.join('\n')}`;
35
+ },
36
+ };
@@ -4,7 +4,7 @@ import type { PosterlyClient } from '../lib/api-client.js';
4
4
  export const listPostsTool = {
5
5
  name: 'list_posts',
6
6
  description:
7
- 'List upcoming or recent posts. Filter by status (scheduled, published, failed, draft), platform, or workspace_id. If workspace_id is omitted, posts across every workspace the caller is a member of are returned.',
7
+ 'List upcoming or recent posts. Filter by status (scheduled, published, failed, draft), platform, account_id, or workspace_id. If workspace_id is omitted, posts across every workspace the caller is a member of are returned.',
8
8
  inputSchema: z.object({
9
9
  status: z
10
10
  .string()
@@ -14,12 +14,16 @@ export const listPostsTool = {
14
14
  .string()
15
15
  .optional()
16
16
  .describe('Filter by platform: instagram, twitter, linkedin, etc.'),
17
+ account_id: z
18
+ .string()
19
+ .optional()
20
+ .describe('Filter to a specific social account ID (from list_accounts).'),
17
21
  limit: z
18
22
  .number()
19
23
  .min(1)
20
- .max(50)
24
+ .max(100)
21
25
  .optional()
22
- .describe('Number of posts to return (default 10, max 50)'),
26
+ .describe('Number of posts to return (default 20, max 100)'),
23
27
  workspace_id: z
24
28
  .string()
25
29
  .optional()
@@ -28,11 +32,17 @@ export const listPostsTool = {
28
32
 
29
33
  async execute(
30
34
  client: PosterlyClient,
31
- input: { status?: string; platform?: string; limit?: number; workspace_id?: string }
35
+ input: {
36
+ status?: string;
37
+ platform?: string;
38
+ account_id?: string;
39
+ limit?: number;
40
+ workspace_id?: string;
41
+ }
32
42
  ) {
33
43
  const { posts, total } = await client.listPosts({
34
44
  ...input,
35
- limit: input.limit || 10,
45
+ limit: input.limit || 20,
36
46
  });
37
47
 
38
48
  if (posts.length === 0) {