posterly-mcp-server 0.19.4 โ†’ 0.19.6

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.
Files changed (33) hide show
  1. package/README.md +38 -18
  2. package/dist/index.js +52 -9
  3. package/dist/lib/api-client.d.ts +87 -0
  4. package/dist/lib/api-client.js +36 -1
  5. package/dist/tools/create-connect-session.d.ts +24 -0
  6. package/dist/tools/create-connect-session.js +38 -0
  7. package/dist/tools/create-webhook.d.ts +2 -2
  8. package/dist/tools/generate-image.d.ts +2 -2
  9. package/dist/tools/get-account-analytics.d.ts +7 -0
  10. package/dist/tools/get-account-analytics.js +121 -56
  11. package/dist/tools/get-agent-signup-info.d.ts +8 -0
  12. package/dist/tools/get-agent-signup-info.js +32 -0
  13. package/dist/tools/get-connect-session.d.ts +16 -0
  14. package/dist/tools/get-connect-session.js +26 -0
  15. package/dist/tools/get-post-analytics.d.ts +7 -0
  16. package/dist/tools/get-post-analytics.js +133 -54
  17. package/dist/tools/get-signup-session.d.ts +16 -0
  18. package/dist/tools/get-signup-session.js +65 -0
  19. package/dist/tools/get-video-job.d.ts +2 -2
  20. package/dist/tools/list-posts.d.ts +2 -2
  21. package/dist/tools/start-signup.d.ts +40 -0
  22. package/dist/tools/start-signup.js +83 -0
  23. package/dist/tools/update-webhook.d.ts +2 -2
  24. package/package.json +1 -1
  25. package/src/index.ts +77 -9
  26. package/src/lib/api-client.ts +142 -2
  27. package/src/tools/create-connect-session.ts +46 -0
  28. package/src/tools/get-account-analytics.ts +127 -64
  29. package/src/tools/get-agent-signup-info.ts +37 -0
  30. package/src/tools/get-connect-session.ts +34 -0
  31. package/src/tools/get-post-analytics.ts +139 -57
  32. package/src/tools/get-signup-session.ts +74 -0
  33. package/src/tools/start-signup.ts +104 -0
@@ -1,7 +1,8 @@
1
1
  import { z } from 'zod';
2
+ const presentationSchema = z.enum(['compact', 'table', 'json']).optional();
2
3
  export const getAccountAnalyticsTool = {
3
4
  name: 'get_account_analytics',
4
- description: 'Get daily analytics snapshots and a period summary for a connected social account. Supports Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, and YouTube. Uses API-provided display_metrics for platform-native dashboard labels such as Google Business Profile Views, Search Views, Maps Views, Customer Actions, and Posts.',
5
+ description: 'Get daily analytics snapshots and a period summary for a connected social account. Supports Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, and YouTube. Uses API-provided display_metrics for platform-native dashboard labels and supports presentation: compact, table, or json.',
5
6
  inputSchema: z.object({
6
7
  account_id: z
7
8
  .number()
@@ -14,74 +15,138 @@ export const getAccountAnalyticsTool = {
14
15
  .string()
15
16
  .optional()
16
17
  .describe('End date (ISO date). Defaults to today.'),
18
+ presentation: presentationSchema.describe('Output style: compact bullets, Markdown table, or raw JSON for client-side chart/card rendering.'),
17
19
  }),
18
20
  async execute(client, input) {
19
21
  const result = await client.getAccountAnalytics(input);
20
- const { account, range, summary, snapshots } = result;
21
- const accountLabel = account.platform === 'google_business'
22
- ? `${account.username} (Google Business, id ${account.id})`
23
- : `@${account.username} (${account.platform}, id ${account.id})`;
24
- const lines = [
25
- `Analytics for ${accountLabel}`,
26
- `Range: ${range.from} โ†’ ${range.to} (${snapshots.length} daily snapshots)`,
27
- '',
28
- 'Summary:',
29
- ];
30
- const nativeRows = getNativeMetricRows(account.platform, summary);
31
- if (nativeRows.length > 0) {
32
- for (const [label, value] of nativeRows) {
33
- pushMetric(lines, label, value);
34
- }
35
- }
36
- else if (account.platform === 'pinterest') {
37
- pushGenericSummary(lines, summary);
38
- pushMetric(lines, 'Outbound clicks', summary.total_website_clicks);
39
- }
40
- else if (account.platform === 'youtube') {
41
- pushGenericSummary(lines, summary);
42
- pushMetric(lines, 'Watch minutes', summary.total_watch_minutes);
43
- pushMetric(lines, 'Likes', summary.total_likes);
44
- }
45
- else {
46
- pushGenericSummary(lines, summary);
47
- }
48
- return lines.join('\n');
22
+ return formatAccountAnalytics(result, input.presentation || 'compact');
49
23
  },
50
24
  };
51
- function getNativeMetricRows(platform, summary) {
52
- // API-provided display_metrics is the contract for platform-native dashboards.
53
- // Add future non-generic analytics platforms there so MCP output never guesses
54
- // from internal storage field names.
55
- const apiRows = (summary.display_metrics || [])
56
- .filter((metric) => metric?.label)
57
- .map((metric) => [metric.label, metric.value]);
58
- if (apiRows.length > 0)
59
- return apiRows;
25
+ function formatAccountAnalytics(result, presentation) {
26
+ const { account, range, summary, snapshots } = result;
27
+ const rows = accountMetricRows(summary, account.platform);
28
+ const insights = analyticsInsights(summary, account.platform);
29
+ if (presentation === 'json') {
30
+ return JSON.stringify({
31
+ type: 'account_analytics',
32
+ account,
33
+ range,
34
+ snapshot_count: snapshots.length,
35
+ metrics: rows.map(([label, value]) => ({ label, value })),
36
+ insights,
37
+ summary,
38
+ snapshots,
39
+ }, null, 2);
40
+ }
41
+ if (presentation === 'table') {
42
+ return [
43
+ `**๐Ÿ“Š Analytics for ${accountLabel(account.username)} (${account.id})**`,
44
+ `_${account.platform} ยท ${range.from} โ†’ ${range.to}_`,
45
+ '',
46
+ '**Account**',
47
+ markdownTable(['Field', 'Value'], [
48
+ ['Account ID', String(account.id)],
49
+ ['Daily snapshots', formatNumber(snapshots.length)],
50
+ ]),
51
+ '',
52
+ '**Metrics**',
53
+ markdownTable(['Metric', 'Value'], rows),
54
+ '',
55
+ '**Quick read**',
56
+ insights.map((insight) => `- ${insight}`).join('\n'),
57
+ ].join('\n');
58
+ }
59
+ return [
60
+ `**๐Ÿ“Š Analytics for ${accountLabel(account.username)} (${account.id})**`,
61
+ `_${account.platform} ยท ${range.from} โ†’ ${range.to}_`,
62
+ '',
63
+ '**Account**',
64
+ `- **Account ID:** ${account.id}`,
65
+ `- **Daily snapshots:** ${formatNumber(snapshots.length)}`,
66
+ '',
67
+ '**Key metrics**',
68
+ rows.map(([label, value]) => `- **${label}:** ${value}`).join('\n'),
69
+ '',
70
+ '**Quick read**',
71
+ insights.map((insight) => `- ${insight}`).join('\n'),
72
+ ].join('\n');
73
+ }
74
+ function accountMetricRows(summary, platform) {
75
+ const nativeRows = nativeMetricRows(summary);
76
+ if (nativeRows.length > 0)
77
+ return nativeRows;
60
78
  if (platform === 'google_business') {
61
79
  const metrics = summary.platform_metrics || {};
62
80
  return [
63
- ['Profile Views', metrics.profile_views ?? summary.total_views],
64
- ['Search Views', metrics.search_views ?? summary.total_reach],
65
- ['Maps Views', metrics.maps_views ?? summary.total_profile_views],
66
- ['Customer Actions', metrics.customer_actions ?? summary.total_accounts_engaged],
67
- ['Posts', metrics.posts ?? summary.current_media_count],
81
+ ['Profile Views', formatNumber(metrics.profile_views ?? summary.total_views)],
82
+ ['Search Views', formatNumber(metrics.search_views ?? summary.total_reach)],
83
+ ['Maps Views', formatNumber(metrics.maps_views ?? summary.total_profile_views)],
84
+ ['Customer Actions', formatNumber(metrics.customer_actions ?? summary.total_accounts_engaged)],
85
+ ['Posts', formatNumber(metrics.posts ?? summary.current_media_count)],
68
86
  ];
69
87
  }
70
- return [];
88
+ const rows = genericMetricRows(summary);
89
+ if (platform === 'pinterest')
90
+ rows.push(['Outbound clicks', formatNumber(summary.total_website_clicks)]);
91
+ if (platform === 'youtube') {
92
+ rows.push(['Watch minutes', formatNumber(summary.total_watch_minutes)]);
93
+ rows.push(['Likes', formatNumber(summary.total_likes)]);
94
+ }
95
+ return rows;
96
+ }
97
+ function nativeMetricRows(summary) {
98
+ return (summary.display_metrics || [])
99
+ .filter((metric) => metric?.label)
100
+ .map((metric) => [metric.label, formatNumber(metric.value)]);
101
+ }
102
+ function genericMetricRows(summary) {
103
+ return [
104
+ ['Followers', `${formatNumber(summary.current_followers)} (${formatDelta(summary.followers_change)} in range)`],
105
+ ['Follows gained / lost', `+${formatNumber(summary.total_follows_gained)} / -${formatNumber(summary.total_follows_lost)}`],
106
+ ['Total reach', formatNumber(summary.total_reach)],
107
+ ['Total views', formatNumber(summary.total_views)],
108
+ ['Profile views', formatNumber(summary.total_profile_views)],
109
+ ['Accounts engaged', formatNumber(summary.total_accounts_engaged)],
110
+ ['Engagement rate by reach', formatPercent(summary.engagement_rate)],
111
+ ['Engagement rate by followers', formatPercent(summary.engagement_rate_by_followers)],
112
+ ];
113
+ }
114
+ function analyticsInsights(summary, platform) {
115
+ const insights = [];
116
+ if (platform === 'google_business' && (summary.display_metrics?.length || 0) > 0) {
117
+ insights.push('Google Business metrics are shown with dashboard-native labels.');
118
+ }
119
+ if ((summary.followers_change || 0) > 0)
120
+ insights.push(`Audience grew by ${formatDelta(summary.followers_change)} followers in this range.`);
121
+ if ((summary.followers_change || 0) < 0)
122
+ insights.push(`Audience dipped by ${formatDelta(summary.followers_change)} followers in this range.`);
123
+ if (summary.engagement_rate != null && platform !== 'google_business')
124
+ insights.push(`Engagement by reach is ${formatPercent(summary.engagement_rate)}.`);
125
+ if (platform === 'youtube' && summary.total_watch_minutes != null)
126
+ insights.push(`${formatNumber(summary.total_watch_minutes)} total watch minutes were recorded.`);
127
+ return insights.length ? insights : ['No standout movement detected in the returned period.'];
71
128
  }
72
129
  function formatDelta(n) {
73
130
  return n >= 0 ? `+${n.toLocaleString()}` : n.toLocaleString();
74
131
  }
75
- function pushGenericSummary(lines, summary) {
76
- lines.push(`โ€ข Followers: ${summary.current_followers.toLocaleString()} (${formatDelta(summary.followers_change)} in range)`, `โ€ข Follows gained / lost: +${summary.total_follows_gained} / -${summary.total_follows_lost}`);
77
- pushMetric(lines, 'Total reach', summary.total_reach);
78
- pushMetric(lines, 'Total views', summary.total_views);
79
- pushMetric(lines, 'Profile views', summary.total_profile_views);
80
- pushMetric(lines, 'Total accounts engaged', summary.total_accounts_engaged);
81
- lines.push(`โ€ข Engagement rate (by reach): ${summary.engagement_rate}%`, `โ€ข Engagement rate (by followers): ${summary.engagement_rate_by_followers}%`);
132
+ function formatNumber(value) {
133
+ return value == null ? 'n/a' : value.toLocaleString();
82
134
  }
83
- function pushMetric(lines, label, value) {
84
- if (value != null) {
85
- lines.push(`โ€ข ${label}: ${value.toLocaleString()}`);
86
- }
135
+ function formatPercent(value) {
136
+ return value == null ? 'n/a' : `${formatNumber(value)}%`;
137
+ }
138
+ function accountLabel(username) {
139
+ if (!username)
140
+ return 'account';
141
+ return username.startsWith('@') || /\s/.test(username) ? username : `@${username}`;
142
+ }
143
+ function markdownTable(headers, rows) {
144
+ return [
145
+ `| ${headers.map(escapeCell).join(' | ')} |`,
146
+ `| ${headers.map(() => '---').join(' | ')} |`,
147
+ ...rows.map((row) => `| ${row.map(escapeCell).join(' | ')} |`),
148
+ ].join('\n');
149
+ }
150
+ function escapeCell(value) {
151
+ return String(value).replace(/\|/g, '\\|').replace(/\r?\n/g, '<br>');
87
152
  }
@@ -0,0 +1,8 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+ export declare const getAgentSignupInfoTool: {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
7
+ execute(client: PosterlyClient): Promise<string>;
8
+ };
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+ export const getAgentSignupInfoTool = {
3
+ name: 'get_agent_signup_info',
4
+ description: 'Explain how to use Posterly MCP for agent-led signup before an API key exists, including the safe human-in-the-browser boundaries.',
5
+ inputSchema: z.object({}),
6
+ async execute(client) {
7
+ const authState = client.hasApiKey()
8
+ ? 'Posterly API key is installed. Authenticated scheduling and connect tools are available.'
9
+ : 'No Posterly API key is installed. Only public signup tools are available until the user completes paid setup and gives this AI Posterly access.';
10
+ return [
11
+ 'Posterly agent signup info',
12
+ '',
13
+ authState,
14
+ '',
15
+ 'Pre-auth tools:',
16
+ '- start_signup: starts paid signup and returns a Posterly checkout handoff URL plus a signup poll URL.',
17
+ '- get_signup_session: polls checkout, payment, password, and agent-access status.',
18
+ '',
19
+ 'After Posterly access is installed:',
20
+ '- whoami: confirm the paid Posterly account/workspace.',
21
+ '- create_connect_session: create a secure browser OAuth handoff for the social platform the user chooses.',
22
+ '- get_connect_session: poll the OAuth handoff until connected.',
23
+ '- create_post: schedule posts after the user confirms the account and content.',
24
+ '',
25
+ 'Boundaries:',
26
+ '- The user pays in Stripe Checkout in their browser.',
27
+ '- The user sets their Posterly password in their browser.',
28
+ '- The user approves social OAuth in their browser.',
29
+ '- The AI should never ask for card details, Posterly passwords, social passwords, or OAuth codes.',
30
+ ].join('\n');
31
+ },
32
+ };
@@ -0,0 +1,16 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+ export declare const getConnectSessionTool: {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: z.ZodObject<{
7
+ session_id: z.ZodString;
8
+ }, "strip", z.ZodTypeAny, {
9
+ session_id: string;
10
+ }, {
11
+ session_id: string;
12
+ }>;
13
+ execute(client: PosterlyClient, input: {
14
+ session_id: string;
15
+ }): Promise<string>;
16
+ };
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ export const getConnectSessionTool = {
3
+ name: 'get_connect_session',
4
+ description: 'Poll a Posterly connect session. Read status_message to tell the user what is happening; stop when status is connected, failed, cancelled, or expired.',
5
+ inputSchema: z.object({
6
+ session_id: z.string().describe('Connect session ID returned by create_connect_session.'),
7
+ }),
8
+ async execute(client, input) {
9
+ const result = await client.getConnectSession(input.session_id);
10
+ return formatConnectSession(result.connect_session);
11
+ },
12
+ };
13
+ function formatConnectSession(session) {
14
+ const connected = session.connected_count ?? session.connected_accounts?.length ?? 0;
15
+ return [
16
+ `Posterly connect session: ${session.id}`,
17
+ `Platform: ${session.platform}`,
18
+ `Status: ${session.status}`,
19
+ `Message: ${session.status_message}`,
20
+ `Connected accounts: ${connected}`,
21
+ session.error_message ? `Error: ${session.error_message}` : '',
22
+ `Expires: ${session.expires_at}`,
23
+ '',
24
+ `Raw connect session:\n${JSON.stringify(session, null, 2)}`,
25
+ ].filter(Boolean).join('\n');
26
+ }
@@ -1,5 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import type { PosterlyClient } from '../lib/api-client.js';
3
+ declare const presentationSchema: z.ZodOptional<z.ZodEnum<["compact", "table", "json"]>>;
4
+ type AnalyticsPresentation = z.infer<typeof presentationSchema>;
3
5
  export declare const getPostAnalyticsTool: {
4
6
  name: string;
5
7
  description: string;
@@ -9,18 +11,21 @@ export declare const getPostAnalyticsTool: {
9
11
  to: z.ZodOptional<z.ZodString>;
10
12
  limit: z.ZodOptional<z.ZodNumber>;
11
13
  offset: z.ZodOptional<z.ZodNumber>;
14
+ presentation: z.ZodOptional<z.ZodEnum<["compact", "table", "json"]>>;
12
15
  }, "strip", z.ZodTypeAny, {
13
16
  account_id: number;
14
17
  limit?: number | undefined;
15
18
  offset?: number | undefined;
16
19
  from?: string | undefined;
17
20
  to?: string | undefined;
21
+ presentation?: "compact" | "table" | "json" | undefined;
18
22
  }, {
19
23
  account_id: number;
20
24
  limit?: number | undefined;
21
25
  offset?: number | undefined;
22
26
  from?: string | undefined;
23
27
  to?: string | undefined;
28
+ presentation?: "compact" | "table" | "json" | undefined;
24
29
  }>;
25
30
  execute(client: PosterlyClient, input: {
26
31
  account_id: number;
@@ -28,5 +33,7 @@ export declare const getPostAnalyticsTool: {
28
33
  to?: string;
29
34
  limit?: number;
30
35
  offset?: number;
36
+ presentation?: AnalyticsPresentation;
31
37
  }): Promise<string>;
32
38
  };
39
+ export {};
@@ -1,7 +1,8 @@
1
1
  import { z } from 'zod';
2
+ const presentationSchema = z.enum(['compact', 'table', 'json']).optional();
2
3
  export const getPostAnalyticsTool = {
3
4
  name: 'get_post_analytics',
4
- description: 'Get per-post engagement metrics (likes, comments, reach, impressions, saves, shares, plays, clicks, watch time) for a connected social account. Supports Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, and YouTube. Returns the most recent posts first.',
5
+ description: 'Get per-post engagement metrics (likes, comments, reach, impressions, saves, shares, plays, clicks, watch time) for a connected social account. Supports Instagram, Facebook, LinkedIn, Google Business Profile, Pinterest, and YouTube. Returns the most recent posts first. presentation controls output: compact for Telegram/mobile, table for Markdown clients, json for custom chart/card renderers.',
5
6
  inputSchema: z.object({
6
7
  account_id: z
7
8
  .number()
@@ -21,61 +22,139 @@ export const getPostAnalyticsTool = {
21
22
  .optional()
22
23
  .describe('Number of posts to return (default 50, max 200)'),
23
24
  offset: z.number().min(0).optional().describe('Pagination offset'),
25
+ presentation: presentationSchema.describe('Output style: compact bullets, Markdown table, or raw JSON for client-side chart/card rendering.'),
24
26
  }),
25
27
  async execute(client, input) {
26
28
  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
- if (account.platform === 'youtube' && p.total_watch_time_ms) {
59
- metrics.push(`${Math.round(p.total_watch_time_ms / 60000).toLocaleString()} watch minutes`);
60
- }
61
- if (account.platform === 'youtube' && p.avg_watch_time_ms) {
62
- metrics.push(`${Math.round(p.avg_watch_time_ms / 1000)}s avg view`);
63
- }
64
- if (p.url_link_clicks) {
65
- metrics.push(account.platform === 'pinterest'
66
- ? `${p.url_link_clicks.toLocaleString()} outbound clicks`
67
- : `${p.url_link_clicks.toLocaleString()} link clicks`);
68
- }
69
- if (p.user_profile_clicks) {
70
- metrics.push(account.platform === 'pinterest'
71
- ? `${p.user_profile_clicks.toLocaleString()} pin clicks`
72
- : `${p.user_profile_clicks.toLocaleString()} profile clicks`);
73
- }
74
- lines.push(`โ€ข [${postedAt}] ${caption}`);
75
- lines.push(` ${metrics.join(' โ€ข ')}`);
76
- if (p.permalink)
77
- lines.push(` ${p.permalink}`);
78
- }
79
- return lines.join('\n');
29
+ return formatPostAnalytics(result, input.presentation || 'compact');
80
30
  },
81
31
  };
32
+ function formatPostAnalytics(result, presentation) {
33
+ const { account, range, posts, total } = result;
34
+ if (posts.length === 0) {
35
+ return `No analytics found for ${accountLabel(account.username)} (${account.platform}) between ${range.from} and ${range.to}.`;
36
+ }
37
+ const totals = posts.reduce((acc, post) => {
38
+ acc.likes += post.likes || 0;
39
+ acc.comments += post.comments || 0;
40
+ acc.reach += post.reach || 0;
41
+ acc.impressions += post.impressions || 0;
42
+ acc.shares += post.shares || 0;
43
+ acc.saves += post.saved || 0;
44
+ return acc;
45
+ }, { likes: 0, comments: 0, reach: 0, impressions: 0, shares: 0, saves: 0 });
46
+ if (presentation === 'json') {
47
+ return JSON.stringify({
48
+ type: 'post_analytics',
49
+ account,
50
+ range,
51
+ returned: {
52
+ count: posts.length,
53
+ total,
54
+ },
55
+ totals,
56
+ posts,
57
+ }, null, 2);
58
+ }
59
+ if (presentation === 'table') {
60
+ return [
61
+ `**๐Ÿ“ˆ Post analytics for ${accountLabel(account.username)} (${account.id})**`,
62
+ `_${account.platform} ยท ${range.from} โ†’ ${range.to} ยท showing ${posts.length} of ${total}_`,
63
+ '',
64
+ '**Returned totals**',
65
+ markdownTable(['Likes', 'Comments', 'Reach', 'Impressions', 'Shares', 'Saves'], [[
66
+ formatNumber(totals.likes),
67
+ formatNumber(totals.comments),
68
+ formatNumber(totals.reach),
69
+ formatNumber(totals.impressions),
70
+ formatNumber(totals.shares),
71
+ formatNumber(totals.saves),
72
+ ]]),
73
+ '',
74
+ '**Posts**',
75
+ markdownTable(['Posted', 'Post', 'Caption', 'Reach', 'Likes', 'Comments', 'Extras'], posts.map((post) => [
76
+ dateTime(post.posted_at),
77
+ post.id ? String(post.id) : post.platform_media_id,
78
+ compact(post.caption_snippet, 70) || '(no caption)',
79
+ formatNumber(post.reach),
80
+ formatNumber(post.likes),
81
+ formatNumber(post.comments),
82
+ postExtras(account.platform, post),
83
+ ])),
84
+ ].join('\n');
85
+ }
86
+ const lines = [
87
+ `**๐Ÿ“ˆ Post analytics for ${accountLabel(account.username)} (${account.id})**`,
88
+ `_${account.platform} ยท ${range.from} โ†’ ${range.to} ยท showing ${posts.length} of ${total}_`,
89
+ '',
90
+ '**Returned totals**',
91
+ `- **Likes:** ${formatNumber(totals.likes)}`,
92
+ `- **Comments:** ${formatNumber(totals.comments)}`,
93
+ `- **Reach:** ${formatNumber(totals.reach)}`,
94
+ `- **Impressions:** ${formatNumber(totals.impressions)}`,
95
+ `- **Shares:** ${formatNumber(totals.shares)}`,
96
+ `- **Saves:** ${formatNumber(totals.saves)}`,
97
+ '',
98
+ '**Posts**',
99
+ ];
100
+ posts.forEach((post, index) => {
101
+ lines.push(`${index + 1}. **${compact(post.caption_snippet, 70) || '(no caption)'}**`);
102
+ lines.push(` Posted: ${dateTime(post.posted_at)}`);
103
+ lines.push(` Post ID: ${post.id ? String(post.id) : post.platform_media_id}`);
104
+ lines.push(` Reach: ${formatNumber(post.reach)}`);
105
+ lines.push(` Likes: ${formatNumber(post.likes)}`);
106
+ lines.push(` Comments: ${formatNumber(post.comments)}`);
107
+ lines.push(` Extras: ${postExtras(account.platform, post)}`);
108
+ });
109
+ return lines.join('\n');
110
+ }
111
+ function postExtras(platform, post) {
112
+ const metrics = [];
113
+ if (post.impressions)
114
+ metrics.push(`${formatNumber(post.impressions)} impressions`);
115
+ if (post.saved)
116
+ metrics.push(`${formatNumber(post.saved)} saved`);
117
+ if (post.shares)
118
+ metrics.push(`${formatNumber(post.shares)} shares`);
119
+ if (post.plays)
120
+ metrics.push(`${formatNumber(post.plays)} plays`);
121
+ if (platform === 'youtube' && post.total_watch_time_ms)
122
+ metrics.push(`${formatNumber(Math.round(post.total_watch_time_ms / 60000))} watch minutes`);
123
+ if (platform === 'youtube' && post.avg_watch_time_ms)
124
+ metrics.push(`${formatNumber(Math.round(post.avg_watch_time_ms / 1000))}s avg view`);
125
+ if (post.url_link_clicks)
126
+ metrics.push(platform === 'pinterest' ? `${formatNumber(post.url_link_clicks)} outbound clicks` : `${formatNumber(post.url_link_clicks)} link clicks`);
127
+ if (post.user_profile_clicks)
128
+ metrics.push(platform === 'pinterest' ? `${formatNumber(post.user_profile_clicks)} pin clicks` : `${formatNumber(post.user_profile_clicks)} profile clicks`);
129
+ if (post.permalink)
130
+ metrics.push(post.permalink);
131
+ return metrics.join(' ยท ') || 'n/a';
132
+ }
133
+ function accountLabel(username) {
134
+ if (!username)
135
+ return 'account';
136
+ return username.startsWith('@') || /\s/.test(username) ? username : `@${username}`;
137
+ }
138
+ function dateTime(value) {
139
+ if (!value)
140
+ return 'n/a';
141
+ const date = new Date(value);
142
+ return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
143
+ }
144
+ function compact(value, maxLength) {
145
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
146
+ return text.length > maxLength ? `${text.slice(0, Math.max(0, maxLength - 1))}โ€ฆ` : text;
147
+ }
148
+ function formatNumber(value) {
149
+ return value == null ? 'n/a' : value.toLocaleString();
150
+ }
151
+ function markdownTable(headers, rows) {
152
+ return [
153
+ `| ${headers.map(escapeCell).join(' | ')} |`,
154
+ `| ${headers.map(() => '---').join(' | ')} |`,
155
+ ...rows.map((row) => `| ${row.map(escapeCell).join(' | ')} |`),
156
+ ].join('\n');
157
+ }
158
+ function escapeCell(value) {
159
+ return String(value).replace(/\|/g, '\\|').replace(/\r?\n/g, '<br>');
160
+ }
@@ -0,0 +1,16 @@
1
+ import { z } from 'zod';
2
+ import type { PosterlyClient } from '../lib/api-client.js';
3
+ export declare const getSignupSessionTool: {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: z.ZodObject<{
7
+ session_id: z.ZodString;
8
+ }, "strip", z.ZodTypeAny, {
9
+ session_id: string;
10
+ }, {
11
+ session_id: string;
12
+ }>;
13
+ execute(client: PosterlyClient, input: {
14
+ session_id: string;
15
+ }): Promise<string>;
16
+ };
@@ -0,0 +1,65 @@
1
+ import { z } from 'zod';
2
+ export const getSignupSessionTool = {
3
+ name: 'get_signup_session',
4
+ description: 'Poll a public Posterly signup session created by start_signup. Use this to narrate checkout, payment, password setup, and agent access status before a Posterly API key is installed.',
5
+ inputSchema: z.object({
6
+ session_id: z
7
+ .string()
8
+ .min(8)
9
+ .describe('Signup session ID returned by start_signup, usually the Stripe checkout session ID.'),
10
+ }),
11
+ async execute(client, input) {
12
+ const result = await client.getSignupSession(input.session_id);
13
+ return formatSignupSession(result.signup_session);
14
+ },
15
+ };
16
+ function formatSignupSession(session) {
17
+ const nextAction = session.next_action;
18
+ const checkoutRedirectUrl = session.checkout_redirect_url || stringFromRecord(session.links, 'checkout_redirect_url');
19
+ const checkoutUrl = stringFromRecord(session.links, 'checkout_url');
20
+ const passwordUrl = stringFromRecord(session.links, 'password_setup_url');
21
+ const completeUrl = stringFromRecord(session.links, 'complete_url');
22
+ const lines = [
23
+ `Posterly signup session: ${session.id}`,
24
+ `Status: ${session.status}`,
25
+ session.status_message ? `Message: ${session.status_message}` : '',
26
+ nextAction?.label ? `Next action: ${nextAction.label}` : '',
27
+ nextAction?.description ? `Next action detail: ${nextAction.description}` : '',
28
+ checkoutRedirectUrl ? `Checkout handoff URL: ${checkoutRedirectUrl}` : '',
29
+ checkoutUrl ? `Checkout URL: ${checkoutUrl}` : '',
30
+ passwordUrl ? `Password setup URL: ${passwordUrl}` : '',
31
+ completeUrl ? `Completion URL: ${completeUrl}` : '',
32
+ '',
33
+ guidanceForStatus(session.status),
34
+ session.agent_next_steps?.length
35
+ ? `Agent next steps:\n${session.agent_next_steps.map((step, index) => `${index + 1}. ${step}`).join('\n')}`
36
+ : '',
37
+ '',
38
+ `Raw signup session:\n${JSON.stringify(session, null, 2)}`,
39
+ ];
40
+ return lines.filter(Boolean).join('\n');
41
+ }
42
+ function stringFromRecord(record, key) {
43
+ const value = record?.[key];
44
+ return typeof value === 'string' ? value : null;
45
+ }
46
+ function guidanceForStatus(status) {
47
+ switch (status) {
48
+ case 'checkout_pending':
49
+ return 'Tell the user checkout is still pending. Keep polling; do not ask for card details.';
50
+ case 'checkout_expired':
51
+ return 'Tell the user checkout expired and offer to call start_signup again.';
52
+ case 'payment_confirmed':
53
+ return 'Tell the user payment is confirmed and Posterly is preparing access. Keep polling.';
54
+ case 'password_required':
55
+ return 'Tell the user Posterly has sent a password setup email. They need to open that email and set a password in the browser.';
56
+ case 'authorize_agent':
57
+ return 'Tell the user Posterly is ready for agent authorization. Send them the provided authorization or completion link if one is present.';
58
+ case 'agent_access_required':
59
+ return 'Tell the user API access is active, but this AI still needs Posterly access. Ask them to paste the Posterly MCP/API setup instructions here or install the Posterly MCP server, then continue with social account connection.';
60
+ case 'api_access_active':
61
+ return 'Posterly API access is active. Ask which social account the user wants to connect first, then create a connect session.';
62
+ default:
63
+ return 'Explain the current status to the user and keep polling unless the session is terminal.';
64
+ }
65
+ }
@@ -8,11 +8,11 @@ export declare const getVideoJobTool: {
8
8
  status: z.ZodOptional<z.ZodEnum<["pending", "processing", "completed", "failed", "dismissed"]>>;
9
9
  limit: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
10
10
  }, "strip", z.ZodTypeAny, {
11
- status?: "pending" | "processing" | "completed" | "failed" | "dismissed" | undefined;
11
+ status?: "failed" | "pending" | "processing" | "completed" | "dismissed" | undefined;
12
12
  limit?: number | undefined;
13
13
  job_id?: string | undefined;
14
14
  }, {
15
- status?: "pending" | "processing" | "completed" | "failed" | "dismissed" | undefined;
15
+ status?: "failed" | "pending" | "processing" | "completed" | "dismissed" | undefined;
16
16
  limit?: number | undefined;
17
17
  job_id?: string | undefined;
18
18
  }>;
@@ -10,15 +10,15 @@ export declare const listPostsTool: {
10
10
  limit: z.ZodOptional<z.ZodNumber>;
11
11
  workspace_id: z.ZodOptional<z.ZodString>;
12
12
  }, "strip", z.ZodTypeAny, {
13
+ status?: string | undefined;
13
14
  workspace_id?: string | undefined;
14
15
  platform?: "instagram" | "instagram-standalone" | "facebook" | "tiktok" | "twitter" | "linkedin" | "youtube" | "pinterest" | "threads" | "google_business" | "telegram" | "bluesky" | "gmb" | "google-business" | "google_business_profile" | "x" | undefined;
15
- status?: string | undefined;
16
16
  account_id?: string | undefined;
17
17
  limit?: number | undefined;
18
18
  }, {
19
+ status?: string | undefined;
19
20
  workspace_id?: string | undefined;
20
21
  platform?: "instagram" | "instagram-standalone" | "facebook" | "tiktok" | "twitter" | "linkedin" | "youtube" | "pinterest" | "threads" | "google_business" | "telegram" | "bluesky" | "gmb" | "google-business" | "google_business_profile" | "x" | undefined;
21
- status?: string | undefined;
22
22
  account_id?: string | undefined;
23
23
  limit?: number | undefined;
24
24
  }>;